Skip to content

#614 Generate a CycloneDX SBOM for each web and worker image - #651

Merged
ajs-uk-dev merged 7 commits into
mainfrom
feature/614-image_sbom
Oct 20, 2026
Merged

ajs-uk-dev merged 7 commits into
mainfrom
feature/614-image_sbom

Conversation

@ajs-uk-dev

@ajs-uk-dev ajs-uk-dev commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Closes #614 (part of #607, OWASP A03).

What

Every web and worker image CI pushes now has a CycloneDX SBOM, in two places:

  • a workflow artifact on the run, sbom-web-<sha>.cdx.json / sbom-worker-<sha>.cdx.json (90 days)
  • attached to the image in GHCR as an OCI referrer (lives as long as the image). GHCR has no
    referrers API, so ORAS uses the referrers tag schema: a sha256-<image digest> tag in the
    package points at the index that lists the SBOM. oras discover <image>:<sha> finds it.

One composite action, .github/actions/image-sbom (anchore/sbom-action → syft → CycloneDX
JSON; oras attach), called after both builds in build-and-deploy.yml and after the weekly
no-cache rebuild of the web image in build-nocache.yml, which pushes a fresh image under the
same sha tag. docs/sbom.md says how to fetch the SBOM for a deployed sha and check an
advisory against it.

