Safeguard
AI Security

How to Generate an SBOM From a Container Image

A practical walkthrough of how to generate an SBOM from a container image using Syft, Trivy, and Docker Scout, plus how to keep the output trustworthy.

Karan Patel
Platform Engineer
7 min read

The fastest way to generate an SBOM from a container image is to point a scanner like Syft at the image reference and let it walk every layer, but getting an inventory you can actually trust takes a little more care than the one-liner suggests. A container image is not a single artifact. It is a stack of layers, each of which can add OS packages, language dependencies, and loose binaries, and a good software bill of materials has to account for all of them.

This post covers the three tools most teams reach for, the format decision you cannot avoid, and the failure modes that quietly produce an incomplete SBOM.

Why a container image is harder than a source tree

When you generate an SBOM from a source repository, you mostly parse lockfiles: package-lock.json, go.sum, poetry.lock. A container image has those too, but it also has the base operating system, packages installed by apt or apk at build time, vendored binaries copied in during a multi-stage build, and sometimes application dependencies that were installed but never recorded in a lockfile.

That is why an image SBOM is usually larger and messier than the one you would get from the same project's source. A typical Debian-based Node image will report a couple hundred OS packages before it ever gets to your node_modules. Skipping the OS layer is the single most common reason an SBOM misses a real vulnerability, because the base image is where stale OpenSSL and glibc versions live.

Generating an SBOM with Syft

Syft, maintained by Anchore, is the tool most people start with because it does one job and does it cleanly: catalog what is in an image and emit an SBOM. It reads directly from a registry, a local Docker daemon, or a tarball.

# Scan an image straight from a registry
syft registry.example.com/api:1.4.2 -o spdx-json > api-sbom.spdx.json

# Or produce CycloneDX instead
syft nginx:1.27 -o cyclonedx-json > nginx-sbom.cdx.json

Syft catalogs OS packages (dpkg, rpm, apk), language ecosystems (npm, PyPI, Go modules, Java archives, Ruby gems, and more), and standalone binaries it can fingerprint. The -o flag controls the output format, and you can emit several at once by repeating it. For pipeline use, pin the Syft version in your CI image so the catalogers do not silently change between runs and produce a diff you cannot explain.

Generating an SBOM with Trivy

Trivy, from Aqua Security, is the all-in-one option. It is best known as a vulnerability scanner, but it also emits an SBOM and can scan an SBOM you already have. If your team already runs Trivy for CVE detection, generating the SBOM from the same tool avoids a second dependency.

# Generate a CycloneDX SBOM from an image
trivy image --format cyclonedx --output api.cdx.json registry.example.com/api:1.4.2

# Generate SPDX
trivy image --format spdx-json --output api.spdx.json registry.example.com/api:1.4.2

The convenient part is the round trip. You can generate the SBOM once, store it as a build artifact, and later run trivy sbom api.cdx.json to re-check it against an updated vulnerability database without pulling the image again. That decoupling matters when the image has been promoted through several environments and you want to re-evaluate risk without a fresh scan of every layer.

Docker Scout and the deprecated docker sbom

Docker's own tooling has moved. The old docker sbom command, which was a thin wrapper around Syft, has been deprecated in favor of Docker Scout. If you are on a recent Docker Desktop or CLI, the current path is:

docker scout sbom --format cyclonedx registry.example.com/api:1.4.2

Scout will generate the inventory and, if you are signed in, cross-reference it against its vulnerability data. For teams standardized on Docker's ecosystem this is the least-friction route, but it ties you to Scout's account model. If you want a tool-agnostic SBOM that any downstream consumer can read, Syft or Trivy output is easier to pass around.

SPDX or CycloneDX: pick one on purpose

You will be asked to choose between two formats. SPDX is the ISO-standardized, license-and-compliance-oriented format with deep roots in the Linux Foundation ecosystem. CycloneDX comes out of OWASP and leans toward security use cases, with first-class support for vulnerability data (VEX) and dependency relationships.

Neither is wrong. The practical rule: if your driver is license compliance and procurement, or a customer contract that names SPDX, produce SPDX. If your driver is vulnerability management and you want to attach VEX statements later, produce CycloneDX. Many teams generate both, since the marginal cost is a second -o flag, and store them side by side as build attestations.

The accuracy traps

Generating the file is easy. Generating an SBOM you can defend is where teams slip.

The first trap is distroless and scratch images. When you strip an image down to a static binary on scratch, there is no package database for the scanner to read, so the SBOM comes back nearly empty even though your Go or Rust binary statically links dozens of libraries. You need a scanner that fingerprints binaries, and even then coverage for statically linked C libraries is partial. Do not read an empty SBOM as "no dependencies."

The second trap is multi-stage builds. If you install build tools in an early stage and copy only the compiled output forward, the final image legitimately should not list those build tools. But if you accidentally copy a whole node_modules with dev dependencies into the runtime stage, they show up in the SBOM and inflate your vulnerability count with issues that are not actually reachable at runtime. The SBOM is telling the truth here; the fix is the Dockerfile.

The third trap is the base image update gap. An SBOM is a snapshot. The moment the upstream base image ships a patched libc, your stored SBOM is stale. This is why SBOM generation belongs in the build pipeline on every image build, not as a one-time exercise, and why you re-scan stored SBOMs against fresh vulnerability data on a schedule.

Once the SBOM exists, the value comes from feeding it into a scanner that maps components to advisories. An SCA tool such as Safeguard can ingest CycloneDX or SPDX from any of these generators and track exposure across every image tag you have shipped, which is more useful than a file sitting in an artifact store.

Wiring it into CI

A workable pipeline stage generates the SBOM, stores it as an attestation alongside the image digest, and gates on the vulnerability scan of that SBOM rather than the live image:

IMAGE=registry.example.com/api:$GIT_SHA
syft "$IMAGE" -o cyclonedx-json > sbom.cdx.json
# Attest and push the SBOM alongside the image
cosign attest --predicate sbom.cdx.json --type cyclonedx "$IMAGE"
# Gate the build on findings from the generated SBOM
trivy sbom sbom.cdx.json --exit-code 1 --severity CRITICAL,HIGH

Attaching the SBOM to the image digest with cosign attest means the inventory travels with the artifact and can be verified at deploy time, which closes the loop for supply chain provenance. For more on gating strategy, our guide to container-specific vulnerability management tools covers how to tune severity thresholds without drowning in noise.

FAQ

Which tool should I use to generate an SBOM from a container image?

Syft is the cleanest single-purpose choice and reads registries, daemons, and tarballs. If you already run Trivy for vulnerability scanning, generate the SBOM there to avoid a second tool. Docker Scout is the least-friction option inside Docker's ecosystem but ties you to its account model.

Does the SBOM include the base operating system packages?

Yes, and it should. Syft, Trivy, and Scout all catalog OS packages from dpkg, rpm, and apk databases. The base OS layer is where stale system libraries like OpenSSL and glibc live, so an SBOM that omits it will miss real vulnerabilities.

Why is my SBOM nearly empty for a distroless image?

Distroless and scratch images have no package database to read, so package-based catalogers find little. Statically linked binaries need binary fingerprinting, and coverage of statically linked C libraries is only partial. An empty SBOM is a coverage gap, not proof of zero dependencies.

SPDX or CycloneDX for container SBOMs?

Choose SPDX for license compliance and procurement requirements, CycloneDX for security and vulnerability workflows since it supports VEX. Generating both is cheap and common; store them together as build attestations.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.