add advisories
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled

This commit is contained in:
master
2025-12-09 18:45:57 +02:00
parent 199aaf74d8
commit a3c7fe5e88
23 changed files with 9284 additions and 762 deletions

View File

@@ -0,0 +1,628 @@
Heres a tight, stepthrough recipe for making every VEX statement **verifiably** tied to build evidence—using CycloneDX (SBOM), deterministic identifiers, and attestations (intoto/DSSE).
---
# 1) Build time: mint stable, contentaddressed IDs
* For every artifact (source, module, package, container layer), compute:
* `sha256` of canonical bytes
* a **deterministic component ID**: `pkg:<ecosystem>/<name>@<version>?sha256=<digest>` (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 intoto).
* 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 components 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 ProofMarket mirror).
* Now you can verify: **component digest ↔ SBOM bomref ↔ 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. Crosscheck 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)"}
]
}
```
**intoto 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 (UTF8, sorted keys if you posthash).
* Store `{ toolVersions, feed snapshots, policy set hash }` to replay decisions.
* For containers, prefer `bom-ref = pkg:oci/<repo>@<digest>` PLUS layer evidence in `components[]`.
---
# 9) “Helloworld” 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 StellaOps (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=<digest>`
* 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/<repo>@sha256:<manifestDigest>`
* 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": "<KID>",
"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.