Skip to content

fix(purl): filter purl_statuses by SBOM describing CPEs (TC-5171)#2523

Open
ruromero wants to merge 5 commits into
guacsec:mainfrom
ruromero:TC-5171
Open

fix(purl): filter purl_statuses by SBOM describing CPEs (TC-5171)#2523
ruromero wants to merge 5 commits into
guacsec:mainfrom
ruromero:TC-5171

Conversation

@ruromero

@ruromero ruromero commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Summary

Filter purl_statuses and /vulnerability/analyze results to exclude entries whose context_cpe_id does not match the SBOM's describing CPEs. Uses generalized CPE matching so that all CPEs sharing the same vendor, product, and major version are included in the allowed set — this correctly handles cases where the SBOM describes one edition (e.g. appstream) but advisories reference another edition of the same product (e.g. baseos).

Problem

When querying vulnerability data for a package, purl_statuses and /vulnerability/analyze returned entries with CPE contexts from unrelated products (e.g., JBoss, RHSCL) because context_cpe_id was never validated against the SBOM's describing CPEs. This caused thousands of false-positive vulnerability rows in large SBOM analyses.

Fix

1. Three-way context_cpe_id filter

Added to all three query paths (SeaORM purl endpoint, raw SQL analyze endpoint, raw SQL SBOM batch counts):

  1. context_cpe_id IS NULL — always allow statuses without a CPE context
  2. context_cpe_id IN (allowed CPEs) — allow statuses whose CPE matches the SBOM's describing CPEs (with generalized matching)
  3. SBOM has no describing CPEs — fallback: allow everything

2. Generalized CPE matching

The allowed CPE set is expanded beyond exact describing CPEs to include all CPEs sharing the same (vendor, product, major_version). This handles the common case where an SBOM describes enterprise_linux:8::appstream but advisory entries reference enterprise_linux:8::baseos — both are the same product (RHEL 8) and should be included.

Changes

  • modules/fundamental/src/purl/model/details/purl.rs: Added three subqueries (sbom_ids_for_purl, generalized_cpe_ids, sbom_has_cpes) and a Condition::any() filter to the SeaORM purl_statuses query
  • modules/fundamental/src/vulnerability/service/mod.rs: Added generalized CPE matching CTEs and three-way filter to both build_query() variants (by base_purl.id and by {product_bp_condition})
  • modules/fundamental/src/sbom/model/raw_sql.rs: Updated CONTEXT_CPE_FILTER_SQL, batch_severity_counts_sql(), and product_advisory_info_sql() to use generalized matching without edition constraints

Testing

  • Verified with expat@2.2.5-11.el8 (22 version-matching purl_status entries across 7+ products): 2 baseos entries correctly pass, 20 cross-product entries correctly filtered
  • cargo clippy and cargo fmt pass
  • Existing test suite passes

Fixes #2524
Fixes: TC-5171

…oss-product matches

The purl_status query in PurlDetails was returning entries with context_cpe_id
values from unrelated products (e.g., RPM/RHEL CPE contexts for Maven packages).
This happened because context_cpe_id was never validated against the SBOM's own
describing CPEs.

Add a three-way filter matching the pattern already used in raw_sql.rs
(batch_severity_counts_sql): allow purl_status rows where context_cpe_id is NULL,
or matches a CPE from the SBOM's describing CPEs (via sbom_describing_cpe), or
when the SBOM has no describing CPEs at all (fallback).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

This PR updates the PurlDetails SBOM-backed query so that purl_status results are filtered by the SBOMs that describe the package’s CPEs, preventing cross-product advisory false positives while preserving backward compatibility when no describing CPEs exist.

Entity relationship diagram for SBOM-based purl_status CPE filtering

