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:
A batched
POST /v1/querybatchmaps each queried coordinate to a list of advisory IDs (the batch response carries IDs only).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) thatOSV_BASE_URLanchors.
- meta_package_manager.sbom.vulnerabilities.OSV_BATCH_LIMIT = 1000¶
Maximum number of queries OSV accepts in a single
querybatchcall.
- 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
modifiedfield 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:
objectNormalized 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.
- severity: str | None = None¶
Coarse label:
low/medium/high/critical, orNonewhen the source provides no rating.
- 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
NetworkErrorfor the caller to handle; per-advisory detail failures are swallowed so a single bad record only drops itself.- Return type:
dict[str,tuple[Vulnerability,...]]