Vulnerability matching
How StackRadar matches SBOM components against 300K+ OSV.dev advisories: Package URL (PURL) matching, semver range evaluation, name-version fallback, and match confidence.
The data source: a local OSV.dev mirror
StackRadar matches against OSV.dev, Google's open, machine-readable vulnerability database that aggregates advisories from GitHub Security Advisories, PyPA, RustSec, Go vulndb, and many other ecosystem sources — 300K+ records. Rather than calling the OSV API per lookup, StackRadar maintains a full local mirror that is updated continuously from OSV's change feed. That means:
- Matching runs against fresh data — a newly published advisory is applied to already-uploaded SBOMs, so you learn an existing image is affected without rescanning or redeploying it.
- Lookups are local database queries, so full-cluster rescans are fast.
- Your dependency inventory is never sent to a third-party API for matching.
Step 1 — Package URL (PURL) matching
Every component in a CycloneDX SBOM carries a Package URL — an ecosystem-qualified identifier like:
pkg:npm/lodash@4.17.20
pkg:golang/github.com/gin-gonic/gin@1.9.0
pkg:deb/debian/openssl@3.0.11-1~deb12u2?arch=amd64StackRadar strips the version and qualifiers to get the base PURL (pkg:npm/lodash) and looks up every advisory whose affected-package list matches it exactly. Matching on PURL rather than bare package names avoids the classic false-positive trap where openssl the Debian package, the Alpine package, and an npm package with a similar name are conflated.
Step 2 — version range evaluation
A PURL match only says "this package has had advisories". The component's version is then evaluated against each advisory's affected ranges, which OSV expresses as events:
| Event | Meaning |
|---|---|
introduced | Start of the affected range (inclusive) |
fixed | First safe version (exclusive end of range) |
last_affected | Last known affected version (inclusive end) |
limit | Hard upper bound on the range (exclusive) |
Semver range evaluation applies to ecosystems with reliable semver ordering — npm, crates.io, Go, NuGet, Hex, and Swift — plus explicit affected-version lists for the rest. When an advisory carries a fix, the fixed version is surfaced with the finding so remediation is a version bump, not research.
Step 3 — name + version fallback
Components without a usable PURL fall back to name-and-version matching. Every finding records how it was matched:
| Match metadata | Values |
|---|---|
| Matched by | purl or name_version |
| Confidence | exact (version confirmed in an affected range) or partial |
This is deliberately transparent: when you triage a finding, you can see whether it came from a precise PURL + version-range match or a fuzzier fallback, instead of every result pretending to be equally certain.
Severity and CVSS scoring
Severity is derived from the advisory in priority order:
- A CVSS v3.1 vector, parsed and scored numerically.
- A CVSS v4 vector, when that's all the advisory provides.
- A database-specific severity label from the source advisory.
Scores map to labels the standard way:
| CVSS score | Severity |
|---|---|
| 9.0 – 10.0 | Critical |
| 7.0 – 8.9 | High |
| 4.0 – 6.9 | Medium |
| 0.1 – 3.9 | Low |
MAL- advisories, e.g. typosquats and hijacked releases) are treated as Critical regardless of CVSS, since a malicious dependency is a compromise, not a weakness.When matching runs
- On SBOM ingestion — every new upload is matched immediately.
- On database updates — as the OSV mirror ingests new and modified advisories, existing components are re-evaluated, which is how a CVE published today shows up on an image you deployed last month.
For what's inside the SBOMs being matched, see the CycloneDX format reference.