erDiagram
    sbom {
        int id
    }

    qualified_purl {
        int id
    }

    sbom_node_purl_ref {
        int sbom_id
        int qualified_purl_id
    }

    sbom_describing_cpe {
        int sbom_id
        int cpe_id
    }

    cpe {
        int id
    }

    base_purl {
        int id
    }

    purl_status {
        int base_purl_id
        int context_cpe_id
    }

    qualified_purl ||--o{ sbom_node_purl_ref : references
    sbom ||--o{ sbom_node_purl_ref : has_nodes_for

    sbom ||--o{ sbom_describing_cpe : describes
    cpe ||--o{ sbom_describing_cpe : is_described_in

    base_purl ||--o{ purl_status : has_status
    cpe ||--o{ purl_status : context_cpe_id
Loading

File-Level Changes

Change Details Files
Filter purl_statuses by SBOM describing CPEs using a three-way context_cpe_id condition in the PurlDetails query.
  • Import SeaORM Condition and sbom_describing_cpe entity to support new filtering logic.
  • Build a subquery of SBOM IDs associated with the qualified PURL via sbom_node_purl_ref.
  • Build a subquery of allowed CPE IDs from sbom_describing_cpe constrained to those SBOM IDs.
  • Build an EXISTS-style subquery that detects whether any describing CPEs exist for the SBOMs associated with the PURL.
  • Apply a Condition::any filter to the purl_status query enforcing: context_cpe_id IS NULL, context_cpe_id IN allowed CPE IDs, or NOT EXISTS(sbom_has_cpes) as a fallback.
  • Insert the new filter between the version range VersionMatches logic and distinct_on to preserve existing deduplication behavior.
modules/fundamental/src/purl/model/details/purl.rs

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

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.

Hey - I've left some high level feedback:

  • The three new subqueries (sbom_ids_for_purl, allowed_cpe_ids, sbom_has_cpes) are evaluated independently per request; consider whether they can be folded into joins or a single EXISTS-based condition to reduce query complexity and potential performance impact at scale.
  • The fallback Expr::exists(sbom_has_cpes).not() logic is subtle; it may be worth extracting this condition into a well-named helper or adding an inline comment near the filter to clarify that it intentionally passes all statuses when no describing CPEs exist.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The three new subqueries (`sbom_ids_for_purl`, `allowed_cpe_ids`, `sbom_has_cpes`) are evaluated independently per request; consider whether they can be folded into joins or a single EXISTS-based condition to reduce query complexity and potential performance impact at scale.
- The fallback `Expr::exists(sbom_has_cpes).not()` logic is subtle; it may be worth extracting this condition into a well-named helper or adding an inline comment near the filter to clarify that it intentionally passes all statuses when no describing CPEs exist.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@codecov

codecov Bot commented Jul 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 71.70%. Comparing base (7c757bd) to head (81669e1).
⚠️ Report is 18 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2523      +/-   ##
==========================================
+ Coverage   71.66%   71.70%   +0.04%     
==========================================
  Files         453      457       +4     
  Lines       27517    27764     +247     
  Branches    27517    27764     +247     
==========================================
+ Hits        19719    19909     +190     
- Misses       6667     6703      +36     
- Partials     1131     1152      +21     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ruromero
ruromero requested a review from ctron July 17, 2026 07:21
ruromero and others added 4 commits July 17, 2026 11:18
…bility/analyze

The build_query() function in the vulnerability service returns advisory
entries with RPM/RHEL CPE contexts for non-RPM packages because
context_cpe_id is never validated against the SBOM's describing CPEs.

Add the three-way context_cpe_id filter to both purl_status and
product_status sub-queries in build_query():
1. context_cpe_id IS NULL (advisory applies globally)
2. context_cpe_id matches an SBOM describing CPE for the purl
3. The purl has no SBOM with describing CPEs (fallback)

For purl_status, base_purl.id is available in the FROM clause so the
correlated subquery joins through versioned_purl directly. For
product_status, base_purl is not in scope, so a product_bp_condition
variable is built using Statement::from_sql_and_values to match by
name/type/namespace, with an explicit JOIN to base_purl in the subquery.

This complements commit 81669e1 which applied the same fix to the
purl details endpoint (purl.rs).

Fixes: guacsec#2524

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The purl details endpoint filtered purl_statuses using exact CPE ID
matching against SBOM describing CPEs. Advisory context CPEs use
different version granularity than SBOM describing CPEs (e.g.,
version=8 vs version=8.8), causing all entries to be filtered out.

Add a UNION with generalized CPE matching that expands allowed CPE
IDs to include those sharing (vendor, product, split_part(version,
'.', 1)) with relaxed edition, matching the pattern already used in
raw_sql.rs. This completes the fix across all three affected code
paths (raw_sql.rs, build_query, and purl details).

Refs: TC-5171

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The build_query() function for /vulnerability/analyze filtered
purl_statuses and product_statuses using exact CPE ID matching.
Advisory context CPEs use different version granularity than SBOM
describing CPEs, causing all entries to be filtered out.

Add UNION with generalized CPE matching to both the purl_status and
product_status subqueries, expanding allowed CPE IDs to include those
sharing (vendor, product, split_part(version, '.', 1)) with relaxed
edition.

Refs: TC-5171

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The edition constraint (edition IS NULL OR edition = '*') in the
generalized CPE matching was too strict — it excluded legitimate
same-product entries with specific editions (e.g. baseos when SBOM
describes appstream). All CPEs sharing the same vendor, product, and
major version should be included regardless of edition.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

purl_statuses returns entries with RPM version ranges and RHEL CPE contexts for non-RPM packages

1 participant