Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

OSV.dev CVE lookup

bomdrift’s CVE enricher queries the Open Source Vulnerability database for added and version-bumped components, populating the Vulnerabilities section in the rendered output with advisory IDs (CVE, GHSA, MAL, etc.) and per-advisory severity.

Why this signal

Published advisories are the broadest supply-chain signal: a newly added or version-bumped dependency may pull in a known-vulnerable release. OSV unifies advisories across ecosystems, so a single lookup covers npm, PyPI, Cargo, Maven, and more.

Why OSV.dev specifically?

  • Cross-ecosystem unification. OSV merges npm advisories from GHSA, PyPI advisories from PyPA, Cargo advisories from RustSec, Maven advisories from GHSA, etc. into a single API, so bomdrift doesn’t need ecosystem-specific clients.
  • Open API, no key required. Every consumer of the /v1/querybatch endpoint gets the same data without registration overhead.
  • Public schema. The response shape is documented at ossf.github.io/osv-schema/, so bomdrift can reason about the shape without depending on an API client crate that drags in tokio.

Algorithm

A two-stage lookup: a batched query for advisory IDs, then a per-id fetch for severity.

Stage 1: /v1/querybatch

A single batched POST returns advisory IDs for every queried component in one round-trip. bomdrift batches up to 1000 queries per request (the documented cap); larger diffs chunk into multiple batches.

Each query is a package@version keyed by purl ecosystem (npm, PyPI, crates.io, Maven, etc.). Components without a parseable purl are skipped silently.

Stage 2: /v1/vulns/{id}

For each unique advisory ID returned by stage 1, bomdrift issues a follow-up GET to populate severity. Severity is sourced (in order):

  1. GHSA’s database_specific.severity text label (LOW|MODERATE|HIGH|CRITICAL). This is the most consistent shape across the OSV corpus.
  2. Highest CVSS_V3 vector score from the severity[] array, mapped to a label by the standard CVSS-v3 severity rating (Critical >= 9.0, High >= 7.0, Medium >= 4.0, Low >= 0.1).
  3. Severity::None when neither shape is present. These advisories render with a none severity label and don’t trip --fail-on critical-cve.

Threshold

OSV findings surface regardless of severity; the gate is --fail-on:

ThresholdTrips when…
noneNever.
cveAny vuln finding present (regardless of severity).
critical-cveAny finding with severity >= High (covers HIGH and CRITICAL).
typosquatAny typosquat finding; OSV findings do not trip it.
license-changeAny same-version license change; OSV findings do not trip it.
anyAny finding of any kind, plus license-changed-without-version-bump.

The critical-cve name covers HIGH-or-CRITICAL because CRITICAL alone is rare in the GHSA tagging and many actively-exploited advisories ship as HIGH. The threshold name stays stable; the threshold value covers the actionable bucket.

Output

The enricher populates the Vulnerabilities section with advisory IDs (CVE, GHSA, MAL, etc.) and per-advisory severity across the markdown, terminal, JSON, and SARIF renderers. The EPSS and CISA KEV enrichers layer probability and exploitation badges onto the same advisories.

Network

  • Source: OSV.dev /v1/querybatch + /v1/vulns/{id}.
  • Per-request timeout: 15 seconds.
  • No authentication: both endpoints are unauthenticated public APIs.
  • User-Agent: bomdrift/<version> so the OSV team can attribute traffic if needed.
  • Failures warn and continue: a network mishap (DNS, timeout, 5xx) emits a single stderr warning and the diff renders without the Vulnerabilities section. The exit code remains 0 unless --fail-on was set and a previously-cached vuln tripped it.

On-disk severity cache

Stage-2 lookups are N+1 in the worst case, one query per unique advisory ID. bomdrift caches stage-2 responses on disk at <XDG_CACHE_HOME>/bomdrift/osv/<advisory_id>.json with a 24h TTL.

~/.cache/bomdrift/osv/
├── CVE-2025-12345.json
├── GHSA-3p68-rc4w-qgx5.json
└── MAL-2026-2306.json

Each cache file looks like:

{
  "fetched_at": 1745878800,
  "severity": "Critical",
  "raw": { ... full /vulns/{id} response ... }
}

Cache behavior:

  • Cache hits log nothing. A successful 24h-fresh hit is silent.
  • Cache misses are silent too. Each miss issues a network fetch and writes the result on success.
  • End-of-run summary. A single line goes to stderr like osv: 18/22 severities served from cache so CI logs show the cache hit ratio without per-file noise.
  • Atomic writes. Cache files are written to <id>.json.tmp then renamed, mirroring the temp-file + rename pattern used by bomdrift refresh-typosquat.
  • Stale TTL. 24h is a deliberate balance between rerun friction (a CI job re-running 30 minutes after the last one wants the cache) and stale-severity risk (a published severity correction after 24h is rare and the renderer’s contract is “best effort”).

Pass --no-osv-cache to force fresh fetches even within the 24h window (it costs N+1 fetches per run; use sparingly).

Disabling

--no-osv skips the entire OSV pipeline (both stages, no cache writes):

bomdrift diff before.json after.json --no-osv

Use for:

  • Tests and example scenarios where determinism matters more than freshness.
  • Air-gapped CI environments.
  • Quick smoke tests of the change-shape signals without the network latency.

Calibration

  • --cache-ttl-hours <N> (v0.9.6+) overrides the default 24h severity cache TTL.
  • --no-osv-cache bypasses the on-disk severity cache for a run without disabling the lookup itself.

See also