Software supply chain regulations, including the European Union Cyber Resilience Act, increasingly require teams to maintain an accurate Software Bill of Materials (SBOM). Many engineering workflows attempt to fulfill this by parsing static lockfiles like requirements.txt, Pipfile.lock, or poetry.lock.
Static manifests do not always reflect the true state of an execution environment. Packages can be manually upgraded, monkey-patched, installed with unrecorded flags, or introduced via operating system package managers. A declared dependency file records intent; it does not record what is physically installed on disk at runtime.
To achieve genuine inventory auditing, you must inspect the active environment directly. This article demonstrates how to use sbom_snapshot_installed_environment.py, a standalone Python tool that discovers installed packages across ecosystems, verifies their cryptographic metadata, and exports standard CycloneDX and SPDX documents without third-party dependencies.
Install & Usage
The script relies solely on the Python standard library and requires Python 3.8 or newer to leverage importlib.metadata. There are no external dependencies to install with pip.
To install the tool, download the single file, save it as sbom_snapshot_installed_environment.py, and run it with Python:
python sbom_snapshot_installed_environment.py --help
Because the utility uses built-in modules—such as importlib.metadata, pathlib, hashlib, json, and subprocess—it can execute in isolated environments without needing external network access or installation overhead.
You can inspect command-line options via the help flag or import the core collection routines directly into your internal tooling to snapshot local environments programmatically.
How It Works
The tool is structured around three primary capabilities: multi-ecosystem component discovery, cryptographic integrity calculation, and schema generation.
1. Python Distribution Discovery
The script queries the Python runtime directly rather than invoking external shell processes. Using importlib.metadata, it walks through all distributions installed in the active environment's search path.
Here is the concrete discovery implementation from the tool:
def collect_python_components() -> List[Component]:
"""Discover actually installed Python distributions via importlib.metadata."""
components: List[Component] = []
seen: set = set()
for dist in importlib.metadata.distributions():
meta = dist.metadata
name = meta.get("Name")
if not name:
continue
version = meta.get("Version", "0.0.0")
key = (name.lower(), version)
if key in seen:
continue
seen.add(key)
# Detect license
license_str = meta.get("License-Expression") or meta.get("License") or "NOASSERTION"
if len(license_str.splitlines()) > 1 or license_str.strip().upper() == "UNKNOWN":
classifiers = meta.get_all("Classifier") or []
license_classifiers = [c.split("::")[-1].strip() for c in classifiers if "License ::" in c]
if license_classifiers:
license_str = license_classifiers[0]
else:
license_str = "NOASSERTION"
author = meta.get("Author") or meta.get("Author-email") or meta.get("Maintainer") or "UNKNOWN"
summary = meta.get("Summary") or ""
# Deterministic component hash based on RECORD file or metadata
sha256_val = None
install_path = None
try:
locate_res = dist.locate_file("")
if locate_res:
install_path = str(locate_res)
record = dist.read_text("RECORD")
if record:
sha256_val = hashlib.sha256(record.encode("utf-8")).hexdigest()
else:
meta_text = dist.read_text("METADATA") or dist.read_text("PKG-INFO")
if meta_text:
sha256_val = hashlib.sha256(meta_text.encode("utf-8")).hexdigest()
except Exception:
pass
comp = Component(
name=name,
version=version,
ecosystem="pypi",
license_name=license_str,
supplier=author,
description=summary,
sha256=sha256_val,
installed_path=install_path,
)
components.append(comp)
components.sort(key=lambda c: c.name.lower())
return components
This extraction logic handles several edge cases common to Python packaging:
- If a package metadata entry contains an unstructured or multiline license description, the code inspects the package classifiers for an explicit
License ::entry. - It computes a deterministic SHA-256 checksum from the package's installed
RECORDmanifest. If noRECORDis available, it hashes the installedMETADATAorPKG-INFOtext. - Each entry is converted to a normalized Package URL (
pkg:pypi/<name>@<version>).
2. Node.js and Operating System Collectors
In addition to Python distributions, the code includes targeted collectors for other components resident on the machine:
-
Node.js: The
collect_npm_componentsfunction recursively scans a specifiednode_modulesdirectory forpackage.jsonmanifests, ignoring hidden files, and records package versions, authors, and directory locations. -
Debian / Ubuntu: The
collect_dpkg_componentsfunction checks fordpkg-queryviashutil.whichand queries installed system packages. -
Red Hat / RPM: The
collect_rpm_componentsfunction executesrpm -qawhen available to catalog host-level RPM packages.
3. Standards-Compliant SBOM Generation
Once components are collected into internal Component models, the tool exports them to standard formats such as CycloneDX 1.5 or SPDX 2.3.
The CycloneDX builder structures the output according to the official specification:
def build_cyclonedx(components: List[Component], doc_name: str = "Installed Environment Snapshot") -> Dict[str, Any]:
"""Generate CycloneDX v1.5 JSON representation."""
now_iso = datetime.datetime.now(datetime.timezone.utc).isoformat()
bom_uuid = str(uuid.uuid4())
cdx_components: List[Dict[str, Any]] = []
for c in components:
entry: Dict[str, Any] = {
"type": "library" if c.ecosystem in ("pypi", "npm") else "operating-system",
"name": c.name,
"version": c.version,
"purl": c.purl,
"licenses": [{"license": {"name": c.license}}],
}
if c.description:
entry["description"] = c.description
if c.supplier and c.supplier != "UNKNOWN":
entry["supplier"] = {"name": c.supplier}
if c.sha256:
entry["hashes"] = [{"alg": "SHA-256", "content": c.sha256}]
cdx_components.append(entry)
return {
"bomFormat": "CycloneDX",
"specVersion": "1.5",
"serialNumber": f"urn:uuid:{bom_uuid}",
"version": 1,
"metadata": {
"timestamp": now_iso,
"tools": [{"vendor": "CRA-Auditor", "name": "sbom-snapshot-installed-environment", "version": "1.0.0"}],
"component": {
"type": "application",
"name": doc_name,
"version": "1.0.0",
},
},
"components": cdx_components,
}
The output contains standard component declarations, unique serial numbers, UTC timestamps, cryptographic digests, and package URLs ready for ingestion by downstream vulnerability scanners.
Limitations
Before deploying the tool in production audit pipelines, keep the following technical boundaries in mind:
-
Python Version Constraint: The script explicitly terminates on Python runtimes older than 3.8 because it relies on standard library features introduced in
importlib.metadata. -
File Hashing Limits: The internal
compute_file_sha256function skips any file whose size exceeds 10 megabytes (10 * 1024 * 1024bytes). Large distribution artifacts or nested assets above this size will not have an integrity hash attached. -
Host Tool Availability: System package discovery (
dpkgandrpm) requires the respective utilities to be present in the execution path. If neither command is detected, the script silently ignores operating system packages and only reports application-level components. -
License String Heuristics: Licenses are parsed from declared package metadata or classifiers. If a package maintainer left license fields empty or entered non-standard identifiers without relevant classifiers, the script labels the license as
NOASSERTION. -
Direct Directory Traversal for npm: The
collect_npm_componentsfunction readsnode_modulesfrom disk. If packages are stored in alternative package-manager stores or symlinked without being populated in the specified path, they will not appear in the inventory.
Frequently Asked Questions
Does the script make external HTTP requests?
No. The tool runs completely offline. It reads metadata strictly from the local filesystem and host package databases, making it safe for air-gapped systems and isolated build agents.
What happens if a Python package does not have a RECORD file?
When a package is installed without a wheel record (for example, via older editable installations), the script falls back to reading the METADATA or PKG-INFO file. It then generates the SHA-256 hash across that metadata content instead.
Can this tool run without administrative privileges?
Yes. Inspecting Python distributions and local node_modules directories requires only read permissions for the target environment paths. System package queries (dpkg-query and rpm) also run in unprivileged query mode.
Full version and the rest of the toolkit: SBOM Snapshot installed environment on PyForge. Product page: https://py-forge.com/products/sbom-snapshot-installed-environment
Top comments (0)