Skip to content
Open
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
10 changes: 5 additions & 5 deletions common/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
}
}
Expand Down
7 changes: 7 additions & 0 deletions modules/fundamental/src/purl/model/details/purl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ impl PurlDetails {
qualified_package.id,
&package.name,
package.namespace.as_deref(),
&package_version.version,
)
.await?;

Expand Down Expand Up @@ -152,6 +153,7 @@ async fn get_product_statuses_for_purl<C: ConnectionTrait>(
qualified_package_id: Uuid,
purl_name: &str,
namespace_name: Option<&str>,
version: &str,
) -> Result<Vec<ProductStatusCatcher>, Error> {
// Subquery to get all SBOM IDs for the given purl
let sbom_ids_query = sbom::Entity::find()
Expand Down Expand Up @@ -187,6 +189,11 @@ async fn get_product_statuses_for_purl<C: ConnectionTrait>(
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),
Expand Down
47 changes: 46 additions & 1 deletion modules/fundamental/src/purl/service/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -933,3 +933,48 @@ async fn versioned_base_purl_by_purl(ctx: &TrustifyContext) -> Result<(), anyhow

Ok(())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (testing): Add a complementary negative test to ensure non-affected or non-RPM PURLs do not get product_status entries

This test verifies that affected RPM PURLs produce product_status entries with a version_range, but it doesn’t cover the false positives described in the PR (e.g., non-RPM packages or already-fixed versions still showing as affected).

To better protect against regressions, please add at least one negative-path test, such as:

  • A PURL for the same package at a version above the fixed boundary (known non-affected in DS3) and assert that no product_status entries are returned.
  • A non-RPM PURL (or a PURL from another ecosystem sharing the same CPE) and assert that RPM-specific version ranges do not produce product_status matches.

These tests will directly validate that VersionMatches blocks the false positives mentioned in the PR and tighten regression coverage.

}

/// 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(())
}