An SBOM is attached once per image digest. When the layer cache is hit, commits with the same
./src build the same image (this PR's first images were the ones main c1b2ace deployed), so a
docs-only PR, a job re-run and the merge build find the SBOM already there and skip the attach;
otherwise the image gets a new digest and its own SBOM. The step summary names the digest and says
which happened. The SBOM file is named after the commit whose build first attached it. Docs find a
deployed environment's SBOM from the digest its pods report, because the weekly no-cache rebuild
moves the sha tag without redeploying.

No Dockerfile, code or package change. No permission change (packages: write was already there).

Proof

  • Local syft 1.55.0 against the compose-built images: web — 3,926 components, 89 NuGet,
    97 Ubuntu packages; worker — 4,037 components, 141 NuGet, 112 Ubuntu packages.
    Both name the application (1.0.0), Npgsql 10.0.3 and libssl3t64 3.0.13-0ubuntu3.16.
  • Build and Deploy run https://github.com/DFE-Digital/check-performance-data/actions/runs/39419830507:
    both SBOM steps green in 13 s (web) / 10 s (worker); artifacts
    sbom-web-2defa314b06682b755476a297d8711068b4756f2.cdx.json and the sbom-worker-… equivalent.
    CI used syft 1.42.3 (CycloneDX 1.6; 89 NuGet / 97 deb web, 141 / 112 worker).
  • oras discover ghcr.io/dfe-digital/check-performance-data:2defa31… → one referrer,
    application/vnd.cyclonedx+json, sha256:74545271556f… (worker: sha256:aa31a763aa3b…);
    oras pull of each gives a file byte-identical to its artifact. The sha256-<digest>
    referrers index tag exists in GHCR (HTTP 200) and lists the SBOM.
  • Build No Cache dispatched on the branch, run
    https://github.com/DFE-Digital/check-performance-data/actions/runs/39421197224: web image rebuilt under
    the same sha tag (new digest sha256:236c350ae01c…), new SBOM sha256:fea67fc92513… attached and
    discoverable.
  • Review app deployed, smoke tests green; whole-suite E2E: 362 passed, 0 failed, 4 skipped
    (run 39421067233).
  • After the review fixes, Build and Deploy run
    https://github.com/DFE-Digital/check-performance-data/actions/runs/39427772270 (43b27cb8):
    • attempt 1, Build job:
      worker skipped for real on GHCR —
      ghcr.io/dfe-digital/check-performance-data-worker:43b27cb8… (sha256:60094523b1b7…) already has an SBOM attached; not attaching another.
      Web had a new digest (sha256:16f00d1c65c1…) and attached sbom-web-43b27cb8….cdx.json.
    • attempt 2, Build re-run (job),
      same merge commit: worker skipped again with the same line; web rebuilt its publish layer
      (cache not reused), got another new digest (sha256:310adfef110d…) and attached to that.
      So the web digest is stable only when the layer cache is hit; e4ee960e says so.
  • Build and Deploy run https://github.com/DFE-Digital/check-performance-data/actions/runs/39430182767
    (e4ee960e, merged onto main 5d3ee95): worker skipped a third time (same line, digest
    60094523b1b7…); web, built from 611 remove unsafe inline and unsafe eval from the csp script src #650's changed sources, attached to its new digest
    sha256:31638f3764a3…. Deployment To Review and smoke tests green.
  • oras discover --artifact-type application/vnd.cyclonedx+json from outside CI: exactly one
    referrer on every image this PR touched — web 5e4c914f, 236c350a, 16f00d1c, 310adfef,
    31638f37; worker 60094523.
  • Whole-suite E2E for e4ee960e: 374 passed, 0 failed, 4 skipped (run https://github.com/DFE-Digital/check-performance-data/actions/runs/39431417722; up from 362 because 611 remove unsafe inline and unsafe eval from the csp script src #650 on main added E2E tests). The suite started by the attempt-2 deploy (run 39429841578) failed one test, SearchByTokens_NamesTheReadablePaths_InTheCountLineAndTheScopeComment, while the review app was redeployed under it for e4ee960e; that test passed in the e4ee960e suite.

Follow-ups (not this PR)

  • Build No Cache rebuilds only the web image weekly; the worker is never refreshed.
  • Submit the SBOM to GitHub's dependency graph (dependency-snapshot: true) so Dependabot
    alerts cover the Ubuntu packages too.
  • Sign the image and SBOM (cosign) / SLSA provenance.
  • SBOMs for the E2E runner and maintenance images.
  • Pin GitHub Actions to commit SHAs and base images to digests #617 (pin actions to commit SHAs) should list anchore/sbom-action and oras-project/setup-oras;
    both run on moving tags and receive the job token, which has packages: write.
  • dependency-snapshot: true needs contents: write on the build job — a permissions change
    for that follow-up to state. Nothing reads these SBOMs yet; the dependency graph would be
    the first consumer.
  • Move the ORAS pin from 1.3.1 to 1.3.5 once setup-oras ships it (chore: update releases.json for 1.3.6,1.3.5 oras-project/setup-oras#222);
    setup-oras@v2 installs only the versions in its bundled list, which stops at 1.3.4.
  • Update the OWASP Top 10 controls document (AB#309509) with the outcome of Generate an SBOM for each image #614.

Review

Opus review of 20 Oct (two should-fix findings, one nit) applied in 296fb6c and 67589e6.
43b27cb pins ORAS to 1.3.1, because setup-oras@v2 cannot install 1.3.5 yet (that pin made run
39427207465 fail at Set up ORAS). e4ee960 stops promising that identical sources always give an
identical image.

🤖 Generated with Claude Code

Andy Smith and others added 3 commits October 20, 2026 13:05
…age and attaches it in GHCR

syft (anchore/sbom-action) reads the image the shared build-docker-image action loaded into
the runner's daemon; the SBOM is kept as a workflow artifact and attached to the image with
ORAS, using the referrers tag schema because GHCR has no referrers API.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Both builds in Build and Deploy and the weekly no-cache rebuild of the web image, which
pushes a fresh image under the same sha tag.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… against it

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Review app for PR 651 was deleted

@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

E2E tests passed

374 passed, 0 failed, 4 skipped in 17 min 36 s

Commit e4ee960 against https://check-performance-data-pr-651.test.teacherservices.cloud (run)

Andy Smith and others added 3 commits October 20, 2026 14:03
… step summary

Identical sources build an identical image, so a docs-only pull request, a job re-run and the
merge build all resolve to a digest that already carries an SBOM; attaching again would rewrite
the referrers index on GHCR. ORAS pinned to 1.3.5, the version the behaviour was checked with.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…rom the sha tag

The weekly no-cache rebuild moves the tag without redeploying, and the SBOM file is named after
the commit that first built the image.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
setup-oras@v2 installs only the versions in its bundled release list, which stops at 1.3.4, so
the 1.3.5 pin failed the build. 1.3.1 is the action's default and was also checked against GHCR.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A re-run of the same merge commit rebuilt the web image's publish layer and produced a new
digest, which took its own SBOM. Each digest still has exactly one; the comment and the docs no
longer promise that identical sources always give an identical image.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@ajs-uk-dev
ajs-uk-dev merged commit c57555c into main Oct 20, 2026
9 checks passed

This branch was successfully deployed

1 active deployment
review — e4ee960e Deployed Oct 20, 2026 by ajs-uk-dev via Delete Review App 651 #348
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.

Generate an SBOM for each image

2 participants