docs(sbom): add SBOM reference page + withDocHelp on SBOM dashboard

- docs/sbom.md: what SBOM is, lockfile semantics, 5-level maturity standard,
  gap types A–E, per-ecosystem guidance, Syft OSS tooling, inter-repo task
  communication convention, ingest commands, compliance check commands
- sbom.md: wire withDocHelp(h1, "/docs/sbom") — ? button on page title
- observablehq.config.js: add SBOM entry to Reference nav section

EP-CUST-002 registered: Syft-based comprehensive SBOM generation
Task 5f8cade5 created: [repo:railiance-bootstrap] Add Ansible lockfile

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-01 19:29:20 +01:00
parent 9bfb0c130a
commit 7caaec25a2
3 changed files with 217 additions and 0 deletions

View File

@@ -22,6 +22,7 @@ export default {
{ name: "Decisions", path: "/docs/decisions" },
{ name: "Decision Health", path: "/docs/decisions-kpi" },
{ name: "Progress Log", path: "/docs/progress-log" },
{ name: "SBOM", path: "/docs/sbom" },
],
},
],

210
dashboard/src/docs/sbom.md Normal file
View File

@@ -0,0 +1,210 @@
---
title: SBOM — Reference
---
# Software Bill of Materials (SBOM)
This page defines what an SBOM is, why it matters, what the Custodian SBOM
standard requires of registered repos, and how to bring a repo into compliance.
---
## What is an SBOM?
An SBOM (Software Bill of Materials) is an inventory of every component a
piece of software depends on — direct and transitive, runtime and build-time.
For software projects this means: every library installed via pip, npm, cargo,
or any other package manager. For infrastructure repos it means: Ansible itself,
Terraform providers, system tools the playbooks invoke. For container images: OS
packages, base image layers, language runtimes.
The key question an SBOM answers is: **"what exactly is running, and at which
version?"**
---
## What a lockfile is — and why it matters
A **lockfile** is the machine-generated, committed answer to that question for
one package manager. When you run `uv lock`, `npm install`, or `terraform init`,
the tool resolves all transitive dependencies, pins them to exact versions, and
writes those pins to a lockfile (`uv.lock`, `package-lock.json`,
`.terraform.lock.hcl`).
Without a lockfile:
| Problem | Consequence |
|---------|-------------|
| Versions are not pinned | Different machines or CI runs get different versions — one may work, another may not |
| No transitive inventory | You know you depend on `ansible`, but not which version of `paramiko` or `cryptography` it pulls in |
| Vulnerability scanning is imprecise | CVE databases require exact versions; a range like `ansible>=8` can't be scanned |
| Licence auditing is impossible | You can't know the licence of every transitive dependency |
| Reproducibility breaks | Debugging a production incident requires knowing the exact versions in use |
The lockfile is the **unit of SBOM evidence** for package-managed dependencies.
The State Hub ingests lockfiles to populate the SBOM store.
---
## The Custodian SBOM Standard
Every registered repo is assessed against five maturity levels. A repo must
reach **Level 3** to be considered SBOM-compliant.
| Level | Name | Criterion |
|-------|------|-----------|
| **0** | Registered | Repo appears in the State Hub `/repos/` |
| **1** | Manifested | For every ecosystem in use, a **manifest file** exists and is committed (`pyproject.toml`, `package.json`, `Cargo.toml`, `go.mod`, `ansible/requirements.yml`, etc.) |
| **2** | Locked | Every manifest file has a corresponding **lockfile** committed to the repo |
| **3** | Ingested | `last_sbom_at` is not null; the ingested packages cover all detected ecosystems |
| **4** | Current | `last_sbom_at` is within 30 days, or since the last lockfile change |
| **5** | Clean | No unreviewed copyleft flags in direct prod dependencies; no unknown licences in direct deps |
### SBOM gap types
**Type A — Missing manifest**: dependencies exist but nothing declares them.
Example: Ansible is installed on the control node but there is no `pyproject.toml`
declaring `ansible` as a dependency. Fix: create the manifest.
**Type B — Manifest without lockfile**: a `pyproject.toml` or `package.json`
exists but no lockfile has been generated. Fix: run `uv lock` / `npm install`.
**Type C — Lockfile not ingested**: lockfile exists but `make ingest-sbom` has
not been run, so the State Hub has no record. Fix: run `make ingest-sbom`.
**Type D — Stale ingest**: lockfile exists and was ingested, but has since been
updated (new deps added) without a fresh ingest. Fix: re-run `make ingest-sbom`.
**Type E — Ecosystem not supported**: the repo uses an ecosystem the ingest
script doesn't yet parse (Go, Java, Ruby, Ansible Galaxy collections). The
SBOM gap is expected until support is added. Register a contribution (FR) if
the ecosystem is important for your domain.
---
## Per-ecosystem guidance
### Python (uv)
```bash
uv init --no-workspace # creates pyproject.toml if absent
uv add ansible # adds dep + resolves transitive tree
uv lock # generates or updates uv.lock
git add pyproject.toml uv.lock && git commit
```
Then ingest: `make ingest-sbom REPO=<slug> SCAN=1 REPO_PATH=<path>`
### Node / npm
`package-lock.json` is generated automatically by `npm install`. Commit it.
Licence metadata is embedded per package — the State Hub reads it directly.
### Rust
`Cargo.lock` is generated automatically by `cargo build` or `cargo check`.
Commit it (for binaries; libraries typically do not commit it, but SBOM
ingestion requires it).
### Terraform
Run `terraform init` in each module directory — this generates
`.terraform.lock.hcl`. The `--scan` mode on `make ingest-sbom` finds it
automatically.
### Ansible (Galaxy collections)
If your playbooks use roles or collections from Ansible Galaxy, add them to
`ansible/requirements.yml`:
```yaml
collections:
- name: community.general
version: ">=9.0"
roles: []
```
*Note: `requirements.yml` does not include version pins for Ansible itself.
Use `pyproject.toml` + `uv.lock` to pin the `ansible` pip package.*
### Infra-only repos (Ansible, shell, no Galaxy collections)
The minimum expectation is still a `pyproject.toml` declaring the
control-node pip dependencies (at least `ansible`). This enables:
- Pinning the Ansible version (reproducibility)
- SBOM ingestion (licence + vulnerability auditing)
- A machine-readable baseline for future syft-based assessment
---
## OSS tooling — Syft (recommended for comprehensive assessment)
The State Hub's current ingest relies on hand-rolled lockfile parsers.
A more powerful alternative is **[Syft](https://github.com/anchore/syft)**
(Anchore, Apache 2.0):
- Scans a directory and detects **50+ ecosystems** automatically
- Works even when lockfiles are absent (uses manifest files to derive deps)
- Outputs standard **SPDX** or **CycloneDX** JSON
- Handles: Python, Node, Rust, Go, Java, Ruby, PHP, .NET, Ansible collections,
Terraform providers, OS packages in container images
```bash
# Install
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
# Scan a directory
syft dir:/home/worsch/railiance-bootstrap -o cyclonedx-json > sbom.json
```
Syft integration is tracked as **EP-CUST-002** in the Extension Points
catalogue. When implemented, `make ingest-sbom-syft` will replace the
hand-rolled parsers for comprehensive coverage.
---
## Inter-repo task communication
When the State Hub or custodian identifies a compliance gap in a registered repo,
the task is communicated through two channels:
1. **State Hub task** — created in the relevant domain workstream with
`[repo:<slug>]` in the title. Visible via `get_state_summary()` at the
start of any domain session.
2. **Workplan file** — a `workplans/<ID>-<slug>.md` file is created in the
target repo itself (ADR-001 convention). When you open that repo in Claude
Code, the session protocol surfaces it.
When working in a registered repo, always run `get_state_summary()` at session
start — the state hub surfaces pending tasks for your domain automatically.
---
## Ingest commands
```bash
# Auto-detect lockfile at repo root
make ingest-sbom REPO=<slug> REPO_PATH=/path/to/repo
# Scan entire tree — required for multi-ecosystem repos
make ingest-sbom REPO=<slug> SCAN=1 REPO_PATH=/path/to/repo
# Explicit lockfile
make ingest-sbom REPO=<slug> LOCKFILE=/path/to/uv.lock
# Dry run (parse but do not submit)
.venv/bin/python scripts/ingest_sbom.py --repo <slug> --scan --repo-path /path --dry-run
```
---
## Checking compliance
```bash
# View all repos and their SBOM status
# Dashboard → Repos (http://127.0.0.1:3000/repos)
# API: check last_sbom_at per repo
curl -s http://127.0.0.1:8000/repos/ | python3 -c "
import json, sys
for r in json.load(sys.stdin):
status = r['last_sbom_at'] or '⚠ NOT INGESTED'
print(f'{r[\"slug\"]:30} {status}')
"
# API: licence risk summary
curl -s http://127.0.0.1:8000/sbom/report/licences/ | python3 -m json.tool
```

View File

@@ -40,6 +40,12 @@ const isCopyleft = spdx => spdx && COPYLEFT_KW.some(k => spdx.toUpperCase().inc
# SBOM
```js
import {withDocHelp} from "./components/doc-overlay.js";
const _h1 = document.querySelector("#observablehq-main h1");
if (_h1) { _h1.style.position = "relative"; withDocHelp(_h1, "/docs/sbom"); }
```
## Overview
```js