# Proof and Evidence Chain Technical Reference **Source Advisories**: - 29-Nov-2025 - SBOM to VEX Proof Pipeline Blueprint - 01-Dec-2025 - Turning SBOM Data Into Verifiable Proofs - 01-Dec-2025 - Proof-Linked VEX User Interface - 02-Dec-2025 - Converting SBOM Data into Proof Chains - 06-Dec-2025 - How to Build a Verifiable SBOM→VEX Chain - 08-Dec-2025 - Defining Stella Ops' Proof‑Linked Advantage - 08-Dec-2025 - Designing UX for Signed Evidence Trails - 03-Dec-2025 - Comparing Proof‑Linked VEX UX Across Tools **Last Updated**: 2025-12-14 --- ## 1. CORE IDENTIFIERS & DATA MODEL ### 1.1 Canonical IDs ``` ArtifactID = sha256: SBOMEntryID = :[@] EvidenceID = hash(canonical_evidence_json) ReasoningID = hash(canonical_reasoning_json) VEXVerdictID = hash(canonical_vex_json) ProofBundleID = merkle_root(SBOMEntryID, EvidenceID[], ReasoningID, VEXVerdictID) GraphRevisionID = merkle_root(nodes[], edges[], policyDigest, feedsDigest, toolchainDigest, paramsDigest) TrustAnchorID = per-dependency anchor (public key + policy) ``` ### 1.2 Component Identifiers (bom-ref) ``` Format: pkg:/@?sha256= Examples: pkg:maven/org.apache.commons/commons-lang3@3.14.0?sha256= pkg:apk/alpine/openssl@3.2.1-r0?sha256=2c0f...54e pkg:oci/@sha256: pkg:npm/lodash@4.17.21 Rules: - Must be stable across regenerations for identical content - Independent of local paths, build numbers - Derived from canonical bytes - Used as CycloneDX bom-ref ``` ### 1.3 Subject Schema ```csharp public sealed record ProofSubject( string Name, // PURL or canonical URI IReadOnlyDictionary Digest // {"sha256": "...", "sha512": "..."} ); ``` ### 1.4 SBOM Identity ``` sbomId = sha256(canonical_sbom_bytes) ``` ### 1.5 Graph Revision ID (graphRev) Use `GraphRevisionID` as the stable snapshot identifier for an artifact's *decision graph* (facts + derived edges) so receipts, UI/API responses, logs, exports, and replays can be correlated without ambiguity. Rules: - Graph revisions are content-addressed: any input change produces a new `GraphRevisionID`. - Inputs must be canonicalized (stable ordering, stable casing, UTC timestamps stripped/isolated) before hashing. - A graph revision must bind to the scan manifest inputs: `sbomDigest`, `feedsDigest`, `policyDigest`, `toolchainDigest`, and `paramsDigest`. Recommended string format: ``` graphRevisionId = "grv_sha256:" + sha256(canonical_graph_bytes) ``` ### 1.6 Proof-of-Integrity Graph (build/runtime ancestry) Treat provenance as a first-class, append-only graph so "what is running?" can be traced back to "how was it built and attested?" Minimum model: - Nodes: `repo`, `commit`, `build`, `sbom`, `attestation`, `image`, `container`, `host` - Edges: `built_from`, `scanned_with`, `attests`, `deployed_as`, `executes_on`, `derived_from` - IDs: use content digests where possible (image digest, SBOM hash, DSSE hash); stable IDs for non-content entities (run IDs, host IDs) Operational rules: - Never delete/overwrite nodes; mark superseded instead. - Every UI traversal must be backed by API queries over this graph (no UI-only inference). ## 2. DSSE ENVELOPE STRUCTURES ### 2.1 Evidence Statement ```json { "payloadType": "application/vnd.in-toto+json", "payload": { "_type": "https://in-toto.io/Statement/v1", "subject": [{"name": "", "digest": {"sha256": "..."}}], "predicateType": "evidence.stella/v1", "predicate": { "source": "scanner/feed name", "sourceVersion": "tool version", "collectionTime": "2025-12-14T00:00:00Z", "sbomEntryId": "", "vulnerabilityId": "CVE-XXXX-YYYY", "rawFinding": "", "evidenceId": "" } }, "signatures": [{"keyid": "", "sig": "BASE64(SIG)"}] } ``` Signer: Scanner/Ingestor key ### 2.2 Reasoning Statement ```json { "payloadType": "application/vnd.in-toto+json", "payload": { "_type": "https://in-toto.io/Statement/v1", "subject": [{"name": "", "digest": {"sha256": "..."}}], "predicateType": "reasoning.stella/v1", "predicate": { "sbomEntryId": "", "evidenceIds": ["", ...], "policyVersion": "v2.3.1", "inputs": { "currentEvaluationTime": "2025-12-14T00:00:00Z", "severityThresholds": {...}, "latticeRules": {...} }, "intermediateFindings": {}, "reasoningId": "" } }, "signatures": [{"keyid": "", "sig": "BASE64(SIG)"}] } ``` Signer: Policy/Authority key ### 2.3 VEX Verdict Statement ```json { "payloadType": "application/vnd.in-toto+json", "payload": { "_type": "https://in-toto.io/Statement/v1", "subject": [{"name": "", "digest": {"sha256": "..."}}], "predicateType": "cdx-vex.stella/v1", "predicate": { "sbomEntryId": "", "vulnerabilityId": "CVE-XXXX-YYYY", "status": "not_affected|affected|fixed|under_investigation", "justification": "vulnerable_code_not_in_execute_path", "policyVersion": "v2.3.1", "reasoningId": "", "vexVerdictId": "" } }, "signatures": [{"keyid": "", "sig": "BASE64(SIG)"}] } ``` Signer: VEXer key or vendor key ### 2.4 Proof Spine Statement ```json { "payloadType": "application/vnd.in-toto+json", "payload": { "_type": "https://in-toto.io/Statement/v1", "subject": [{"name": "", "digest": {"sha256": "..."}}], "predicateType": "proofspine.stella/v1", "predicate": { "sbomEntryId": "", "evidenceIds": ["", ""], "reasoningId": "", "vexVerdictId": "", "policyVersion": "v2.3.1", "proofBundleId": "" } }, "signatures": [{"keyid": "", "sig": "BASE64(SIG)"}] } ``` Signer: Authority key ### 2.5 Verdict Receipt Statement (per finding/verdict) Use a receipt to bind the *final surfaced decision* to `graphRevisionId` and to the upstream proof objects (evidence/reasoning/VEX/spine). This is the primary export object for audit kits and benchmarks. ```json { "payloadType": "application/vnd.in-toto+json", "payload": { "_type": "https://in-toto.io/Statement/v1", "subject": [{"name": "", "digest": {"sha256": "..."}}], "predicateType": "verdict.stella/v1", "predicate": { "graphRevisionId": "", "findingKey": {"sbomEntryId": "", "vulnerabilityId": "CVE-XXXX-YYYY"}, "rule": {"id": "POLICY-RULE-123", "version": "v2.3.1"}, "decision": {"status": "block|warn|pass", "reason": "short human-readable summary"}, "inputs": {"sbomDigest": "sha256:...", "feedsDigest": "sha256:...", "policyDigest": "sha256:..."}, "outputs": {"proofBundleId": "", "reasoningId": "", "vexVerdictId": ""}, "createdAt": "2025-12-14T00:00:00Z" } }, "signatures": [{"keyid": "", "sig": "BASE64(SIG)"}] } ``` ### 2.6 SBOM Linkage Statement ```json { "_type": "https://in-toto.io/Statement/v1", "subject": [ {"name": "pkg:npm/lodash@4.17.21", "digest": {"sha256": "...", "sha512": "..."}} ], "predicateType": "https://stella-ops.org/predicates/sbom-linkage/v1", "predicate": { "sbom": { "id": "", "format": "CycloneDX", "specVersion": "1.6", "mediaType": "application/vnd.cyclonedx+json", "sha256": "", "location": "oci://... or file://..." }, "generator": { "name": "StellaOps.Sbomer", "version": "x.y.z" }, "generatedAt": "2025-12-14T00:00:00Z", "incompleteSubjects": [], "tags": { "tenantId": "...", "projectId": "...", "pipelineRunId": "..." } } } ``` ## 3. CYCLONEDX VEX STRUCTURE ```json { "bomFormat": "CycloneDX", "specVersion": "1.6", "version": 1, "vulnerabilities": [ { "id": "CVE-2024-12345", "source": {"name": "NVD"}, "analysis": { "state": "not_affected", "justification": "vulnerable_code_not_present", "response": ["will_not_fix"], "detail": "Linked OpenSSL feature set excludes the vulnerable cipher." }, "affects": [ {"ref": "pkg:apk/alpine/openssl@3.2.1-r0?sha256=2c0f...54e"} ], "properties": [ {"name": "evidence.sbomDigest", "value": "sha256:91f2...9a"}, {"name": "evidence.rekorLogID", "value": "425c1d1e..."}, {"name": "reachability.report", "value": "sha256:reacha..."}, {"name": "policy.decision", "value": "TrustGate#R-17.2"} ] } ] } ``` ### VEX Status Values ``` not_affected affected fixed under_investigation ``` ### VEX Justification Values ``` vulnerable_code_not_present vulnerable_code_not_in_execute_path vulnerable_code_not_configured vulnerable_code_cannot_be_controlled_by_adversary component_not_present inline_mitigations_exist ``` ## 4. STORAGE SCHEMA ### 4.1 PostgreSQL Tables ```sql CREATE TABLE sbom_entries ( entry_id UUID PRIMARY KEY, bom_digest VARCHAR(64) NOT NULL, purl TEXT NOT NULL, version TEXT, artifact_digest VARCHAR(64), trust_anchor_id UUID, created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE TABLE dsse_envelopes ( env_id UUID PRIMARY KEY, entry_id UUID REFERENCES sbom_entries(entry_id), predicate_type TEXT NOT NULL, signer_keyid TEXT NOT NULL, body_hash VARCHAR(64) NOT NULL, envelope_blob_ref TEXT NOT NULL, signed_at TIMESTAMPTZ NOT NULL, INDEX idx_entry_predicate (entry_id, predicate_type) ); CREATE TABLE spines ( entry_id UUID PRIMARY KEY REFERENCES sbom_entries(entry_id), bundle_id VARCHAR(64) NOT NULL, evidence_ids TEXT[] NOT NULL, reasoning_id VARCHAR(64) NOT NULL, vex_id VARCHAR(64) NOT NULL, anchor_id UUID, policy_version TEXT NOT NULL, created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE TABLE trust_anchors ( anchor_id UUID PRIMARY KEY, purl_pattern TEXT NOT NULL, allowed_keyids TEXT[] NOT NULL, policy_ref TEXT, revoked_keys TEXT[], created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE TABLE rekor_entries ( dsse_sha256 VARCHAR(64) PRIMARY KEY, log_index BIGINT NOT NULL, log_id TEXT NOT NULL, integrated_time BIGINT NOT NULL, inclusion_proof JSONB NOT NULL, created_at TIMESTAMPTZ DEFAULT NOW() ); ``` ### 4.2 Proof Graph Nodes ``` Node Types: - Artifact (container image, binary, Helm chart) - SbomDocument (by sbomId) - InTotoStatement (by statement hash) - DsseEnvelope (by envelope hash) - RekorEntry (by log index/UUID) - VexStatement (by vex hash) - Subject (component from SBOM) Edge Types: - DESCRIBED_BY: Artifact → SbomDocument - ATTESTED_BY: SbomDocument → InTotoStatement - WRAPPED_BY: InTotoStatement → DsseEnvelope - LOGGED_IN: DsseEnvelope → RekorEntry - HAS_VEX: Artifact/Subject → VexStatement - CONTAINS_SUBJECT: InTotoStatement → Subject - PRODUCES: Build → SBOM - AFFECTS: VEX → Component - SIGNED_BY: Envelope → Key - RECORDED_AT: Envelope → Rekor ``` ## 5. API CONTRACTS ### 5.1 Proof Spine API ``` POST /proofs/:entry/spine Body: { "evidenceIds": ["", ...], "reasoningId": "", "vexVerdictId": "", "policyVersion": "v2.3.1" } Response: 201 Created, {"proofBundleId": "..."} GET /proofs/:entry/receipt Response: { "proofBundleId": "...", "verifiedAt": "2025-12-14T00:00:00Z", "verifierVersion": "1.0.0", "anchorId": "...", "result": "pass|fail", "details": {...} } GET /proofs/:entry/vex Response: GET /anchors/:anchor Response: { "anchorId": "...", "purlPattern": "pkg:npm/*", "allowedKeyids": ["key1", "key2"], "policyRef": "...", "revokedKeys": [] } ``` ### 5.2 Verification API ``` POST /verify Body: { "artifactDigest": "sha256:...", "sbom": , "vex": , "signatures": [...], "logs": [...] } Response: { "artifact": "pkg:oci/...", "sbomVerified": true, "vexVerified": true, "components": [ { "bomRef": "pkg:...", "vulnerabilities": [ { "id": "CVE-...", "state": "not_affected", "justification": "..." } ] } ] } ``` ## 6. CANONICALIZATION RULES ### 6.1 JSON Canonicalization ``` 1. UTF-8 encoding 2. Sorted keys (lexicographic) 3. No insignificant whitespace 4. No volatile fields beyond semantic need 5. Version schema: evidence.stella/v1, reasoning.stella/v1 6. Deterministic array ordering where semantically unordered ``` ### 6.2 SBOM Canonicalization ```csharp public interface ISbomCanonicalizer { byte[] Canonicalize(ReadOnlySpan rawSbom, string mediaType); } public interface IBlobHasher { string ComputeSha256Hex(ReadOnlySpan data); } ``` Rules: - Remove insignificant whitespace - Sort object keys lexicographically - Sort arrays deterministically (by bom-ref or purl) - Strip volatile fields: generation timestamps, tool build IDs, non-deterministic UUIDs - Convert to internal JSON, then canonicalize ### 6.3 Subject Extraction ```csharp IEnumerable ToSubjects(CycloneDxSbom sbom) { foreach (var c in sbom.Metadata.Components) { if (c.Hashes == null || c.Hashes.Count == 0) continue; var name = $"pkg:{c.Type}/{c.Name}@{c.Version}"; var dig = c.Hashes .OrderBy(h => h.Algorithm) .ToDictionary( h => h.Algorithm.ToLowerInvariant(), h => h.Value.ToLowerInvariant() ); yield return new Subject(name, dig); } } ``` Digest requirements: - Must have at least sha256 or sha512 - Normalize algorithm keys to lowercase - Sort subjects by: 1) Name ascending, 2) Algorithm:value pairs ## 7. REKOR INTEGRATION ### 7.1 Rekor Entry Structure ```json { "dsseSha256": "sha256:...", "rekor": { "uuid": "...", "logIndex": 12345, "logId": "...", "integratedTime": 1733736000, "inclusionProof": { "rootHash": "...", "hashes": ["...", "..."], "checkpoint": "..." } } } ``` ### 7.2 Offline Update Bundle Structure ``` /bundle-2025-12-14/ manifest.json # version, created_at, entries[], sha256s payload.tar.zst # actual DB/indices/feeds payload.tar.zst.sha256 statement.dsse.json # DSSE-wrapped statement over payload hash rekor-receipt.json # Rekor v2 inclusion/verification material ``` ### 7.3 Offline Update DSSE Predicate ```json { "predicateType": "https://stella-ops.org/attestations/offline-update/1", "predicate": { "offline_manifest_sha256": "sha256:...", "feeds": [ { "name": "nvd", "snapshot_date": "2025-12-14", "archive_digest": "sha256:..." } ], "builder": "ci-workflow-id / git-commit / job-id", "created_at": "2025-12-14T00:00:00Z", "oukit_channel": "stable|edge|fips-profile" } } ``` ### 7.4 Verification Sequence (Offline Kit) ``` 1. Validate Cosign signature of tarball 2. Validate offline-manifest.json with JWS signature 3. Verify file digests for all entries (including /attestations/*) 4. Verify DSSE: - Call StellaOps.Attestor.Verify with: - offline-update.dsse.json - offline-update.rekor.json - local Rekor log snapshot/segment - Ensure payload digest matches kit tarball + manifest digests 5. Only after all checks pass: - Swap Scanner's feed pointer to new snapshot - Emit audit event (kit filename, tarball digest, DSSE digest, Rekor UUID + log index) ``` ## 8. CRYPTOGRAPHIC SPECIFICATIONS ### 8.1 Signing Keys & Profiles ``` Default Profile: Hash: SHA-256 Signature: Ed25519 or ECDSA P-256 Future Profiles: GOST R 34.10-2012 eIDAS-compliant algorithms FIPS 140-2/140-3 SM2/SM3 (Chinese standards) PQC: Dilithium/Falcon for long-term archives Key Storage: KMS/HSM (production) PKCS#11 for air-gap Per-environment keysets: dev, staging, prod Per-role keysets: Authority, VEXer, Evidence Ingestor ``` ### 8.2 Key Rotation ``` Rotation Process: 1. Add new allowed_keyids to TrustAnchor 2. Never mutate old DSSE envelopes 3. Publish key material via attestation feed or Rekor-mirror 4. Record all changes in audit log 5. Maintain key version history ``` ### 8.3 Trust Anchor Structure ```json { "trustAnchorId": "UUID", "purlPattern": "pkg:npm/*", "allowedKeyids": ["keyid1", "keyid2"], "allowedPredicateTypes": [ "evidence.stella/v1", "reasoning.stella/v1", "cdx-vex.stella/v1" ], "policyVersion": "v2.3.1", "revokedKeys": [] } ``` ## 9. VERIFICATION PIPELINE ### 9.1 Verification Algorithm ``` Input: SBOMEntryID or ProofBundleID Steps: 1. Resolve SBOMEntryID → TrustAnchorID 2. Fetch spine and trust anchor 3. Verify spine DSSE signature against TrustAnchor.allowedKeyids 4. Verify VEX DSSE signature 5. Verify reasoning DSSE signature 6. Verify evidence DSSE signatures 7. Recompute EvidenceIDs from stored canonical evidence 8. Recompute ReasoningID from reasoning 9. Recompute VEXVerdictID from VEX body 10. Recompute ProofBundleID (merkle root) from above 11. Compare all computed IDs to stored IDs 12. If using Rekor: - Verify log inclusion proof - Verify payload hashes match local files 13. Emit Receipt Output: Receipt { proofBundleId, verifiedAt, verifierVersion, anchorId, result: "pass|fail", details: [...] } ``` ### 9.2 Receipt Structure ```json { "proofBundleId": "sha256:...", "verifiedAt": "2025-12-14T00:00:00Z", "verifierVersion": "1.0.0", "anchorId": "UUID", "result": "pass", "checks": [ { "check": "spine_signature", "status": "pass", "keyid": "..." }, { "check": "evidence_id_recompute", "status": "pass", "expected": "sha256:...", "actual": "sha256:..." }, { "check": "rekor_inclusion", "status": "pass", "logIndex": 12345 } ], "toolDigests": { "verifier": "sha256:...", "canonicalizer": "sha256:..." } } ``` ## 10. DETERMINISM CONSTRAINTS ### 10.1 Non-Negotiable Invariants ``` 1. Immutability of Signed Facts - DSSE envelopes are append-only - Never edit or delete content inside signed envelope - Corrections via superseding (new statement pointing to old) 2. Determinism - Same {SBOMEntryID, Evidence set, policyVersion} ⇒ same {ReasoningID, VEXVerdictID, ProofBundleID} - No non-deterministic inputs in ID computation - No current time, random IDs in verdict logic 3. Traceability - Every VEX verdict → SBOM entry, evidence blobs, policy snapshot, trust anchor 4. Least Trust/Least Privilege - Trust always explicit via TrustAnchors + signature verification - Never "because it's in our DB" 5. Backwards Compatibility - New code verifies old proofs - New policies generate new spines, old spines intact ``` ### 10.2 Temporal Handling ``` UTC ISO-8601 only No local time Timestamps only when semantically required Derivation from content preferred over wall-clock time Record evaluation time as explicit input if policy needs it ``` ### 10.3 Ordering Requirements ``` Subjects: sorted by Name ascending, then digest keys Evidence IDs: sorted lexicographically Keys in JSON: sorted lexicographically Array elements: stable sort by semantic key (bom-ref, purl) ``` ## 11. IMPLEMENTATION INTERFACES ### 11.1 .NET 10 Core Interfaces ```csharp // DSSE Signing public interface IDsseSigner { Task SignAsync( ReadOnlyMemory payload, string payloadType, string keyProfile, CancellationToken ct = default ); } // Verification public record DsseEnvelope( string PayloadType, byte[] Payload, Signature[] Signatures ); public record Signature( string Keyid, string Sig, string? Cert ); // Subject Extraction public sealed record ProofSubject( string Name, IReadOnlyDictionary Digest ); // Predicate Models public record SbomLinkagePredicate( SbomDescriptor Sbom, GeneratorDescriptor Generator, DateTimeOffset GeneratedAt, IReadOnlyList? IncompleteSubjects, IReadOnlyDictionary? Tags ); public record EvidencePredicate( string Source, string SourceVersion, DateTimeOffset CollectionTime, string SbomEntryId, string? VulnerabilityId, object RawFinding, string EvidenceId ); public record ReasoningPredicate( string SbomEntryId, string[] EvidenceIds, string PolicyVersion, Dictionary Inputs, Dictionary? IntermediateFindings, string ReasoningId ); public record VexPredicate( string SbomEntryId, string VulnerabilityId, string Status, string Justification, string PolicyVersion, string ReasoningId, string VexVerdictId ); public record ProofSpinePredicate( string SbomEntryId, string[] EvidenceIds, string ReasoningId, string VexVerdictId, string PolicyVersion, string ProofBundleId ); ``` ### 11.2 Rekor Client Interface ```csharp public interface IRekorClient { Task SubmitDsseAsync( DsseEnvelope envelope, CancellationToken ct = default ); Task VerifyInclusionAsync( RekorEntry entry, byte[] payloadDigest, byte[] rekorPublicKey, CancellationToken ct = default ); } public record RekorEntry( string Uuid, long LogIndex, string LogId, long IntegratedTime, InclusionProof Proof ); public record InclusionProof( string RootHash, string[] Hashes, string Checkpoint ); ``` ## 12. CONFIGURATION SCHEMA ### 12.1 Scanner Offline Kit Config ```yaml scanner: offlineKit: requireDsse: true rekorOfflineMode: true attestationVerifier: https://attestor.internal trustAnchors: - anchorId: "UUID" purlPattern: "pkg:npm/*" allowedKeyids: ["key1", "key2"] ``` ### 12.2 Signer Config ```yaml signer: profiles: default: algorithm: "SHA256-ED25519" keyStore: "kms://..." fips: algorithm: "SHA256-ECDSA-P256" keyStore: "hsm://..." pqc: algorithm: "SHA256-DILITHIUM3" keyStore: "kms://..." ``` ### 12.3 Authority Config ```yaml authority: trustRoots: - id: "root-ca-1" publicKey: "..." validFrom: "2025-01-01T00:00:00Z" validUntil: "2030-01-01T00:00:00Z" keystores: - type: "kms" url: "aws-kms://..." region: "us-east-1" ``` ## 13. ERROR HANDLING ### 13.1 Ingestion Failures ``` If SBOM invalid: - Reject SBOM - Record DSSE failure attestation: { "error": "schema_validation_failed", "file": "sbom.json", "system_version": "1.0.0" } - Maintain proof trail for "we tried and it failed" ``` ### 13.2 Missing Digests ``` If component lacks sha256/sha512: - Do NOT use as primary subject in proof chain - Log in "incompleteSubjects" block in predicate - Expose in UI as "unverifiable component" ``` ### 13.3 Rekor Failures ``` If Rekor unavailable: - Store DSSE envelope locally - Queue for retry - Mark proof chain as "rekorStatus: pending" - Internal-only until Rekor sync succeeds - Flag in verification results ``` ## 14. METRICS & OBSERVABILITY ### 14.1 Pipeline Metrics ``` sboms_ingested_total sbom_ingest_errors_total{reason} evidence_statements_created_total reasoning_statements_created_total vex_statements_created_total proof_spines_created_total proof_verifications_total{result} *_duration_seconds (latency histograms per stage) offlinekit_import_total{status="success|failed_dsse|failed_rekor|failed_cosign"} offlinekit_attestation_verify_latency_seconds attestor_rekor_success_total attestor_rekor_retry_total rekor_inclusion_latency ``` ### 14.2 Structured Logging Fields ``` sbomEntryId proofBundleId anchorId policyVersion requestId / traceId rekorUuid attestationDigest offlineKitHash failureReason ``` ## 15. CI/CD INTEGRATION ### 15.1 Pipeline Hooks ``` On SBOM ingest: - Create/refresh SBOMEntry rows - Attach TrustAnchor On scan completion: - Produce Evidence Statements (DSSE) immediately On policy evaluation: - Produce Reasoning + VEX - Assemble Spine Release gates: - Require: GET /proofs/:entry/receipt == PASS ``` ### 15.2 CLI Exit Codes ``` 0 = no policy violation 1 = policy violation 2 = scanner/system error (distinguish from "found vulns") ``` ### 15.3 CLI Output Modes ``` Default: Human-readable summary (3-5 lines) --output json: Machine-readable with: - Web UI run page link - Proof bundle ID - Rekor/ledger reference -v / -vv: Verbose details ``` --- **Document Version**: 1.0 **Target Platform**: .NET 10, PostgreSQL ≥16, Angular v17