Use when SBOM vulnerability results may be false positives or negatives because PURL qualifiers, distro release lines, package ecosystems, or native version ordering were lost during matching.
Installs into .claude/skills of the current project.
Are you the author of Sbom Package Identity Vulnerability Matching?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/mouadja02-sbom-package-identity-vulnerability-matching)
---
name: sbom-package-identity-vulnerability-matching
description: Use when SBOM vulnerability results may be false positives or negatives because PURL qualifiers, distro release lines, package ecosystems, or native version ordering were lost during matching.
version: "1.0.0"
license: MIT
platforms: [linux, macos, windows]
metadata:
hermes:
tags: [sbom, purl, vulnerability-management, osv, supply-chain-security]
---
# 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:
```bash
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:
1. the complete versioned PURL;
2. 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:
1. retrieve the exact advisory revision used by the scanner when possible;
2. select the affected entry whose package ecosystem/name matches the frozen identity;
3. inspect all range events and explicit versions;
4. evaluate the component version with the native comparator;
5. record a canonical evidence URL and database timestamp;
6. 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.
```json
{
"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:
```bash
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
1. **Normal:** “OSV reports an Alpine BusyBox CVE. Verify a full PURL with `arch` and `distro` against a name/ecosystem/version query, inspect the affected range with APK ordering, and decide whether matching evidence passes.”
2. **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.”
3. **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.
1. [Package URL — specification documentation](https://www.packageurl.org/docs/purl/) (accessed 2026-08-05)
2. [OSV — POST /v1/query](https://google.github.io/osv.dev/post-v1-query/) (accessed 2026-08-05)
3. [OSV issue #3126 — Query API ignores PURL qualifiers](https://github.com/google/osv.dev/issues/3126) (accessed 2026-08-05)
4. [Trustify issue #2240 — RPM OS release suffix cross-matching](https://github.com/guacsec/trustify/issues/2240) (accessed 2026-08-05)
5. [Dependency-Track issue #6764 — Go pseudo-version false positive](https://github.com/DependencyTrack/dependency-track/issues/6764) (accessed 2026-08-05)