Skip to content
Merged
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
103 changes: 99 additions & 4 deletions modules/fundamental/src/purl/model/details/purl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ use sea_orm::{
ModelTrait, QueryFilter, QueryOrder, QueryResult, QuerySelect, QueryTrait, RelationTrait,
Select, SelectColumns,
};
use sea_query::{Asterisk, ColumnRef, Expr, Func, IntoIden, JoinType, SimpleExpr};
use sea_query::{
Alias, Asterisk, ColumnRef, Condition, Expr, Func, IntoIden, JoinType, SimpleExpr, UnionType,
};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, hash_map::Entry};
use trustify_common::{
Expand All @@ -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;
Expand Down Expand Up @@ -147,6 +149,83 @@ impl PurlDetails {
}
}

/// Build the two subqueries needed for CPE context filtering:
/// 1. `allowed_cpe_ids` — CPE IDs from the describing CPEs of SBOMs
/// containing the given PURL, plus generalized (major-version-only)
/// variants.
/// 2. `sbom_has_cpes` — EXISTS subquery that checks whether any SBOM
/// containing the PURL has describing CPEs at all.
///
/// The returned pair is used in a three-way filter:
/// context_cpe_id IS NULL
/// OR context_cpe_id IN (allowed_cpe_ids)
/// OR NOT EXISTS (sbom_has_cpes)
fn cpe_context_subqueries(
qualified_purl_id: Uuid,
) -> (sea_query::SelectStatement, sea_query::SelectStatement) {
let sbom_ids = sbom_package_purl_ref::Entity::find()
.select_only()
.column(sbom_package_purl_ref::Column::SbomId)
.filter(sbom_package_purl_ref::Column::QualifiedPurlId.eq(qualified_purl_id))
.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.clone()),
)
.to_owned();

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.clone()))
.into_query();
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))
.to_owned();

(allowed_cpe_ids, sbom_has_cpes)
}

