# Sbom Package Identity Vulnerability Matching

> 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.

- Skill: `mouadja02/sbom-package-identity-vulnerability-matching` (Agent Skill, multi-file: 7 files)
- Install (CLI): `npx skillmds@latest add mouadja02/sbom-package-identity-vulnerability-matching`
- Raw SKILL.md: https://api.skillmd.com/api/skills/mouadja02/sbom-package-identity-vulnerability-matching/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: MIT
- Author: mouadja02 (https://skillmd.com/u/mouadja02)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/mouadja02/sbom-package-identity-vulnerability-matching

---


# 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)

