Files
backup-tool/tools/generate_sbom.py
T

56 lines
2.0 KiB
Python

#!/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()