diff --git a/modules/fundamental/src/common/mod.rs b/modules/fundamental/src/common/mod.rs index 1f278a4d5..8efa22233 100644 --- a/modules/fundamental/src/common/mod.rs +++ b/modules/fundamental/src/common/mod.rs @@ -1 +1,29 @@ +use crate::sbom::service::sbom::LicenseBasicInfo; +use sea_orm::FromQueryResult; +use sea_query::FromValueTuple; +use serde::{Deserialize, Serialize}; +use trustify_entity::sbom_package_license::LicenseCategory; +use utoipa::ToSchema; + pub mod service; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, ToSchema, FromQueryResult)] +pub struct LicenseRefMapping { + pub license_id: String, + pub license_name: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, ToSchema)] +pub struct LicenseInfo { + pub license_name: String, + pub license_type: LicenseCategory, +} + +impl From for LicenseInfo { + fn from(license_basic_info: LicenseBasicInfo) -> Self { + LicenseInfo { + license_name: license_basic_info.license_name, + license_type: LicenseCategory::from_value_tuple(license_basic_info.license_type), + } + } +} diff --git a/modules/fundamental/src/common/service.rs b/modules/fundamental/src/common/service.rs index 91a6016d6..43453174d 100644 --- a/modules/fundamental/src/common/service.rs +++ b/modules/fundamental/src/common/service.rs @@ -1,5 +1,7 @@ -use crate::{Error, source_document::model::SourceDocument}; +use crate::{Error, common::LicenseRefMapping, source_document::model::SourceDocument}; use sea_orm::{ConnectionTrait, DbBackend, FromQueryResult, PaginatorTrait, Statement}; +use spdx_expression; +use std::collections::BTreeMap; use trustify_module_storage::service::{StorageBackend, StorageKey, dispatch::DispatchBackend}; #[derive(Copy, Clone, Eq, PartialEq)] @@ -85,6 +87,39 @@ impl DocumentDelete for &DispatchBackend { } } +/// Extract LicenseRef mappings from SPDX license expressions +/// +/// This function parses SPDX license expressions and extracts LicenseRef mappings, +/// which are then added to the provided `licenses_ref_mapping` vector. +/// +/// # Arguments +/// * `license_name` - The SPDX license expression to parse +/// * `licensing_infos` - A BTreeMap containing license ID to license name mappings +/// * `licenses_ref_mapping` - A mutable vector where LicenseRef mappings will be added +pub fn extract_license_ref_mappings( + license_name: &str, + licensing_infos: &BTreeMap, + licenses_ref_mapping: &mut Vec, +) { + if let Ok(parsed) = spdx_expression::SpdxExpression::parse(license_name) { + parsed + .licenses() + .into_iter() + .filter(|license| license.license_ref) + .for_each(|license| { + let license_id = license.to_string(); + let license_name = licensing_infos + .get(&license_id) + .cloned() + .unwrap_or_default(); + licenses_ref_mapping.push(LicenseRefMapping { + license_id, + license_name, + }); + }); + } +} + #[cfg(test)] mod test { use super::*; diff --git a/modules/fundamental/src/license/service/mod.rs b/modules/fundamental/src/license/service/mod.rs index db814b4c0..ecc94825e 100644 --- a/modules/fundamental/src/license/service/mod.rs +++ b/modules/fundamental/src/license/service/mod.rs @@ -1,6 +1,6 @@ -use crate::sbom::model::LicenseRefMapping; use crate::{ Error, + common::LicenseRefMapping, license::model::{ SpdxLicenseDetails, SpdxLicenseSummary, sbom_license::{ diff --git a/modules/fundamental/src/purl/endpoints/test.rs b/modules/fundamental/src/purl/endpoints/test.rs index 50545b94a..694e96638 100644 --- a/modules/fundamental/src/purl/endpoints/test.rs +++ b/modules/fundamental/src/purl/endpoints/test.rs @@ -3,7 +3,7 @@ use crate::purl::model::summary::base_purl::BasePurlSummary; use crate::purl::model::summary::purl::PurlSummary; use crate::test::caller; use actix_web::test::TestRequest; -use serde_json::Value; +use serde_json::{Value, json}; use std::str::FromStr; use test_context::test_context; use test_log::test; @@ -11,7 +11,7 @@ use trustify_common::db::Database; use trustify_common::model::PaginatedResults; use trustify_common::purl::Purl; use trustify_module_ingestor::graph::Graph; -use trustify_test_context::{TrustifyContext, call::CallService}; +use trustify_test_context::{TrustifyContext, call::CallService, subset::ContainsSubset}; use urlencoding::encode; use uuid::Uuid; @@ -243,3 +243,70 @@ async fn purl_filter_queries(ctx: &TrustifyContext) -> Result<(), anyhow::Error> Ok(()) } + +#[test_context(TrustifyContext)] +#[test(actix_web::test)] +async fn test_purl_license_details(ctx: &TrustifyContext) -> Result<(), anyhow::Error> { + let app = caller(ctx).await?; + + ctx.ingest_documents(["spdx/OCP-TOOLS-4.11-RHEL-8.json"]) + .await?; + + let uri = "/api/v2/purl?q=graphite2"; + let request = TestRequest::get().uri(uri).to_request(); + let response: PaginatedResults = app.call_and_read_body_json(request).await; + + assert_eq!(1, response.items.len()); + + let uuid = response.items[0].head.uuid; + + let uri = format!("/api/v2/purl/{uuid}"); + + let request = TestRequest::get().uri(&uri).to_request(); + let response: Value = app.call_and_read_body_json(request).await; + + let expected_result = json!({ + "uuid": "7ff60cd2-d779-586e-b829-cc6d51750450", + "purl": "pkg:rpm/redhat/graphite2@1.3.10-10.el8?arch=ppc64le", + "version": { + "uuid": "57664d22-7f7f-56a0-9c38-9b0dc203b322", + "purl": "pkg:rpm/redhat/graphite2@1.3.10-10.el8", + "version": "1.3.10-10.el8" + }, + "base": { + "uuid": "ba5eb886-34f6-5830-8902-a6182a6a8d7d", + "purl": "pkg:rpm/redhat/graphite2" + }, + "advisories": [], + "licenses": [ + { + "license_name": "(LicenseRef-8 OR LicenseRef-0 OR LicenseRef-MPL) AND (LicenseRef-Netscape OR LicenseRef-0 OR LicenseRef-8)", + "license_type": "declared" + }, + { + "license_name": "NOASSERTION", + "license_type": "concluded" + } + ], + "licenses_ref_mapping": [ + { + "license_id": "LicenseRef-Netscape", + "license_name": "Netscape" + }, + { + "license_id": "LicenseRef-MPL", + "license_name": "MPL" + }, + { + "license_id": "LicenseRef-8", + "license_name": "LGPLv2+" + }, + { + "license_id": "LicenseRef-0", + "license_name": "GPLv2+" + } + ] + }); + assert!(expected_result.contains_subset(response.clone())); + Ok(()) +} diff --git a/modules/fundamental/src/purl/model/details/purl.rs b/modules/fundamental/src/purl/model/details/purl.rs index dc2e0d80f..127e2e7f1 100644 --- a/modules/fundamental/src/purl/model/details/purl.rs +++ b/modules/fundamental/src/purl/model/details/purl.rs @@ -1,14 +1,17 @@ +use crate::common::LicenseInfo; +use crate::sbom::service::sbom::LicenseBasicInfo; use crate::{ Error, advisory::model::AdvisoryHead, + common::{LicenseRefMapping, service::extract_license_ref_mappings}, purl::model::{BasePurlHead, PurlHead, VersionedPurlHead}, - sbom::model::SbomHead, + sbom::{model::SbomHead, service::SbomService}, vulnerability::model::VulnerabilityHead, }; use sea_orm::{ ColumnTrait, ConnectionTrait, DbErr, EntityTrait, FromQueryResult, Iterable, LoaderTrait, ModelTrait, QueryFilter, QueryOrder, QueryResult, QuerySelect, QueryTrait, RelationTrait, - Select, + Select, SelectColumns, }; use sea_query::{Asterisk, ColumnRef, Expr, Func, IntoIden, JoinType, SimpleExpr}; use serde::{Deserialize, Serialize}; @@ -23,7 +26,8 @@ use trustify_cvss::cvss3::{Cvss3Base, score::Score, severity::Severity}; use trustify_entity::{ advisory, base_purl, cpe, cvss3, license, organization, product, product_status, product_version, product_version_range, purl_status, qualified_purl, sbom, sbom_package, - sbom_package_purl_ref, status, version_range, versioned_purl, vulnerability, + sbom_package_license, sbom_package_purl_ref, status, version_range, versioned_purl, + vulnerability, }; use trustify_module_ingestor::common::{Deprecation, DeprecationForExt}; use utoipa::ToSchema; @@ -36,7 +40,15 @@ pub struct PurlDetails { pub version: VersionedPurlHead, pub base: BasePurlHead, pub advisories: Vec, - pub licenses: Vec, + pub licenses: Vec, + pub licenses_ref_mapping: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, ToSchema, FromQueryResult)] +pub struct PurlLicenseResult { + pub sbom_id: Uuid, + pub license_name: String, + pub license_type: i32, } impl PurlDetails { @@ -95,12 +107,49 @@ impl PurlDetails { ) .await?; + let purl_license_results: Vec = sbom_package_purl_ref::Entity::find() + .distinct() + .select_only() + .select_column(sbom_package::Column::SbomId) + .select_column_as(license::Column::Text, "license_name") + .select_column(sbom_package_license::Column::LicenseType) + .filter(sbom_package_purl_ref::Column::QualifiedPurlId.eq(qualified_package.id)) + .join( + JoinType::Join, + sbom_package_purl_ref::Relation::Package.def(), + ) + .join(JoinType::Join, sbom_package::Relation::PackageLicense.def()) + .join( + JoinType::Join, + sbom_package_license::Relation::License.def(), + ) + .into_model() + .all(tx) + .await?; + + let mut purl_license_info = Vec::new(); + let mut license_ref_mapping = Vec::new(); + + for plr in purl_license_results { + let licensing_infos = SbomService::get_licensing_infos(tx, plr.sbom_id).await?; + extract_license_ref_mappings( + plr.license_name.as_str(), + &licensing_infos, + &mut license_ref_mapping, + ); + purl_license_info.push(LicenseInfo::from(LicenseBasicInfo { + license_name: plr.license_name, + license_type: plr.license_type, + })); + } + Ok(PurlDetails { head: PurlHead::from_entity(&package, &package_version, qualified_package), version: VersionedPurlHead::from_entity(&package, &package_version), base: BasePurlHead::from_entity(&package), advisories: PurlAdvisory::from_entities(purl_statuses, product_statuses, tx).await?, - licenses: vec![], // Leave it empty for now and wait to add relevant content later. + licenses: purl_license_info, + licenses_ref_mapping: license_ref_mapping, }) } } diff --git a/modules/fundamental/src/sbom/endpoints/mod.rs b/modules/fundamental/src/sbom/endpoints/mod.rs index 3fb3349ab..fc00c7b41 100644 --- a/modules/fundamental/src/sbom/endpoints/mod.rs +++ b/modules/fundamental/src/sbom/endpoints/mod.rs @@ -6,10 +6,9 @@ mod test; pub use query::*; -use crate::sbom::model::LicenseRefMapping; use crate::{ Error, - common::service::delete_doc, + common::{LicenseRefMapping, service::delete_doc}, license::{ get_sanitize_filename, service::{LicenseService, license_export::LicenseExporter}, diff --git a/modules/fundamental/src/sbom/model/mod.rs b/modules/fundamental/src/sbom/model/mod.rs index 8bdffbf5a..21327cd7d 100644 --- a/modules/fundamental/src/sbom/model/mod.rs +++ b/modules/fundamental/src/sbom/model/mod.rs @@ -2,19 +2,18 @@ pub mod details; pub mod raw_sql; use super::service::SbomService; +use crate::common::LicenseInfo; use crate::{ - Error, purl::model::summary::purl::PurlSummary, sbom::service::sbom::LicenseBasicInfo, + Error, common::LicenseRefMapping, purl::model::summary::purl::PurlSummary, source_document::model::SourceDocument, }; -use sea_orm::{ConnectionTrait, FromQueryResult, ModelTrait, PaginatorTrait, prelude::Uuid}; -use sea_query::FromValueTuple; +use sea_orm::{ConnectionTrait, ModelTrait, PaginatorTrait, prelude::Uuid}; use serde::{Deserialize, Serialize}; use time::OffsetDateTime; use tracing::instrument; use trustify_common::{cpe::Cpe, model::Paginated, purl::Purl}; use trustify_entity::{ - labels::Labels, relationship::Relationship, sbom, sbom_node, sbom_package, - sbom_package_license::LicenseCategory, source_document, + labels::Labels, relationship::Relationship, sbom, sbom_node, sbom_package, source_document, }; use utoipa::ToSchema; @@ -138,27 +137,6 @@ pub struct SbomPackage { pub licenses_ref_mapping: Vec, } -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, ToSchema)] -pub struct LicenseInfo { - pub license_name: String, - pub license_type: LicenseCategory, -} - -impl From for LicenseInfo { - fn from(license_basic_info: LicenseBasicInfo) -> Self { - LicenseInfo { - license_name: license_basic_info.license_name, - license_type: LicenseCategory::from_value_tuple(license_basic_info.license_type), - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, ToSchema, FromQueryResult)] -pub struct LicenseRefMapping { - pub license_id: String, - pub license_name: String, -} - #[derive(Copy, Clone, Eq, PartialEq, Debug)] pub enum SbomPackageReference<'a> { Internal(&'a str), diff --git a/modules/fundamental/src/sbom/service/sbom.rs b/modules/fundamental/src/sbom/service/sbom.rs index 28dd6ac5a..35080fa99 100644 --- a/modules/fundamental/src/sbom/service/sbom.rs +++ b/modules/fundamental/src/sbom/service/sbom.rs @@ -1,7 +1,7 @@ use super::SbomService; -use crate::sbom::model::LicenseRefMapping; use crate::{ Error, + common::LicenseRefMapping, sbom::model::{ SbomExternalPackageReference, SbomNodeReference, SbomPackage, SbomPackageRelation, SbomSummary, Which, details::SbomDetails, @@ -235,7 +235,8 @@ impl SbomService { } /// Get all the tuples License ID, License Name from the licensing_infos table for a single SBOM - async fn get_licensing_infos( + #[instrument(skip(connection), err(level=tracing::Level::INFO))] + pub async fn get_licensing_infos( connection: &C, sbom_id: Uuid, ) -> Result, Error> { @@ -624,9 +625,9 @@ where JoinType::LeftJoin, sbom_package::Relation::PackageLicense.def(), ).join( - JoinType::LeftJoin, - sbom_package_license::Relation::License.def(), - ) + JoinType::LeftJoin, + sbom_package_license::Relation::License.def(), + ) } #[derive(FromQueryResult)] diff --git a/modules/fundamental/tests/sbom/spdx/perf.rs b/modules/fundamental/tests/sbom/spdx/perf.rs index 55b6c2d9b..0d297a1e4 100644 --- a/modules/fundamental/tests/sbom/spdx/perf.rs +++ b/modules/fundamental/tests/sbom/spdx/perf.rs @@ -4,7 +4,7 @@ use test_log::test; use tracing::instrument; use trustify_common::model::Paginated; use trustify_entity::sbom_package_license::LicenseCategory; -use trustify_module_fundamental::sbom::model::{LicenseInfo, SbomPackage}; +use trustify_module_fundamental::{common::LicenseInfo, sbom::model::SbomPackage}; use trustify_test_context::TrustifyContext; #[test_context(TrustifyContext)] diff --git a/openapi.yaml b/openapi.yaml index 3c709db04..43b11fb7c 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -4665,6 +4665,7 @@ components: - base - advisories - licenses + - licenses_ref_mapping properties: advisories: type: array @@ -4675,7 +4676,11 @@ components: licenses: type: array items: - $ref: '#/components/schemas/PurlLicenseSummary' + $ref: '#/components/schemas/LicenseInfo' + licenses_ref_mapping: + type: array + items: + $ref: '#/components/schemas/LicenseRefMapping' version: $ref: '#/components/schemas/VersionedPurlHead' PurlHead: @@ -4691,18 +4696,6 @@ components: type: string format: uuid description: The ID of the qualified PURL - PurlLicenseSummary: - type: object - required: - - sbom - - licenses - properties: - licenses: - type: array - items: - type: string - sbom: - $ref: '#/components/schemas/SbomHead' PurlStatus: type: object required: