SBOM: Software Bill of Materials

Context

The Log4Shell vulnerability debacle was a wake-up call for the industry. This dependency was deeply embedded in the legacy stack of companies and administrations. They all had huge difficulty to identify its presence, writing custom detection scripts and scanning their software artifacts.

As a response to this crisis, SBOM tools have now became a category of their own. To the point that a US executive order has also been released to modernize cybersecurity practices and enforce the production of SBOM to track the software supply chain.

mpm can export the list of installed packages as a SBOM in two standards and multiple formats:

Standard

SPDX

CycloneDX

JSON

XML

YAML

RDF XML

TAG VALUE

SBOM export is the compliance corner of mpm’s inventory exports: for re-installable snapshots see Snapshot and export, and for ad-hoc JSON or CSV piping of a listing see JSON & CSV exports.

For example:

$ mpm --brew --gem sbom --spdx --format yaml
291 packages total (brew: 229, gem: 62).
229/291 packages enriched with metadata.
SPDXID: SPDXRef-DOCUMENT
creationInfo:
  created: '2024-07-30T15:48:45Z'
  creators:
  - 'Tool: meta-package-manager-5.18.0'
dataLicense: CC0-1.0
documentNamespace: https://github.com/kdeldycke/meta-package-manager/releases/tag/v5.18.0/dd72ff542938a2d40620dc249e91e35
name: macOS-Darwin-23.6.0-arm64
packages:
- SPDXID: SPDXRef-Package-brew-curl
  downloadLocation: https://www.example.com
  filesAnalyzed: false
  name: curl
  primaryPackagePurpose: INSTALL
  supplier: 'Organization: Homebrew Formulae'
  versionInfo: 8.9.0
- SPDXID: SPDXRef-Package-brew-ffmpeg
  downloadLocation: https://www.example.com
  filesAnalyzed: false
  name: ffmpeg
  primaryPackagePurpose: INSTALL
  supplier: 'Organization: Homebrew Formulae'
  versionInfo: 7.0.1
- SPDXID: SPDXRef-Package-brew-xz
  downloadLocation: https://www.example.com
  filesAnalyzed: false
  name: xz
  primaryPackagePurpose: INSTALL
  supplier: 'Organization: Homebrew Formulae'
  versionInfo: 5.6.2
(...)
- SPDXID: SPDXRef-Package-gem-bundler
  downloadLocation: https://www.example.com
  filesAnalyzed: false
  name: bundler
  primaryPackagePurpose: INSTALL
  supplier: 'Organization: RubyGems'
  versionInfo: 2.4.22
- SPDXID: SPDXRef-Package-gem-libxml-ruby
  downloadLocation: https://www.example.com
  filesAnalyzed: false
  name: libxml-ruby
  primaryPackagePurpose: INSTALL
  supplier: 'Organization: RubyGems'
  versionInfo: 4.1.2
(...)
relationships:
- relatedSpdxElement: SPDXRef-Package-brew-curl
  relationshipType: DESCRIBES
  spdxElementId: SPDXRef-DOCUMENT
- relatedSpdxElement: SPDXRef-Package-brew-ffmpeg
  relationshipType: DESCRIBES
  spdxElementId: SPDXRef-DOCUMENT
- relatedSpdxElement: SPDXRef-Package-brew-xz
  relationshipType: DESCRIBES
  spdxElementId: SPDXRef-DOCUMENT
(...)
- relatedSpdxElement: SPDXRef-Package-gem-bundler
  relationshipType: DESCRIBES
  spdxElementId: SPDXRef-DOCUMENT
- relatedSpdxElement: SPDXRef-Package-gem-libxml-ruby
  relationshipType: DESCRIBES
  spdxElementId: SPDXRef-DOCUMENT
(...)
spdxVersion: SPDX-2.3

To export only a subset of the installed packages, filter with --query. The match is fuzzy by default (case-insensitive, tokenized), the same semantics as mpm search; pass --exact for a verbatim match on the package ID or name:

$ mpm --brew sbom --query openssl > openssl.spdx.json

Scan mode: --bundled vs --minimal

mpm sbom defaults to bundled mode: every manager that knows how is queried for richer per-package metadata (license, supplier, homepage, declared dependencies, source URL, checksums), per-package upstream SBOM documents are merged into the aggregate, and the result lands in the rendered SPDX or CycloneDX document. Bundled mode is the default because the magic of mpm sbom is collapsing N different manager APIs into one self-contained file.

When I only need a fast inventory pass, the --minimal flag short-circuits the metadata extractors and produces today’s bare output (name, version, purl):

$ mpm --brew sbom --minimal > inventory.spdx.json

Use --minimal for snapshot-style runs (cron jobs, drift detection) and --bundled (the default) for compliance, supply-chain audit, and vulnerability-scanner ingestion.

Layered SBOMs: aggregate + per-package upstream

Some package managers now publish their own per-package SBOM documents. Homebrew, for example, writes <prefix>/Cellar/<formula>/<version>/sbom.spdx.json when a formula is installed under HOMEBREW_SBOM=1 (added in 5.2.0). These are full SPDX 2.3 documents with the formula’s complete dependency closure, real download URLs, and bottle checksums.

mpm sbom --bundled discovers those files, splices them into the aggregate document, and records each one in externalDocumentRefs with its SHA1 so the merge is auditable. Transitive packages from the upstream document are renamed under a SPDXRef-brew-<formula>-<dep> namespace to avoid collisions across formulae that share dependencies.

For the same data in CycloneDX, the per-formula file is attached to its component via an externalReferences[type=bom] entry.

If HOMEBREW_SBOM=1 was never set, the file does not exist and mpm falls back silently to brew info --json=v2 for the same fields.

To get the deepest data possible:

$ HOMEBREW_SBOM=1 brew reinstall <formula>
$ mpm --brew sbom > deep.spdx.json

Coverage matrix

Manager

License

Homepage

Download URL

Checksums

Dependency graph

Per-package SBOM

Vulnerabilities

Homebrew

✓ (opt-in)

pip

✓ (--network)

npm

✓ (--network)

cargo

✓ (--network)

gem

✓ (--network)

composer

✓ (--network)

Others

Coverage will expand: every manager exposes its metadata differently, and richer extractors land per manager over time. The vulnerability column tracks OSV.dev’s indexed ecosystems; a manager OSV does not index (Homebrew, mas, the distro managers OSV needs a release qualifier for) gets no advisories rather than an error.

For the license column specifically, Tern is a useful reference: a Python tool that derives per-package licenses across OS package managers and integrates ScanCode for file-level license detection, the data mpm would need to fill licenses beyond Homebrew and pip.

How mpm compares to other SBOM tools

mpm is not the only tool that emits an SBOM. The widely-used ones each occupy a different spot in the supply-chain landscape:

Tool

By

Language

License

What it reads

SPDX

CycloneDX

purl

mpm

this project

Python

GPL-2.0-or-later

the live package managers on a host, queried directly

✓ 2.3

✓ 1.7

Syft

Anchore

Go

Apache-2.0

container images and filesystems (package DBs, lockfiles)

✓ 2.3

✓ 1.6

Trivy

Aqua Security

Go

Apache-2.0

images, filesystems, repositories, VMs, clusters

✓ 2.3

✓ 1.5

Tern

tern-tools

Python

BSD-2-Clause

container image layers (runs package managers in a chroot)

cdxgen

OWASP CycloneDX

JavaScript

Apache-2.0

project manifests and lockfiles; a live host via obom

✓ 3.0

✓ 1.7

component-detection

Microsoft

C#

MIT

source-tree manifests and lockfiles (~30 detectors)

via sbom-tool

own schema

sbom-tool

Microsoft

C#

MIT

build output and source tree (wraps component-detection)

✓ 2.2, 3.0

