#!/usr/bin/env python3 """Generate deterministic SBOM composition fixtures. Outputs fragment JSON, DSSE envelopes, a composition manifest, a composed CycloneDX BOM, and a hashes file suitable for offline verification. """ import base64 import hashlib import json from pathlib import Path ROOT = Path(__file__).parent def canonical(obj) -> str: return json.dumps(obj, separators=(",", ":"), sort_keys=True) def write_json(path: Path, obj) -> str: text = canonical(obj) + "\n" path.write_text(text) return hashlib.sha256(text.encode()).hexdigest() def merkle_root(hex_hashes: list[str]) -> str: if not hex_hashes: return "" nodes = [bytes.fromhex(h) for h in hex_hashes] while len(nodes) > 1: nxt = [] it = iter(nodes) for a in it: b = next(it, a) nxt.append(hashlib.sha256(a + b).digest()) nodes = nxt return nodes[0].hex() def dsse_envelope(payload_json: str, label: str) -> dict: payload_b64 = base64.b64encode(payload_json.encode()).decode() signature = base64.b64encode(f"deterministic-fixture-{label}".encode()).decode() return { "payloadType": "application/vnd.stellaops.scanner.fragment+json", "payload": payload_b64, "signatures": [ { "keyid": "demo-ed25519", "sig": signature, } ], } def main() -> None: ROOT.mkdir(parents=True, exist_ok=True) fragments_src = [ { "schemaVersion": "1.0", "layerDigest": "sha256:1111111111111111111111111111111111111111111111111111111111111101", "generatedAtUtc": "2025-12-01T00:00:00Z", "components": [ { "identity": {"purl": "pkg:apk/alpine/busybox@1.36.1"}, "evidence": { "paths": ["/bin/busybox"], "licenses": ["BSD-3-Clause"], "contentHash": "sha256:7c3f4d30bfcd8ff2b09f1ba39f43c524d6ce1b7a5f3c2bde321e0f5e0e6c3d10", }, "source": "apk", } ], }, { "schemaVersion": "1.0", "layerDigest": "sha256:2222222222222222222222222222222222222222222222222222222222222202", "generatedAtUtc": "2025-12-01T00:00:00Z", "components": [ { "identity": {"purl": "pkg:npm/express@4.18.2"}, "evidence": { "paths": ["/workspace/node_modules/express"], "licenses": ["MIT"], "contentHash": "sha256:8ab103fed58e7dc0b1819b7358126d1c44cc979f409775888b859504a829148b", }, "source": "npm", } ], }, ] fragments_meta = [] for idx, fragment in enumerate(fragments_src, start=1): fragment_path = ROOT / f"fragment-layer{idx}.json" fragment_hash = write_json(fragment_path, fragment) envelope_obj = dsse_envelope(canonical(fragment), f"fragment-{idx}") envelope_path = ROOT / f"fragment-layer{idx}.dsse.json" envelope_hash = write_json(envelope_path, envelope_obj) fragments_meta.append( { "layerDigest": fragment["layerDigest"], "fragmentPath": fragment_path.name, "dssePath": envelope_path.name, "fragmentSha256": fragment_hash, "dsseEnvelopeSha256": envelope_hash, } ) fragments_meta.sort(key=lambda f: f["layerDigest"]) merkle = merkle_root([f["fragmentSha256"] for f in fragments_meta]) bom = { "bomFormat": "CycloneDX", "specVersion": "1.6", "serialNumber": "urn:uuid:00000000-7e57-4c0d-baad-000000000301", "version": 1, "metadata": { "timestamp": "2025-12-03T00:00:00Z", "component": { "type": "container", "bom-ref": "pkg:docker/registry.local/demo@sha256:9999999999999999999999999999999999999999999999999999999999999900", "name": "registry.local/demo", "purl": "pkg:docker/registry.local/demo@sha256:9999999999999999999999999999999999999999999999999999999999999900", }, }, "components": [ { "bom-ref": "pkg:apk/alpine/busybox@1.36.1", "type": "library", "name": "busybox", "version": "1.36.1", "purl": "pkg:apk/alpine/busybox@1.36.1", "properties": [ {"name": "stellaops:stella.contentHash", "value": fragments_meta[0]["fragmentSha256"]} ], }, { "bom-ref": "pkg:npm/express@4.18.2", "type": "library", "name": "express", "version": "4.18.2", "purl": "pkg:npm/express@4.18.2", "properties": [ {"name": "stellaops:stella.contentHash", "value": fragments_meta[1]["fragmentSha256"]} ], }, ], "properties": [ {"name": "stellaops:merkle.root", "value": merkle}, {"name": "stellaops:composition.manifest", "value": "cas://scanner/deterministic-compose/_composition.json"}, {"name": "stellaops:stella.contentHash", "value": merkle}, ], } bom_path = ROOT / "bom.cdx.json" bom_hash = write_json(bom_path, bom) composition = { "schemaVersion": "1.0", "imageDigest": "sha256:9999999999999999999999999999999999999999999999999999999999999900", "generatedAtUtc": "2025-12-03T00:00:00Z", "fragments": fragments_meta, "merkleRootSha256": merkle, "composedBomSha256": bom_hash, "determinism": { "fixedClock": "2025-12-01T00:00:00Z", "rngSeed": 1337, "feedSnapshotId": "feeds-2025.320.1", "policySnapshotId": "policy-2025.310.0", }, } composition_path = ROOT / "_composition.json" composition_hash = write_json(composition_path, composition) hashes = { "_composition.json": composition_hash, "bom.cdx.json": bom_hash, } for meta in fragments_meta: hashes[meta["fragmentPath"]] = meta["fragmentSha256"] hashes[meta["dssePath"]] = meta["dsseEnvelopeSha256"] hashes_lines = [f"{hashes[name]} {name}" for name in sorted(hashes.keys())] (ROOT / "hashes.txt").write_text("\n".join(hashes_lines) + "\n") if __name__ == "__main__": main()