SBOM Package Identity Vulnerability Matching
When to Use
- Two scanners disagree about whether an SBOM component is affected.
- A syntactically valid PURL produces results for the wrong distribution, architecture, repository, or release line.
- RPM releases, Go pseudo-versions, APK revisions, or another ecosystem's versions were compared with generic SemVer or lexical ordering.
- A team needs auditable evidence before suppressing a vulnerability finding.
Do not activate for SBOM completeness, license analysis, exploitability/VEX review, source-code vulnerability discovery, or generic dependency upgrades. This workflow verifies package identity and advisory-range matching; it does not decide exploitability.
Prerequisites
- A secret-free SBOM component record with its complete PURL, ecosystem, and version.
- Raw scanner/API results and the corresponding advisory affected records.
- The package ecosystem's native version-comparison rules or implementation.
- Network access only when refreshing public advisory evidence; the bundled checker and fixtures are offline.
Quick Reference
| Gate | Required evidence |
|---|---|
| Identity | Original PURL retained with version, qualifiers, and subpath |
| Differential query | Full-PURL and name/ecosystem/version representations compared |
| Release boundary | Distro/product line agrees across component and advisory |
| Version semantics | Native ecosystem comparator, not a guessed generic comparator |
| Advisory range | Returned IDs inspected against affected package and range events |
| Uncertainty | Disagreement or unsupported ordering routes to manual review |
| Suppression | Exact-PURL scope, evidence-backed reason, owner workflow, and expiry |
Validate an evidence packet:
python3 scripts/check_sbom_match_evidence.py evidence.json
Exit 0 means pass, 1 means valid evidence with blocking findings, and 2 means malformed input or I/O failure. Always inspect status and decision.
Procedure
1. Freeze the observed identity
Copy the component identity from the source SBOM before normalization:
- full PURL, including version, every qualifier, and subpath;
- SBOM format and component locator (
bom-ref, SPDX ID, or equivalent); - scanner name/version and database snapshot time;
- package ecosystem and distribution/product release line;
- advisory IDs originally returned.
Package URL defines qualifiers as key/value data that refine package identity and a subpath as a path within the package [1]. Preserve both through every intermediate representation. A parser may canonicalize encoding and qualifier order, but it must not silently drop identity fields.
Completion: a round trip through the actual parser retains the same decoded type, namespace/name, version, qualifier map, and subpath.
2. Derive representations without conflating them
Construct two requests when the service supports them:
- the complete versioned PURL;
- an explicit package name, exact service ecosystem, and version.
OSV documents these as alternatives and disallows combining name/ecosystem with PURL in one package object [2]. Record request and response identifiers separately. Do not treat equal package names as equal release identities, and do not strip a PURL qualifier merely because an API cannot interpret it.
If only one representation is supported, mark the differential gate unsupported and route to manual review. Never invent agreement.
Completion: each result set is reproducible from a recorded, redacted request and database snapshot.
3. Compare identity and result sets
Before evaluating versions, verify:
- package type maps to the intended advisory ecosystem;
- namespace and name normalization is documented;
- distribution, repository, architecture, and product-line qualifiers were retained or explicitly evaluated;
- both representations target the same release line;
- returned advisory ID sets agree.
OSV issue #3126 reports that a distro qualifier can be ignored during PURL query matching, producing Alpine false positives [3]. Treat representation divergence as evidence of ambiguity, not proof that either side is correct.
Completion: differences are enumerated by identity field and advisory ID.
4. Apply native version semantics
Choose the comparator from the package ecosystem, then test boundary values on both sides of every range event:
| Package type | Comparator evidence |
|---|---|
apk |
Alpine/APK version semantics and release revision |
rpm |
RPM epoch-version-release plus product/release-line identity |
golang |
Go module ordering, including pseudo-versions |
pypi |
PEP 440 |
npm, cargo |
The ecosystem's SemVer interpretation |
| unknown | Unsupported; manual review |
Do not use lexical ordering or generic SemVer as a universal fallback. Trustify issue #2240 reports cross-release RPM matches even when RPM ordering itself is internally consistent [4]. Dependency-Track issue #6764 reports false positives when a Go pseudo-version boundary is compared incorrectly against later tagged versions [5].
Completion: every advisory records comparator, tested version, lower/upper events, and affected, not_affected, or unknown.
5. Inspect advisory affected records
For every unioned result ID:
- retrieve the exact advisory revision used by the scanner when possible;
- select the affected entry whose package ecosystem/name matches the frozen identity;
- inspect all range events and explicit versions;
- evaluate the component version with the native comparator;
- record a canonical evidence URL and database timestamp;
- classify missing package entries, conflicting ranges, or unsupported comparators as
unknown.
A successful API response is not proof that the component is affected. Conversely, one empty service response is not proof that it is safe.
Completion: every returned ID has an inspected affected record or an explicit manual-review finding.
6. Route the decision safely
affected: identities agree, the native comparator places the component inside an applicable range, and evidence is complete.not_affected: identities agree and an inspected applicable range excludes the version.manual_review: identity representations diverge, qualifiers were ignored, release lines differ, comparator support is absent, or advisory evidence is incomplete.
Never auto-suppress merely because scanners disagree. A suppression must be scoped to the exact PURL and advisory ID, cite the inspected evidence, use the organization's approval path, and expire for re-evaluation. The bundled checker enforces exact-PURL scope and a future expiry but does not replace ownership approval.
7. Emit and check the evidence packet
The packet uses task_kind: sbom_identity_vulnerability_match_preflight, one component, the two query representations, inspected advisories, and optional suppressions. tests/fixtures/normal.json is the complete example.
{
"task_kind": "sbom_identity_vulnerability_match_preflight",
"component": {
"purl": "pkg:apk/alpine/busybox@1.36.1-r19?arch=x86_64&distro=alpine-3.19",
"ecosystem": "Alpine:v3.19",
"version": "1.36.1-r19"
},
"queries": [
{"representation": "full_purl", "identity": "...", "result_ids": ["CVE-2023-42364"]},
{"representation": "name_ecosystem_version", "identity": "Alpine:v3.19|busybox|1.36.1-r19", "result_ids": ["CVE-2023-42364"]}
],
"advisories": [{
"id": "CVE-2023-42364", "ecosystem": "Alpine:v3.19", "comparator": "apk",
"range_evaluation": "affected", "qualifiers_considered": ["arch", "distro"],
"source": "https://osv.dev/vulnerability/CVE-2023-42364"
}],
"suppressions": []
}
Verification
Run from the installed skill directory:
PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -s tests -p 'test_*.py' -v
python3 scripts/check_sbom_match_evidence.py tests/fixtures/normal.json
python3 scripts/check_sbom_match_evidence.py tests/fixtures/edge.json # expected exit 1
Release a matching decision only when the packet parses, both query identities preserve the component, result sets agree, all returned advisories were inspected, native comparators were used, and the checker reports pass.
Failure Recovery and Pitfalls
- Parser drops qualifiers: preserve the original record, stop automation, and fix or replace the normalization boundary. Do not mutate the SBOM to match the scanner.
- Queries disagree: retain both raw result sets and route to manual review; do not union-and-suppress automatically.
- Release suffix sorts across product lines: partition identity by distro/product release before applying native version ordering.
- Comparator is unavailable: report
unknown; a lexical fallback is not conservative because it can create both false positives and false negatives. - Advisory changed: record revision/snapshot provenance and rerun the packet. Do not silently rewrite a historical decision.
- Suppression expired: re-query and re-evaluate; do not extend expiry without fresh evidence.
- Sensitive SBOM metadata: synthesize package names and URLs in public fixtures and keep internal repository paths or product identifiers out of reports.
Evaluation Prompts
- Normal: “OSV reports an Alpine BusyBox CVE. Verify a full PURL with
archanddistroagainst a name/ecosystem/version query, inspect the affected range with APK ordering, and decide whether matching evidence passes.” - Difficult edge: “An el8 RPM PURL loses its distro qualifier, the alternate query targets el9, result sets disagree, generic SemVer is used, and a global permanent suppression is proposed. Produce stable fail-closed findings.”
- Should not activate: “Review this CycloneDX document for NTIA minimum-element completeness and license conflicts.”
Sources and Recommendation Boundary
Sourced facts: Package URL component semantics, OSV query alternatives/version rules, and the linked projects' reported qualifier, RPM release-line, and Go pseudo-version failure modes [1]–[5].
Recommendations: the differential-query matrix, fail-closed routing, boundary fixture procedure, evidence packet, and narrowly scoped expiring suppression policy are original operational guidance.
- Package URL — specification documentation (accessed 2026-08-05)
- OSV — POST /v1/query (accessed 2026-08-05)
- OSV issue #3126 — Query API ignores PURL qualifiers (accessed 2026-08-05)
- Trustify issue #2240 — RPM OS release suffix cross-matching (accessed 2026-08-05)
- Dependency-Track issue #6764 — Go pseudo-version false positive (accessed 2026-08-05)