Versions shown are each tool’s current default; Syft and cdxgen can also emit older spec revisions on request. mpm is on the newest CycloneDX (1.7), and cdxgen and sbom-tool reach the newest SPDX (3.0). Tern’s README states no spec versions or purl support for its output.

mpm differs from these tools in its data source, not its output format. Syft and Trivy read packages at rest: they parse the package databases already written into a container image or filesystem (the dpkg, apk, or rpm database, or a committed lockfile). cdxgen, component-detection, and sbom-tool parse a project’s declared manifests and lockfiles.

mpm invokes the package managers’ own command-line tools. It shells out to brew, apt, pip, npm, cargo, and the rest, and records what they report on the running host. That covers managers the file scanners do not model: Homebrew casks, mas, flatpak, snap, mise, and the others listed in Benchmark. The trade-off is symmetric: mpm needs the managers installed and runnable, while Syft or Trivy can scan an image or directory the host never executed.

This invoke-the-real-tool approach is not unique to mpm. CycloneDX’s own cargo-cyclonedx invokes Cargo rather than only parsing Cargo.lock, and Tern runs each container layer’s package manager in a chroot rather than reading its on-disk database. Both reflect what the manager itself resolves. mpm applies that principle across every manager it drives, on the live host.

The tools are complementary. Reach for Syft, Trivy, or cdxgen to inventory a build artifact, container, or source repository; reach for mpm sbom to inventory the software actually installed on a machine.

Vulnerability scanning

By default mpm sbom works entirely offline. Pass the global --network flag to enrich the document with known vulnerabilities, looked up against OSV.dev:

$ mpm --network sbom --cyclonedx > inventory.cdx.json

In CycloneDX output each advisory lands in the document’s vulnerabilities array, described once and pointing (through affects) at every component it impacts, with its severity rating, CVSS vector, CWE ids, aliases (the CVE behind a GHSA, for instance), and advisory links. SPDX 2.3 has no first-class vulnerability section, so each advisory is attached to its package as a SECURITY-category external reference of type advisory, with the severity and fixed-version facts folded into the reference comment.

Coverage tracks OSV’s ecosystems: language managers like pip, npm, cargo, gem, and composer resolve to OSV ecosystems and get scanned; system managers like Homebrew are not indexed by OSV, so their packages simply come back without advisories. Responses are cached on disk (under the OS user-cache directory) so repeat scans are fast and stay within OSV’s rate limits.

Network failures degrade gracefully: a missing extra, an unreachable OSV, or an unwritable cache logs a warning and still produces the SBOM, just without vulnerability data.

Caution

Running --network transmits the ecosystem coordinates of your installed packages (name, version, ecosystem) to OSV.dev. The offline default never makes network calls.

Installation

SBOM export is gated behind optional extras, split by usage:

  • [sbom-offline] pulls the CycloneDX and SPDX writer libraries needed to render documents from local data. This is what mpm sbom needs for its default offline operation.

  • [sbom-online] adds the HTTP client and cache used by mpm --network sbom for the OSV.dev vulnerability lookups described above.

$ pip install meta-package-manager[sbom-offline]

For vulnerability scanning, install both:

$ uv tool install 'meta-package-manager[sbom-offline,sbom-online]'

Without [sbom-offline], mpm sbom exits with an explanatory error pointing at this install step. Without [sbom-online], the --network flag logs a warning and falls back to an offline document.

See also

  • JSON & CSV exports — JSON and CSV table exports for ad-hoc piping of installed, outdated, and search results.

  • Snapshot and export — TOML manifest and Brewfile snapshots for re-installation workflows.

  • Cooldown — release-age gates that complement the SBOM workflow on the install side.

meta_package_manager.sbom API

Format-agnostic SBOM base class and export-format enum.

Kept deliberately free of SPDX or CycloneDX dependencies: instantiating SBOM directly is meaningless, but importing the symbols here is safe even when the optional [sbom-offline] extra is not installed.

class meta_package_manager.sbom.base.ExportFormat(*values)[source]

Bases: StrEnum

A user-friendly version of spdx_tools.spdx.formats.FileFormat.

