Add tests and implement timeline ingestion options with NATS and Redis subscribers
- Introduced `BinaryReachabilityLifterTests` to validate binary lifting functionality. - Created `PackRunWorkerOptions` for configuring worker paths and execution persistence. - Added `TimelineIngestionOptions` for configuring NATS and Redis ingestion transports. - Implemented `NatsTimelineEventSubscriber` for subscribing to NATS events. - Developed `RedisTimelineEventSubscriber` for reading from Redis Streams. - Added `TimelineEnvelopeParser` to normalize incoming event envelopes. - Created unit tests for `TimelineEnvelopeParser` to ensure correct field mapping. - Implemented `TimelineAuthorizationAuditSink` for logging authorization outcomes.
This commit is contained in:
190
docs/modules/scanner/fixtures/deterministic-compose/generate.py
Normal file
190
docs/modules/scanner/fixtures/deterministic-compose/generate.py
Normal file
@@ -0,0 +1,190 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user