Here’s a tight, practical blueprint to turn your SBOM→VEX links into an auditable “proof spine”—using signed DSSE statements and a per‑dependency trust anchor—so every VEX verdict can be traced, verified, and replayed. # What this gives you * A **chain of evidence** from each SBOM entry → analysis → VEX verdict. * **Tamper‑evident** DSSE‑signed records (offline‑friendly). * **Deterministic replay**: same inputs → same verdicts (great for audits/regulators). # Core objects (canonical IDs) * **ArtifactID**: digest of package/container (e.g., `sha256:…`). * **SBOMEntryID**: stable ID for a component in an SBOM (`sbomDigest:package@version[:purl]`). * **EvidenceID**: hash of raw evidence (scanner JSON, reachability, exploit intel). * **ReasoningID**: hash of normalized reasoning (rules/lattice inputs used). * **VEXVerdictID**: hash of the final VEX statement body. * **ProofBundleID**: merkle root of {SBOMEntryID, EvidenceID[], ReasoningID, VEXVerdictID}. * **TrustAnchorID**: per‑dependency anchor (public key + policy) used to validate the above. # Signed DSSE envelopes you’ll produce 1. **Evidence Statement** (per evidence item) * `subject`: SBOMEntryID * `predicateType`: `evidence.stella/v1` * `predicate`: source, tool version, timestamps, EvidenceID * **Signers**: scanner/ingestor key 2. **Reasoning Statement** * `subject`: SBOMEntryID * `predicateType`: `reasoning.stella/v1` (your lattice/policy inputs + ReasoningID) * **Signers**: “Policy/Lattice Engine” key (Authority) 3. **VEX Verdict Statement** * `subject`: SBOMEntryID * `predicateType`: CycloneDX or CSAF VEX body + VEXVerdictID * **Signers**: VEXer key (or vendor key if you have it) 4. **Proof Spine Statement** (the spine itself) * `subject`: SBOMEntryID * `predicateType`: `proofspine.stella/v1` * `predicate`: EvidenceID[], ReasoningID, VEXVerdictID, ProofBundleID * **Signers**: Authority key # Trust model (per‑dependency anchor) * **TrustAnchor** (per package/purl): { TrustAnchorID, allowed signers (KMS refs, PKs), accepted predicateTypes, policy version, revocation list }. * Store anchors in **Authority** and pin them in your graph by SBOMEntryID→TrustAnchorID. * Optional: PQC mode (Dilithium/Falcon) for long‑term archives. # Verification pipeline (deterministic) 1. Resolve SBOMEntryID → TrustAnchorID. 2. Verify every DSSE envelope’s signature **against the anchor’s allowed keys**. 3. Recompute EvidenceID/ReasoningID/VEXVerdictID from raw content; compare hashes. 4. Recompute ProofBundleID (merkle root) and compare to the spine. 5. Emit a **Receipt**: {ProofBundleID, verification log, tool digests}. Cache it. # Storage layout (Postgres + blob store) * `sbom_entries(entry_id PK, bom_digest, purl, version, artifact_digest, trust_anchor_id)` * `dsse_envelopes(env_id PK, entry_id, predicate_type, signer_keyid, body_hash, envelope_blob_ref, signed_at)` * `spines(entry_id PK, bundle_id, evidence_ids[], reasoning_id, vex_id, anchor_id, created_at)` * `trust_anchors(anchor_id PK, purl_pattern, allowed_keyids[], policy_ref, revoked_keys[])` * Blobs (immutable): raw evidence, normalized reasoning JSON, VEX JSON, DSSE bytes. # API surface (clean and small) * `POST /proofs/:entry/spine` → submit or update spine (idempotent by ProofBundleID) * `GET /proofs/:entry/receipt` → full verification receipt (JSON) * `GET /proofs/:entry/vex` → the verified VEX body * `GET /anchors/:anchor` → fetch trust anchor (for offline kits) # Normalization rules (so hashes are stable) * Canonical JSON (UTF‑8, sorted keys, no insignificant whitespace). * Strip volatile fields (timestamps that aren’t part of the semantic claim). * Version your schemas: `evidence.stella/v1`, `reasoning.stella/v1`, etc. # Signing keys & rotation * Keep keys in your **Authority** module (KMS/HSM; offline export for air‑gap). * Publish key material via an **attestation feed** (or Rekor‑mirror) for third‑party audit. * Rotate by **adding** new allowed_keyids in the TrustAnchor; never mutate old envelopes. # CI/CD hooks * On SBOM ingest → create/refresh SBOMEntry rows + attach TrustAnchor. * On scan completion → produce Evidence Statements (DSSE) immediately. * On policy evaluation → produce Reasoning + VEX, then assemble Spine. * Gate releases on `GET /proofs/:entry/receipt` == PASS. # UX (auditor‑friendly) * **Proof timeline** per entry: SBOM → Evidence tiles → Reasoning → VEX → Receipt. * One‑click “Recompute & Compare” to show deterministic replay passes. * Red/amber flags when a signature no longer matches a TrustAnchor or a key is revoked. # Minimal dev checklist * [ ] Implement canonicalizers (Evidence, Reasoning, VEX). * [ ] Implement DSSE sign/verify (ECDSA + optional PQC). * [ ] TrustAnchor registry + resolver by purl pattern. * [ ] Merkle bundling to get ProofBundleID. * [ ] Receipt generator + verifier. * [ ] Postgres schema + blob GC (content‑addressed). * [ ] CI gates + API endpoints above. * [ ] Auditor UI: timeline + diff + receipts download. If you want, I can drop in a ready‑to‑use JSON schema set (`evidence.stella/v1`, `reasoning.stella/v1`, `proofspine.stella/v1`) and sample DSSE envelopes wired to your .NET 10 stack. Here’s a focused **Stella Ops Developer Guidelines** doc, specifically for the pipeline that turns **SBOM data into verifiable proofs** (your SBOM → Evidence → Reasoning → VEX → Proof Spine). Feel free to paste this into your internal handbook and tweak names to match your repos/services. --- # Stella Ops Developer Guidelines ## Turning SBOM Data Into Verifiable Proofs --- ## 1. Mental Model: What You’re Actually Building For every component in an SBOM, Stella must be able to answer, *“Why should anyone trust our VEX verdict for this dependency, today and ten years from now?”* We do that with a pipeline: 1. **SBOM Ingest** Raw SBOM → validated → normalized → `SBOMEntryID`. 2. **Evidence Collection** Scans, feeds, configs, reachability, etc. → canonical evidence blobs → `EvidenceID` → DSSE-signed. 3. **Reasoning / Policy** Policy + evidence → deterministic reasoning → `ReasoningID` → DSSE-signed. 4. **VEX Verdict** VEX statement (CycloneDX / CSAF) → canonicalized → `VEXVerdictID` → DSSE-signed. 5. **Proof Spine** `{SBOMEntryID, EvidenceIDs[], ReasoningID, VEXVerdictID}` → merkle bundle → `ProofBundleID` → DSSE-signed. 6. **Verification & Receipts** Re-run verification → `Receipt` that proves everything above is intact and anchored to trusted keys. Everything you do in this area should keep this spine intact and verifiable. --- ## 2. Non‑Negotiable Invariants These are the rules you don’t break without an explicit, company-level decision: 1. **Immutability of Signed Facts** * DSSE envelopes (evidence, reasoning, VEX, spines) are append‑only. * You never edit or delete content inside a previously signed envelope. * Corrections are made by **superseding** (new statement pointing at the old one). 2. **Determinism** * Same `{SBOMEntryID, Evidence set, policyVersion}` ⇒ same `{ReasoningID, VEXVerdictID, ProofBundleID}`. * No non-deterministic inputs (e.g., “current time”, random IDs) in anything that affects IDs or verdicts. 3. **Traceability** * Every VEX verdict must be traceable back to: * The precise SBOM entry * Concrete evidence blobs * A specific policy & reasoning snapshot * A trust anchor defining allowed signers 4. **Least Trust / Least Privilege** * Services only know the keys and data they need. * Trust is always explicit: through **TrustAnchors** and signature verification, never “because it’s in our DB”. 5. **Backwards Compatibility** * New code must continue to verify **old proofs**. * New policies must **not rewrite history**; they produce *new* spines, leaving old ones intact. --- ## 3. SBOM Ingestion Guidelines **Goal:** Turn arbitrary SBOMs into stable, addressable `SBOMEntryID`s and safe internal models. ### 3.1 Inputs & Formats * Support at least: * CycloneDX (JSON) * SPDX (JSON / Tag-Value) * For each ingested SBOM, store: * Raw SBOM bytes (immutable, content-addressed) * A normalized internal representation (your own model) ### 3.2 IDs * Generate: * `sbomDigest` = hash(raw SBOM, canonical form) * `SBOMEntryID` = `sbomDigest + purl + version` (or equivalent stable tuple) * `SBOMEntryID` must: * Not depend on ingestion time or database IDs. * Be reproducible from SBOM + deterministic normalization. ### 3.3 Validation & Errors * Validate: * Syntax (JSON, schema) * Core semantics (package identifiers, digests, versions) * If invalid: * Reject the SBOM **but** record a small DSSE “failure attestation” explaining: * Why it failed * Which file * Which system version * This still gives you a proof trail for “we tried and it failed”. --- ## 4. Evidence Collection Guidelines **Goal:** Capture all inputs that influence the verdict in a canonical, signed form. Typical evidence types: * SCA / vuln scanner results * CVE feeds & advisory data * Reachability / call graph analysis * Runtime context (where this component is used) * Manual assessments (e.g., security engineer verdicts) ### 4.1 Evidence Canonicalization For every evidence item: * Normalize to a schema like `evidence.stella/v1` with fields such as: * `source` (scanner name, feed) * `sourceVersion` (tool version, DB version) * `collectionTime` * `sbomEntryId` * `vulnerabilityId` (if applicable) * `rawFinding` (or pointer to it) * Canonical JSON rules: * Sorted keys * UTF‑8, no extraneous whitespace * No volatile fields beyond what’s semantically needed (e.g., you might include `collectionTime`, but then know it affects the hash and treat that consciously). Then: * Compute `EvidenceID = hash(canonicalEvidenceJson)`. * Wrap in DSSE: * `subject`: `SBOMEntryID` * `predicateType`: `evidence.stella/v1` * `predicate`: canonical evidence + `EvidenceID`. * Sign with **evidence-ingestor key** (per environment). ### 4.2 Ops Rules * **Idempotency:** Re-running the same scan with same inputs should produce the same evidence object and `EvidenceID`. * **Tool changes:** When tool version or configuration changes, that’s a *new* evidence statement with a new `EvidenceID`. Do not overwrite old evidence. * **Partial failure:** If a scan fails, produce a minimal failure evidence record (with error details) instead of “nothing”. --- ## 5. Reasoning & Policy Engine Guidelines **Goal:** Turn evidence into a defensible, replayable reasoning step with a clear policy version. ### 5.1 Reasoning Object Define a canonical reasoning schema, e.g. `reasoning.stella/v1`: * `sbomEntryId` * `evidenceIds[]` (sorted) * `policyVersion` * `inputs`: normalized form of all policy inputs (severity thresholds, lattice rules, etc.) * `intermediateFindings`: optional but useful — e.g., “reachable vulns = …” Then: * Canonicalize JSON and compute `ReasoningID = hash(canonicalReasoning)`. * Wrap in DSSE: * `subject`: `SBOMEntryID` * `predicateType`: `reasoning.stella/v1` * `predicate`: canonical reasoning + `ReasoningID`. * Sign with **Policy/Authority key**. ### 5.2 Determinism * Reasoning functions must be **pure**: * Inputs: SBOMEntryID, evidence set, policy version, configuration. * No hidden calls to external APIs at decision time (fetch feeds earlier and record them as evidence). * If you need “current time” in policy: * Treat it as **explicit input** and record it inside reasoning under `inputs.currentEvaluationTime`. ### 5.3 Policy Evolution * When policy changes: * Bump `policyVersion`. * New evaluations produce new `ReasoningID` and new VEX/spines. * Don’t retroactively apply new policy to old reasoning objects; generate new ones alongside. --- ## 6. VEX Verdict Guidelines **Goal:** Generate VEX statements that are strongly tied to SBOM entries and your reasoning. ### 6.1 Shape * Target standard formats: * CycloneDX VEX * or CSAF * Required linkages: * Component reference = `SBOMEntryID` or a resolvable component identifier from your SBOM normalize layer. * Vulnerability IDs (CVE, GHSA, internal IDs). * Status (`not_affected`, `affected`, `fixed`, etc.). * Justification & impact. ### 6.2 Canonicalization & Signing * Define a canonical VEX body schema (subset of the standard + internal metadata): * `sbomEntryId` * `vulnerabilityId` * `status` * `justification` * `policyVersion` * `reasoningId` * Canonicalize JSON → `VEXVerdictID = hash(canonicalVexBody)`. * DSSE-envelope: * `subject`: `SBOMEntryID` * `predicateType`: e.g. `cdx-vex.stella/v1` * `predicate`: canonical VEX + `VEXVerdictID`. * Sign with **VEXer key** or vendor key (depending on trust anchor). ### 6.3 External VEX * When importing vendor VEX: * Verify signature against vendor’s TrustAnchor. * Canonicalize to your internal schema but preserve: * Original document * Original signature material * Record “source = vendor” vs “source = stella” so auditors see origin. --- ## 7. Proof Spine Guidelines **Goal:** Build a compact, tamper-evident “bundle” that ties everything together. ### 7.1 Structure For each `SBOMEntryID`, gather: * `EvidenceIDs[]` (sorted lexicographically). * `ReasoningID`. * `VEXVerdictID`. Compute: * Merkle tree root (or deterministic hash) over: * `sbomEntryId` * sorted `EvidenceIDs[]` * `ReasoningID` * `VEXVerdictID` * Result is `ProofBundleID`. Create a DSSE “spine”: * `subject`: `SBOMEntryID` * `predicateType`: `proofspine.stella/v1` * `predicate`: * `evidenceIds[]` * `reasoningId` * `vexVerdictId` * `policyVersion` * `proofBundleId` * Sign with **Authority key**. ### 7.2 Ops Rules * Spine generation is idempotent: * Same inputs → same `ProofBundleID`. * Never mutate existing spines; new policy or new evidence ⇒ new spine. * Keep a clear API contract: * `GET /proofs/:entry` returns **all** spines, each labeled with `policyVersion` and timestamps. --- ## 8. Storage & Schema Guidelines **Goal:** Keep proofs queryable forever without breaking verification. ### 8.1 Tables (conceptual) * `sbom_entries`: `entry_id`, `bom_digest`, `purl`, `version`, `artifact_digest`, `trust_anchor_id`. * `dsse_envelopes`: `env_id`, `entry_id`, `predicate_type`, `signer_keyid`, `body_hash`, `envelope_blob_ref`, `signed_at`. * `spines`: `entry_id`, `proof_bundle_id`, `policy_version`, `evidence_ids[]`, `reasoning_id`, `vex_verdict_id`, `anchor_id`, `created_at`. * `trust_anchors`: `anchor_id`, `purl_pattern`, `allowed_keyids[]`, `policy_ref`, `revoked_keys[]`. ### 8.2 Schema Changes Always follow: 1. **Expand** * Add new columns/tables. * Make new code tolerant of old data. 2. **Backfill** * Idempotent jobs that fill in new IDs/fields without touching old DSSE payloads. 3. **Contract** * Only after all code uses the new model. * Never drop the raw DSSE or raw SBOM blobs. --- ## 9. Verification & Receipts **Goal:** Make it trivial (for you, customers, and regulators) to recheck everything. ### 9.1 Verification Flow Given `SBOMEntryID` or `ProofBundleID`: 1. Fetch spine and trust anchor. 2. Verify: * Spine DSSE signature against TrustAnchor’s allowed keys. * VEX, reasoning, and evidence DSSE signatures. 3. Recompute: * `EvidenceIDs` from stored canonical evidence. * `ReasoningID` from reasoning. * `VEXVerdictID` from VEX body. * `ProofBundleID` from the above. 4. Compare to stored IDs. Emit a **Receipt**: * `proofBundleId` * `verifiedAt` * `verifierVersion` * `anchorId` * `result` (pass/fail, with reasons) ### 9.2 Offline Kit * Provide a minimal CLI (`stella verify`) that: * Accepts a bundle export (SBOM + DSSE envelopes + anchors). * Verifies everything without network access. Developers must ensure: * Export format is documented and stable. * All fields required for verification are included. --- ## 10. Security & Key Management (for Devs) * Keys live in **KMS/HSM**, not env vars or config files. * Separate keysets: * `dev`, `staging`, `prod` * Authority vs VEXer vs Evidence Ingestor. * TrustAnchors: * Edit via Authority service only. * Every change: * Requires code-reviewed change. * Writes an audit log entry. Never: * Log private keys. * Log full DSSE envelopes in plaintext logs (log IDs and hashes instead). --- ## 11. Observability & On‑Call Expectations ### 11.1 Metrics For the SBOM→Proof pipeline, expose: * `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}` (pass/fail reason) * Latency histograms per stage (`_duration_seconds`) ### 11.2 Logging Include in structured logs wherever relevant: * `sbomEntryId` * `proofBundleId` * `anchorId` * `policyVersion` * `requestId` / `traceId` ### 11.3 Runbooks You should maintain runbooks for at least: * “Pipeline is stalled” (backlog of SBOMs, evidence, or spines). * “Verification failures increased”. * “Trust anchor or key issues” (rotation, revocation, misconfiguration). * “Backfill gone wrong” (how to safely stop, resume, and audit). --- ## 12. Dev Workflow & PR Checklist (SBOM→Proof Changes Only) When your change touches SBOM ingestion, evidence, reasoning, VEX, or proof spines, check: * [ ] IDs (`SBOMEntryID`, `EvidenceID`, `ReasoningID`, `VEXVerdictID`, `ProofBundleID`) remain **deterministic** and fully specified. * [ ] No mutation of existing DSSE envelopes or historical proof data. * [ ] Schema changes follow **expand → backfill → contract**. * [ ] New/updated TrustAnchors reviewed by Authority owner. * [ ] Unit tests cover: * Canonicalization for any new/changed predicate. * ID computation. * [ ] Integration test covers: * SBOM → Evidence → Reasoning → VEX → Spine → Verification → Receipt. * [ ] Observability updated: * New paths emit logs & metrics. * [ ] Rollback plan documented (especially for migrations & policy changes). --- If you tell me which microservices/repos map to these stages (e.g. `stella-sbom-ingest`, `stella-proof-authority`, `stella-vexer`), I can turn this into a more concrete, service‑by‑service checklist with example API contracts and class/interface sketches.