Map format to user-friendly IDs.

JSON = 'json'
XML = 'xml'
YAML = 'yaml'
TAG_VALUE = 'tag'
RDF_XML = 'rdf'
class meta_package_manager.sbom.base.SBOM(export_format=ExportFormat.JSON)[source]

Bases: object

Utilities shared by all SBOM classes.

See also

Anchore’s Syft and Microsoft’s sbom-tool are mature SPDX and CycloneDX emitters, useful references for field-population conventions. Both inventory packages by parsing on-disk databases and lockfiles, whereas mpm queries the live managers directly.

Defaults to JSON export format.

packages_per_manager: dict[str, int]
enriched_per_manager: dict[str, int]
vulnerabilities_by_purl: dict[str, tuple[Vulnerability, ...]]
all_purls()[source]

Yield every package purl present in the document.

Powers the vulnerability scan: the network layer queries OSV once with the full purl set rather than once per package. Subclasses implement this against their own component index.

Return type:

Iterator[str]

attach_vulnerabilities(vulnerabilities)[source]

Bind cross-package vulnerability data to the document.

Called by the CLI between the per-package add_package loop and finalize, only in --network mode. Renderers read the stored data in their finalize override and project it into the format-native vulnerability surface (CycloneDX vulnerabilities array, SPDX security externalRefs).

Return type:

None

stats()[source]

Return a summary of what landed in the document.

Format-agnostic counters live in the base implementation; SPDX and CycloneDX subclasses extend the returned dict with their own merged-documents, dependency-graph, and any other format-specific counts. Surfaced by the CLI as a post-run INFO-level summary and usable by tests or programmatic consumers without re-parsing the rendered document.

Return type:

dict[str, object]

finalize()[source]

Resolve any deferred state before export().

Some constructs cannot be emitted at add_package() time because they reference packages that may not have been added yet: a Homebrew formula’s runtime dependency on another formula listed later in the scan, for example. Subclasses queue those during add_package and flush them here. The base implementation is a no-op so subclasses can rely on it being called exactly once.

Return type:

None

static autodetect_export_format(file_path)[source]

Better version of spdx_tools.spdx.formats.file_name_to_format which is based on Path objects and is case-insensitive.

Todo

Contribute generic autodetection method to Click Extra?

Return type:

ExportFormat | None

CycloneDX 1.7 writer.

Heavy cyclonedx-python-lib imports are guarded behind a try/except block; cyclonedx_support reports whether the CycloneDX class can actually be used.

The license-normalization helper is shared with spdx and is imported from there rather than duplicated: SPDX license expressions are the lingua franca CycloneDX builds on, so the dependency direction is intentional and acyclic.

class meta_package_manager.sbom.cyclonedx.CycloneDX(export_format=ExportFormat.JSON)[source]

Bases: SBOM

Generates a CycloneDX document from a list of packages.

CycloneDX 1.7 specifications.

Defaults to JSON export format.

document: Bom
component_index: dict[tuple[str, str], Component]
pending_dependencies: list[tuple[Component, str, str]]
init_doc()[source]

CycloneDX document metadata specifications.

Return type:

None

add_package(manager, package, metadata=PackageMetadata(download_url=None, homepage=None, vcs_url=None, issue_tracker_url=None, distribution_url=None, license_declared=None, license_concluded=None, copyright_text=None, supplier=None, originator=None, description=None, summary=None, cpe=None, dependencies=(), checksums=(), files=(), files_analyzed=False, install_date=None, build_date=None, release_date=None, external_sbom_path=None, extra_purls=(), extras={}))[source]

CycloneDX package metadata specifications.

Return type:

None

all_purls()[source]

Yield every component purl in insertion order.

Each component’s bom_ref is its purl string, so the same values double as the vulnerability affects targets in finalize().

Return type:

Any

finalize()[source]

Resolve queued dependency edges and attach vulnerability records.

Mirrors meta_package_manager.sbom.spdx.SPDX.finalize(). Dangling references (the dependency target is not in the inventory) are dropped silently.

