From 3443de8610712026dab71f836196df4ff7938413 Mon Sep 17 00:00:00 2001 From: wshallwshall Date: Thu, 6 Aug 2026 19:34:58 -0500 Subject: [PATCH] docs(supply-chain): correct at least two claims the shipped v0.3.2 release assets do not support Verified against the actual release assets, not the prose: `gh release download v0.3.2` gives an SBOM with licenses on 40/40 components and hashes on 0/40, and a VEX with `"statements": []`. Two claims did not survive that check. VEX contents. `docs/SUPPLY-CHAIN.md` described the OpenVEX asset as "our per-CVE exploitability assessments" and told the reader it "records, per CVE, whether the vulnerable code is reachable" -- a statement about the contents of a published artifact, false in any tense. It sat immediately after "Do not demand a zero-CVE clean scan", so a procurement reader who applied the VEX, saw no suppressions, and read that as an assessed all-clear would have had no assessment behind it: a compensating control resting on a false premise (CLAUDE.md section 11). The page now names the artifact, says what a statement carries when one exists, and says plainly that where we have not assessed a CVE the document is silent and the scanner's finding stands. The empty-state rule stays stated once, at security/vex/README.md:17-18 and ADR 0149, and is linked rather than re-copied -- the removed sentence was itself the divergent third copy. Component hashes. The inventory sentence enumerated "components, versions, PackageURLs, hashes, and licenses". Backfilling hashes was investigated and rejected on semantics rather than effort: CycloneDX `component.hashes` means the hash of THE file, while requirements-core.lock carries 301 `--hash=sha256:` lines over 41 packages and no package with exactly one (cryptography alone has 40). cyclonedx-py deliberately routes lock hashes to `externalReferences` for that reason, the 1.6 schema imposes no uniqueness constraint so a multi-entry set would validate clean as a silent false claim, and `pip` is an inventoried component with no lock line at all. So the sentence is corrected instead: "at least" replaces the closed enumeration, the reason given is the verified one, and no substitute integrity control is offered -- Sigstore and SLSA attest the SBOM document and our own release files, which is an adjacent question, and the lock is not a released artifact. The same false enumeration sat in scripts/security/sbom_finalize.py's docstring, one hop from the corrected page, which names that script by path in its "for auditors" section. Fixed there too rather than leaving the repo self-contradictory on the fact this commit is about. Nothing is deployed, so nobody has been misled; the defect is that the shipped page WOULD mislead a first reader who tried to verify components against hashes the SBOM does not carry. Deliberately unchanged: the true "hash-locked" phrases at :16 and :86 refer to the lock the inventory is built from, and a blanket scrub of the word would have deleted accurate claims -- the new text disambiguates them instead. No VEX statement is written here; see the notes handed to the coordinator. --- docs/SUPPLY-CHAIN.md | 20 +++++++++++++------- scripts/security/sbom_finalize.py | 7 +++++-- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/docs/SUPPLY-CHAIN.md b/docs/SUPPLY-CHAIN.md index aa2e794f..ca6208d6 100644 --- a/docs/SUPPLY-CHAIN.md +++ b/docs/SUPPLY-CHAIN.md @@ -14,7 +14,7 @@ decision record is [ADR 0149](adr/0149-multi-ecosystem-sbom-vex-and-sbom-quality |---|---|---| | `messagefoundry-*.whl` / `*.tar.gz` | The Python engine (wheel + sdist) | GitHub release + PyPI | | `messagefoundry-sbom.cdx.json` | **CycloneDX SBOM** of the engine — license-complete, from the hash-locked core runtime, lifecycle = `build` | GitHub release | -| `messagefoundry-vex.openvex.json` | **OpenVEX** — our per-CVE exploitability assessments | GitHub release | +| `messagefoundry-vex.openvex.json` | **OpenVEX** — the document carrying our exploitability assessment for a CVE, once one has been made | GitHub release | | `*.sigstore*` bundles | Sigstore signatures for the wheel, sdist, **SBOM, and VEX** | GitHub release | | PEP 740 attestations | PyPI-side provenance (Trusted Publishing) | PyPI | | SLSA build provenance | in-toto attestation binding each artifact (incl. SBOM + VEX) to the source commit | GitHub attestations / Sigstore bundle | @@ -59,12 +59,15 @@ Attestations, and our releases, if you standardize on one tool across ecosystems ## Using the SBOM + VEX -The SBOM (CycloneDX 1.6) is a machine-readable inventory: components, versions, PackageURLs, hashes, and -**licenses**. Feed it to your own tooling: +The SBOM (CycloneDX 1.6) is a machine-readable inventory carrying at least a name, version, PackageURL and +**license** for every component. It does **not** carry per-component file hashes — the generator we run does +not emit them (see [How the SBOMs are generated](#how-the-sboms-are-generated-for-auditors)) — so use it as an +inventory, not as an integrity check on the components it lists. "Hash-locked" elsewhere on this page refers +to the lock file the inventory is built from, not to a field inside the SBOM. Feed it to your own tooling: ```bash -# Scan the SBOM for known CVEs, applying our VEX to suppress vulnerabilities we've assessed as -# not-affected/fixed — so you triage real risk, not unreachable CVEs: +# Scan the SBOM for known CVEs. --vex applies whatever assessments our VEX carries; --show-suppressed +# lists what was suppressed, so a run with nothing to apply is visibly a no-op: trivy sbom messagefoundry-sbom.cdx.json --vex messagefoundry-vex.openvex.json --show-suppressed # Or score the SBOM's completeness (0-10, NTIA minimum elements): @@ -72,8 +75,11 @@ sbomqs score -b messagefoundry-sbom.cdx.json ``` **Do not demand a zero-CVE "clean scan."** Per CISA's *Minimum Requirements for VEX* and NTIA's -*Software Consumers Playbook*, the correct posture is to accept a valid VEX assessment. Our VEX -(`security/vex/README.md`) records, per CVE, whether the vulnerable code is reachable in MessageFoundry. +*Software Consumers Playbook*, the correct posture is to accept a valid VEX assessment. Our VEX is the +`messagefoundry-vex.openvex.json` release asset above. Where we have assessed a CVE, its statement records +whether the vulnerable code is reachable in MessageFoundry and carries an OpenVEX `justification`. Where we +have not, the document says nothing about that CVE and your scanner's finding stands unsuppressed — see +[`security/vex/README.md`](../security/vex/README.md) for the assessment process and when a statement is added. ## How the SBOMs are generated (for auditors) diff --git a/scripts/security/sbom_finalize.py b/scripts/security/sbom_finalize.py index 3832e1ea..2ba0ec01 100644 --- a/scripts/security/sbom_finalize.py +++ b/scripts/security/sbom_finalize.py @@ -6,8 +6,11 @@ WHY THIS EXISTS (ADR 0149). The SBOM generators we run — ``cyclonedx-py environment`` (Python), ``@cyclonedx/cyclonedx-npm`` (the VS Code extension), and ``trivy image`` (the container) — emit a -CycloneDX BOM with components, hashes, licenses, and ``metadata.tools`` (the generating tool, i.e. -the draft-2025 CISA "Tool Name" minimum element). Two gaps remain that this closes: +CycloneDX BOM carrying at least components, licenses, and ``metadata.tools`` (the generating tool, i.e. +the draft-2025 CISA "Tool Name" minimum element). Per-component *hashes* are NOT among them for the +Python SBOM: ``cyclonedx-py environment`` emits none at all, so do not describe the finalized artifact +as hash-bearing (docs/SUPPLY-CHAIN.md says so to operators, and the two must not drift apart again). +Two gaps remain that this closes: 1. None of them set ``metadata.lifecycles`` — the CycloneDX field that records WHERE in the SDLC the BOM was produced. That maps to CISA's "Build" SBOM Type and the draft-2025 CISA "Generation