generated from coulomb/repo-seed
221 lines
8.1 KiB
Markdown
221 lines
8.1 KiB
Markdown
---
|
|
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-cluster -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`](/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=<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
|
|
```
|