#!/usr/bin/env python3 """Generate a CycloneDX SBOM from the resolved GovOPlaN release environment.""" from __future__ import annotations import argparse import json import os from pathlib import Path import subprocess import sys import tomllib META_ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(META_ROOT / "tools" / "release")) from govoplan_release.sbom import ( # noqa: E402 build_release_sbom, parse_sbom_timestamp, render_sbom, sbom_sha256, timestamp_from_source_date_epoch, validate_sbom, ) def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--python", default=sys.executable, help="Python executable for pip inspect.") parser.add_argument("--core-root", type=Path, default=META_ROOT.parent / "govoplan-core") parser.add_argument( "--output", type=Path, default=META_ROOT / "runtime" / "release-artifacts" / "govoplan-sbom.cdx.json", ) parser.add_argument("--pip-inspect", type=Path, help="Use an existing pip inspect report instead of executing pip.") parser.add_argument( "--timestamp", help="ISO-8601 build timestamp. Defaults to SOURCE_DATE_EPOCH, then the current time.", ) args = parser.parse_args() core_root = args.core_root.expanduser().resolve() product_version = _project_version(core_root / "pyproject.toml") npm_lock_path = core_root / "webui" / "package-lock.release.json" if not npm_lock_path.exists(): parser.error(f"release npm lockfile does not exist: {npm_lock_path}") pip_inspect = _read_json(args.pip_inspect) if args.pip_inspect else _run_pip_inspect(args.python) npm_lock = _read_json(npm_lock_path) try: timestamp = _resolved_timestamp(args.timestamp) payload = build_release_sbom( product_version=product_version, pip_inspect=pip_inspect, npm_lock=npm_lock, timestamp=timestamp, ) except ValueError as exc: parser.error(str(exc)) validate_sbom(payload) output = args.output.expanduser().resolve() output.parent.mkdir(parents=True, exist_ok=True) output.write_text(render_sbom(payload), encoding="utf-8") print(f"Wrote {len(payload['components'])} components to {output}") print(f"SHA-256: {sbom_sha256(payload)}") return 0 def _run_pip_inspect(python: str) -> dict: result = subprocess.run( [python, "-m", "pip", "inspect", "--local"], check=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=120, ) if result.returncode != 0: raise SystemExit(f"pip inspect failed: {result.stderr.strip()}") return json.loads(result.stdout) def _read_json(path: Path) -> dict: payload = json.loads(path.read_text(encoding="utf-8")) if not isinstance(payload, dict): raise ValueError(f"expected a JSON object in {path}") return payload def _project_version(path: Path) -> str: with path.open("rb") as handle: payload = tomllib.load(handle) project = payload.get("project") if not isinstance(project, dict) or not isinstance(project.get("version"), str): raise ValueError(f"project.version is missing from {path}") return project["version"] def _resolved_timestamp(explicit: str | None): if explicit is not None: return parse_sbom_timestamp(explicit) source_date_epoch = os.environ.get("SOURCE_DATE_EPOCH") if source_date_epoch is not None: return timestamp_from_source_date_epoch(source_date_epoch) return None if __name__ == "__main__": raise SystemExit(main())