Vulnerability data bound via meta_package_manager.sbom.base.SBOM.attach_vulnerabilities() is projected into the CycloneDX vulnerabilities array. Each advisory is described once at the document level with an affects list pointing at every component (by bom_ref, which equals the purl) it impacts.

Return type:

None

stats()[source]

Extend the base stats with CycloneDX-specific counters.

CycloneDX has no merge-content equivalent: per-package upstream SBOMs are linked through externalReferences[type=bom] rather than spliced in. The merged-document count therefore reports the number of components carrying a BOM external reference. The dependency-edge total walks the registered dependency graph and sums the dependsOn collection size across every entry.

Return type:

dict[str, object]

export()[source]

Serialize the document to its string representation.

Note

Unlike meta_package_manager.sbom.spdx.SPDX.export(), the generated document is not validated against its schema here. CycloneDX schema validation relies on cyclonedx-python-lib’s [validation] extra, which pulls in jsonschema and, transitively, rfc3987-syntax, lark, and lxml. To keep that stack out of mpm’s runtime dependencies, the validation runs in the test suite instead. See tests/test_cli_sbom.py.

Return type:

str

SPDX 2.3 writer plus the per-package upstream-SBOM merge logic.

Heavy spdx_tools imports are guarded behind a try/except block so this module is importable even when the optional [sbom-offline] extra is not installed; in that case spdx_support is False and the SPDX class is still defined for type-hint compatibility but will not function (every public method depends on the missing imports).

_parse_license_expression and _coerce_spdx_string live here because they both touch spdx_tools types; cyclonedx imports the former for its own license normalization, which is one-way and acyclic.

class meta_package_manager.sbom.spdx.SPDX(export_format=ExportFormat.JSON)[source]

Bases: SBOM

Generates an SPDX document from a list of packages.

SPDX 2.3 specifications.

Defaults to JSON export format.

DOC_ID = 'SPDXRef-DOCUMENT'

Document root ID.

document: Document
seen_ids: set[str]
name_index: dict[tuple[str, str], str]
pending_relationships: list[tuple[str, str, str, Any]]
merged_docs: dict[str, str]
classmethod normalize_spdx_id(value)[source]

SPDX IDs must only contain letters, numbers, . and -.

Return type:

str

init_doc()[source]

SPDX document metadata specifications.

Return type:

None

add_package(manager, package, metadata=PackageMetadata(download_url=None, homepage=None, vcs_url=None, issue_tracker_url=None, distribution_url=None, license_declared=None, license_concluded=None, copyright_text=None, supplier=None, originator=None, description=None, summary=None, cpe=None, dependencies=(), checksums=(), files=(), files_analyzed=False, install_date=None, build_date=None, release_date=None, external_sbom_path=None, extra_purls=(), extras={}))[source]

SPDX package metadata specifications.

Return type:

None

all_purls()[source]

Yield every inventory package purl in insertion order.

Only the directly-installed packages carry a purl in purl_index; transitive packages spliced in from merged upstream SBOMs are not queried for vulnerabilities (their own upstream document already carries that provenance, and they are not what the user installed).

Return type:

Any

finalize()[source]

Emit pending dependency relationships and vulnerability refs.

Walks the queue built by add_package() and emits each relationship only when both ends resolve to packages we actually included in the document. Dangling references (the target package is not installed) are dropped silently: the SBOM only describes what is on the system, not what could be.

Then attaches any vulnerability data bound via attach_vulnerabilities(). SPDX 2.3 has no first-class vulnerability section, so each advisory becomes a SECURITY-category ExternalPackageRef of type advisory on the affected package, pointing at the advisory URL.

Return type:

None

stats()[source]

Extend the base stats with SPDX-specific counters.

Adds the number of upstream documents merged into the aggregate, the count of transitive packages those upstream documents contributed (over and above the inventory pass), and the total relationship count partitioned into dependency vs descriptive edges. packages_total from the base reports inventory packages only; packages_in_document here is the full count after merge, which is what consumers of the file actually see.

