Skip to content

feat(bom): add CycloneDX 1.6 image BOM generator#747

Merged
mchmarny merged 5 commits into
mainfrom
feat/bom-tool
May 5, 2026
Merged

feat(bom): add CycloneDX 1.6 image BOM generator#747
mchmarny merged 5 commits into
mainfrom
feat/bom-tool

Conversation

@mchmarny

@mchmarny mchmarny commented May 5, 2026

Copy link
Copy Markdown
Member

Summary

Adds make bom plus a reusable pkg/bom package that produces an authoritative inventory of every container image AICR can deploy — CycloneDX 1.6 JSON + Markdown summary.

Motivation / Context

Today, ~19 of 22 components (~86%) defer sub-image selection to upstream Helm chart defaults — there's no way to enumerate "every image AICR can deploy" without rendering charts manually. This blocks security review, air-gap mirror lists, supply-chain attestation, and the docs we owe customers.

Fixes: #740
Related: #739, #741, #744, #745

Type of Change

  • New feature (non-breaking change that adds functionality)
  • Build/CI/tooling

Component(s) Affected

  • Core libraries (pkg/bom — new package)
  • Build/tooling (tools/bom, Makefile)

Implementation Notes

Architecture (refactored from initial proposal — see #739 thread):

  • pkg/bom holds the reusable core: image extraction from rendered YAML, ImageRef parsing, OCI purl construction, CycloneDX assembly, Markdown rendering. Public API is intentionally minimal so pkg/bundler can plug in next.
  • tools/bom is a thin CLI wrapper that adds registry parsing, helm template invocation, and manifest walking.

CycloneDX modeling:

  • metadata.component = AICR itself (type: application).
  • Each registry entry = application component with bom-ref: aicr/<name> and helm chart metadata in properties[].
  • Each unique image = container component with bom-ref: img:<image> and purl: pkg:oci/<name>@<version>?repository_url=<registry>/<namespace>.
  • dependencies[] wires AICR → components → images.

This shape is consumable by Trivy, Grype, and cosign attest --predicate without conversion.

Helm template handling: the recipes/components/*/manifests/ tree contains files mixing YAML with Helm templates (network-operator, kubeflow-trainer, gpu-operator, kgateway, nodewright-customizations). pkg/bom.ExtractImagesFromYAML strips {{ ... }} directives — control-flow-only lines are dropped, inline directives become a placeholder — so static image: values still surface from these files. Covered by tests.

Why a new dep (github.com/CycloneDX/cyclonedx-go): producing valid CycloneDX 1.6 by hand is finicky and the official library is small (~3 transitive deps, all already MIT/Apache compatible). Vendored.

Out of scope (follow-up): integrating BOM generation into aicr bundle so each generated bundle ships its own CycloneDX SBOM. The pkg/bom API was designed for this — BuildBOM takes a generic Metadata{} so the bundler can identify a recipe/bundle as the root component instead of AICR-the-repo. The follow-up will make per-bundle SBOM the default — no flag required. Will file as a separate issue once this lands.

Testing

Live registry smoke test:

  • make bom (helm rendering enabled): 22 components, 71 unique images across 11 registries.
  • make bom BOM_SKIP_HELM=1 (manifest-only): 22 components, 13 image refs.
make qualify  # all gates pass:
# - test-coverage: 75.1% (threshold 70%)
# - lint-go: 0 issues
# - lint-yaml: ok
# - license: headers ok
# - check-agents-sync: ok
# - check-docs-sidebar: ok
# - scan: only pre-existing low/med findings in npm/python (docs site deps)

pkg/bom coverage: 87.7% (new package).

Risk Assessment

  • Low — Net-new code in pkg/bom and tools/bom. Adds one new vendor dependency. Touches Makefile (one new target, one untouched target). No existing code paths modified. Easy to revert.

Rollout notes: none. The make bom target is opt-in. No CI changes in this PR (will follow up to wire BOM into release artifacts and PR diff comments — separate issues).

Checklist

  • Tests pass locally (make test with -race)
  • Linter passes (make lint)
  • I did not skip/disable tests to make CI green
  • I added/updated tests for new functionality
  • I updated docs if user-facing behavior changed (tools/bom/README.md)
  • Changes follow existing patterns in the codebase
  • Commits are cryptographically signed (git commit -S)

Adds `make bom` plus a reusable `pkg/bom` package that produces an
authoritative inventory of every container image AICR can deploy by
rendering each Helm chart in `recipes/registry.yaml` at its pinned
version, walking embedded manifests under `recipes/components/*/manifests/`,
and emitting:

- `bom.cdx.json` — CycloneDX 1.6 JSON (canonical, machine-readable)
- `bom.md` — human-readable Markdown summary

Modeling: AICR is the root component; each registry entry is an
`application` component; each unique image is a `container` component
with an OCI `purl`; `dependencies[]` wires the deployment graph. Output
is consumable by Trivy, Grype, and Cosign attestation without conversion.

Closes #740. Refs #739.

Image extraction handles Helm-templated manifests by stripping
`{{ ... }}` directives before YAML parsing — control-flow-only lines are
dropped, inline directives become a placeholder, so static `image:`
values still surface from mixed YAML/Helm files like the
network-operator and kubeflow-trainer manifests.

`pkg/bom` is structured for reuse: a follow-up will plumb it into
`pkg/bundler` so `aicr bundle` emits a per-bundle CycloneDX SBOM
alongside the generated install scripts (planned to be on by default,
no flag required).

Tested against the live registry: 22 components, 71 unique images
across 11 registries (NGC, ghcr.io, gcr.io, ECR, quay.io,
registry.k8s.io, cr.kgateway.dev, Docker Hub).
@coderabbitai

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

@mchmarny mchmarny self-assigned this May 5, 2026
@github-actions

github-actions Bot commented May 5, 2026

Copy link
Copy Markdown
Contributor

Coverage Report ✅

Metric Value
Coverage 75.0%
Threshold 70%
Status Pass
Coverage Badge
![Coverage](https://img.shields.io/badge/coverage-75.0%25-green)

Merging this branch will increase overall coverage

Impacted Packages Coverage Δ 🤖
github.com/NVIDIA/aicr/pkg/bom 89.90% (+89.90%) 🌟
github.com/NVIDIA/aicr/tools/bom 0.00% (ø)

Coverage by file

Changed files (no unit tests)

Changed File Coverage Δ Total Covered Missed 🤖
github.com/NVIDIA/aicr/pkg/bom/bom.go 98.04% (+98.04%) 51 (+51) 50 (+50) 1 (+1) 🌟
github.com/NVIDIA/aicr/pkg/bom/doc.go 0.00% (ø) 0 0 0
github.com/NVIDIA/aicr/pkg/bom/extract.go 97.70% (+97.70%) 87 (+87) 85 (+85) 2 (+2) 🌟
github.com/NVIDIA/aicr/pkg/bom/markdown.go 74.29% (+74.29%) 70 (+70) 52 (+52) 18 (+18) 🌟
github.com/NVIDIA/aicr/tools/bom/helm.go 0.00% (ø) 29 (+29) 0 29 (+29)
github.com/NVIDIA/aicr/tools/bom/main.go 0.00% (ø) 106 (+106) 0 106 (+106)
github.com/NVIDIA/aicr/tools/bom/registry.go 0.00% (ø) 12 (+12) 0 12 (+12)

Please note that the "Total", "Covered", and "Missed" counts above refer to code statements instead of lines of code. The value in brackets refers to the test coverage of that file in the old version of the code.

- Replace custom newUUIDv4 with google/uuid (already in go.mod) so BOM
  serial numbers are RFC 4122-conformant without hand-rolled crypto.
- Use pkg/errors with structured error codes throughout pkg/bom and
  tools/bom, replacing fmt.Errorf wraps to match project convention.
- Fix OCI purl spec compliance: repository_url now includes the full
  artifact path (registry/namespace/name) instead of stopping at the
  namespace; when both digest and tag are present, tag moves to the
  qualifier per spec. Tag-as-version remains the pragmatic fallback for
  the common no-digest case.
- BuildBOM and WriteMarkdown now copy the input slice before sorting so
  callers (notably the planned pkg/bundler integration) don't observe
  their input reordered.
- Replace boolStr helper with strconv.FormatBool.
- Lift helm-render ctx into renderHelmComponent so the WithTimeout
  cancel runs at end of helm work, not at end of the whole survey.
- Extract goconst-tripping defaults ("aicr", "NVIDIA Corporation") to
  named constants.
- Drop misleading "or empty string" docstring on componentValuesPath /
  componentManifestsDir; the path-joiner role is documented and callers
  stat-check before using.

PURL test updated to cover the spec-correct repository_url shape and
the digest+tag qualifier case.
@mchmarny mchmarny enabled auto-merge (squash) May 5, 2026 14:39
coderabbitai[bot]

This comment was marked as resolved.

Recurse into n.Alias for AliasNode so an `image:` value reached via a
YAML anchor + alias is still surveyed. Rare in K8s manifests but cheap
to handle and now covered by a regression test.
lalitadithya
lalitadithya previously approved these changes May 5, 2026
coderabbitai[bot]

This comment was marked as resolved.

@mchmarny mchmarny disabled auto-merge May 5, 2026 14:53
… Hub refs

Two correctness fixes surfaced in review:

1. `image: *anchor` was dropped. The recursion path landed in the
   ScalarNode case which is intentionally a no-op, so a scalar reached
   through an alias was never recorded. The image-key branch now resolves
   AliasNode targets before checking for a scalar.
2. `busybox:1.36` and `docker.io/library/busybox:1.36` produced different
   ImageRef.Repository values and therefore different PURLs, so the same
   Docker Hub image showed up twice in the BOM. ParseImageRef now
   canonicalizes single-segment Docker Hub refs to `library/<name>` per
   the Docker Hub default-namespace rule, matching the OCI purl-spec
   example `repository_url=docker.io/library/<name>` for official images.

Both fixes are covered by new test cases (direct scalar alias + library/
canonicalization across single-segment and fully-qualified inputs).
lalitadithya
lalitadithya previously approved these changes May 5, 2026
coderabbitai[bot]

This comment was marked as resolved.

The decode-failure branch in ExtractImagesFromYAML was previously
unexercised by tests. Add a regression test that feeds malformed YAML
(unclosed flow sequence) and asserts a non-nil error is returned.
@mchmarny mchmarny merged commit b4e6ea0 into main May 5, 2026
30 checks passed
@mchmarny mchmarny deleted the feat/bom-tool branch May 5, 2026 15:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Decide pinning policy for high-implicit-surface components

2 participants