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..c63482946 100644 --- a/modules/fundamental/src/purl/model/details/purl.rs +++ b/modules/fundamental/src/purl/model/details/purl.rs @@ -103,6 +103,7 @@ impl PurlDetails { qualified_package.id, &package.name, package.namespace.as_deref(), + &package_version.version, ) .await?; @@ -152,6 +153,7 @@ async fn get_product_statuses_for_purl( qualified_package_id: Uuid, purl_name: &str, namespace_name: Option<&str>, + version: &str, ) -> Result, Error> { // Subquery to get all SBOM IDs for the given purl let sbom_ids_query = sbom::Entity::find() @@ -187,6 +189,11 @@ async fn get_product_statuses_for_purl( Expr::col(product_status::Column::Package).eq(format!("{ns}/{purl_name}")) }), )) + .filter(SimpleExpr::FunctionCall( + Func::cust(VersionMatches) + .arg(Expr::value(version.to_string())) + .arg(Expr::col((version_range::Entity, Asterisk))), + )) .distinct_on([ (product_status::Entity, product_status::Column::ContextCpeId), (product_status::Entity, product_status::Column::StatusId), diff --git a/modules/fundamental/src/purl/service/test.rs b/modules/fundamental/src/purl/service/test.rs index e54130ea3..130f719b5 100644 --- a/modules/fundamental/src/purl/service/test.rs +++ b/modules/fundamental/src/purl/service/test.rs @@ -7,7 +7,7 @@ use trustify_common::{ model::Paginated, purl::Purl, }; -use trustify_test_context::TrustifyContext; +use trustify_test_context::{Dataset, TrustifyContext}; async fn ingest_extra_packages(ctx: &TrustifyContext) -> Result<(), anyhow::Error> { ctx.graph @@ -933,3 +933,48 @@ async fn versioned_base_purl_by_purl(ctx: &TrustifyContext) -> Result<(), anyhow Ok(()) } + +/// Proves that `version_matches` filtering works on the **product_status** path. +/// +/// DS3 contains a CSAF advisory for CVE-2024-28834 affecting gnutls on RHEL 8 +/// AppStream, and an ubi8 SPDX SBOM whose product package carries the same CPE. +/// The shared CPE bridges the product_status join chain: +/// product_status → context_cpe → product (via cpe_key) → product_version → SBOM +/// +/// gnutls@3.6.16-6.el8_7 (from the ubi8 SBOM) falls within the advisory's +/// affected version range, so product_status entries with CPE context must appear. +#[test_context(TrustifyContext)] +#[test(actix_web::test)] +async fn product_status_version_filtering(ctx: &TrustifyContext) -> Result<(), anyhow::Error> { + let service = PurlService::new(); + ctx.ingest_dataset(Dataset::DS3).await?; + + // gnutls version from the ubi8 SBOM — affected (below fix 3.6.16-8.el8_9.3) + let purl = Purl::from_str("pkg:rpm/redhat/gnutls@3.6.16-6.el8_7?arch=x86_64")?; + let details = service + .purl_by_purl(&purl, Default::default(), &ctx.db) + .await? + .expect("gnutls purl must exist after DS3 ingestion"); + + // Product_status entries carry StatusContext::Cpe (not ::Purl). + let cpe_statuses: Vec<_> = details + .advisories + .iter() + .flat_map(|a| &a.status) + .filter(|s| matches!(&s.context, Some(StatusContext::Cpe(_)))) + .collect(); + + assert!( + !cpe_statuses.is_empty(), + "affected gnutls version must have product_status entries with CPE context" + ); + + assert!( + cpe_statuses + .iter() + .any(|s| s.vulnerability.identifier == "CVE-2024-28834"), + "product_statuses must include CVE-2024-28834" + ); + + Ok(()) +}