Here’s a tight, step‑through recipe for making every VEX statement **verifiably** tied to build evidence—using CycloneDX (SBOM), deterministic identifiers, and attestations (in‑toto/DSSE). --- # 1) Build time: mint stable, content‑addressed IDs * For every artifact (source, module, package, container layer), compute: * `sha256` of canonical bytes * a **deterministic component ID**: `pkg:/@?sha256=` (CycloneDX supports `bom-ref`; use this value as the `bom-ref`). * Emit SBOM (CycloneDX 1.6) with: * `metadata.component` = the top artifact * each `components[].bom-ref` = the deterministic ID * `properties[]` for extras: build system run ID, git commit, tool versions. **Example (SBOM fragment):** ```json { "bomFormat": "CycloneDX", "specVersion": "1.6", "serialNumber": "urn:uuid:7b4f3f64-8f0b-4a7d-9b3f-7a0a2b6cf6a9", "version": 1, "metadata": { "component": { "type": "container", "name": "stellaops/scanner", "version": "1.2.3", "bom-ref": "pkg:docker/stellaops/scanner@1.2.3?sha256=7e1a...b9" } }, "components": [ { "type": "library", "name": "openssl", "version": "3.2.1", "purl": "pkg:apk/alpine/openssl@3.2.1-r0", "bom-ref": "pkg:apk/alpine/openssl@3.2.1-r0?sha256=2c0f...54e", "properties": [ {"name": "build.git", "value": "ef3d9b4"}, {"name": "build.run", "value": "gha-61241"} ] } ] } ``` --- # 2) Sign the SBOM as evidence * Wrap the SBOM in **DSSE** and sign it (cosign or in‑toto). * Record to Rekor (or your offline mirror). Store the **log index**/UUID. **Provenance note:** keep `{ sbomDigest, dsseSignature, rekorLogID }`. --- # 3) Normalize vulnerability findings to the same IDs * Your scanner should output findings where `affected.bom-ref` equals the component’s deterministic ID. * If using CVE/OSV, keep both the upstream ID and your local `bom-ref`. **Finding (internal record):** ```json { "vulnId": "CVE-2024-12345", "affected": "pkg:apk/alpine/openssl@3.2.1-r0?sha256=2c0f...54e", "source": "grype@0.79.0", "introducedBy": "stellaops/scanner@1.2.3", "evidence": {"scanDigest": "sha256:aa1b..."} } ``` --- # 4) Issue VEX with deterministic targets * Create a CycloneDX **VEX** doc where each `vulnerabilities[].affects[].ref` equals the SBOM `bom-ref`. * Use `analysis.justification` and `analysis.state` (`not_affected`, `affected`, `fixed`, `under_investigation`). * Add **tight reasons** (reachability, config, platform) and a **link back to evidence** via properties. **VEX (CycloneDX) minimal:** ```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"} ] } ] } ``` --- # 5) Sign the VEX and anchor it * Wrap the VEX in DSSE, sign, and (optionally) publish to Rekor (or your Proof‑Market mirror). * Now you can verify: **component digest ↔ SBOM bom‑ref ↔ VEX affects.ref ↔ signatures/log**. --- # 6) Verifier flow (what your UI/CLI should do) 1. Load VEX → verify DSSE signature → (optional) Rekor inclusion. 2. For each `affects.ref`, check there exists an SBOM component with the **exact same value**. 3. Verify the SBOM signature and Rekor entry (hash of SBOM equals what VEX references in `properties.evidence.sbomDigest`). 4. Cross‑check the running artifact/container digest matches the SBOM `metadata.component.bom-ref` (or OCI manifest digest). 5. Render the decision with **explainable evidence** (links to proofs, reachability report hash, policy rule ID). --- # 7) Attestation shapes (quick starters) **DSSE envelope (JSON) around SBOM or VEX payload:** ```json { "payloadType": "application/vnd.cyclonedx+json;version=1.6", "payload": "BASE64(SBOM_OR_VEX_JSON)", "signatures": [ {"keyid": "SHA256-PUBKEY", "sig": "BASE64(SIG)"} ] } ``` **in‑toto Statement for provenance → attach SBOM hash:** ```json { "_type": "https://in-toto.io/Statement/v1", "predicateType": "https://slsa.dev/provenance/v1", "subject": [{"name": "stellaops/scanner", "digest": {"sha256": "7e1a...b9"}}], "predicate": { "buildType": "stellaops/ci", "materials": [{"uri": "git+https://...#ef3d9b4"}], "metadata": {"buildInvocationID": "gha-61241"}, "externalParameters": {"sbomDigest": "sha256:91f2...9a"} } } ``` --- # 8) Practical guardrails (so it stays deterministic) * **Never** generate `bom-ref` from mutable fields (like file paths). Use content digests + stable PURL. * Pin toolchains and normalize JSON (UTF‑8, sorted keys if you post‑hash). * Store `{ toolVersions, feed snapshots, policy set hash }` to replay decisions. * For containers, prefer `bom-ref = pkg:oci/@` PLUS layer evidence in `components[]`. --- # 9) “Hello‑world” verification script (pseudo) ```bash # 1) Verify SBOM sig -> get sbomDigest cosign verify-blob --signature sbom.sig sbom.json # 2) Verify VEX sig cosign verify-blob --signature vex.sig vex.json # 3) Check that every VEX affects.ref exists in SBOM jq -r '.vulnerabilities[].affects[].ref' vex.json | while read ref; do jq -e --arg r "$ref" '.components[] | select(.["bom-ref"]==$r)' sbom.json >/dev/null done # 4) Compare running image digest to SBOM metadata.component.bom-ref ``` --- ## Where this fits in Stella Ops (quick wiring) * **Sbomer**: emits CycloneDX with deterministic `bom-ref`s + DSSE sig. * **Scanner**: normalizes findings to `bom-ref`. * **Vexer**: produces/signed VEX; includes `properties` back to SBOM/reachability/policy. * **Authority/Verifier**: one click “Prove it” view → checks DSSE, Rekor, and `ref` equality. * **Proof Graph**: edge types: `produces(SBOM)`, `affects(VEX↔component)`, `signedBy`, `recordedAt(Rekor)`. If you want, I can turn this into: * a **.NET 10** helper lib for stable `bom-ref` generation, * a **CLI** that takes `sbom.json` + `vex.json` and runs the full verification, * or **fixtures** (golden SBOM/VEX/DSSE triplets) for your CI. Below is a developer-oriented blueprint you can hand to engineers as “How we build a verifiable SBOM→VEX chain”. --- ## 1. Objectives and Trust Model **Goal:** Any VEX statement about a component must be: 1. **Precisely scoped** to one or more concrete artifacts. 2. **Cryptographically linked** to the SBOM that defined those artifacts. 3. **Replayable**: a third party can re-run verification and reach the same conclusion. 4. **Auditable**: every step is backed by signatures and immutable logs (e.g., Rekor or internal ledger). **Questions you must be able to answer deterministically:** * “Which exact artifact does this VEX statement apply to?” * “Show me the SBOM where this artifact is defined, and prove it was not tampered with.” * “Prove that the VEX document I am looking at was authored and/or approved by the expected party.” --- ## 2. Canonical Identifiers: Non-Negotiable Foundation You cannot build a verifiable chain without **stable, content-addressed IDs**. ### 2.1 Component IDs For every component, choose a deterministic scheme: * Base: PURL or URN, e.g., `pkg:maven/org.apache.commons/commons-lang3@3.14.0` * Extend with content hash: `pkg:maven/org.apache.commons/commons-lang3@3.14.0?sha256=` * Use this value as the **CycloneDX `bom-ref`**. **Developer rule:** * `bom-ref` must be: * Stable across SBOM regenerations for identical content. * Independent of local, ephemeral data (paths, build numbers). * Derived from canonical bytes (normalized archive/layer, not “whatever we saw on disk”). ### 2.2 Top-Level Artifact IDs For images, archives, etc.: * Prefer OCI-style naming: `pkg:oci/@sha256:` * Set this as `metadata.component.bom-ref` in the SBOM. --- ## 3. SBOM Generation Guidelines ### 3.1 Required Properties When emitting a CycloneDX SBOM (1.5/1.6): * `metadata.component`: * `name`, `version`, `bom-ref`. * `components[]`: * `name`, `version`, `purl` (if available), **`bom-ref`**. * `hashes[]`: include at least `SHA-256`. * `properties[]`: * Build metadata: * `build.gitCommit` * `build.pipelineRunId` * `build.toolchain` (e.g., `dotnet-10.0.100`, `maven-3.9.9`) * Optional: * `provenance.statementDigest` * `scm.url` Minimal JSON fragment: ```json { "bomFormat": "CycloneDX", "specVersion": "1.6", "metadata": { "component": { "type": "container", "name": "example/api-gateway", "version": "1.0.5", "bom-ref": "pkg:oci/example/api-gateway@sha256:abcd..." } }, "components": [ { "type": "library", "name": "openssl", "version": "3.2.1", "purl": "pkg:apk/alpine/openssl@3.2.1-r0", "bom-ref": "pkg:apk/alpine/openssl@3.2.1-r0?sha256:1234...", "hashes": [ { "alg": "SHA-256", "content": "1234..." } ] } ] } ``` ### 3.2 SBOM Normalization Developer directions: * Normalize JSON before hashing/signing: * Sorted keys, UTF-8, consistent whitespace. * Ensure SBOM generation is **deterministic** given the same: * Inputs (image, source tree) * Tool versions * Settings/flags --- ## 4. Signing and Publishing the SBOM ### 4.1 DSSE Envelope Wrap the raw SBOM bytes in a DSSE envelope and sign: ```json { "payloadType": "application/vnd.cyclonedx+json;version=1.6", "payload": "BASE64(SBOM_JSON)", "signatures": [ { "keyid": "", "sig": "BASE64(SIGNATURE)" } ] } ``` Guidelines: * Use a **dedicated signing identity** (keypair or KMS key) for SBOMs. * Publish signature and payload hash to: * Rekor or * Your internal immutable log / ledger. Persist: * `sbomDigest = sha256(SBOM_JSON)`. * `sbomLogId` (Rekor UUID or internal ledger ID). --- ## 5. Vulnerability Findings → Normalized Targets Your scanners (or imports from external scanners) must map findings onto **the same IDs used in the SBOM**. ### 5.1 Mapping Rule For each finding: * `vulnId`: CVE, GHSA, OSV ID, etc. * `affectedRef`: **exact `bom-ref`** from SBOM. * Optional: secondary keys (file path, package manager coordinates). Example internal record: ```json { "vulnId": "CVE-2025-0001", "affectedRef": "pkg:apk/alpine/openssl@3.2.1-r0?sha256:1234...", "scanner": "grype@0.79.0", "sourceSbomDigest": "sha256:91f2...", "foundAt": "2025-12-09T12:34:56Z" } ``` Developer directions: * Build a **component index** keyed by `bom-ref` when ingesting SBOMs. * Any finding that cannot be mapped to a known `bom-ref` must be flagged: * `status = "unlinked"` and either: * dropped from VEX scope, or * fixed by improving normalization rules. --- ## 6. VEX Authoring Guidelines Use CycloneDX VEX (or OpenVEX) with a strict mapping to SBOM `bom-ref`s. ### 6.1 Minimal VEX Structure ```json { "bomFormat": "CycloneDX", "specVersion": "1.6", "version": 1, "vulnerabilities": [ { "id": "CVE-2025-0001", "source": { "name": "NVD" }, "analysis": { "state": "not_affected", "justification": "vulnerable_code_not_in_execute_path", "response": ["will_not_fix"], "detail": "The vulnerable function is not reachable in this configuration." }, "affects": [ { "ref": "pkg:apk/alpine/openssl@3.2.1-r0?sha256:1234..." } ], "properties": [ { "name": "evidence.sbomDigest", "value": "sha256:91f2..." }, { "name": "evidence.sbomLogId", "value": "rekor:abcd-..." }, { "name": "policy.decisionId", "value": "TRUST-ALG-001#rule-7" } ] } ] } ``` ### 6.2 Required Analysis Discipline For each `(vulnId, affectedRef)`: * `state` ∈ { `not_affected`, `affected`, `fixed`, `under_investigation` }. * `justification`: * `vulnerable_code_not_present` * `vulnerable_code_not_in_execute_path` * `vulnerable_code_not_configured` * `vulnerable_code_cannot_be_controlled_by_adversary` * etc. * `detail`: **concrete explanation**, not generic text. * Reference back to SBOM and other proofs via `properties`. Developer rules: * Every `affects.ref` must match **exactly** a `bom-ref` in at least one SBOM. * VEX generator must fail if it cannot confirm this mapping. --- ## 7. Cryptographic Linking: SBOM ↔ VEX To make the chain verifiable: 1. Compute `sbomDigest = sha256(SBOM_JSON)`. 2. Inside each VEX vulnerability (or at top-level), include: * `properties.evidence.sbomDigest = sbomDigest` * `properties.evidence.sbomLogId` if a transparency log is used. 3. Sign the VEX document with DSSE: * Separate key from SBOM key, or the same with different usage metadata. 4. Optionally publish VEX DSSE to Rekor (or equivalent). Resulting verification chain: * Artifact digest → matches SBOM `metadata.component.bom-ref`. * SBOM `bom-ref`s → referenced by `vulnerabilities[].affects[].ref`. * VEX references SBOM by hash/log ID. * Both SBOM and VEX have valid signatures and log inclusion proofs. --- ## 8. Verifier Implementation Guidelines You should implement a **verifier library** and then thin wrappers: * CLI * API endpoint * UI “Prove it” button ### 8.1 Verification Steps (Algorithm) Given: artifact digest, SBOM, VEX, signatures, logs. 1. **Verify SBOM DSSE signature.** 2. **Verify VEX DSSE signature.** 3. If using Rekor/log: * Verify SBOM and VEX entries: * log inclusion proof * payload hashes match local files. 4. Confirm that: * `artifactDigest` matches `metadata.component.bom-ref` or the indicated digest. 5. Build a map of `bom-ref` from SBOM. 6. For each VEX `affects.ref`: * Ensure it exists in SBOM components. * Ensure `properties.evidence.sbomDigest == sbomDigest`. 7. Compile per-component decisions: For each component: * List associated VEX records. * Derive effective state using a policy (e.g., most recent, highest priority source). Verifier output should be **structured** (not just logs), e.g.: ```json { "artifact": "pkg:oci/example/api-gateway@sha256:abcd...", "sbomVerified": true, "vexVerified": true, "components": [ { "bomRef": "pkg:apk/alpine/openssl@3.2.1-r0?sha256:1234...", "vulnerabilities": [ { "id": "CVE-2025-0001", "state": "not_affected", "justification": "vulnerable_code_not_in_execute_path" } ] } ] } ``` --- ## 9. Data Model and Storage A minimal relational / document model: * `Artifacts` * `id` * `purl` * `digest` * `bomRef` (top level) * `Sboms` * `id` * `digest` * `dsseSignature` * `logId` * `rawJson` * `SbomComponents` * `id` * `sbomId` * `bomRef` (unique per SBOM) * `purl` * `hash` * `VexDocuments` * `id` * `digest` * `dsseSignature` * `logId` * `rawJson` * `VexEntries` * `id` * `vexId` * `vulnId` * `affectedBomRef` * `state` * `justification` * `evidenceSbomDigest` * `policyDecisionId` Guideline: store **raw JSON** plus an **indexed view** for efficient queries. --- ## 10. Testing: Golden Chains Developers should maintain **golden fixtures** where: * A known image or package → SBOM (JSON) → VEX (JSON) → DSSE envelopes → log entries. * For each fixture: * A test harness runs the verifier. * Asserts: * All signatures valid. * All `affects.ref` map to a SBOM `bom-ref`. * The final summarized decision for specific `(vulnId, bomRef)` pairs matches expectations. Include negative tests: * VEX referencing unknown `bom-ref` → verification error. * Mismatching `evidence.sbomDigest` → verification error. * Tampered SBOM or VEX → signature/log verification failure. --- ## 11. Operational Practices and Guardrails Developer-facing rules of thumb: 1. **Never** generate `bom-ref` from mutable fields (paths, timestamps). 2. Treat tool versions and feed snapshots as part of the “scan config”: * Include hashes/versions in SBOM/VEX properties. 3. Enforce **strict types** in code (e.g., enums for VEX states/justifications). 4. Keep keys and signing policies separate per role: * Build pipeline SBOM signer. * Security team VEX signer. 5. Offer a single, stable API: * `POST /verify`: * Inputs: artifact digest (or image reference), SBOM+VEX or references. * Outputs: structured verification report. --- If you want, next step I can do is sketch a small reference implementation outline (e.g., .NET 10 service with DTOs and verification pipeline) that you can drop directly into your codebase.