Check Performance Data - Software bill of materials (SBOM)
Every web and worker image that CI pushes to GHCR has a CycloneDX SBOM: a list of what is inside the image, so that a new security advisory can be checked against what is deployed without rebuilding anything (OWASP Top 10 2025, A03 software supply chain failures; #614).
What it contains
syft scans the pushed image and writes CycloneDX JSON with:
- the NuGet packages the published application carries, read from
/app/*.deps.json(direct and transitive, with versions — about 90 for the web image), including the application itself (DfE.CheckPerformanceData.Web,DfE.CheckPerformanceData.RulesEngineWorker) - the Ubuntu packages in the runtime image (about 100, as left by the base image plus the
apt-get upgradein the Dockerfile) and the operating system version - the files those Ubuntu packages own
It does not contain the SDK or the build tools (they are in the build stage, not the image), and it does not contain the test projects' packages (Playwright, xUnit).
Where it is
The image tag is the commit sha. Each image digest has one SBOM, named after the commit whose
build first attached it. When the layer cache is hit, commits with the same ./src build the
same image, so the file on an image can carry an earlier commit's sha than the tag you looked
up; otherwise the image gets a new digest and its own SBOM.
-
Workflow artifact on the
Build and Deploy(orBuild No Cache) run:sbom-web-<sha>.cdx.jsonandsbom-worker-<sha>.cdx.json. Kept 90 days. -
Attached to the image in GHCR as an OCI referrer, kept as long as the image. GHCR has no
referrers API, so ORAS uses the referrers tag schema: the package gains a tag named
sha256-<image manifest digest>that points at an index listing the SBOM. Those tags are the SBOMs; do not delete them.
The action that does this is .github/actions/image-sbom, called after each image build in
.github/workflows/build-and-deploy.yml and .github/workflows/build-nocache.yml.
Finding the SBOM for a deployed environment
Go by the image digest the running pods report, not by the sha tag: the weekly
Build No Cache run rebuilds the web and worker images and moves each sha tag to the new image
without redeploying anything, so a running environment can be on an earlier digest than its tag.
-
Read the digest from the pods (the namespace and deployment names are
schoolstandards-development/schoolstandards-test/schoolstandards-productionandcheck-performance-data-<environment>/check-performance-data-worker-<environment>, where<environment>isdevelopment,qa,preproduction,productionorpr-<n>):kubectl -n schoolstandards-production get pods -l app=check-performance-data-production \ -o jsonpath='{.items[*].status.containerStatuses[*].imageID}'Each value is
ghcr.io/dfe-digital/check-performance-data@sha256:<digest>. -
List and fetch the SBOM with ORAS (the packages are public; no login):
oras discover ghcr.io/dfe-digital/check-performance-data@sha256:<digest from step 1> oras pull ghcr.io/dfe-digital/check-performance-data@sha256:<digest the discover output lists>The pull writes the SBOM (
sbom-web-<sha of the commit whose build attached it>.cdx.json) into the current directory. The worker image isghcr.io/dfe-digital/check-performance-data-worker.Without ORAS installed, run it from its image (Linux/macOS; on Windows Git Bash prefix the command with
MSYS_NO_PATHCONV=1):docker run --rm -v "$PWD:/work" -w /work ghcr.io/oras-project/oras:v1.3.5 discover ghcr.io/dfe-digital/check-performance-data@sha256:<digest>To look at the image a tag points to now (for example the
Build and Deployrun of a merge commit), useghcr.io/dfe-digital/check-performance-data:<sha>in place of the@sha256:reference; ORAS resolves the digest itself.
Checking an advisory against it
Look a package up by name:
jq '.components[] | select(.name == "Npgsql") | {name, version, purl}' sbom-web-<sha>.cdx.json
Or scan the whole SBOM for known vulnerabilities with grype:
grype sbom:./sbom-web-<sha>.cdx.json
The weekly Build No Cache run rebuilds the web and worker images with current Ubuntu packages,
pushes each under the same sha tag and attaches a new SBOM to the new image; the earlier image
keeps its own.
Running pods stay on the digest they started with until the next deployment, which is why the
steps above start from the pod's digest rather than the tag.