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
28 changes: 28 additions & 0 deletions modules/fundamental/src/common/mod.rs
Original file line number Diff line number Diff line change
@@ -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<LicenseBasicInfo> 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),
}
}
}
37 changes: 36 additions & 1 deletion modules/fundamental/src/common/service.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand Down Expand Up @@ -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<String, String>,
licenses_ref_mapping: &mut Vec<LicenseRefMapping>,
) {
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::*;
Expand Down
2 changes: 1 addition & 1 deletion modules/fundamental/src/license/service/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::sbom::model::LicenseRefMapping;
use crate::{
Error,
common::LicenseRefMapping,
license::model::{
SpdxLicenseDetails, SpdxLicenseSummary,
sbom_license::{
Expand Down
71 changes: 69 additions & 2 deletions modules/fundamental/src/purl/endpoints/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ 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;
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;

Expand Down Expand Up @@ -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<PurlSummary> = 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/[email protected]?arch=ppc64le",
"version": {
"uuid": "57664d22-7f7f-56a0-9c38-9b0dc203b322",
"purl": "pkg:rpm/redhat/[email protected]",
"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(())
}
59 changes: 54 additions & 5 deletions modules/fundamental/src/purl/model/details/purl.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -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;
Expand All @@ -36,7 +40,15 @@ pub struct PurlDetails {
pub version: VersionedPurlHead,
pub base: BasePurlHead,
pub advisories: Vec<PurlAdvisory>,
pub licenses: Vec<PurlLicenseSummary>,
pub licenses: Vec<LicenseInfo>,
pub licenses_ref_mapping: Vec<LicenseRefMapping>,
}

#[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 {
Expand Down Expand Up @@ -95,12 +107,49 @@ impl PurlDetails {
)
.await?;

let purl_license_results: Vec<PurlLicenseResult> = 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,
})
}
}
Expand Down
3 changes: 1 addition & 2 deletions modules/fundamental/src/sbom/endpoints/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down
30 changes: 4 additions & 26 deletions modules/fundamental/src/sbom/model/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -138,27 +137,6 @@ pub struct SbomPackage {
pub licenses_ref_mapping: Vec<LicenseRefMapping>,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, ToSchema)]
pub struct LicenseInfo {
pub license_name: String,
pub license_type: LicenseCategory,
}

impl From<LicenseBasicInfo> 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),
Expand Down
11 changes: 6 additions & 5 deletions modules/fundamental/src/sbom/service/sbom.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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<C: ConnectionTrait>(
#[instrument(skip(connection), err(level=tracing::Level::INFO))]
pub async fn get_licensing_infos<C: ConnectionTrait>(
connection: &C,
sbom_id: Uuid,
) -> Result<BTreeMap<String, String>, Error> {
Expand Down Expand Up @@ -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)]
Expand Down
2 changes: 1 addition & 1 deletion modules/fundamental/tests/sbom/spdx/perf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
Loading
Loading