--- 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) ```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: `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`: ```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 a compliance gap is identified in a registered repo, the finding is routed as an **ecosystem todo**: a state hub task with `[repo:]` 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`](/docs/inter-repo-communication) --- ## Ingest commands ### From the repo root (recommended) ```bash # 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) ```bash # Auto-detect lockfile at repo root make ingest-sbom REPO= REPO_PATH=/path/to/repo # Scan entire tree — required for multi-ecosystem repos make ingest-sbom REPO= SCAN=1 REPO_PATH=/path/to/repo # Explicit lockfile make ingest-sbom REPO= LOCKFILE=/path/to/uv.lock # Dry run (parse but do not submit) .venv/bin/python scripts/ingest_sbom.py --repo --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 ```