diff --git a/modules/fundamental/src/purl/model/details/purl.rs b/modules/fundamental/src/purl/model/details/purl.rs index 60e5d8fb8..80e939b9f 100644 --- a/modules/fundamental/src/purl/model/details/purl.rs +++ b/modules/fundamental/src/purl/model/details/purl.rs @@ -12,11 +12,13 @@ use crate::{ vulnerability::model::VulnerabilityHead, }; use sea_orm::{ - ColumnTrait, ConnectionTrait, DbErr, EntityTrait, FromQueryResult, LoaderTrait, ModelTrait, - QueryFilter, QueryOrder, QueryResult, QuerySelect, QueryTrait, RelationTrait, Select, - SelectColumns, + ColumnTrait, Condition, ConnectionTrait, DbErr, EntityTrait, FromQueryResult, 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::{ @@ -28,8 +30,8 @@ use trustify_common::{ use trustify_entity::{ advisory, advisory_vulnerability_score, base_purl, cpe, license, organization, product, product_status, product_version, product_version_range, purl_status, qualified_purl, sbom, - sbom_license_expanded, sbom_node, sbom_node_purl_ref, sbom_package_license, status, - version_range, versioned_purl, vulnerability, + sbom_describing_cpe, sbom_license_expanded, sbom_node, sbom_node_purl_ref, + sbom_package_license, status, version_range, versioned_purl, vulnerability, }; use trustify_module_ingestor::common::{Deprecation, DeprecationForExt}; use utoipa::ToSchema; @@ -84,6 +86,66 @@ impl PurlDetails { .ok_or(Error::Data("underlying package missing".to_string()))? }; + let sbom_ids_for_purl = sbom_node_purl_ref::Entity::find() + .select_only() + .column(sbom_node_purl_ref::Column::SbomId) + .filter(sbom_node_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) @@ -93,6 +155,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 51beb5680..ca7e022ba 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 ) @@ -86,7 +85,6 @@ pub fn batch_severity_counts_sql() -> &'static str { JOIN cpe c ON c.vendor = sc.vendor AND c.product = sc.product AND c.version = split_part(sc.version, '.', 1) - AND (c.edition IS NULL OR c.edition = '*') ), sbom_allowed_cpes AS ( SELECT sbom_id, id AS cpe_id FROM sbom_cpes @@ -218,8 +216,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 5ab6acb01..007c5aa7d 100644 --- a/modules/fundamental/src/vulnerability/service/mod.rs +++ b/modules/fundamental/src/vulnerability/service/mod.rs @@ -575,6 +575,38 @@ GROUP BY AND base_purl.name = $2 AND base_purl.type = $3 AND version_matches($4, version_range.*) = TRUE + AND ( + purl_status.context_cpe_id IS NULL + OR purl_status.context_cpe_id IN ( + SELECT sdc.cpe_id + FROM sbom_describing_cpe sdc + JOIN sbom_node_purl_ref snpr ON snpr.sbom_id = sdc.sbom_id + JOIN qualified_purl qp ON snpr.qualified_purl_id = qp.id + JOIN versioned_purl vp ON qp.versioned_purl_id = vp.id + WHERE vp.base_purl_id = base_purl.id + + UNION + + SELECT c.id + FROM cpe c + JOIN cpe sc ON c.vendor = sc.vendor + AND c.product = sc.product + AND c.version = split_part(sc.version, '.', 1) + JOIN sbom_describing_cpe sdc ON sdc.cpe_id = sc.id + JOIN sbom_node_purl_ref snpr ON snpr.sbom_id = sdc.sbom_id + JOIN qualified_purl qp ON snpr.qualified_purl_id = qp.id + JOIN versioned_purl vp ON qp.versioned_purl_id = vp.id + WHERE vp.base_purl_id = base_purl.id + ) + OR NOT EXISTS ( + SELECT 1 + FROM sbom_describing_cpe sdc + JOIN sbom_node_purl_ref snpr ON snpr.sbom_id = sdc.sbom_id + JOIN qualified_purl qp ON snpr.qualified_purl_id = qp.id + JOIN versioned_purl vp ON qp.versioned_purl_id = vp.id + WHERE vp.base_purl_id = base_purl.id + ) + ) "#).as_str(), "r.data", ); @@ -612,6 +644,23 @@ GROUP BY } }; + let product_bp_condition = match &purl.namespace { + Some(namespace) => { + Statement::from_sql_and_values( + connection.get_database_backend(), + "bp.name = $1 AND bp.type = $2 AND bp.namespace = $3", + [purl.name.clone().into(), purl.ty.clone().into(), namespace.into()], + ).to_string() + } + None => { + Statement::from_sql_and_values( + connection.get_database_backend(), + "bp.name = $1 AND bp.type = $2 AND bp.namespace IS NULL", + [purl.name.clone().into(), purl.ty.clone().into()], + ).to_string() + } + }; + let product_status_sql = Self::build_vulnerabilities_query_string( r#" 'advisory_id', product_status.advisory_id, 'context_cpe', cpe.id @@ -629,6 +678,41 @@ GROUP BY "#, format!(r#" {package_condition} AND product_status.package IS NOT NULL + AND ( + product_status.context_cpe_id IS NULL + OR product_status.context_cpe_id IN ( + SELECT sdc.cpe_id + FROM sbom_describing_cpe sdc + JOIN sbom_node_purl_ref snpr ON snpr.sbom_id = sdc.sbom_id + JOIN qualified_purl qp ON snpr.qualified_purl_id = qp.id + JOIN versioned_purl vp ON qp.versioned_purl_id = vp.id + JOIN base_purl bp ON vp.base_purl_id = bp.id + WHERE {product_bp_condition} + + UNION + + SELECT c.id + FROM cpe c + JOIN cpe sc ON c.vendor = sc.vendor + AND c.product = sc.product + AND c.version = split_part(sc.version, '.', 1) + JOIN sbom_describing_cpe sdc ON sdc.cpe_id = sc.id + JOIN sbom_node_purl_ref snpr ON snpr.sbom_id = sdc.sbom_id + JOIN qualified_purl qp ON snpr.qualified_purl_id = qp.id + JOIN versioned_purl vp ON qp.versioned_purl_id = vp.id + JOIN base_purl bp ON vp.base_purl_id = bp.id + WHERE {product_bp_condition} + ) + OR NOT EXISTS ( + SELECT 1 + FROM sbom_describing_cpe sdc + JOIN sbom_node_purl_ref snpr ON snpr.sbom_id = sdc.sbom_id + JOIN qualified_purl qp ON snpr.qualified_purl_id = qp.id + JOIN versioned_purl vp ON qp.versioned_purl_id = vp.id + JOIN base_purl bp ON vp.base_purl_id = bp.id + WHERE {product_bp_condition} + ) + ) "#).as_str(), r#" jsonb_set(r.data, '{product_ids}', COALESCE(