feat: backport CPE-identity vulnerability correlation (cpe_status) to release/0.4.z - #2598
Conversation
Reviewer's GuideBackport CPE applicability correlation to release/0.4.z by parsing CPE 2.3 inputs, persisting normalized CPE identities and version ranges from CNA/ADP CVE data, and adding forward/reverse SQL matching so CPE-only SBOM nodes surface vulnerabilities without requiring a PURL. Sequence diagram for CPE vulnerability correlationsequenceDiagram
participant CVE as CVE Loader
participant DB as Database
participant SBOM as SBOM Ingestor
participant API as Vulnerability API
CVE->>CVE: Cpe::from_str()
CVE->>CVE: Cpe::with_any_version()
CVE->>DB: Insert cpe_status with version_range
SBOM->>SBOM: Parse cpe22Type or cpe23Type
SBOM->>DB: Store sbom_package_cpe_ref
API->>DB: Match vendor/product and version_matches()
DB-->>API: Return vulnerability for CPE-only or PURL-backed node
Entity relationship diagram for CPE applicability statuserDiagram
CPE ||--o{ CPE_STATUS : identifies
VERSION_RANGE ||--o{ CPE_STATUS : constrains
ADVISORY ||--o{ CPE_STATUS : declares
VULNERABILITY ||--o{ CPE_STATUS : affects
SBOM_PACKAGE_CPE_REF }o--|| CPE : references
CPE {
uuid id PK
string vendor
string product
string version
}
CPE_STATUS {
uuid id PK
uuid advisory_id FK
string vulnerability_id FK
uuid cpe_id FK
uuid version_range_id FK
uuid status_id FK
}
VERSION_RANGE {
uuid id PK
string low_version
string high_version
}
SBOM_PACKAGE_CPE_REF {
uuid cpe_id FK
uuid sbom_id
string node_id
}
Flow diagram for CPE-only SBOM vulnerability matchingflowchart LR
A["CVE CNA or ADP affected entry"] --> B["Parse CPE 2.2 or CPE 2.3"]
B --> C["Normalize identity to version ANY"]
C --> D["Persist cpe_status and version range"]
E["SBOM package cpe23Type reference"] --> F["Store sbom_package_cpe_ref"]
D --> G["Match vendor and product"]
F --> G
G --> H["Check version_matches()"]
H --> I["SBOM advisory and vulnerability backlink"]
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 2 issues
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="modules/fundamental/src/vulnerability/model/details/vulnerability_advisory.rs" line_range="287-289" />
<code_context>
+ let cpe_status_query = r#"
+ SELECT
+ "cpe_status"."advisory_id",
+ "sbom_package_purl_ref"."sbom_id",
+ "sbom_package_purl_ref"."node_id",
+ "sbom_package_purl_ref"."qualified_purl_id",
+ "sbom"."sbom_id" AS "sbom$sbom_id",
+ "sbom"."node_id" AS "sbom$node_id",
</code_context>
<issue_to_address>
**issue (bug_risk):** The reverse vulnerability backlink query selects `sbom_id`, `node_id`, and `qualified_purl_id` from the optional `sbom_package_purl_ref` join instead of from `sbom_package_cpe_ref`. For a CPE-only package the PURL join is NULL, so `SbomStatusCatcher` cannot deserialize the required `sbom_id` and `node_id`, and `/vulnerability/{id}` returns an error instead of exposing the CPE-only match.
**Triggers:** When a matched SBOM package has a CPE reference but no PURL reference.
**Suggested fix:** Select `sbom_package_cpe_ref.sbom_id` and `.node_id` for the matched package identity, while retaining only `qualified_purl_id` from the optional PURL join.
</issue_to_address>
### Comment 2
<location path="common/src/cpe.rs" line_range="456" />
<code_context>
+ // ANY is the empty component in URI syntax
+ "*" => String::new(),
+ "-" => "-".to_string(),
+ _ => encode_uri_component(&unescape_cpe23(raw)),
+ }
+}
</code_context>
<issue_to_address>
**issue (bug_risk):** An escaped literal wildcard such as `\*` or `\?` is unescaped before encoding, and `encode_uri_component` then converts it to the CPE wildcard encoding `%02` or `%01`. The parser therefore changes a literal vendor/product/version character into a wildcard, producing an incorrect CPE identity and potentially incorrect vulnerability matches.
**Triggers:** When a CPE 2.3 component contains an escaped literal `*` or `?`.
**Suggested fix:** Preserve whether a wildcard was escaped before calling `encode_uri_component`, or encode escaped wildcard characters as literal percent-encoded characters rather than `%01`/`%02`.
</issue_to_address>Sourcery assessment
Needs a human reviewer. 2 findings to address first, and an incorrect CPE normalization or version-range match could cause vulnerability statuses to be reported for the wrong packages or omit affected packages, changing security-relevant runtime behavior and persisting cpe_status records. Reverting removes the new matching path, but any incorrect records or reports created before the revert require cleanup or re-ingestion rather than being fully undone by the revert.
Blocking findings: modules/fundamental/src/vulnerability/model/details/vulnerability_advisory.rs:289, common/src/cpe.rs:456
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
cpe23_component_to_uri unescaped the component before encoding, so an escaped literal `\*` / `\?` was turned into the `%02` / `%01` wildcard. Mid-component this produced e.g. `pro%02duct`, which the CPE-2.2 URI parser rejects — so a CPE 2.3 string with an escaped wildcard failed to parse and was silently dropped during ingestion. Encode the raw (still-escaped) component in a single pass: an unescaped `*`/`?` is a wildcard; an escaped `\*`/`\?` (and any other `\x`) is a literal, percent-encoded via the shared push_literal helper. Adds cpe23_escaped_wildcard_is_literal. Addresses sourcery review finding on guacsec#2598. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Backport of 1c10474 (CPE 2.3 formatted-string parsing: cpe:2.3:... is converted to the 2.2 URI form) and the common/src/cpe.rs hunk of 2d6fd0e (Cpe::with_any_version, version->ANY identity normalization; split_cpe23 refactor). Prerequisite for cpe_status ingestion and cpe23Type SBOM refs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Backport of 087cacd. The SPDX loader matched only cpe22Type external refs, silently dropping cpe23Type locators emitted by NTIA-conformant SBOMs. Accept both (Cpe::from_str now handles cpe:2.3: via the previous commit). Prerequisite for populating sbom_package_cpe_ref from real SBOMs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Backport of f03b635 (cpe_status entity + migration, renumbered m0002132) and the non-SQL Option<Uuid> parts of 0dcab72: IdSet.qualified_purl_id, QueryCatcher.qualified_purl and SbomStatusCatcher.qualified_purl become optional so CPE-only (PURL-less) nodes can flow through the detail and backlink paths. Behavior-preserving for existing purl-only queries. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Backport of 2d6fd0e (ingestor parts). The CVE 5.x loader now populates cpe_status alongside purl_status: each parseable product.cpes[] entry is stored as a vendor/product identity (version normalized to ANY via Cpe::with_any_version), with affected versions carried by version_range. Shared version_spec_and_status/status_slug helpers keep the purl path byte-identical. Adds CpeStatusCreator + CpeStatus graph types and the CVE-2099-0001 fixture + idempotency test. Adapted to 0.4.z (cvss3 scoring, &tx convention, self-managed load transaction). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…backlink Backport of 829a118 (cpe_advisory_info_sql + SBOM-detail wiring), 79551df (vulnerability backlink via package CPE) and the SQL parts of 0dcab72 (CPE-only PURL-less nodes: no qualified_purl_id IS NOT NULL gate; LEFT JOIN the purl ref in the backlink). Adapted to 0.4.z: sbom_node_* -> sbom_package_*, 0.4.z sbom column set (no properties/revision). Adds the sbom_details_cpe_matching test + CVE-2099-0002/0003 fixtures. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Backport of eadcd74. extract_vuln_info only read containers.cna.affected; ADP containers (CISA vulnrichment, and Red Hat as ADP on upstream-CNA CVEs) carry additional affected[] entries -- often the only source of CPE data. Chain cna.affected with every adp[].affected into a single Vec<&Product> through the same purl/cpe_status write path. This populates cpe_status from ADP-sourced CPEs, fixing real Red Hat CPE-only-node matches (e.g. S7 CVE-2026-12151/-33815 whose hummingbird CPE lives in the ADP container). Adds the ADP loader test; CVE-2099-0002 fixture already present. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds cpe_status_vulnerability_backlink: ingests the SPDX firmware SBOM
(OpenSSL 0.9.8w via cpe23Type) + CVE-2099-0001 (affects 0.9.8w) and asserts
the SBOM is backlinked on /vulnerability/{id} via the cpe_status match;
CVE-2099-0003 (openssl 2.0.0..3.0.0) is the negative version guard. Closes
the P6/P7 backlink test gap; confirms the LEFT-JOINed purl-ref is harmless
for CPE-only nodes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
cpe23_component_to_uri unescaped the component before encoding, so an escaped literal `\*` / `\?` was turned into the `%02` / `%01` wildcard. Mid-component this produced e.g. `pro%02duct`, which the CPE-2.2 URI parser rejects — so a CPE 2.3 string with an escaped wildcard failed to parse and was silently dropped during ingestion. Encode the raw (still-escaped) component in a single pass: an unescaped `*`/`?` is a wildcard; an escaped `\*`/`\?` (and any other `\x`) is a literal, percent-encoded via the shared push_literal helper. Adds cpe23_escaped_wildcard_is_literal. Addresses sourcery review finding on guacsec#2598. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
178c8f7 to
84a8213
Compare
|
@mrrajan what are your thoughts on the issue that sourcery.ai has highlighted? |
|
I am also concerned about the migration numbering. Especially users upgrading from various 0.4.x versions to 0.6.x or 0.5.x version and onward. I'll try to see if this works. |
ctron
left a comment
There was a problem hiding this comment.
Migration numbering will break upgrade paths
The backport uses m0002132_create_cpe_status while main uses m0002250_create_cpe_status. These create the identical table and indexes, but SeaORM tracks migrations by name. This means:
0.4 with this fix → 0.6 upgrade will fail: seaql_migrations records m0002132 but not m0002250. When 0.6 runs, it tries to execute m0002250 — the CREATE TABLE ... IF NOT EXISTS succeeds (no-op), but the two CREATE INDEX calls (which lack .if_not_exists()) fail because the indexes already exist from m0002132.
Suggested fix: Use the same migration name as main — m0002250_create_cpe_status. Add a comment noting it matches main's numbering for upgrade-path compatibility. The numeric gap on 0.4.z (m0002120 → m0002250) is cosmetic; SeaORM runs migrations in registration order from lib.rs, not by filename number.
This way, upgrading from 0.4+fix to 0.6 sees m0002250 already applied and skips it cleanly.
…rade-path compat) Rename m0002132_create_cpe_status -> m0002250_create_cpe_status so seaql_migrations records the same name as main. The 0.4.z -> 0.6.z upgrade then sees m0002250 already applied and skips it, instead of re-running its non-idempotent CREATE INDEX (which would fail because the indexes already exist). Migration body is byte-identical to main; lib.rs registration order is unchanged (SeaORM runs by registration order, not filename number). Addresses ctron review feedback on guacsec#2598. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Assisted-by: Claude Code
|
@PhilipCattanach thanks for the ping — went through both sourcery items: 1. Backlink query selects The concern is that
This is verified by the regression test The 2. Escaped Also pushed |
Backport of b716efa. Real-world CVE records (e.g. Red Hat CNA entries) carry defaultStatus "unknown" with cpes and no versions list; the CPE ingestion mapped that to the status slug "unknown", which has no row in the status table, failing the whole document with "Invalid status unknown" (caught by the dataset ingest test). Skip unknown-status CPE entries instead, matching main's guard. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Assisted-by: Claude Code
Backports the CPE-identity vulnerability-correlation feature (
cpe_status) torelease/0.4.z, so SBOM nodes identified only by a package CPE (no PURL) arecorrelated against advisory CPE applicability — the reverse and forward paths
that
release/0.4.zwas missing. Fixes TC-5630 on the z-stream.Backported from upstream #2518 (feat: implement cpe matching and nvd importer)
and the CPE-only-node fix
0dcab72dfrom #2582 (fix: vuln correlation fixes),adapted to
release/0.4.z.Backport commits (this PR)
b732f68ffeat(common): parse CPE 2.3 strings + add with_any_version1c104740+ cpe.rs of2d6fd0e8(#2518)cpe:2.3:(0.4.z only handledcpe:/); version→ANY identity01d36a93feat(ingestor): ingest cpe23Type external references from SPDX087cacd6(#2518)cpe23Typerefs sosbom_package_cpe_refis populatedbd19daa0feat(entity): add cpe_status table + wire optional qualified_purlf03b635f(#2518) + Option parts of0dcab72d(#2582)cpe_statustable (migrationm0002132); optionalqualified_purlfor PURL-less nodes2f0a179cfeat(ingestor): store CPE applicability from CVE records as cpe_status2d6fd0e8(#2518)cpe_statusa72a2e07feat(fundamental): match package CPEs against cpe_status in detail + backlink829a118b+79551df3(#2518) + SQL of0dcab72d(#2582)/sbom/{id}/advisory+/vulnerability/{id}CPE matching, incl. CPE-only nodes623469a5feat(ingestor): ingest affected entries from CVE ADP containerseadcd740(#2518)cpe_statusfrom ADP containers (Red Hat applicability on upstream-CNA CVEs)Adaptations to
release/0.4.zsbom_node_{purl,cpe}_ref→sbom_package_{purl,cpe}_ref(0.4.z predates the rename).cvss3/average_severity); did not pull inadvisory_vulnerability_score.m0002250→m0002132(skips main's twom0002130_*files); additiveCREATE TABLE, cleandown.&tx, self-managedloadtransaction,Graph::new(db));Paginatedhas nototalfield.Out of scope (documented parity gaps)
release/0.4.zhas no NVD service.cpe_statusis populated from CVE records (CNA + ADP) only.batch_severity_counts_sqlCPE additions (e176a7da/d3729549) — that SBOM-list function does not exist in 0.4.z.Verification
cargo check+clippy -D warningsclean; migrationm0002132applies (test-context).cpe::(14),ingest_spdx_cpe23_refs,cve_loader+cve_loader_stores_cpe_status+cve_loader_stores_cpe_status_from_adp_container,sbom_details_cpe_matching(positive OpenSSL/BusyBox; negatives u-boot + out-of-range openssl), regressionsbom::details/csaf::reingest./sbom/{id}/advisoryand the/vulnerability/{cve}backlink. (PURL-keyed/purland/analyzedo not report PURL-less nodes — same behavior asmain.)Summary by Sourcery
Backport CPE-based vulnerability correlation to release/0.4.z, including CPE applicability ingestion and matching for PURL-less SBOM nodes.
New Features:
Bug Fixes:
Enhancements:
Tests:
Chores: