Skip to content
Draft
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
31 changes: 29 additions & 2 deletions modules/fundamental/src/sbom/endpoints/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use crate::{
sbom::{
model::{
SbomExternalPackageReference, SbomNodeReference, SbomPackage, SbomPackageRelation,
SbomSummary, Which, details::SbomAdvisory,
SbomStatus, SbomSummary, Which, details::SbomAdvisory,
},
service::SbomService,
},
Expand Down Expand Up @@ -69,11 +69,38 @@ pub fn configure(
.service(download)
.service(label::set)
.service(label::update)
.service(get_license_export);
.service(get_license_export)
.service(get_sbom_status);
}

const CONTENT_TYPE_GZIP: &str = "application/gzip";

#[utoipa::path(
tag = "sbom",
operation_id = "getSbomStatus",
params(
("id" = String, Path,),
),
responses(
(status = 200, description = "sbom status", body = PaginatedResults<SbomStatus>),
(status = 404, description = "The sbom status could not be found"),
),
)]
#[get("/v2/sbom/{id}/sbom-status")]
pub async fn get_sbom_status(
fetcher: web::Data<SbomService>,
db: web::Data<Database>,
id: web::Path<String>,
) -> actix_web::Result<impl Responder> {
let id = Id::from_str(&id).map_err(Error::IdKey)?;
println!("get_sbom_statusget_sbom_statusget_sbom_statusget_sbom_status");
let result = fetcher
.fetch_sbom_status(id.try_as_uid().unwrap_or_default(), db.as_ref())
.await?;

Ok(HttpResponse::Ok().json(result))
}

#[utoipa::path(
tag = "sbom",
operation_id = "getLicenseExport",
Expand Down
19 changes: 19 additions & 0 deletions modules/fundamental/src/sbom/endpoints/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,25 @@ use trustify_test_context::{TrustifyContext, call::CallService, document_bytes};
use urlencoding::encode;
use uuid::Uuid;

#[test_context(TrustifyContext)]
#[test(actix_web::test)]
async fn sbom_status(ctx: &TrustifyContext) -> Result<(), anyhow::Error> {
let app = caller(ctx).await?;
let id = ctx
.ingest_document("spdx/OCP-TOOLS-4.11-RHEL-8.json")
.await?
.id
.to_string();

let uri = format!("/api/v2/sbom/{id}/sbom-status");
let req = TestRequest::get().uri(&uri).to_request();
let sbom: Value = app.call_and_read_body_json(req).await;
assert_eq!(3274, sbom["total_packages"]);
assert_eq!(10, sbom["total_licenses"]);

Ok(())
}

