Skip to content

Fix SBOM root component: replace synthetic closure name with OCI image metadata#3

Open
mrdavidlaing wants to merge 3 commits intomainfrom
claude/fix-postgres-sbom-license-BDXgd
Open

Fix SBOM root component: replace synthetic closure name with OCI image metadata#3
mrdavidlaing wants to merge 3 commits intomainfrom
claude/fix-postgres-sbom-license-BDXgd

Conversation

@mrdavidlaing
Copy link
Contributor

@mrdavidlaing mrdavidlaing commented Mar 15, 2026

Summary

  • Fix SBOM root component metadata so bombon-generated SBOMs describe the OCI image instead of the synthetic symlinkJoin closure name
  • Extract patching logic into a shared, tested script (bin/patch-sbom-root)
  • Split sbom-generate-upload.yml into three focused workflows with a PR quality gate
  • Add SBOM quality scoring (sbomqs) and regression detection (sbomlyze) with PR comment reporting

Problem

bombon names the SBOM root component after the symlinkJoin derivation (e.g. postgres-closure), which has no version, PURL, license, or dependency links. This causes:

  1. Missing license on the Postgres SBOM root component
  2. WARNING: SBOM dependency graph is incomplete: root component 'postgres-closure' from sbomify-action
  3. No quality gates to prevent SBOM regressions

Workflow architecture

The old sbom-generate-upload.yml is replaced by three workflows:

1. sbom-generate.yml (PR + main)

Generates, patches, and enriches SBOMs. Enrichment uses sbomify-action with UPLOAD: false — the enriched SBOM is saved as an artifact only.

nix build .#postgres-sbom
→ bombon CycloneDX output (root = “postgres-closure”)
→ bin/patch-sbom-root –name wellmaintained/packages/postgres-image –version 17.4 –purl pkg:docker/… –license PostgreSQL
→ sbomify-action (AUGMENT + ENRICH, no upload)
→ artifact: sbom-postgres/postgres.enriched.cdx.json

2. sbom-quality-gate.yml (PR only)

Triggered by sbom-generate.yml completion. Scores each image's SBOM, compares against the baseline from the last successful main run, and posts a sticky PR comment.


┌─────────────────────────────────────────────────┐
│  score (matrix: 8 images)                       │
│                                                 │
│  Download PR SBOM artifact                      │
│  → bin/sbom-score –image postgres sbom.json    │
│  Fetch baseline from last main run              │
│  → bin/sbom-score (baseline)                    │
│  → bin/sbom-compare –baseline … –current .. │
│  Upload per-image result JSON                   │
└──────────────────────┬──────────────────────────┘
│
┌──────────────────────▼──────────────────────────┐
│  report (aggregates all results)                │
│                                                 │
│  → bin/sbom-report –scores-dir results/        │
│  → Post sticky PR comment                       │
│  → Exit non-zero if any score regressed         │
└─────────────────────────────────────────────────┘

Example PR comment output:

## SBOM Quality Gate

| Image | Score | Baseline | Delta | Status |
|-------|-------|----------|-------|--------|
| postgres | 7.2 | 7.0 | +0.2 | pass |
| redis | 6.8 | 6.8 | 0 | pass |
| minio | 6.5 | N/A | N/A | new baseline |

<details>
<summary>Diff: postgres</summary>

- 1 component added
- 0 components removed
- Version drift: 1 upgrade

</details>

Blocking behavior: The check fails if any image’s sbomqs score decreases. Configure branch protection to require this check for merge.
3. sbom-upload.yml (main only)
Triggered by sbom-generate.yml completion on main. Downloads the enriched SBOM artifacts and uploads to sbomify.

Download sbom-postgres artifact
  → sbomify-action (UPLOAD only)
  → Published to <sbomify.com>


Download sbom-postgres artifact
  → sbomify-action (UPLOAD only)
  → Published to <sbomify.com>

Scripts (TDD with shellspec)
All scripts built red/green with shellspec. 45 examples, 0 failures.
bin/patch-sbom-root (18 specs)
Rewrites the bombon root component from stdin → stdout:

