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
82 changes: 75 additions & 7 deletions modules/fundamental/src/purl/model/details/purl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
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 @@ -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)
Expand All @@ -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(),
Expand Down
10 changes: 4 additions & 6 deletions modules/fundamental/src/sbom/model/raw_sql.rs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
)
Expand Down Expand Up @@ -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
)
Expand Down
3 changes: 2 additions & 1 deletion modules/fundamental/src/vulnerability/service/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Comment on lines 287 to 289

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.

suggestion: Consider naming the JSON key to reflect that it carries an ID, not a full CPE object.

The value here is purl_status.context_cpe_id, so the key name suggests a full CPE rather than an ID. Consider renaming the key (e.g. to context_cpe_id, or whatever matches existing API conventions) to avoid confusing downstream consumers about what this field contains.

) AS advisories
FROM base_purl
Expand Down
2 changes: 1 addition & 1 deletion modules/fundamental/tests/dataset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

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.

suggestion (testing): Strengthen this assertion to validate which advisories are present, not just the count.

Simply increasing the expected advisory count to 3 keeps the test brittle and not very descriptive. Since the broader CPE matching is intentional, also assert on which advisories are returned (e.g., IDs, statuses, or a subset) rather than only their number. This will make future failures more informative and clearly document which advisories are expected for ubi8.

Suggested implementation:

    let ubi_details = ubi_details.unwrap();
    let ubi_advisories = ubi_details.advisories;

    // Assert on the specific advisories we expect for ubi8, not just the count.
    let advisory_ids: std::collections::HashSet<_> = ubi_advisories
        .iter()
        .map(|advisory| advisory.id.as_str())
        .collect();

    let expected_ids: std::collections::HashSet<&'static str> = [
        "RHSA-YYYY:0001",
        "RHSA-YYYY:0002",
        "RHSA-YYYY:0003",
    ]
    .into_iter()
    .collect();

    assert_eq!(
        advisory_ids, expected_ids,
        "unexpected advisories for ubi8: got {:?}, expected {:?}",
        advisory_ids, expected_ids
    );

    assert!(
  1. Replace "RHSA-YYYY:0001", "RHSA-YYYY:0002", and "RHSA-YYYY:0003" with the actual advisory IDs that are expected for the ubi8 test fixture in this test.
  2. If the Advisory struct does not expose the ID as advisory.id: String, adjust the mapping closure map(|advisory| advisory.id.as_str()) to use the correct field (e.g., advisory.advisory_id.as_str() or similar).
  3. If you’d like to also assert on statuses or other fields, you can extend the expected_ids concept into a struct or tuple set (e.g., (id, status)) and map ubi_advisories accordingly before comparing the sets.

assert!(
ubi_advisories
.iter()
Expand Down