Vulnerability matching
How StackRadar matches SBOM components against 900K+ OSV.dev advisories: PURL matching, version-range evaluation from npm to Debian and RPM, and 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 — 900K+ 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.
Distro binary packages get a second lookup through their source package: a Debian or Alpine PURL carries an upstream= qualifier naming the source package it was built from, and advisories are routinely filed against the source rather than each binary built from it. So libcrypto3 is also checked against openssl advisories — using the source version, since that's what the advisory's ranges are written against. Findings made this way are labelled indirect, with the upstream package name and version recorded, so you can always see which identity actually matched.
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) |
Version ordering is ecosystem-specific, so StackRadar ships a dedicated comparator per versioning scheme — twelve in all: semver (npm, crates.io, Hex, Swift), PEP 440 (PyPI), Maven, Go, NuGet, RubyGems, Composer, and — the headline for container images — the distro package schemes: apk (Alpine, Wolfi, Chainguard), dpkg (Debian, Ubuntu), and rpm (Red Hat, Rocky, Alma, SUSE, and the rest of the RPM family). That means real range evaluation for the OS packages that dominate a container image's inventory — 3.0.11-1~deb12u2 is compared under Debian's own ordering rules, not string comparison or a semver approximation.
Advisories that enumerate explicit affected-version lists are evaluated too, for every ecosystem, alongside the ranges. And a bound that can't be ordered at all — a version outside its scheme's grammar — surfaces as an unconfirmed match (see below) rather than a guess. When an advisory carries a fix, the fixed version is surfaced with the finding so remediation is a version bump, not research.
No PURL, no match — and every match explains itself
There is deliberately no name-and-version fallback: a component whose PURL doesn't parse is not matched at all. Bare package names are exactly how openssl the Debian package gets conflated with an unrelated npm package, and a matcher that guesses produces findings nobody trusts. Instead, every finding records precisely how it was made:
| Match metadata | Values |
|---|---|
| Matched by | purl — CPE and name lookups are reserved for the future |
| Match type | direct (the component's own identity) or indirect (via its upstream= source package) |
| Confidence | exact, fuzzy, or unconfirmed |
exact means the version was compared under a real comparator for its ecosystem and fell inside a stated range (or was named outright in the advisory's version list). fuzzy means it fell inside a range, but a comparison along the way had to guess — an ecosystem without a dedicated comparator, or a bound outside its scheme's grammar. unconfirmed means the package matched but the advisory's own bounds couldn't answer the version question at all — reported rather than dropped, because the identity match is real and a silent drop is a false negative with no paper trail. When you triage a finding, you see which of these it is, instead of every result pretending to be equally certain.
Severity and CVSS scoring
Severity is derived from the advisory in priority order — each source is consulted only when every earlier one produced nothing, and a CVSS vector anywhere beats a text label anywhere:
- A CVSS v3 (3.0 or 3.1) vector, parsed and scored numerically.
- A CVSS v4 vector.
- A CVSS v2 vector.
- Ubuntu's priority label, which OSV publishes in the same severity field.
- Per-package severity entries on the advisory's affected list — worst wins.
- A severity, urgency, or priority text label from the source database.
- Per-package ecosystem labels — Debian's
urgencylives here. - A last-resort heuristic: a known-malicious package (
MAL-advisory) with no stated severity is treated as Critical.
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) rarely carry any severity data at all, so the last-resort heuristic is what scores them: a malicious dependency is a compromise, not a weakness, and Critical is the only honest label for it. If a MAL record does state a severity, that stated value wins — the heuristic is a fallback, not an override.When every source above comes up empty, the advisory is stored with severity unknown and displayed as Unscored. The dashboard hides unscored findings by default — most are distro tracker entries nobody has triaged — with one exception: an unscored finding that is listed in CISA KEV or carries an EPSS score of 0.1 or higher is always shown, because hiding an exploited vulnerability over a missing CVSS field would be the worst possible outcome of a noise filter. This filter is also the usual answer to "why don't your counts match scanner X?".
From severity to priority: the Radar Score
CVSS severity tells you how bad a vulnerability would be; it says nothing about how likely anyone is to exploit it. To rank findings by what deserves attention first, each finding gets a 0–100 contribution to your Radar Score — the published formula is on its own page — computed from three open data sources:
- CVSS — the advisory's severity, as above.
- EPSS — the exploitation probability published by FIRST.org, synced daily. A critical that nobody exploits scores below a high that's being exploited in the wild.
- CISA KEV — presence in the Known Exploited Vulnerabilities catalogue raises the score, and the finding carries a KEV badge with the listing date and CISA remediation deadline.
Images, workloads, Helm releases, clusters, and your whole organization each carry a Radar Score: the plain sum of their findings' contributions. The score is deliberately unbounded — it falls as findings are fixed, so it works as a trend — while the colour band always reflects the worst single finding, never the total, so a pile of lows can't masquerade as a critical (or dilute one). An unscanned image shows no score at all rather than zero: "we haven't looked" is never presented as "it's clean".
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.
Next steps
- The Radar ScoreHow the Radar Score uses the EPSS score, the CISA KEV catalogue and CVSS to rank what to fix first — the published formula, worked examples, and the thresholds that matter.
- CycloneDX SBOM formatA real, downloadable CycloneDX 1.6 SBOM example generated from a container image, with a field-by-field walkthrough: components, PURLs, the dependency graph, and what a scanner reads.