96 lines
2.9 KiB
Python
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())
|