custodian_cli.py: - new ingest-sbom subcommand: auto-detects repo slug from local_path registration, runs ingest_sbom.py --scan from the repo root - --dry-run flag passes through to the underlying script - --slug override for repos where path lookup fails repos.md: - ? button on "⚠ not ingested" now opens /docs/sbom (not /docs/repos) docs/sbom.md: - Ingest commands section now leads with `custodian ingest-sbom` (repo-root) - make ingest-sbom kept as low-level alternative - Per-ecosystem and gap-type references updated to new command Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
8.1 KiB
title
| 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 custodian ingest-sbom
has not been run, so the State Hub has no record. Fix: run custodian ingest-sbom
from the repo root.
Type D — Stale ingest: lockfile exists and was ingested, but has since been
updated (new deps added) without a fresh ingest. Fix: re-run custodian 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)
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: custodian ingest-sbom (from the repo root)
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:
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 (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
# 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 a compliance gap is identified in a registered repo, the finding is routed
as an ecosystem todo: a state hub task with [repo:<slug>] in the title,
created in the target domain's workstream. The target repo's session protocol
surfaces it automatically at next session start.
See the full standard: /docs/inter-repo-communication
Ingest commands
From the repo root (recommended)
# Scan all lockfiles in the current repo and ingest
custodian ingest-sbom
# Dry run — parse and report without submitting
custodian ingest-sbom --dry-run
custodian ingest-sbom looks up the repo slug from the State Hub registration
(local_path match), then scans the whole tree for all supported lockfile
formats. The repo must be registered first — see custodian register-project.
From the state-hub directory (low-level)
# 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
# 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