56 lines
2.0 KiB
Python
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()
|