From 62fbe884e3d6d8698f8b77fa227037d33436fd92 Mon Sep 17 00:00:00 2001 From: tegwick Date: Mon, 2 Mar 2026 13:31:08 +0100 Subject: [PATCH] feat(sbom): add custodian ingest-sbom + fix help button target MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- custodian_cli.py | 43 ++++++++++++++++++++++++++++++++++++++ dashboard/src/docs/sbom.md | 25 ++++++++++++++++++---- dashboard/src/repos.md | 2 +- 3 files changed, 65 insertions(+), 5 deletions(-) diff --git a/custodian_cli.py b/custodian_cli.py index af26550..0b0ba46 100644 --- a/custodian_cli.py +++ b/custodian_cli.py @@ -284,6 +284,41 @@ def cmd_register(args: argparse.Namespace) -> None: print(" The repo agent will pick up 4 onboarding tasks and integrate autonomously.") +def cmd_ingest_sbom(args: argparse.Namespace) -> None: + """Ingest SBOM for the current (or specified) repo. Auto-detects slug from registration.""" + project_path = Path(args.path).resolve() + + _api_get("/state/health") + + # Resolve repo slug: explicit override, or look up by local_path + repo_slug = args.slug + if not repo_slug: + repos = _api_get("/repos/") + repo = next((r for r in repos if r.get("local_path") == str(project_path)), None) + if not repo: + print(f"ERROR: No registered repo found for path '{project_path}'.") + print(" Register first: custodian register-project --domain ") + print(" Or pass --slug explicitly.") + sys.exit(1) + repo_slug = repo["slug"] + + print(f"==> Ingesting SBOM for '{repo_slug}' from {project_path} ...") + + python = STATE_HUB_DIR / ".venv" / "bin" / "python" + ingest_script = STATE_HUB_DIR / "scripts" / "ingest_sbom.py" + + if not python.exists(): + print(f"ERROR: .venv not found at {STATE_HUB_DIR}. Run 'make install' in the state-hub directory.") + sys.exit(1) + + cmd = [str(python), str(ingest_script), "--repo", repo_slug, "--scan", "--repo-path", str(project_path)] + if args.dry_run: + cmd.append("--dry-run") + + result = subprocess.run(cmd) + sys.exit(result.returncode) + + def cmd_create_workstream(args: argparse.Namespace) -> None: """Create a workstream under a domain's topic.""" _api_get("/state/health") @@ -406,6 +441,12 @@ def main() -> None: help="Project directory (defaults to current directory)", ) + # ingest-sbom + ing = sub.add_parser("ingest-sbom", help="Ingest SBOM for the repo at the current directory") + ing.add_argument("--path", default=os.getcwd(), help="Repo directory (defaults to cwd)") + ing.add_argument("--slug", default=None, help="Repo slug (auto-detected from path if omitted)") + ing.add_argument("--dry-run", action="store_true", help="Parse lockfiles but do not submit to API") + # create-workstream cws = sub.add_parser("create-workstream", help="Create a workstream under a domain topic") cws.add_argument("--domain", required=True, help="Domain slug to create the workstream under") @@ -429,6 +470,8 @@ def main() -> None: if args.command == "register-project": cmd_register(args) + elif args.command == "ingest-sbom": + cmd_ingest_sbom(args) elif args.command == "create-workstream": cmd_create_workstream(args) elif args.command == "create-task": diff --git a/dashboard/src/docs/sbom.md b/dashboard/src/docs/sbom.md index a60694f..dff1703 100644 --- a/dashboard/src/docs/sbom.md +++ b/dashboard/src/docs/sbom.md @@ -70,11 +70,12 @@ 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 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 `make ingest-sbom`. +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 @@ -92,7 +93,7 @@ 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= SCAN=1 REPO_PATH=` +Then ingest: `custodian ingest-sbom` (from the repo root) ### Node / npm `package-lock.json` is generated automatically by `npm install`. Commit it. @@ -168,6 +169,22 @@ See the full standard: [`/docs/inter-repo-communication`](/docs/inter-repo-commu ## 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 diff --git a/dashboard/src/repos.md b/dashboard/src/repos.md index 4b4425e..051975c 100644 --- a/dashboard/src/repos.md +++ b/dashboard/src/repos.md @@ -143,7 +143,7 @@ display(html`
// Returns a new "⚠ not ingested" span with a ? help button each time it's called. function _sbomGap() { const el = html`⚠ not ingested`; - withDocHelp(el, "/docs/repos"); + withDocHelp(el, "/docs/sbom"); return el; }