diff --git a/common/src/config.rs b/common/src/config.rs index 7d1bc9dcf..8ac234292 100644 --- a/common/src/config.rs +++ b/common/src/config.rs @@ -143,12 +143,12 @@ impl Database { format!( "postgres://{username}:{password}@{host}:{port}/{db_name}?sslmode={sslmode}", - username = &self.username, - password = &self.password.0, - host = &self.host, + username = self.username, + password = self.password.0, + host = self.host, port = self.port, - db_name = &self.name, - sslmode = &self.sslmode, + db_name = self.name, + sslmode = self.sslmode, ) } } diff --git a/modules/fundamental/src/purl/model/details/purl.rs b/modules/fundamental/src/purl/model/details/purl.rs index 6ac0bf8ec..6b72762c9 100644 --- a/modules/fundamental/src/purl/model/details/purl.rs +++ b/modules/fundamental/src/purl/model/details/purl.rs @@ -7,11 +7,13 @@ use crate::{ vulnerability::model::VulnerabilityHead, }; use sea_orm::{ - ColumnTrait, ConnectionTrait, DbErr, EntityTrait, FromQueryResult, Iterable, LoaderTrait, - ModelTrait, QueryFilter, QueryOrder, QueryResult, QuerySelect, QueryTrait, RelationTrait, - Select, SelectColumns, + ColumnTrait, Condition, ConnectionTrait, DbErr, EntityTrait, FromQueryResult, Iterable, + LoaderTrait, ModelTrait, QueryFilter, QueryOrder, QueryResult, QuerySelect, QueryTrait, + RelationTrait, Select, SelectColumns, +}; +use sea_query::{ + Alias, Asterisk, ColumnRef, Expr, Func, IntoIden, JoinType, SimpleExpr, UnionType, }; -use sea_query::{Asterisk, ColumnRef, Expr, Func, IntoIden, JoinType, SimpleExpr}; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, hash_map::Entry}; use trustify_common::{ @@ -23,9 +25,9 @@ use trustify_common::{ 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_license, sbom_package_purl_ref, status, version_range, versioned_purl, - vulnerability, + product_version, product_version_range, purl_status, qualified_purl, sbom, sbom_describing_cpe, + sbom_package, sbom_package_license, sbom_package_purl_ref, status, version_range, + versioned_purl, vulnerability, }; use trustify_module_ingestor::common::{Deprecation, DeprecationForExt}; use utoipa::ToSchema; @@ -80,6 +82,66 @@ impl PurlDetails { .ok_or(Error::Data("underlying package missing".to_string()))? }; + let sbom_ids_for_purl = sbom_package_purl_ref::Entity::find() + .select_only() + .column(sbom_package_purl_ref::Column::SbomId) + .filter(sbom_package_purl_ref::Column::QualifiedPurlId.eq(qualified_package.id)) + .into_query(); + + let mut allowed_cpe_ids = sbom_describing_cpe::Entity::find() + .select_only() + .column(sbom_describing_cpe::Column::CpeId) + .filter(sbom_describing_cpe::Column::SbomId.in_subquery(sbom_ids_for_purl.clone())) + .into_query(); + + let c = Alias::new("c"); + let sc = Alias::new("sc"); + let sdc = Alias::new("sdc"); + let generalized_cpe_ids = sea_query::Query::select() + .expr(Expr::col((c.clone(), cpe::Column::Id))) + .from_as(cpe::Entity, c.clone()) + .join_as( + JoinType::InnerJoin, + cpe::Entity, + sc.clone(), + Condition::all() + .add( + Expr::col((c.clone(), cpe::Column::Vendor)) + .equals((sc.clone(), cpe::Column::Vendor)), + ) + .add( + Expr::col((c.clone(), cpe::Column::Product)) + .equals((sc.clone(), cpe::Column::Product)), + ) + .add( + Expr::col((c.clone(), cpe::Column::Version)).eq(SimpleExpr::FunctionCall( + Func::cust(Alias::new("split_part")) + .arg(Expr::col((sc.clone(), cpe::Column::Version))) + .arg(Expr::value(".")) + .arg(Expr::value(1i32)), + )), + ), + ) + .join_as( + JoinType::InnerJoin, + sbom_describing_cpe::Entity, + sdc.clone(), + Expr::col((sdc.clone(), sbom_describing_cpe::Column::CpeId)) + .equals((sc.clone(), cpe::Column::Id)), + ) + .and_where( + Expr::col((sdc.clone(), sbom_describing_cpe::Column::SbomId)) + .in_subquery(sbom_ids_for_purl.clone()), + ) + .to_owned(); + allowed_cpe_ids.union(UnionType::Distinct, generalized_cpe_ids); + + let sbom_has_cpes = sea_query::Query::select() + .expr(Expr::value(1i32)) + .from(sbom_describing_cpe::Entity) + .and_where(sbom_describing_cpe::Column::SbomId.in_subquery(sbom_ids_for_purl)) + .to_owned(); + let purl_statuses = purl_status::Entity::find() .filter(purl_status::Column::BasePurlId.eq(package.id)) .left_join(version_range::Entity) @@ -90,6 +152,12 @@ impl PurlDetails { .arg(Expr::value(package_version.version.clone())) .arg(Expr::col((version_range::Entity, Asterisk))), )) + .filter( + Condition::any() + .add(purl_status::Column::ContextCpeId.is_null()) + .add(purl_status::Column::ContextCpeId.in_subquery(allowed_cpe_ids)) + .add(Expr::exists(sbom_has_cpes).not()), + ) .distinct_on([ColumnRef::TableColumn( purl_status::Entity.into_iden(), purl_status::Column::Id.into_iden(), diff --git a/modules/fundamental/src/sbom/model/raw_sql.rs b/modules/fundamental/src/sbom/model/raw_sql.rs index 718c370ee..966b582dd 100644 --- a/modules/fundamental/src/sbom/model/raw_sql.rs +++ b/modules/fundamental/src/sbom/model/raw_sql.rs @@ -1,8 +1,8 @@ /// This constant is a SQL subquery that filters the context_cpe_id /// based on the given sbom_id. It reads from the materialized /// sbom_describing_cpe table instead of computing the join at query time. -/// The generalized CPE logic expands matches to include CPEs without edition -/// and with major-version-only matching. +/// The generalized CPE logic expands matches to include all CPEs sharing +/// the same vendor, product, and major version. pub const CONTEXT_CPE_FILTER_SQL: &str = r#" ( context_cpe_id IS NULL OR @@ -16,8 +16,7 @@ pub const CONTEXT_CPE_FILTER_SQL: &str = r#" generalized_cpes AS ( SELECT * FROM cpe - WHERE (edition IS NULL OR edition = '*') - AND (vendor, product, version) IN ( + WHERE (vendor, product, version) IN ( SELECT vendor, product, split_part(version, '.', 1) FROM filtered_cpes ) @@ -47,8 +46,7 @@ pub fn product_advisory_info_sql() -> String { generalized_cpes AS ( SELECT * FROM cpe - WHERE (edition IS NULL OR edition = '*') - AND (vendor, product, version) IN ( + WHERE (vendor, product, version) IN ( SELECT vendor, product, split_part(version, '.', 1) FROM filtered_cpes ) diff --git a/modules/fundamental/src/vulnerability/service/mod.rs b/modules/fundamental/src/vulnerability/service/mod.rs index 6b47d4c88..57e2ec1db 100644 --- a/modules/fundamental/src/vulnerability/service/mod.rs +++ b/modules/fundamental/src/vulnerability/service/mod.rs @@ -285,7 +285,8 @@ SELECT jsonb_agg( jsonb_build_object( 'status', status.slug, - 'id', purl_status.advisory_id + 'id', purl_status.advisory_id, + 'context_cpe', purl_status.context_cpe_id ) ) AS advisories FROM base_purl diff --git a/modules/fundamental/tests/dataset.rs b/modules/fundamental/tests/dataset.rs index 58fc8110d..90a375c66 100644 --- a/modules/fundamental/tests/dataset.rs +++ b/modules/fundamental/tests/dataset.rs @@ -94,7 +94,7 @@ async fn ingest(ctx: TrustifyContext) -> anyhow::Result<()> { assert!(ubi_details.is_some()); let ubi_details = ubi_details.unwrap(); let ubi_advisories = ubi_details.advisories; - assert_eq!(ubi_advisories.len(), 1); + assert_eq!(ubi_advisories.len(), 3); assert!( ubi_advisories .iter()