193 lines
10 KiB
Markdown
193 lines
10 KiB
Markdown
# Software Bill of Materials (SBOM)
|
|
|
|
This document is the canonical SBOM workflow for the platform. It explains what a Software Bill of Materials is, why we ship one, how it is generated, how it is distributed, and how it is consumed by vulnerability scanners.
|
|
|
|
> Cross-references:
|
|
> - Generator: [`scripts/generate-sbom.mjs`](../../scripts/generate-sbom.mjs)
|
|
> - Threat model component coverage: [`threat-model.md`](./threat-model.md)
|
|
> - Incident-response forensics: [`incident-response.md`](./incident-response.md) §4 "Forensics"
|
|
|
|
## 1. What is an SBOM?
|
|
|
|
A Software Bill of Materials is a machine-readable inventory of every software component that ships in a build artifact. For this platform it includes:
|
|
|
|
- Every npm/pnpm workspace package shipped in the runtime images (Web, API, Worker)
|
|
- Every transitive dependency resolved by pnpm (including dev-only and production-only)
|
|
- The version, license (when declared), and resolved tarball hash of each dependency
|
|
- The git commit and build identifier of the repository at the time of build
|
|
|
|
An SBOM is not a vulnerability report. It is the input to a vulnerability scanner — the scanner correlates the SBOM against a CVE feed (NIST NVD, GHSA, OSV) and produces the actual vulnerability report.
|
|
|
|
## 2. Why we ship one
|
|
|
|
- **Customer procurement** — enterprise procurement teams require an SBOM with every release. Without one, the platform cannot be evaluated by a regulated customer.
|
|
- **Executive Order 14028 / NIST SP 800-218 (SSDF)** — US federal procurement requires SBOMs for software shipped to federal customers. Even if the operator is not a US federal supplier today, the SBOM lowers the bar to that market.
|
|
- **EU Cyber Resilience Act (CRA)** — once the CRA's reporting requirements bite, an SBOM is the input to the operator's CRA-mandated vulnerability handling process.
|
|
- **Internal vulnerability handling** — when a new CVE lands, the operator can answer "are we affected?" in minutes by querying the SBOM, not by re-resolving the lockfile under time pressure.
|
|
- **Incident forensics** — when investigating an incident potentially caused by a third-party library, the SBOM is the canonical record of what shipped.
|
|
|
|
## 3. Format
|
|
|
|
The repository currently ships a **pnpm-native dependency manifest** rather than a strict CycloneDX or SPDX document. The reason is documented at the top of `scripts/generate-sbom.mjs`:
|
|
|
|
- `@cyclonedx/cyclonedx-npm` and SPDX-tooling for npm tend to mis-handle pnpm workspaces (`apps/*`, `packages/*`), producing partial trees or wrongly-attributed transitive edges.
|
|
- pnpm's own resolver is the source of truth for what ships in our images, so we capture its `pnpm list -r --json --depth=Infinity` output verbatim.
|
|
- The artifact is consumed for **audit traceability** ("which version of which package shipped in this image"), not for CycloneDX-validator gating.
|
|
|
|
The output JSON has this shape:
|
|
|
|
```json
|
|
{
|
|
"schema": "toolctrl.sbom-pnpm/v1",
|
|
"generatedAt": "<ISO timestamp>",
|
|
"generator": "scripts/generate-sbom.mjs",
|
|
"source": "pnpm list -r --json --depth=Infinity",
|
|
"repo": { "name": "<root pkg name>", "version": "<root pkg version>" },
|
|
"workspaces": [ /* pnpm list output */ ]
|
|
}
|
|
```
|
|
|
|
The file is named `sbom-pnpm.json` and is committed at the **build artifact level only** — it is generated at build time and shipped inside the runtime images. It is **not** committed to the source repository.
|
|
|
|
### 3.1 Migration path to CycloneDX
|
|
|
|
When a customer procurement step requires a strict CycloneDX 1.4+ document, the operator can:
|
|
|
|
1. Run `pnpm dlx @cyclonedx/cyclonedx-npm --output-format JSON --output-file sbom.cdx.json --omit dev`
|
|
2. Validate with `pnpm dlx @cyclonedx/cyclonedx-cli validate --input-file sbom.cdx.json`
|
|
3. Diff against `sbom-pnpm.json` to confirm component coverage
|
|
|
|
The operator-side conversion is **out of band** from the build; it does not gate releases. If/when a future task makes CycloneDX a build gate, this section becomes the migration target.
|
|
|
|
## 4. When the SBOM is generated
|
|
|
|
The Dockerfile attempts to generate the SBOM at **every Docker build**. It invokes `scripts/generate-sbom.mjs` in the `build` stage and copies the resulting `sbom-pnpm.json` into each runtime stage (Web, API, Worker). SBOM generation is intentionally skip-safe in the Dockerfile: a generator failure logs `SBOM generation skipped` instead of failing the image build, because the SBOM is an audit-traceability artifact rather than a deploy gate.
|
|
|
|
Each runtime image therefore carries the same repository dependency inventory for the built revision. Treat it as the npm/pnpm component record for that build, then pair it with an image scanner such as Trivy for OS packages and runtime binaries.
|
|
|
|
Manual local generation (for example, to answer "are we affected by CVE-X?" before a release):
|
|
|
|
```bash
|
|
pnpm exec node scripts/generate-sbom.mjs
|
|
# Output: ./sbom-pnpm.json
|
|
```
|
|
|
|
The script reads `SBOM_OUTPUT_PATH` for a non-default destination; it is otherwise side-effect free.
|
|
|
|
## 5. Distribution
|
|
|
|
### 5.1 Inside runtime images
|
|
|
|
Every container image carries `/myapp/sbom-pnpm.json`. Operators can inspect it with:
|
|
|
|
```bash
|
|
docker run --rm <image> cat /myapp/sbom-pnpm.json | jq '.workspaces | length'
|
|
```
|
|
|
|
### 5.2 Per release
|
|
|
|
Release owners may publish the SBOM as a release asset named `sbom-pnpm-v<version>.json` when a customer or procurement process requires a standalone artifact. The baseline deploy workflow always embeds the SBOM in the runtime images; standalone release-asset publication is operator-owned unless a release workflow explicitly adds that upload step.
|
|
|
|
### 5.3 To customers
|
|
|
|
Customers who need an SBOM under their procurement agreement receive:
|
|
|
|
- The `sbom-pnpm.json` for the version they run
|
|
- On request: a converted CycloneDX 1.4 JSON (see [§3.1](#31-migration-path-to-cyclonedx))
|
|
- On request: a signed manifest (see [§6](#6-signing))
|
|
|
|
The operator does **not** publish SBOMs for unreleased branches or pre-release tags. Customers running staging or pre-release builds receive the SBOM bundled with the build.
|
|
|
|
### 5.4 To researchers
|
|
|
|
The SBOM is **not** published publicly by default. It is included in the artifact pack provided to penetration testers under engagement (see [`pen-test-scope.md`](./pen-test-scope.md) §10) and is provided to coordinated-disclosure reporters who request it.
|
|
|
|
Rationale: SBOM transparency is a security trade-off. The SBOM lets a scanner correlate against CVE feeds, which is good for defence; it also tells an attacker exactly which library versions to target. NIST EO-14028 and the EU CRA accept the trade-off; the operator accepts the same trade-off for procurement customers but does not publish to the open internet without a reason.
|
|
|
|
## 6. Signing
|
|
|
|
When the operator's threat model requires it (typically: enterprise customers requiring tamper-evident SBOM delivery), the SBOM is signed with [Cosign](https://github.com/sigstore/cosign):
|
|
|
|
```bash
|
|
cosign sign-blob \
|
|
--key cosign.key \
|
|
--output-signature sbom-pnpm-v<version>.json.sig \
|
|
sbom-pnpm-v<version>.json
|
|
```
|
|
|
|
Customers verify with:
|
|
|
|
```bash
|
|
cosign verify-blob \
|
|
--key cosign.pub \
|
|
--signature sbom-pnpm-v<version>.json.sig \
|
|
sbom-pnpm-v<version>.json
|
|
```
|
|
|
|
The signing key is held in the operator's password manager. The public key is published on the operator's security page. Rotation cadence: every 12 months or on suspected compromise.
|
|
|
|
## 7. Vulnerability scanner integration
|
|
|
|
The SBOM is the input to whichever scanner the operator uses. Documented integrations:
|
|
|
|
### 7.1 Snyk
|
|
|
|
```bash
|
|
snyk test --file=sbom-pnpm.json --package-manager=npm
|
|
```
|
|
|
|
Snyk consumes the dependency tree and reports CVE matches.
|
|
|
|
### 7.2 Trivy
|
|
|
|
```bash
|
|
trivy sbom sbom-pnpm.json
|
|
```
|
|
|
|
Trivy is the recommended **per-image** scanner: it can also scan the OS-layer of the runtime image (Alpine packages) which the pnpm SBOM does not cover.
|
|
|
|
### 7.3 Dependency-Track
|
|
|
|
The operator's preferred long-term SBOM-management tool is [Dependency-Track](https://dependencytrack.org/). Workflow:
|
|
|
|
1. Convert `sbom-pnpm.json` → CycloneDX (see [§3.1](#31-migration-path-to-cyclonedx))
|
|
2. Upload the CycloneDX document to the Dependency-Track project for this release
|
|
3. Dependency-Track watches the OSV / NVD / GHSA feeds and notifies the operator when a new CVE matches a shipped component
|
|
|
|
### 7.4 GitHub Dependabot / Gitea Renovate
|
|
|
|
These tools operate on the **source repository's lockfile**, not on the shipped SBOM. They are complementary — Dependabot/Renovate raises PRs to upgrade dependencies; the SBOM proves which version actually shipped.
|
|
|
|
## 8. Incident workflow
|
|
|
|
When a new CVE is announced that may affect a shipped dependency, the [incident-response runbook](./incident-response.md) Phase 1 ("Detection") and Phase 2 ("Triage") apply. The SBOM-specific steps are:
|
|
|
|
1. Identify the affected component name and version range from the CVE record
|
|
2. Query the SBOM for the affected component across **every shipped image**:
|
|
```bash
|
|
jq -r '.workspaces[] | .. | objects | select(.name=="<component>") | "\(.name)@\(.version)"' sbom-pnpm.json | sort -u
|
|
```
|
|
3. Decide impact: is the affected codepath invoked by our code? (Mapping to call-sites is operator engineering.)
|
|
4. If yes, the incident enters Phase 3 ("Containment") as documented in [`incident-response.md`](./incident-response.md) §3.
|
|
|
|
## 9. Limitations
|
|
|
|
The current SBOM:
|
|
|
|
- **Covers npm/pnpm dependencies only.** It does not include OS-level packages (Alpine apk), runtime binaries (node itself, ClamAV daemon, ffmpeg, etc.), nor Rust/Go native modules compiled into Prisma / sharp / similar. Trivy on the image fills that gap.
|
|
- **Does not include declared licenses** in a normalised form. The pnpm-list output includes whatever the package metadata declares; license-compliance work is operator-side.
|
|
- **Is generated at build time, not at run time.** If an operator hot-patches a node_module inside a running container (which is forbidden by the deployment policy but possible), the SBOM does not reflect that mutation.
|
|
|
|
## 10. Maintenance
|
|
|
|
This document is reviewed:
|
|
|
|
- When the SBOM generator script changes
|
|
- When the format requirement from a procurement customer changes
|
|
- When the operator adds or replaces the vulnerability scanner integration
|
|
- At every major release tag
|
|
|
|
Update the "Last review" line below as part of the review.
|
|
|
|
_Last review: docs/improvement-plan/10-compliance-polish/security-docs.md task #69._
|