meta_package_manager.sbom.vulnerabilities module

Vulnerability lookup against OSV.dev.

The scan_vulnerabilities() entry point takes the set of purls a rendered SBOM holds and returns the advisories affecting each, normalized into the source-agnostic Vulnerability dataclass. The SBOM renderers consume that mapping in their finalize step (CycloneDX into Bom.vulnerabilities, SPDX into per-package security externalRefs).

OSV is the single source for this first iteration because it indexes by ecosystem coordinates directly, sidestepping the fuzzy package-name to CPE matching that NVD would require. Coverage is strongest for language ecosystems (PyPI, npm, crates.io, RubyGems, Packagist). No advisory database indexes a system manager’s own coordinate, so a pkg:brew/… or pkg:apt/… purl comes back with no advisories rather than an error.

Note

A system package is still reachable, through the coordinate its upstream carries rather than the one its manager assigns. Homebrew does exactly this in its own brew vulns, shipped in 6.0.11: its Homebrew::Vulns::Identify resolver turns a formula’s source URL into either a registry purl (ten ecosystems, from PyPI to CPAN) or a forge repository URL plus release tag, and queries OSV’s GIT ecosystem with the latter. Managers whose recipes name an upstream source URL can all be read this way.

mpm takes the first half today: the Homebrew extractor lifts the registry purl Homebrew records in each formula’s sbom.spdx.json into extra_purls, which the renderers index as an alias of the formula’s own purl. An advisory found under pkg:pypi/yt-dlp@… therefore lands on the pkg:brew/yt-dlp@… package.

CPE is the fallback for what neither half reaches, and stays deliberately deferred: mapping a package name to its CPE is fuzzy and the main source of false positives, and NVD offers no batch coordinate lookup to match OSV’s querybatch. Pointing a CPE-based scanner (OSV-Scanner, Grype, Trivy, or Intel’s cve-bin-tool) directly at the host covers that remainder. Feeding them the rendered CycloneDX/SPDX is not enough: the exported components carry purls but no CPEs, so a pkg:brew/... entry decodes as an unknown-ecosystem package and silently matches nothing (verified against Grype 0.115.0).

Todo

Query OSV’s GIT ecosystem with a forge repository URL and release tag, derived from the download_url and vcs_url every extractor already fills. That is the second half of Homebrew’s approach, the half brew vulns itself runs on, and it reaches a package built from a plain forge tarball, which no registry purl covers.

It is also where the coverage is. Driving Homebrew’s own resolver over 246 installed formulae on one macOS host, 8 resolve to a registry purl and 124 to a repository URL plus tag, with no formula in both and 114 in neither.

Needs a tag parser and a forge-URL normalizer, not a new advisory source, and widens the scan past Homebrew to every manager that records an upstream source URL.

See also

VulnerableCode is the candidate second source: it aggregates OSV, GitHub and the Linux distro trackers (Debian, Arch, Gentoo, Red Hat, SUSE, Ubuntu) plus project-specific feeds behind a purl-keyed API, with a public instance at public.vulnerablecode.io. That would widen coverage beyond OSV’s language ecosystems without mpm taking on CPE mapping itself.

Two-stage protocol:

  1. A batched POST /v1/querybatch maps each queried coordinate to a list of advisory IDs (the batch response carries IDs only).

  2. A per-ID GET /v1/vulns/{id} fetches the full record. These records are immutable once published, so they cache effectively forever; the batch listings get a finite TTL since new advisories can appear.

Network transport, retries, and caching are handled by meta_package_manager.sbom._network.NetworkClient.

meta_package_manager.sbom.vulnerabilities.OSV_BASE_URL = 'https://api.osv.dev'

Base URL of the OSV.dev REST API.

meta_package_manager.sbom.vulnerabilities.OSV_ADVISORY_URL = 'https://osv.dev/vulnerability'

Prefix of the human-facing OSV advisory page.

Deliberately the web host (osv.dev), distinct from the API host (api.osv.dev) that OSV_BASE_URL anchors.

meta_package_manager.sbom.vulnerabilities.OSV_BATCH_LIMIT = 1000

Maximum number of queries OSV accepts in a single querybatch call.

meta_package_manager.sbom.vulnerabilities.VULN_DETAIL_TTL = 2592000

Cache TTL for per-advisory detail records (30 days).

OSV advisory records are effectively immutable once published (the modified field changes rarely), so a long TTL avoids re-fetching the same record on every scan while still picking up the occasional correction within a month.

meta_package_manager.sbom.vulnerabilities.OSV_ECOSYSTEMS: dict[str, str] = {'cargo': 'crates.io', 'composer': 'Packagist', 'cran': 'CRAN', 'gem': 'RubyGems', 'golang': 'Go', 'hackage': 'Hackage', 'hex': 'Hex', 'maven': 'Maven', 'npm': 'npm', 'nuget': 'NuGet', 'pip': 'PyPI', 'pipx': 'PyPI', 'pypi': 'PyPI', 'yarn': 'npm'}

Maps purl types to OSV ecosystem names.

class meta_package_manager.sbom.vulnerabilities.Vulnerability(id, source='OSV', summary=None, description=None, severity=None, cvss_vector=None, cwe_ids=(), aliases=(), references=(), fixed_versions=(), published_date=None, modified_date=None, advisory_url='')[source]

Bases: object

Normalized vulnerability record, source-agnostic on its surface.

Populated from OSV today; the shape deliberately avoids OSV-specific fields so a future NVD or GHSA source can fill the same structure.

id: str

Primary advisory identifier (GHSA-..., CVE-..., OSV-...).

source: str = 'OSV'

Origin database. Only OSV is produced today.

summary: str | None = None
description: str | None = None
severity: str | None = None

Coarse label: low / medium / high / critical, or None when the source provides no rating.

cvss_vector: str | None = None

Raw CVSS vector string when present (e.g. CVSS:3.1/AV:N/...).

The numeric base score is intentionally not computed here: deriving it requires the full CVSS formula, which is out of scope for this first iteration. Consumers that need the number can parse the vector.

cwe_ids: tuple[str, ...] = ()
aliases: tuple[str, ...] = ()

Cross-references, like the CVE id behind a GHSA advisory.

references: tuple[str, ...] = ()
fixed_versions: tuple[str, ...] = ()
published_date: datetime | None = None
modified_date: datetime | None = None
advisory_url: str = ''

Canonical human-facing URL for the advisory.

meta_package_manager.sbom.vulnerabilities.scan_vulnerabilities(purls, client)[source]

Look up advisories for every supported purl, via OSV.

Returns a mapping from purl string to the tuple of vulnerabilities affecting it. Purls with no advisories (or no OSV coverage) are simply absent from the result. A network failure on the batch query propagates as NetworkError for the caller to handle; per-advisory detail failures are swallowed so a single bad record only drops itself.

Return type:

dict[str, tuple[Vulnerability, ...]]