Skip to content

Vulnerability Checking

Vulnerability data is fetched from OSV (Open Source Vulnerabilities) via a standalone vulnerability proxy service (vuln-proxy/). Clients never call OSV directly — all queries are routed through the proxy, which provides shared caching, resilience against OSV downtime, and centralized severity/fix-version logic.


OSV is queried in two phases to minimise network requests. A single vuln record referenced by multiple packages is fetched only once.

sequenceDiagram
participant V as vuln.rs
participant C as ~/.pkgprobe/vuln-cache/
participant OSV as osv.dev
V->>C: check SHA-256(purl).json for each dependency
C-->>V: cache hits (skip these)
Note over V: Build batch of cache misses
V->>OSV: POST /v1/querybatch
OSV-->>V: [{vulns: [{id}, ...]}, ...]
Note over V: Collect all unique vuln IDs
V->>OSV: POST /v1/vulnsbatch
OSV-->>V: [full VulnRecord, ...]
Note over V: Map severity, filter fix versions
V->>C: write SHA-256(purl).json (TTL 24 h)

Why two phases? The querybatch response only contains IDs — not full records. Many packages can be affected by the same CVE. Collecting all IDs first and deduplicating before the vulnsbatch call means each vuln record is fetched once regardless of how many packages reference it.


  • Location: ~/.pkgprobe/vuln-cache/
  • Key: SHA-256(purl) -> filename <hex>.json
  • Default TTL: 24 hours (configurable via scan_with_vulns(path, Some(hours)))
  • Clear with: mise run clear-cache

PURL types are mapped to OSV ecosystem strings:

PURL typeOSV ecosystem
pkg:npm/npm
pkg:nuget/NuGet
pkg:composer/Packagist
pkg:pypi/PyPI
pkg:cargo/crates.io
pkg:gem/RubyGems
pkg:golang/Go
pkg:maven/Maven

Packages whose PURL type has no known ecosystem mapping are silently skipped — they will have vulnerabilities: null in output.


A vulnerability is only surfaced if its fix version is strictly greater than the installed version.

flowchart TD
A[OSV returns vuln record\nwith affected ranges] --> B{Parse installed version\nas semver}
B -- invalid semver --> C[Return first fix version\nfound in any range]
B -- valid semver --> D[Collect all fixed versions\nfrom all ranges]
D --> E{Any fix >\ninstalled?}
E -- no --> F[Suppress vulnerability\nentirely]
E -- yes --> G[Return smallest fix\n> installed version]

Why suppress? The OSV affected range can span multiple release lines. A fix for an old release line may be lower than the currently installed version — surfacing it would be a false positive.


Three-tier fallback, evaluated in priority order:

flowchart TD
A[OSV VulnRecord] --> B{severity[]\ntype == 'CVSS_V3'?}
B -- yes --> C[Parse CVSS v3 vector string]
C --> D[Compute base score\nusing FIRST.org formula]
D --> E[Map score to label\n>=9.0 CRITICAL\n>=7.0 HIGH\n>=4.0 MEDIUM\n>=0.1 LOW]
B -- no --> F{database_specific\n.severity present?}
F -- yes --> G[Use plain label\nMODERATE -> MEDIUM]
F -- no --> H[UNKNOWN]

Terminal window
# Fast scan — no network, no CVEs
pkgprobe scan
# Scan + CVE enrichment
pkgprobe scan --vulns
# Custom TTL (12 hours)
pkgprobe scan --vulns --vuln-cache-ttl 12