async fn get_product_statuses_for_purl<C: ConnectionTrait>(
tx: &C,
qualified_package_id: Uuid,
Expand All @@ -162,6 +241,11 @@ async fn get_product_statuses_for_purl<C: ConnectionTrait>(
.column(sbom::Column::SbomId)
.into_query();

// CPE context filtering — only return product statuses whose
// context CPE matches the describing CPEs of SBOMs containing this
// PURL. Prevents wrong-product false positives (TC-5630).
let (allowed_cpe_ids, sbom_has_cpes) = cpe_context_subqueries(qualified_package_id);

// Main query to get product statuses
let product_statuses_query = product_status::Entity::find()
.join(JoinType::Join, product_status::Relation::ContextCpe.def())
Expand All @@ -182,6 +266,17 @@ async fn get_product_statuses_for_purl<C: ConnectionTrait>(
product_status::Relation::Vulnerability.def(),
)
.filter(product_version::Column::SbomId.in_subquery(sbom_ids_query))
// NOTE: no version_matches filter here. The version_range in
// product_status refers to the *product* version (e.g. Quarkus
// 2.x), not the *package* version (e.g. keycloak-core 18.0.6).
// Product applicability is validated through the product_version
// → SBOM join chain above.
.filter(
Condition::any()
.add(product_status::Column::ContextCpeId.is_null())
.add(product_status::Column::ContextCpeId.in_subquery(allowed_cpe_ids))
.add(Expr::exists(sbom_has_cpes).not()),
)
.filter(Expr::col(product_status::Column::Package).eq(purl_name).or(
namespace_name.map_or(Expr::value(false), |ns| {
Expr::col(product_status::Column::Package).eq(format!("{ns}/{purl_name}"))
Expand Down
4 changes: 2 additions & 2 deletions modules/importer/src/runner/clearly_defined/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,10 @@ impl super::ImportRunner {
}
Err(err) => Err(ScannerError::Normal {
err: err.into(),
output: RunOutput {
output: Box::new(RunOutput {
report: report.lock().await.clone().build(),
continuation: None,
},
}),
}),
}
}
Expand Down
4 changes: 2 additions & 2 deletions modules/importer/src/runner/clearly_defined_curation/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,10 @@ impl super::ImportRunner {
}
.map_err(|err| ScannerError::Normal {
err: err.into(),
output: RunOutput {
output: Box::new(RunOutput {
report: report.lock().clone().build(),
continuation: None,
},
}),
})?;

// extract the report
Expand Down
4 changes: 2 additions & 2 deletions modules/importer/src/runner/csaf/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,10 @@ impl super::ImportRunner {
// further processing, like storing the marker
.map_err(|err| ScannerError::Normal {
err: err.into(),
output: RunOutput {
output: Box::new(RunOutput {
report: report.lock().clone().build(),
continuation: None,
},
}),
})?;

Ok(match Arc::try_unwrap(report) {
Expand Down
4 changes: 2 additions & 2 deletions modules/importer/src/runner/cve/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,10 +126,10 @@ impl super::ImportRunner {
}
.map_err(|err| ScannerError::Normal {
err: err.into(),
output: RunOutput {
output: Box::new(RunOutput {
report: report.lock().clone().build(),
continuation: None,
},
}),
})?;

// extract the report
Expand Down
4 changes: 2 additions & 2 deletions modules/importer/src/runner/cwe/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,10 @@ impl super::ImportRunner {
}
Err(err) => Err(ScannerError::Normal {
err: err.into(),
output: RunOutput {
output: Box::new(RunOutput {
report: report.lock().await.clone().build(),
continuation: None,
},
}),
}),
}
}
Expand Down
4 changes: 2 additions & 2 deletions modules/importer/src/runner/quay/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,10 @@ impl super::ImportRunner {
}
Err(err) => Err(ScannerError::Normal {
err: err.into(),
output: RunOutput {
output: Box::new(RunOutput {
report: report.lock().await.clone().build(),
continuation: None,
},
}),
}),
}
}
Expand Down
4 changes: 2 additions & 2 deletions modules/importer/src/runner/report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ pub enum ScannerError {
Normal {
#[source]
err: anyhow::Error,
output: RunOutput,
output: Box<RunOutput>,
},
}

Expand All @@ -207,7 +207,7 @@ impl SplitScannerError for Result<RunOutput, ScannerError> {
fn split(self) -> anyhow::Result<(RunOutput, anyhow::Result<()>)> {
match self {
Ok(output) => Ok((output, Ok(()))),
Err(ScannerError::Normal { err, output }) => Ok((output, Err(err))),
Err(ScannerError::Normal { err, output }) => Ok((*output, Err(err))),
Err(ScannerError::Critical(err)) => Err(err),
}
}
Expand Down
4 changes: 2 additions & 2 deletions modules/importer/src/runner/sbom/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,10 @@ impl super::ImportRunner {
// further processing, like storing the marker
.map_err(|err| ScannerError::Normal {
err: err.into(),
output: RunOutput {
output: Box::new(RunOutput {
report: report.lock().clone().build(),
continuation: None,
},
}),
})?;

Ok(match Arc::try_unwrap(report) {
Expand Down
10 changes: 5 additions & 5 deletions modules/importer/src/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,13 +164,13 @@ async fn import(
report,
continuation,
}) => (None, Some(report), continuation),
Err(ScannerError::Normal {
err,
output: RunOutput {
Err(ScannerError::Normal { err, output }) => {
let RunOutput {
report,
continuation,
},
}) => (Some(err.to_string()), Some(report), continuation),
} = *output;
(Some(err.to_string()), Some(report), continuation)
}
Err(ScannerError::Critical(err)) => (Some(err.to_string()), None, None),
};

Expand Down
2 changes: 1 addition & 1 deletion test-context/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ git2 = { workspace = true }
log = { workspace = true }
peak_alloc = { workspace = true }
postgresql_embedded = { workspace = true }
reqwest = { workspace = true }
reqwest = { workspace = true, features = ["stream"] }
serde = { workspace = true }
serde_json = { workspace = true }
sha2 = { workspace = true }
Expand Down
2 changes: 1 addition & 1 deletion xtask/src/openapi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ pub async fn generate_openapi(base: Option<&Path>) -> anyhow::Result<()> {

// write

println!("Writing openapi to {:?}", &path);
println!("Writing openapi to {path:?}");

fs::write(path, doc).context("Failed to write openapi spec")?;

Expand Down
Loading