Files
state-hub/dashboard/src/docs/sbom.md
tegwick 62fbe884e3 feat(sbom): add custodian ingest-sbom + fix help button target
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>
2026-03-02 13:31:08 +01:00

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

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

# 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