Return type:

dict[str, object]

export()[source]

Similar to spdx_tools.spdx.writer.write_anything.write_file but write directly to provided stream instead of file path.

Return type:

str

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, the rendered document is standard CycloneDX/SPDX, so anyone needing system-package coverage can feed it to an external CPE-based scanner (OSV-Scanner, Grype, Trivy, or Intel’s cve-bin-tool).

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_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, ...]]

HTTP client and on-disk response cache for the opt-in online SBOM mode.

This is the shared plumbing behind mpm --network sbom. The NetworkClient wraps httpx with a filesystem cache and a bounded retry/backoff policy, so the higher-level adapters (currently just meta_package_manager.sbom.vulnerabilities, which queries OSV.dev) stay free of transport concerns.

Heavy imports (httpx, platformdirs) are guarded behind a try/except exactly like the SPDX and CycloneDX writers: a default install does not pull them, so this module is importable but network_support reports False until the user installs the [sbom-online] extra.

The cache is mandatory rather than optional. The online mode is only worth using with a warm cache: vulnerability records are immutable once published, batch queries are large, and remote services rate-limit. The cache lives under the OS-appropriate user cache directory (resolved via platformdirs) so repeat runs hit disk instead of the network.

meta_package_manager.sbom._network.DEFAULT_TTL = 86400

Default cache time-to-live in seconds (24 hours).

Vulnerability listings (which advisories affect a package) can change as new advisories are published, so the batch-query responses get this finite TTL. Immutable per-advisory detail records are cached with a far longer TTL by their callers.

meta_package_manager.sbom._network.DEFAULT_TIMEOUT = 30.0

Per-request timeout in seconds.

OSV batch queries over a few hundred purls comfortably answer within this window; the value is generous enough to absorb a slow link without hanging a scan indefinitely.

meta_package_manager.sbom._network.MAX_RETRIES = 3

Number of retry attempts on transient failures before giving up.

meta_package_manager.sbom._network.CACHE_SIZE_CEILING = 1000000000

Soft ceiling (1 GB) past which the cache directory is pruned.

The cache is keyed by unique request payloads the user has ever issued, so in practice it stays tiny (a few MB of JSON). The ceiling is a runaway backstop, not an expected operating point.

exception meta_package_manager.sbom._network.NetworkError[source]

Bases: Exception

Raised when a network operation cannot complete.

The CLI catches this at the orchestration layer and degrades gracefully: the SBOM still renders, just without the data the failed call would have contributed.

class meta_package_manager.sbom._network.NetworkClient(*, cache_dir=None, default_ttl=86400, timeout=30.0, trust_env=True)[source]

Bases: object

Caching HTTP client for the online SBOM adapters.

Construct one per mpm sbom run and pass it to the adapter functions. The same instance reuses a single httpx.Client (connection pooling) and one cache directory for the whole run.

Warning

Instantiating requires the [sbom-online] extra. Callers must check network_support before constructing, mirroring the spdx_support / cyclonedx_support guards used by the renderers.

Set up the cache directory and the underlying HTTP client.

cache_dir defaults to <user-cache>/meta-package-manager/sbom when not supplied. The directory is created if missing.

trust_env is forwarded to httpx.Client: left True so a user’s HTTP(S)_PROXY / ALL_PROXY environment is honored. The test suite sets it False to bypass any ambient proxy.

property client: Client

The underlying HTTP client, constructed on first access.

A construction failure (notably a configured SOCKS proxy with no socksio installed) is converted to NetworkError so the caller degrades gracefully rather than surfacing a raw ImportError from deep in httpx.

close()[source]

Release the underlying HTTP connection pool, if one was opened.

Return type:

None

get(url, *, ttl=None)[source]

GET url, returning the decoded JSON body (cached).

Return type:

object

post(url, json_body, *, ttl=None)[source]

POST json_body to url, returning the decoded JSON body (cached).

Return type:

object