Summary
PR #225 introduces a merge-with-Syft pattern for Docker Hub images: fetch the publisher's upstream SBOM, run Syft on the live image to catch COPYd application content, and merge the two (sbomify_action/cli/main.py:1200-1290, sbom_merge.py). The Chainguard branch a few lines above (sbomify_action/cli/main.py:1153-1198) does NOT follow this pattern — it stops at the upstream fetch, bypasses Syft entirely, and writes Chainguard's published SBOM out as the final result.
That asymmetry is documented in the user-facing warning at lines 1181-1192:
"Using the SBOM published by Chainguard for this image. It covers the packages in the Chainguard base image only — anything your Dockerfile adds on top (your application binary, files brought in via COPY/ADD, artifacts from other build stages via COPY --from=..., etc.) will NOT appear in the resulting SBOM. To include them, provide additional packages via ADDITIONAL_PACKAGES…"
That workaround places the burden on the user. The merge infrastructure landed in #225 obsoletes it.
What good looks like
The Chainguard branch should mirror the Docker Hub branch:
fetch_chainguard_sbom() produces the SPDX SBOM (unchanged)
- Run
generate_sbom() to get a Syft scan of the live image
merge_cyclonedx(upstream, syft) / merge_spdx(upstream, syft) — Chainguard wins for base packages, Syft fills gaps + overlays COPYd content
- Tag components with
sbomify:source = chainguard-upstream or syft-overlay, matching the convention from dockerhub.py
- Drop the warning at
cli/main.py:1181-1192 — no longer accurate
The sbom_merge.py primitives from #225 work without modification: strict PURL-core dedup + the loose (type, name, version) fallback already handle apk-vs-syft namespace differences the same way they handle Amazon Linux's amazonlinux/ vs amzn/ mismatch.
Why this matters concretely
For a typical user app built FROM cgr.dev/chainguard/python with COPY ./app /app:
| Path |
Base packages (apk) |
Application packages (pypi) |
| Today's Chainguard path |
Full closure from Chainguard SBOM |
Dropped |
| Proposed merge |
Full closure from Chainguard SBOM |
Overlaid by Syft |
The same gap exists for any non-trivial Chainguard image, regardless of language: Node node_modules, Ruby gems, Go binaries built in a multi-stage FROM cgr.dev/chainguard/go and COPY --from=builder'd into a runtime image, etc.
Proposed implementation shape
Refactor cli/main.py:1153-1198 so the Chainguard branch follows the control flow of the Docker Hub branch (cli/main.py:1200-1290):
if chainguard_info:
upstream_spdx = fetch_chainguard_sbom(chainguard_info)
syft_doc = run_syft(config.docker_image) # via generate_sbom()
if config.sbom_format == "cyclonedx":
upstream_cdx = convert_spdx_to_cyclonedx(upstream_spdx, spec)
merged = merge_cyclonedx(upstream_cdx, syft_doc)
else:
merged = merge_spdx(deepcopy(upstream_spdx), syft_doc)
write(merged)
Code is largely a copy-paste of the Docker Hub branch with chainguard_info substituted for dockerhub_info. Most of the duplication could probably be factored into a single merge_with_syft(upstream_sbom, docker_image, fmt, spec) helper that both branches call — but that's a stylistic call for maintainers.
Open questions for maintainers
- Fallback on Syft failure — today the Chainguard path falls back to "normal generation" if
fetch_chainguard_sbom() fails (cli/main.py:1158-1160). If the merge succeeds on the upstream fetch but the Syft scan fails, what's the right behavior — fall back to the upstream-only SBOM (today's behavior, but with a warning that app code is missing) or fail the whole run?
- Conflict policy — confirm Chainguard-wins for overlapping base packages (matches the Docker Hub policy:
merge_cyclonedx(upstream, syft) puts upstream first).
- Provenance-detected Chainguard images — the provenance path (
chainguard.py:78-100) sets image_ref to the base image, not the user's built image. Confirm Syft is run against config.docker_image (the user's image), not chainguard_info.image_ref (the base) — otherwise overlay would miss COPYd content.
Related
Companion issue #232 covers the non-detected image path (default cdxgen -t oci vs Syft priority swap). Together they close the Syft-coverage gap across both detected (Chainguard / Docker Hub / DHI) and non-detected image scans.
Dependency
Builds on #225. Should not merge before #225 lands.
Summary
PR #225 introduces a merge-with-Syft pattern for Docker Hub images: fetch the publisher's upstream SBOM, run Syft on the live image to catch
COPYd application content, and merge the two (sbomify_action/cli/main.py:1200-1290,sbom_merge.py). The Chainguard branch a few lines above (sbomify_action/cli/main.py:1153-1198) does NOT follow this pattern — it stops at the upstream fetch, bypasses Syft entirely, and writes Chainguard's published SBOM out as the final result.That asymmetry is documented in the user-facing warning at lines 1181-1192:
That workaround places the burden on the user. The merge infrastructure landed in #225 obsoletes it.
What good looks like
The Chainguard branch should mirror the Docker Hub branch:
fetch_chainguard_sbom()produces the SPDX SBOM (unchanged)generate_sbom()to get a Syft scan of the live imagemerge_cyclonedx(upstream, syft)/merge_spdx(upstream, syft)— Chainguard wins for base packages, Syft fills gaps + overlaysCOPYd contentsbomify:source = chainguard-upstreamorsyft-overlay, matching the convention fromdockerhub.pycli/main.py:1181-1192— no longer accurateThe
sbom_merge.pyprimitives from #225 work without modification: strict PURL-core dedup + the loose(type, name, version)fallback already handle apk-vs-syft namespace differences the same way they handle Amazon Linux'samazonlinux/vsamzn/mismatch.Why this matters concretely
For a typical user app built
FROM cgr.dev/chainguard/pythonwithCOPY ./app /app:The same gap exists for any non-trivial Chainguard image, regardless of language: Node
node_modules, Ruby gems, Go binaries built in a multi-stageFROM cgr.dev/chainguard/goandCOPY --from=builder'd into a runtime image, etc.Proposed implementation shape
Refactor
cli/main.py:1153-1198so the Chainguard branch follows the control flow of the Docker Hub branch (cli/main.py:1200-1290):Code is largely a copy-paste of the Docker Hub branch with
chainguard_infosubstituted fordockerhub_info. Most of the duplication could probably be factored into a singlemerge_with_syft(upstream_sbom, docker_image, fmt, spec)helper that both branches call — but that's a stylistic call for maintainers.Open questions for maintainers
fetch_chainguard_sbom()fails (cli/main.py:1158-1160). If the merge succeeds on the upstream fetch but the Syft scan fails, what's the right behavior — fall back to the upstream-only SBOM (today's behavior, but with a warning that app code is missing) or fail the whole run?merge_cyclonedx(upstream, syft)puts upstream first).chainguard.py:78-100) setsimage_refto the base image, not the user's built image. Confirm Syft is run againstconfig.docker_image(the user's image), notchainguard_info.image_ref(the base) — otherwise overlay would missCOPYd content.Related
Companion issue #232 covers the non-detected image path (default
cdxgen -t ocivs Syft priority swap). Together they close the Syft-coverage gap across both detected (Chainguard / Docker Hub / DHI) and non-detected image scans.Dependency
Builds on #225. Should not merge before #225 lands.