Files
ITWorx_MCP_Hub-Public/scripts/generate_image_sboms.py
T

96 lines
2.9 KiB
Python

#!/usr/bin/env python3
"""Generate a CycloneDX SBOM per owned image using trivy, plus a digest index."""
from __future__ import annotations
import argparse
import json
import subprocess
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[1]
OWNED_SERVICES = (
"control-api",
"worker",
"dataplane-router",
"web",
"connector-knowledge",
"connector-gitea",
"connector-unraid",
"connector-mobilityops",
"connector-projectbrain",
"connector-appops",
"connector-stitch",
"docker-read-broker",
"observability-collector",
)
def _image_id(tag: str) -> str:
command = ["docker", "inspect", "--format", "{{.Id}}", tag]
return subprocess.run( # noqa: S603
command,
cwd=ROOT,
capture_output=True,
text=True,
check=True,
).stdout.strip()
def generate_for_image(tag: str, service: str, output_dir: Path) -> dict[str, Any]:
sbom_path = output_dir / f"{service}.cdx.json"
command = ["trivy", "image", "--format", "cyclonedx", "--output", str(sbom_path), tag]
subprocess.run( # noqa: S603
command,
cwd=ROOT,
check=True,
)
sbom = json.loads(sbom_path.read_text(encoding="utf-8"))
return {
"service": service,
"tag": tag,
"imageId": _image_id(tag),
"sbomPath": str(sbom_path.relative_to(ROOT)),
"componentCount": len(sbom.get("components", [])),
}
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--tag-prefix",
default="local/itworx-mcp-hub-",
help="image tag prefix; service name and ':dev' are appended",
)
parser.add_argument("--output-dir", type=Path, required=True)
args = parser.parse_args()
args.output_dir = args.output_dir.resolve()
args.output_dir.mkdir(parents=True, exist_ok=True)
index = []
# docs/09-release/PRODUCTION_PROMOTION_RUNBOOK.md is the authoritative
# naming source (WP-190 corrected an earlier "control" repository-name
# mismatch -- the real pushed/deployed image is "control-api", matching
# its SERVICE_NAME build arg, not a shortened alias). Only
# dataplane-router and observability-collector use a distinct,
# deliberately shorter tag name.
tag_service_overrides = {
"dataplane-router": "dataplane",
"observability-collector": "observability",
}
for service in OWNED_SERVICES:
tag_service = tag_service_overrides.get(service, service)
tag = f"{args.tag_prefix}{tag_service}:dev"
index.append(generate_for_image(tag, service, args.output_dir))
(args.output_dir / "INDEX.json").write_text(
json.dumps({"schemaVersion": "1.0", "images": index}, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
print(f"wrote {len(index)} image SBOMs to {args.output_dir}")
return 0
if __name__ == "__main__":
raise SystemExit(main())