Files
git.stella-ops.org/docs/product-advisories/14-Dec-2025 - Proof and Evidence Chain Technical Reference.md
2025-12-14 19:58:38 +02:00

22 KiB
Raw Blame History

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' ProofLinked Advantage
  • 08-Dec-2025 - Designing UX for Signed Evidence Trails
  • 03-Dec-2025 - Comparing ProofLinked VEX UX Across Tools

Last Updated: 2025-12-14


1. CORE IDENTIFIERS & DATA MODEL

1.1 Canonical IDs

ArtifactID        = sha256:<digest>
SBOMEntryID      = <sbomDigest>:<purl>[@<version>]
EvidenceID       = hash(canonical_evidence_json)
ReasoningID      = hash(canonical_reasoning_json)
VEXVerdictID     = hash(canonical_vex_json)
ProofBundleID    = merkle_root(SBOMEntryID, EvidenceID[], ReasoningID, VEXVerdictID)
TrustAnchorID    = per-dependency anchor (public key + policy)

1.2 Component Identifiers (bom-ref)

Format: pkg:<ecosystem>/<name>@<version>?sha256=<digest>

Examples:
  pkg:maven/org.apache.commons/commons-lang3@3.14.0?sha256=<digest>
  pkg:apk/alpine/openssl@3.2.1-r0?sha256=2c0f...54e
  pkg:oci/<repo>@sha256:<manifestDigest>
  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

public sealed record ProofSubject(
    string Name,                       // PURL or canonical URI
    IReadOnlyDictionary<string,string> Digest  // {"sha256": "...", "sha512": "..."}
);

1.4 SBOM Identity

sbomId = sha256(canonical_sbom_bytes)

2. DSSE ENVELOPE STRUCTURES

2.1 Evidence Statement

{
  "payloadType": "application/vnd.in-toto+json",
  "payload": {
    "_type": "https://in-toto.io/Statement/v1",
    "subject": [{"name": "<SBOMEntryID>", "digest": {"sha256": "..."}}],
    "predicateType": "evidence.stella/v1",
    "predicate": {
      "source": "scanner/feed name",
      "sourceVersion": "tool version",
      "collectionTime": "2025-12-14T00:00:00Z",
      "sbomEntryId": "<SBOMEntryID>",
      "vulnerabilityId": "CVE-XXXX-YYYY",
      "rawFinding": "<pointer or data>",
      "evidenceId": "<EvidenceID>"
    }
  },
  "signatures": [{"keyid": "<KID>", "sig": "BASE64(SIG)"}]
}

Signer: Scanner/Ingestor key

2.2 Reasoning Statement

{
  "payloadType": "application/vnd.in-toto+json",
  "payload": {
    "_type": "https://in-toto.io/Statement/v1",
    "subject": [{"name": "<SBOMEntryID>", "digest": {"sha256": "..."}}],
    "predicateType": "reasoning.stella/v1",
    "predicate": {
      "sbomEntryId": "<SBOMEntryID>",
      "evidenceIds": ["<EvidenceID>", ...],
      "policyVersion": "v2.3.1",
      "inputs": {
        "currentEvaluationTime": "2025-12-14T00:00:00Z",
        "severityThresholds": {...},
        "latticeRules": {...}
      },
      "intermediateFindings": {},
      "reasoningId": "<ReasoningID>"
    }
  },
  "signatures": [{"keyid": "<KID>", "sig": "BASE64(SIG)"}]
}

Signer: Policy/Authority key

2.3 VEX Verdict Statement

{
  "payloadType": "application/vnd.in-toto+json",
  "payload": {
    "_type": "https://in-toto.io/Statement/v1",
    "subject": [{"name": "<SBOMEntryID>", "digest": {"sha256": "..."}}],
    "predicateType": "cdx-vex.stella/v1",
    "predicate": {
      "sbomEntryId": "<SBOMEntryID>",
      "vulnerabilityId": "CVE-XXXX-YYYY",
      "status": "not_affected|affected|fixed|under_investigation",
      "justification": "vulnerable_code_not_in_execute_path",
      "policyVersion": "v2.3.1",
      "reasoningId": "<ReasoningID>",
      "vexVerdictId": "<VEXVerdictID>"
    }
  },
  "signatures": [{"keyid": "<KID>", "sig": "BASE64(SIG)"}]
}

Signer: VEXer key or vendor key

2.4 Proof Spine Statement

{
  "payloadType": "application/vnd.in-toto+json",
  "payload": {
    "_type": "https://in-toto.io/Statement/v1",
    "subject": [{"name": "<SBOMEntryID>", "digest": {"sha256": "..."}}],
    "predicateType": "proofspine.stella/v1",
    "predicate": {
      "sbomEntryId": "<SBOMEntryID>",
      "evidenceIds": ["<ID1>", "<ID2>"],
      "reasoningId": "<ID>",
      "vexVerdictId": "<ID>",
      "policyVersion": "v2.3.1",
      "proofBundleId": "<ProofBundleID>"
    }
  },
  "signatures": [{"keyid": "<KID>", "sig": "BASE64(SIG)"}]
}

Signer: Authority key

2.5 SBOM Linkage Statement

{
  "_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": "<sbomId>",
      "format": "CycloneDX",
      "specVersion": "1.6",
      "mediaType": "application/vnd.cyclonedx+json",
      "sha256": "<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

{
  "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

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": ["<ID1>", ...],
    "reasoningId": "<ID>",
    "vexVerdictId": "<ID>",
    "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: <VEX JSON body>

GET /anchors/:anchor
  Response: {
    "anchorId": "...",
    "purlPattern": "pkg:npm/*",
    "allowedKeyids": ["key1", "key2"],
    "policyRef": "...",
    "revokedKeys": []
  }

5.2 Verification API

POST /verify
  Body: {
    "artifactDigest": "sha256:...",
    "sbom": <SBOM JSON or reference>,
    "vex": <VEX JSON or reference>,
    "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

public interface ISbomCanonicalizer
{
    byte[] Canonicalize(ReadOnlySpan<byte> rawSbom, string mediaType);
}

public interface IBlobHasher
{
    string ComputeSha256Hex(ReadOnlySpan<byte> 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

IEnumerable<Subject> 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

{
  "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

{
  "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

{
  "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

{
  "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

// DSSE Signing
public interface IDsseSigner
{
    Task<DsseEnvelope> SignAsync(
        ReadOnlyMemory<byte> 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<string,string> Digest
);

// Predicate Models
public record SbomLinkagePredicate(
    SbomDescriptor Sbom,
    GeneratorDescriptor Generator,
    DateTimeOffset GeneratedAt,
    IReadOnlyList<IncompleteSubject>? IncompleteSubjects,
    IReadOnlyDictionary<string,string>? 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<string,object> Inputs,
    Dictionary<string,object>? 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

public interface IRekorClient
{
    Task<RekorEntry> SubmitDsseAsync(
        DsseEnvelope envelope,
        CancellationToken ct = default
    );

    Task<bool> 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

scanner:
  offlineKit:
    requireDsse: true
    rekorOfflineMode: true
    attestationVerifier: https://attestor.internal
    trustAnchors:
      - anchorId: "UUID"
        purlPattern: "pkg:npm/*"
        allowedKeyids: ["key1", "key2"]

12.2 Signer Config

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

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