Policy
.github/sbom-policy.json — sbomlyze policy for diff-mode checks:

{
  "max_added": 50,
  "max_removed": 50,
  "deny_licenses": [],
  "require_licenses": true,
  "deny_duplicates": true
}

Test plan
	∙	Verify shellspec passes all 45 specs locally
	∙	Trigger sbom-generate.yml on a PR and confirm enriched artifacts are uploaded
	∙	Verify sbom-quality-gate.yml fires on generate completion, posts PR comment
	∙	Verify sbom-upload.yml only runs on main merges
	∙	Test regression detection: temporarily lower a score and confirm PR is blocked
	∙	Confirm sbom-generate-upload.yml removal doesn’t break existing main branch
https://claude.ai/code/session_01NfQFeoLwbv5VJNEne5NRoA

claude added 3 commits March 15, 2026 17:02
…e metadata

bombon names the root component after the symlinkJoin derivation (e.g.
"postgres-closure"), which has no version, PURL, license, or dependency
links. This causes sbomify-action to warn about an incomplete dependency
graph. Post-process the bombon output to set the root component to the
actual OCI image with proper name, version, container type, PURL, SPDX
license, and dependency relationships to all closure components.

https://claude.ai/code/session_01NfQFeoLwbv5VJNEne5NRoA
…spec tests

Move the jq post-processing logic that rewrites bombon's synthetic root
component into a standalone script (bin/patch-sbom-root) that reads from
stdin and writes to stdout. Both generate-sboms.sh and the CI workflow
now call this shared script instead of duplicating the jq filter.

Add shellspec with 18 specs covering argument validation, root component
patching, dependency graph wiring, and passthrough behavior.

https://claude.ai/code/session_01NfQFeoLwbv5VJNEne5NRoA
Split sbom-generate-upload.yml into three workflows:
- sbom-generate.yml: generate, patch, and enrich SBOMs (PR + main)
- sbom-quality-gate.yml: score with sbomqs, diff with sbomlyze,
  post PR comment, block merge on quality regression
- sbom-upload.yml: upload enriched SBOMs to sbomify (main only)

New TDD scripts (45 shellspec examples, all passing):
- bin/sbom-score: wraps sbomqs, outputs structured JSON
- bin/sbom-compare: wraps sbomlyze diff with policy checking
- bin/sbom-report: aggregates results into markdown, exits non-zero
  on quality regression

https://claude.ai/code/session_01NfQFeoLwbv5VJNEne5NRoA
- name: postgres
sbom_package: postgres-sbom
nixpkg: postgresql_17
license: PostgreSQL
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we rather get the license data from the nix package / derivation that we're making for the image?

Ideally by lining the image license to the license of its primary package; in the same way the we do for version

Comment on lines +113 to +118
bin/patch-sbom-root \
--name "wellmaintained/packages/${{ matrix.image.name }}-image" \
--version "$VERSION" \
--purl "pkg:docker/wellmaintained/packages/${{ matrix.image.name }}@${VERSION}" \
--license "${{ matrix.image.license }}" \
< "$SBOM" > "${SBOM}.tmp" && mv "${SBOM}.tmp" "$SBOM"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same argument here - can we it get the data we need for this patch from the nix expression used to describe the image?

Comment on lines +42 to +51
- name: Install sbomqs
run: |
curl -sSfL https://github.com/interlynk-io/sbomqs/releases/latest/download/sbomqs-linux-amd64 \
-o /usr/local/bin/sbomqs
chmod +x /usr/local/bin/sbomqs

- name: Install sbomlyze
run: |
curl -sSfL https://raw.githubusercontent.com/rezmoss/sbomlyze/main/install.sh | sh
mv ./sbomlyze /usr/local/bin/sbomlyze
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we rather make custom nix packages for these so we can pin to specific versions

if: >-
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success'
runs-on: blacksmith-4vcpu-ubuntu-2404
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a cheaper blacksmith image type we can use for these jobs where we aren't doing compilation or image building

sbomify-keycloak
sbomify-caddy-dev
sbomify-minio-init
postgres:PostgreSQL
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment here - let's embed license info in the nix; not in the CI

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants