Skip to content

Apply Docker Hub merge-with-Syft pattern to Chainguard images — COPYd application content currently dropped #233

Description

@nissessenap

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:

  1. fetch_chainguard_sbom() produces the SPDX SBOM (unchanged)
  2. Run generate_sbom() to get a Syft scan of the live image
  3. merge_cyclonedx(upstream, syft) / merge_spdx(upstream, syft) — Chainguard wins for base packages, Syft fills gaps + overlays COPYd content
  4. Tag components with sbomify:source = chainguard-upstream or syft-overlay, matching the convention from dockerhub.py
  5. 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

  1. 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?
  2. Conflict policy — confirm Chainguard-wins for overlapping base packages (matches the Docker Hub policy: merge_cyclonedx(upstream, syft) puts upstream first).
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions