#!/usr/bin/env python3 """Generate a deterministic, dependency-only CycloneDX SBOM for the M14 OCI images.""" from __future__ import annotations import json import re import tomllib from pathlib import Path ROOT = Path(__file__).resolve().parents[1] OUTPUT = ROOT / "docs" / "release" / "m14-sbom.json" def component(name: str, version: str, ecosystem: str) -> dict[str, str]: return { "name": name, "type": "library", "version": version, "purl": f"pkg:{ecosystem}/{name}@{version}", } def main() -> None: try: project = tomllib.loads((ROOT / "backend" / "pyproject.toml").read_text()) package_lock = json.loads((ROOT / "frontend" / "package-lock.json").read_text()) except (OSError, json.JSONDecodeError, tomllib.TOMLDecodeError) as error: raise RuntimeError("could not load pinned dependency metadata") from error components: list[dict[str, str]] = [] for dependency in project["project"]["dependencies"]: match = re.fullmatch( r"([A-Za-z0-9_.-]+)(?:\[[A-Za-z0-9_,-]+\])?==([A-Za-z0-9_.+-]+)", dependency ) if match is None: raise ValueError(f"un-pinned Python dependency: {dependency}") components.append(component(match.group(1), match.group(2), "pypi")) for path, item in package_lock.get("packages", {}).items(): if not path.startswith("node_modules/") or "version" not in item: continue components.append(component(path.removeprefix("node_modules/"), item["version"], "npm")) payload = { "bomFormat": "CycloneDX", "components": sorted(components, key=lambda item: (item["purl"], item["version"])), "metadata": { "component": component("backup-tool", project["project"]["version"], "generic") }, "specVersion": "1.5", "version": 1, } OUTPUT.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n") if __name__ == "__main__": main()