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); system package managers like Homebrew are not in OSV, so their packages come back with no advisories rather than an error.

Note

Covering system package managers (brew, apt, macports, mas, …) means going through NVD, which indexes by CPE (vendor/product plus version ranges) rather than by ecosystem coordinate. That route is 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. Until that lands, system-package coverage means pointing a CPE-based scanner (OSV-Scanner, Grype, Trivy, or Intel’s cve-bin-tool) directly at the host. 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).

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', 'gem': 'RubyGems', 'npm': 'npm', 'pip': 'PyPI', 'pipx': 'PyPI', 'yarn': 'npm'}

Maps mpm manager ids 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, ...]]