#[test_context(TrustifyContext)]
#[test(actix_web::test)]
async fn license_export(ctx: &TrustifyContext) -> Result<(), anyhow::Error> {
Expand Down
7 changes: 7 additions & 0 deletions modules/fundamental/src/sbom/model/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ impl SbomHead {
db: &C,
) -> Result<Self, Error> {
let number_of_packages = sbom.find_related(sbom_package::Entity).count(db).await?;

Ok(Self {
id: sbom.sbom_id,
document_id: sbom.document_id.clone(),
Expand Down Expand Up @@ -114,6 +115,12 @@ pub struct SbomPackage {
pub cpe: Vec<String>,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, ToSchema, SimpleObject, Default)]
pub struct SbomStatus {
pub total_packages: usize,
pub total_licenses: usize,
}

#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub enum SbomPackageReference<'a> {
Internal(&'a str),
Expand Down
88 changes: 79 additions & 9 deletions modules/fundamental/src/sbom/service/sbom.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,21 @@ use crate::{
Error,
sbom::model::{
SbomExternalPackageReference, SbomNodeReference, SbomPackage, SbomPackageRelation,
SbomSummary, Which, details::SbomDetails,
SbomStatus, SbomSummary, Which, details::SbomDetails,
},
};
use futures_util::{StreamExt, TryStreamExt, stream};
use sea_orm::{
ColumnTrait, ConnectionTrait, DbErr, EntityTrait, FromQueryResult, IntoSimpleExpr, QueryFilter,
QueryOrder, QueryResult, QuerySelect, RelationTrait, Select, SelectColumns, StreamTrait,
prelude::Uuid,
ColumnTrait, ConnectionTrait, DbErr, EntityTrait, FromQueryResult, IntoSimpleExpr,
PaginatorTrait, QueryFilter, QueryOrder, QueryResult, QuerySelect, RelationTrait, Select,
SelectColumns, StreamTrait, prelude::Uuid,
};
use sea_query::{Expr, JoinType, extension::postgres::PgExpr};
use sea_query::{Condition, Expr, JoinType, extension::postgres::PgExpr};
use serde_json::Value;
use std::{collections::HashMap, fmt::Debug};
use std::{
collections::{HashMap, HashSet},
fmt::Debug,
};
use tracing::instrument;
use trustify_common::{
cpe::Cpe,
Expand All @@ -27,19 +30,70 @@ use trustify_common::{
model::{Paginated, PaginatedResults},
purl::Purl,
};
use trustify_entity::sbom_package_license::LicenseCategory;
use trustify_entity::{
advisory, advisory_vulnerability, base_purl,
cpe::{self, CpeDto},
labels::Labels,
organization, package_relates_to_package,
license, organization, package_relates_to_package,
qualified_purl::{self, CanonicalPurl},
relationship::Relationship,
sbom::{self, SbomNodeLink},
sbom_node, sbom_package, sbom_package_cpe_ref, sbom_package_purl_ref, source_document, status,
versioned_purl, vulnerability,
sbom_node, sbom_package, sbom_package_cpe_ref, sbom_package_license, sbom_package_purl_ref,
source_document, status, versioned_purl, vulnerability,
};

impl SbomService {
#[instrument(skip(self, connection), err(level=tracing::Level::INFO))]
pub async fn fetch_sbom_status<C: ConnectionTrait>(
&self,
sbom_id: Uuid,
connection: &C,
) -> Result<SbomStatus, Error> {
let package_count = sbom_package::Entity::find()
.filter(sbom_package::Column::SbomId.eq(sbom_id))
.count(connection)
.await?;

let package_count_usize = package_count as usize;

let result_package_licenses: Vec<Option<Vec<String>>> =
sbom_package_license::Entity::find()
.filter(sbom_package_license::Column::SbomId.eq(sbom_id))
.join(
JoinType::InnerJoin,
sbom_package_license::Relation::License.def(),
)
.filter(
Condition::all().add(
sbom_package_license::Column::LicenseType.eq(LicenseCategory::Declared),
),
)
.select_only()
.column_as(license::Column::SpdxLicenses, "license_ids")
.into_tuple::<(Option<Vec<String>>,)>()
.all(connection)
.await?
.into_iter()
.map(|(ids,)| ids)
.collect();
let unique_licenses: HashSet<_> = result_package_licenses
.into_iter()
.flatten()
.flatten()
.collect();
let unique_licenses: HashSet<_> = unique_licenses
.iter()
.filter(|l| l.to_string() != *"NOASSERTION")
.collect();
let result = SbomStatus {
total_packages: package_count_usize,
total_licenses: unique_licenses.len(),
};

Ok(result)
}

#[instrument(skip(self, connection), err(level=tracing::Level::INFO))]
async fn fetch_sbom<C: ConnectionTrait>(
&self,
Expand Down Expand Up @@ -665,6 +719,22 @@ mod test {
use trustify_entity::labels::Labels;
use trustify_test_context::TrustifyContext;

#[test_context(TrustifyContext)]
#[test(tokio::test)]
async fn sboms_status(ctx: &TrustifyContext) -> Result<(), anyhow::Error> {
let result = ctx
.ingest_document("spdx/OCP-TOOLS-4.11-RHEL-8.json")
.await?;

let fetch = SbomService::new(ctx.db.clone());
let sbom_status = fetch
.fetch_sbom_status(result.id.try_as_uid().unwrap_or_default(), &ctx.db)
.await?;
assert_eq!(3274, sbom_status.total_packages);
assert_eq!(10, sbom_status.total_licenses);
Ok(())
}

#[test_context(TrustifyContext)]
#[test(tokio::test)]
async fn all_sboms(ctx: &TrustifyContext) -> Result<(), anyhow::Error> {
Expand Down
79 changes: 55 additions & 24 deletions openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1531,6 +1531,26 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/PaginatedResults_SbomPackageRelation'
/api/v2/sbom/{id}/sbom-status:
get:
tags:
- sbom
operationId: getSbomStatus
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
'200':
description: sbom status
content:
application/json:
schema:
$ref: '#/components/schemas/PaginatedResults_SbomStatus'
'404':
description: The sbom status could not be found
/api/v2/sbom/{key}/download:
get:
tags:
Expand Down Expand Up @@ -2776,6 +2796,30 @@ components:
type: integer
format: int64
minimum: 0
PaginatedResults_SbomStatus:
type: object
required:
- items
- total
properties:
items:
type: array
items:
type: object
required:
- total_packages
- total_licenses
properties:
total_licenses:
type: integer
minimum: 0
total_packages:
type: integer
minimum: 0
total:
type: integer
format: int64
minimum: 0
PaginatedResults_SbomSummary:
type: object
required:
Expand Down Expand Up @@ -3294,30 +3338,17 @@ components:
relationship:
$ref: '#/components/schemas/Relationship'
SbomStatus:
allOf:
- $ref: '#/components/schemas/VulnerabilityHead'
- type: object
required:
- average_severity
- average_score
- status
- packages
properties:
average_score:
type: number
format: double
average_severity:
$ref: '#/components/schemas/Severity'
context:
oneOf:
- type: 'null'
- $ref: '#/components/schemas/StatusContext'
packages:
type: array
items:
$ref: '#/components/schemas/SbomPackage'
status:
type: string
type: object
required:
- total_packages
- total_licenses
properties:
total_licenses:
type: integer
minimum: 0
total_packages:
type: integer
minimum: 0
SbomSummary:
allOf:
- $ref: '#/components/schemas/SbomHead'
Expand Down