Generating an SBOM for a DevContainer Image
When a CVE lands, you need to know instantly whether your dev image is affected. This page generates a Software Bill of Materials (SBOM) for the pinned image with syft and stores it, turning vulnerability response from a rebuild-and-scan into a fast query.
This task matters because a dev image is a moving target that fans out across every workstation and CI runner on the team. A single mcr.microsoft.com/devcontainers/base layer may carry hundreds of OS packages, language runtimes, and transitively installed libraries, and no one memorises that list. When a headline vulnerability drops on a Friday afternoon, the difference between a five-minute answer and a two-day audit is whether you already captured that inventory at build time. Generating the SBOM up front, from the same digest you shipped, means the evidence exists before you need it rather than being reconstructed under pressure after the fact.
Reach for an SBOM whenever an image leaves your machine and becomes something other people depend on: a base pushed to a registry, an image tagged for a release, or a devcontainer that CI pulls on every run. The mental model is simple — treat the SBOM as a receipt for the exact bytes in the image. syft reads the image, walks its layers, and records every package it can identify along with versions and, where available, the file paths that prove the finding. Because that receipt is generated from an immutable digest, it stays truthful for the life of that digest, and querying it later is just reading a file rather than re-running a scanner against a registry you may no longer be able to reach.
Prerequisites
You need syft and the pinned image to inventory.
syftinstalled (or an SBOM action in CI).- The digest-pinned image reference.
- A place to store the SBOM artifact.
The one detail people get wrong is the image reference itself. syft will happily inventory whatever you point it at, including a floating tag like :latest or :ubuntu, and produce a perfectly valid SBOM — of an image that may already have been replaced upstream. The prerequisite that carries the most weight is having the digest-pinned reference in hand, the same @sha256: string you built and pushed with. Without it, every later step is describing bytes you cannot prove you ever ran. If you do not yet pin digests, resolve that first; the SBOM is only as trustworthy as the reference it was generated from.
The storage prerequisite is easy to under-plan. An SBOM you generate and then discard at the end of a CI job is no better than not having one, because the whole value is being able to read it months later when the CVE arrives. Decide up front where it lives — a build artifact, an OCI attestation attached to the image, or an object in a bucket keyed by digest — so the inventory travels with the image rather than evaporating when the runner is torn down.
Step-by-Step Implementation
- Generate the SBOM from the pinned image.
syft mcr.microsoft.com/devcontainers/base@sha256:PINNED -o spdx-json > sbom.spdx.json
The reference here ends in @sha256:PINNED, not a tag, and that is the load-bearing part of the command. Pointing syft at the digest guarantees it inventories the identical image bytes you pinned elsewhere, so the resulting sbom.spdx.json describes the exact filesystem your team runs. The -o spdx-json flag selects a structured, machine-readable output rather than the human table syft prints by default, which is what makes the file greppable and diffable later. Redirecting to a file instead of leaving it on stdout is deliberate too — you want a durable artifact on disk, because the failure this whole step prevents is having to re-pull and re-scan the image at the worst possible moment.
- Generate it in CI and attach it to the build.
- run: syft $IMAGE -o cyclonedx-json > sbom.cdx.json
- uses: actions/upload-artifact@v4
with: { name: sbom, path: sbom.cdx.json }
Generating the SBOM in CI is what makes it reliable rather than something one engineer remembers to run occasionally. The syft $IMAGE step reuses whatever digest the pipeline just built, so the inventory is produced from the same reference the rest of the workflow already trusts, and cyclonedx-json here shows the alternate format syft supports for security-oriented tooling. The second step is the one people skip: actions/upload-artifact@v4 attaches sbom.cdx.json to the build run so the file outlives the ephemeral runner. Without that upload the SBOM is generated and immediately thrown away when the job's filesystem is reclaimed, which is exactly the stored-nowhere failure the pitfalls table below calls out.
- Query it when a CVE lands.
grep -i "openssl" sbom.spdx.json # affected? which version?
This is the payoff step, and its whole appeal is how unremarkable it is: a plain grep against a file already on disk. Because the SBOM is JSON, grep -i "openssl" case-insensitively surfaces every line that mentions the package, and the surrounding version fields tell you not just whether the library is present but which build you are running — the two facts a CVE advisory asks for. There is no registry pull, no scanner warm-up, and no network dependency, so the answer holds even if the image has since been deleted upstream. For anything beyond a quick look you can swap grep for a jq query over the same file, but the point stands: triage becomes reading, not rebuilding.
- Regenerate whenever you refresh the pinned digest.
Regeneration is a discipline, not a command, which is why it is easy to let slip. The moment you bump the pinned @sha256: digest — a base image rebuild, a security patch, a version upgrade — the package set inside the image changes, and the old sbom.spdx.json now describes bytes you no longer run. A stale SBOM is arguably worse than none, because it answers CVE questions confidently and wrongly. Tie regeneration to the same change that updates the pin: whatever commit or automation moves the digest should also re-run syft and replace the stored artifact, so the inventory and the image are never out of step.
Common Pitfalls
SBOM gaps are none generated, or generated from a tag not a digest.
The subtler trap sits between those two: an SBOM generated from a tag that happened to point at the right digest on the day you ran syft. It looks correct, and it may even be correct at that instant, but the reference recorded in your process is a moving pointer. Weeks later, when someone re-runs the same tag-based command to "refresh" the inventory, they capture a different image entirely, and the drift is invisible because both files are valid SPDX. Always generate from the immutable @sha256: digest so the SBOM and the pin describe the same fixed target; a tag makes the whole chain of evidence quietly unfalsifiable.
If you attach the SBOM as a build artifact or push it to shared storage, watch the ownership and permissions on that path the same way you would a cache volume. An artifact written by a root-run CI step and then read by a lower-privileged job — or the reverse — can leave the SBOM unreadable exactly when you reach for it during an incident, and a permission error at 2am reads like the file is simply missing. Keep the write and read stages consistent about which user owns the artifact, and prefer storing it keyed by the image digest so the correct SBOM is unambiguous rather than something you reconstruct by guesswork.
| Symptom | Root Cause | Remediation |
|---|---|---|
| Can't tell if a CVE affects us | No SBOM | Generate one with syft per build |
| SBOM doesn't match the image | Generated from a tag | Generate from the pinned digest |
| SBOM lost after the build | Not stored | Upload it as a build artifact |
| SBOM stale after a bump | Not regenerated | Regenerate on each digest refresh |
Conclusion
An SBOM makes vulnerability response a query, not an investigation. Generate it with syft from the pinned digest, store it alongside the image, and regenerate it whenever you refresh the pin — then answering 'are we affected by CVE-X?' takes seconds.
The strategic payoff is that the SBOM turns your dev image from an opaque black box into an auditable asset. Once every published digest ships with a matching inventory, "which of our images contain the vulnerable library" stops being a research project spread across teams and becomes a grep over a directory of stored SBOMs. That composability is why the practice scales: the more images you pin and inventory, the more valuable each individual SBOM becomes, because the same query answers the question for the whole fleet at once instead of one image at a time.
This is also where the SBOM ties back into the broader pin-and-cache and reproducibility story. The digest pin gives you an image whose bytes never change; the SBOM is the human-and-machine-readable description of what those frozen bytes contain. Together they close the loop — you can prove both what you ran and what was in it, from the same immutable @sha256: reference. Generating from a tag would break that guarantee, which is why the digest is non-negotiable throughout: the pin and the SBOM are two views of one reproducible artifact, and keeping them in lockstep is what makes fast, confident vulnerability response possible.
FAQ
What is an SBOM and why generate one for a dev image? A Software Bill of Materials is a machine-readable inventory of every package in an image. For a dev image, it lets you answer 'are we affected by this CVE?' instantly by querying the SBOM, instead of rebuilding and re-scanning each project when a vulnerability is announced. A dev image tends to accumulate more than a production runtime — compilers, package managers, debuggers, and their dependencies — so the surface area you would otherwise have to audit by hand is large. Capturing it once at build time, from the pinned digest, converts that audit into a lookup that anyone on the team can run without special tooling.
Which format should I use, SPDX or CycloneDX?
Either works; both are widely supported. SPDX is common for license/compliance workflows and CycloneDX for security tooling. syft emits both — pick the one your downstream tools consume, and be consistent across images. The command changes only in the -o flag: -o spdx-json versus -o cyclonedx-json. If you are unsure which way your organisation will lean, generating both from the same digest is cheap, since syft reads the image once; what matters far more than the format choice is that the reference you scan is a digest rather than a tag.
When should I regenerate the SBOM?
On every build, and specifically whenever you refresh the pinned base digest, since the package set changes. Store it as a build artifact next to the image so the SBOM always describes the exact bytes you shipped. The cleanest rule is to bind regeneration to the pin itself: the same commit that updates @sha256: should re-run syft, so there is never a window where the stored inventory and the running image disagree. Treat an SBOM whose digest no longer matches any live image as expired and delete it, rather than leaving it around to answer questions about bytes nobody runs.
Does the SBOM catch application dependencies or only OS packages? syft inventories both. Beyond the Debian or Alpine packages in the base layers, it detects language ecosystems it can find on disk — npm, pip, Go modules, and others — by reading their manifests and lockfiles inside the image. That breadth is part of why generating from the built image rather than from source is valuable: it captures what actually landed in the filesystem, including anything a feature or post-create step pulled in, not just what your Dockerfile appears to install.
Is an SBOM the same as a vulnerability scan? No, and the distinction is worth keeping straight. syft produces the inventory — the list of what is present — while a scanner like grype consumes that inventory and matches it against a vulnerability database. The SBOM is the durable, offline record; the scan is the time-sensitive interpretation of it. Because the two are decoupled, you can re-scan yesterday's SBOM against today's advisories without touching the image at all, which is exactly what makes response fast.
Can I generate the SBOM without pulling the whole image locally?
Often, yes. syft can read from a registry reference directly, and in CI it typically inventories the image the pipeline just built and still has cached, so there is no separate large pull. The constraint that never relaxes is the reference: whether syft reads from a local store or a remote registry, point it at the @sha256: digest so the inventory is tied to immutable bytes rather than a tag that may move underneath you.
Related
- Up to Container Registry Best Practices for Dev Images — the overview of image strategy.
- Scanning DevContainer Images for Vulnerabilities — the scanning companion.
- Pinning Base Image Digests with sha256 — the digest the SBOM describes.