Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions modules/fundamental/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,14 @@ actix-web = { workspace = true }
anyhow = { workspace = true }
async-trait = { workspace = true }
base64 = { workspace = true }
bytes = { workspace = true }
cpe = { workspace = true }
csv = { workspace = true }
flate2 ={ workspace = true }
futures-util = { workspace = true }
itertools = { workspace = true }
log = { workspace = true }
reqwest = { workspace = true, features = ["json"] }
reqwest = { workspace = true, features = ["json", "stream"] }
sanitize-filename = { workspace = true }
sea-orm = { workspace = true }
sea-query = { workspace = true }
Expand All @@ -54,7 +55,6 @@ async-graphql = { workspace = true, features = ["uuid", "time"], optional = true

[dev-dependencies]
actix-http = { workspace = true }
bytes = { workspace = true }
bytesize = { workspace = true }
chrono = { workspace = true }
criterion = { workspace = true, features = ["html_reports", "async_tokio"] }
Expand Down
6 changes: 6 additions & 0 deletions modules/fundamental/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,14 @@ pub enum Error {
#[error(transparent)]
Query(#[from] trustify_common::db::query::Error),
#[error(transparent)]
Http(#[from] reqwest::Error),
#[error(transparent)]
HttpHeaderValue(#[from] reqwest::header::InvalidHeaderValue),
#[error(transparent)]
Ingestor(#[from] trustify_module_ingestor::service::Error),
#[error(transparent)]
Json(#[from] serde_json::error::Error),
#[error(transparent)]
Purl(#[from] PurlErr),
#[error("Bad request: {0}")]
BadRequest(String),
Expand Down
109 changes: 87 additions & 22 deletions modules/fundamental/src/sbom/endpoints/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,21 @@ use crate::{
sbom::{
model::{
SbomExternalPackageReference, SbomNodeReference, SbomPackage, SbomPackageRelation,
SbomSummary, Which, details::SbomAdvisory,
SbomSummary, Which,
details::SbomAdvisory,
exploitiq::{
ExploitIqRequest, ReportRequest, ReportResult, create_report, fetch_report,
},
},
service::SbomService,
},
};
use actix_web::{HttpResponse, Responder, delete, get, http::header, post, web};
use actix_web::{
HttpResponse, Responder, delete, get,
http::header::{self, ContentType},
post,
web::{self, BytesMut},
};
use config::Config;
use futures_util::TryStreamExt;
use sea_orm::{TransactionTrait, prelude::Uuid};
Expand Down Expand Up @@ -69,7 +78,9 @@ pub fn configure(
.service(label::update)
.service(label::all)
.service(get_unique_licenses)
.service(get_license_export);
.service(get_license_export)
.service(create_exploitiq_report)
.service(fetch_exploitiq_report);
}

const CONTENT_TYPE_GZIP: &str = "application/gzip";
Expand Down Expand Up @@ -500,26 +511,80 @@ pub async fn download(
_: Require<ReadSbom>,
) -> Result<impl Responder, Error> {
let id = Id::from_str(&key).map_err(Error::IdKey)?;
match sbom
.fetch_sbom_summary(id, db.as_ref())
.await?
.and_then(|sbom| sbom.source_document)
{
Some(doc) => {
let storage_key = doc.try_into()?;
Ok(match ingestor.storage().retrieve(storage_key).await? {
Some(s) => HttpResponse::Ok().streaming(s),
None => HttpResponse::NotFound().finish(),
})
}
None => Ok(HttpResponse::NotFound().finish()),
}
}

let Some(sbom) = sbom.fetch_sbom_summary(id, db.as_ref()).await? else {
return Ok(HttpResponse::NotFound().finish());
};

if let Some(doc) = &sbom.source_document {
let storage_key = doc.try_into()?;

let stream = ingestor
.storage()
.retrieve(storage_key)
.await
.map_err(Error::Storage)?
.map(|stream| stream.map_err(Error::Storage));

Ok(match stream {
Some(s) => HttpResponse::Ok().streaming(s),
/// Create ExploitIQ report
#[utoipa::path(
tag = "sbom",
operation_id = "createExploitIQReport",
request_body = ReportRequest,
params(
("id" = Id, Path, description = "The id of the SBOM"),
),
responses(
(status = 201, description = "Create a report", body = ReportResult),
(status = 400, description = "Unable to read advisory list"),
(status = 404, description = "The SBOM could not be found"),
)
)]
#[post("/v2/sbom/{id}/exploitiq")]
pub async fn create_exploitiq_report(
ingestor: web::Data<IngestorService>,
db: web::Data<Database>,
sbom: web::Data<SbomService>,
id: web::Path<String>,
web::Json(ReportRequest { vulnerabilities }): web::Json<ReportRequest>,
_: Require<ReadSbom>,
) -> Result<impl Responder, Error> {
let id = Id::from_str(&id).map_err(Error::IdKey)?;
match sbom
.fetch_sbom_summary(id, db.as_ref())
.await?
.and_then(|sbom| sbom.source_document)
{
Some(doc) => Ok(match ingestor.storage().retrieve(doc.try_into()?).await? {
Some(s) => {
let buf = s.try_collect::<BytesMut>().await?;
let sbom: serde_json::Value = serde_json::from_slice(buf.as_ref())?;
let req = ExploitIqRequest::new(sbom, vulnerabilities);
HttpResponse::Created().json(create_report(req).await?)
}
None => HttpResponse::NotFound().finish(),
})
} else {
Ok(HttpResponse::NotFound().finish())
}),
None => Ok(HttpResponse::NotFound().finish()),
}
}

/// Fetch ExploitIQ report
#[utoipa::path(
tag = "sbom",
operation_id = "fetchExploitIQReport",
params(
("id" = String, description = "ExploitIQ report id"),
),
responses(
(status = 200, description = "The proxied ExploitIQ report", body = serde_json::Value),
)
)]
#[get("/v2/sbom/exploitiq/{id}")]
pub async fn fetch_exploitiq_report(id: web::Path<String>) -> Result<impl Responder, Error> {
let id = id.into_inner();
let stream = fetch_report(id).await?;
Ok(HttpResponse::Ok()
.content_type(ContentType::json())
.streaming(stream))
}
81 changes: 81 additions & 0 deletions modules/fundamental/src/sbom/model/exploitiq.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
use crate::Error;
use bytes::Bytes;
use futures_util::{Stream, TryStreamExt};
use reqwest::header;
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use std::env;
use utoipa::ToSchema;

const ENV_URL: &str = "EXPLOITIQ_API_URL";
const ENV_TOKEN: &str = "EXPLOITIQ_API_TOKEN";

#[derive(Serialize, Deserialize, Debug, ToSchema)]
pub struct ReportRequest {
pub vulnerabilities: Vec<String>,
}

#[derive(Serialize, Deserialize, Debug, ToSchema, Default)]
#[serde(rename_all = "camelCase")]
pub struct ReportResult {
pub id: String,
pub report_id: String,
}

#[derive(Serialize, Deserialize, Debug)]

pub struct ExploitIqRequest {
pub vulnerabilities: Vec<String>,
pub sbom: Value,
pub sbom_info_type: String,
pub metadata: Value,
}

impl ExploitIqRequest {
pub fn new(sbom: Value, vulnerabilities: Vec<String>) -> Self {
ExploitIqRequest {
vulnerabilities,
sbom,
sbom_info_type: "manual".into(),
metadata: json!({}),
}
}
}

pub async fn create_report(req: ExploitIqRequest) -> Result<ReportResult, Error> {
let url = format!("{}/reports/new", base_url()?);
let client = authorized_client()?;
Ok(client.post(url).json(&req).send().await?.json().await?)
}

pub async fn fetch_report(id: String) -> Result<impl Stream<Item = Result<Bytes, Error>>, Error> {
let url = format!("{}/reports/{id}", base_url()?);
let client = authorized_client()?;
let response = client.get(url).send().await?.error_for_status()?;
Ok(response.bytes_stream().map_err(Error::Http))
}

fn base_url() -> Result<String, Error> {
match env::var(ENV_URL) {
Ok(s) => Ok(s),
_ => {
log::error!("ExploitIQ reports require {ENV_URL} to be set");
Err(Error::Unavailable)
}
}
}

fn authorized_client() -> Result<reqwest::Client, Error> {
let Ok(token) = env::var(ENV_TOKEN) else {
log::error!("ExploitIQ reports require {ENV_TOKEN} to be set");
return Err(Error::Unavailable);
};
let token = format!("Bearer {token}");
let mut auth_value = header::HeaderValue::from_str(&token)?;
auth_value.set_sensitive(true);
let mut headers = header::HeaderMap::new();
headers.insert(header::AUTHORIZATION, auth_value);
Ok(reqwest::Client::builder()
.default_headers(headers)
.build()?)
}
1 change: 1 addition & 0 deletions modules/fundamental/src/sbom/model/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod details;
pub mod exploitiq;
pub mod raw_sql;

use super::service::SbomService;
Expand Down
68 changes: 68 additions & 0 deletions openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2376,6 +2376,25 @@ paths:
items:
type: integer
format: int64
/api/v2/sbom/exploitiq/{id}:
get:
tags:
- sbom
summary: Fetch ExploitIQ report
operationId: fetchExploitIQReport
parameters:
- name: id
in: path
description: ExploitIQ report id
required: true
schema:
type: string
responses:
'200':
description: The proxied ExploitIQ report
content:
application/json:
schema: {}
/api/v2/sbom/{id}:
get:
tags:
Expand Down Expand Up @@ -2463,6 +2482,36 @@ paths:
$ref: '#/components/schemas/LicenseRefMapping'
'400':
description: Invalid UUID format.
/api/v2/sbom/{id}/exploitiq:
post:
tags:
- sbom
summary: Create ExploitIQ report
operationId: createExploitIQReport
parameters:
- name: id
in: path
description: The id of the SBOM
required: true
schema:
$ref: '#/components/schemas/Id'
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/ReportRequest'
required: true
responses:
'201':
description: Create a report
content:
application/json:
schema:
$ref: '#/components/schemas/ReportResult'
'400':
description: Unable to read advisory list
'404':
description: The SBOM could not be found
/api/v2/sbom/{id}/label:
put:
tags:
Expand Down Expand Up @@ -4789,6 +4838,25 @@ components:
type: string
format: date-time
description: Start of the import run
ReportRequest:
type: object
required:
- vulnerabilities
properties:
vulnerabilities:
type: array
items:
type: string
ReportResult:
type: object
required:
- id
- reportId
properties:
id:
type: string
reportId:
type: string
Revisioned_Importer:
type: object
description: |-
Expand Down