Generate and Scan Container SBOMs with Trivy

Generate CycloneDX and SPDX SBOMs with Trivy, rescan stored inventories for vulnerabilities, compare formats, and archive digest-linked release artifacts.

Published

Updated

Read time 11 min read

Reviewed byDeepak Prasad

Trivy generating CycloneDX and SPDX software bill of materials from a container image digest with package inventory and vulnerability rescan
Tested on Rocky Linux 10.2 (Red Quartz) workstation
Package trivy 0.72.0
podman 5.8.2
Applies to Ubuntu, Debian, Kali Linux, Linux Mint, Pop!_OS, Raspberry Pi OS, elementary OS, Zorin OS, Parrot OS, MX Linux, RHEL, Rocky Linux, AlmaLinux, Oracle Linux, CentOS Stream, Fedora
Cert prep CKS
Lab environment Workstation with Podman — reuse localhost/trivy-demo:bloated from build secure minimal container images or export with podman save (Docker equivalents below)
Privilege Normal user for SBOM generation and scans; sudo only if Trivy is not yet installed
Scope SBOM versus vulnerability reports, immutable image metadata, CycloneDX and SPDX generation, format comparison, layer-to-inventory mapping, trivy sbom rescans, image scan versus SBOM scan differences, release artifact storage, and attestation pointers. Does not cover general image scanning, legal compliance advice, dependency remediation, every SBOM format, runtime discovery, or registry implementation.
Related guides Build and push container images
Verify Kubernetes release artifacts

A software bill of materials (SBOM) answers a different question than a vulnerability scan. This walkthrough generates CycloneDX and SPDX inventories from one pinned image digest on Rocky Linux 10.2, compares how each format represents the same packages, rescans the stored SBOM with an updated database, and archives the files beside your release metadata.

IMPORTANT
This guide covers SBOM generation and SBOM-based rescans with Trivy. For image vulnerability tables, CI exit codes, and SARIF export, start with scan container images with Trivy. SBOM output documents inventory; it does not prove an image is safe.

SBOM vs vulnerability report

SBOM Vulnerability report
Package inventory tied to artifact contents Known vulnerability matches tied to scanner database time
Portable document you can store and exchange Analysis result that changes when the DB updates
Lists names, versions, and relationships Lists CVEs, severities, and fix versions
Does not prove safety Does not replace inventory

You generate the SBOM once per digest at release time. You can rescan that file later without pulling the image, but the vulnerability rows always reflect the database version active when you run trivy sbom.


Select an immutable image

Pin the exact bytes you plan to ship. This lab uses the bloated Rocky-based image from the secure Dockerfile walkthrough. Capture the local image ID and digest before you export—tags can move between inspect and save:

bash
IMAGE_REF='localhost/trivy-demo:bloated'

IMAGE_ID=$(
  podman image inspect "$IMAGE_REF" \
    --format '{{.Id}}'
)

IMAGE_DIGEST=$(
  podman image inspect "$IMAGE_REF" \
    --format '{{.Digest}}'
)

printf 'image=%s\nid=%s\ndigest=%s\n' \
  "$IMAGE_REF" "$IMAGE_ID" "$IMAGE_DIGEST"
output
image=localhost/trivy-demo:bloated
id=sha256:3dcb33da34eb0e1dc247da92bbac6114441b349c45292b0e65da8467e1cba300
digest=sha256:3dcb33da34eb0e1dc247da92bbac6114441b349c45292b0e65da8467e1cba300

Record these fields in your release record alongside the SBOM files:

  • Repository and tag (localhost/trivy-demo:bloated)
  • Image ID and digest (sha256:3dcb33da…)
  • Architecture (amd64 from podman image inspect "$IMAGE_REF" --format '{{.Architecture}}')
  • Build ID or CI run number
  • Source commit hash
  • Release version or promotion tag

Name SBOM files with the digest prefix to make release mapping easier, but do not treat the filename alone as proof. Retain the image digest, tarball checksum, SBOM checksum, and preferably a signed attestation.

On this host Trivy could not reach the rootful Podman socket, so exports use a tarball. Save by image ID, not the mutable tag:

bash
mkdir -p ~/trivy-sbom-lab
cd ~/trivy-sbom-lab

podman save "$IMAGE_ID" -o bloated.tar
sha256sum bloated.tar | tee bloated.tar.sha256

Exporting by image ID prevents a moved tag from changing the selected local image. The image digest records the source image identity, while the tarball checksum detects replacement or corruption of the exported archive. Because tarball-based SBOM metadata may identify the target as bloated.tar, retain this mapping with the release artifacts.

Trivy normally embeds image identifiers such as ImageID and RepoDigest when that metadata is available, but tar input may expose only the archive name.

If podman save fails with no space left on device, reuse an existing tarball only when its recorded checksum and image-ID/digest mapping match the intended release artifact.

Podman Docker
podman image inspect docker image inspect
podman save -o bloated.tar IMAGE docker save -o bloated.tar IMAGE
podman history --no-trunc docker history --no-trunc

The Trivy --input commands remain the same after either engine creates the archive.


Generate CycloneDX

Trivy writes CycloneDX JSON when you pass --format cyclonedx. SBOM-only output disables vulnerability scanning unless you add --scanners vuln explicitly:

bash
trivy image --input bloated.tar --format cyclonedx --output trivy-demo-3dcb33da.cdx.json
output
"--format cyclonedx" disables security scanning. Specify "--scanners vuln" explicitly if you want to include vulnerabilities in the "cyclonedx" report.
Detected OS	family="rocky" version="10.2"
Number of language-specific files	num=1

Trivy writes the SBOM body to the output file rather than printing it to the terminal; informational detection messages may still appear. Inspect metadata and component counts:

bash
python3 -c "import json;d=json.load(open('trivy-demo-3dcb33da.cdx.json'));print('bomFormat:',d['bomFormat']);print('specVersion:',d['specVersion']);print('serialNumber:',d['serialNumber']);print('timestamp:',d['metadata']['timestamp']);print('tool:',d['metadata']['tools']);print('components:',len(d['components']));print('dependencies:',len(d.get('dependencies',[])))"
output
bomFormat: CycloneDX
specVersion: 1.7
serialNumber: urn:uuid:b6afcb2d-e220-4e4c-ae36-342ee6814bce
timestamp: 2026-07-28T11:27:33+00:00
tool: {'components': [{'type': 'application', 'manufacturer': {'name': 'Aqua Security Software Ltd.'}, 'group': 'aquasecurity', 'name': 'trivy', 'version': '0.72.0'}]}
components: 252
dependencies: 253

Key CycloneDX fields in this file:

  • metadata.component — root container reference (bloated.tar)
  • components — OS packages, language libraries, and the server application binary
  • purl — package URLs where Trivy can construct them (for example pkg:golang/stdlib@…)
  • dependencies — parent-to-child links between components
  • tools — scanner name and version (trivy 0.72.0)

Generate SPDX

SPDX JSON is the machine-readable format most compliance tools ingest. Tag-value output is human-readable and diff-friendly:

bash
trivy image --input bloated.tar --format spdx-json --output trivy-demo-3dcb33da.spdx.json
output
"--format spdx-json" disables security scanning. Specify "--scanners vuln" explicitly if you want to include vulnerabilities in the "spdx-json" report.
Detected OS	family="rocky" version="10.2"
Number of language-specific files	num=1
bash
trivy image --input bloated.tar --format spdx --output trivy-demo-3dcb33da.spdx

Inspect the SPDX JSON document header:

bash
python3 -c "import json;d=json.load(open('trivy-demo-3dcb33da.spdx.json'));print('spdxVersion:',d['spdxVersion']);print('documentNamespace:',d['documentNamespace']);print('created:',d['creationInfo']['created']);print('creators:',d['creationInfo']['creators']);print('packages:',len(d['packages']));print('relationships:',len(d['relationships']))"
output
spdxVersion: SPDX-2.3
documentNamespace: http://trivy.dev/container_image/bloated.tar-78a7edc6-b806-4a27-a486-7e6a878228eb
created: 2026-07-28T11:27:37Z
creators: ['Organization: aquasecurity', 'Tool: trivy-0.72.0']
packages: 253
relationships: 1035

The tag-value file opens with document metadata and per-package blocks:

text
SPDXVersion: SPDX-2.3
DocumentName: bloated.tar
DocumentNamespace: http://trivy.dev/container_image/bloated.tar-6c52e68c-dc0f-4dff-9da0-2d214ad41812
Creator: Tool: trivy-0.72.0
Created: 2026-07-28T11:27:38Z

##### Package: server
PackageName: server
SPDXID: SPDXRef-Application-5c371e4b81c3285f
PrimaryPackagePurpose: APPLICATION

SPDX adds licenseConcluded and licenseDeclared fields per package. Trivy populated Rocky RPM licenses where the distribution metadata was available; many entries remain NOASSERTION when license data is missing from the source.


Compare the formats

Do not expect identical representation between CycloneDX and SPDX. Both describe the same image digest, but structure and emphasis differ.

Aspect CycloneDX (this lab) SPDX (this lab)
Spec version 1.7 2.3
Inventory representation Root target in metadata.component, plus 252 entries in components Root target included among 253 packages
Detected non-root components 252 252 after excluding the root archive/container package
Relationships 253 dependency edges 1035 relationship rows
Component identity bom-ref, type, purl SPDXID, name, versionInfo
Licensing Optional per component licenseConcluded per package
File-level data Minimal in Trivy output FilesAnalyzed: false for most packages
Typical consumers Cloud-native security platforms, dependency graphs Open-source compliance, SPDX tooling

The raw array lengths differ by one because the formats place the root scanned artifact differently. This result does not show that SPDX detected an extra dependency.

CycloneDX tends toward dependency graphs for security automation. SPDX tends toward license and provenance workflows. Generate both from the same digest when partners require different formats.


Compare SBOM to image layers

Cross-check the inventory against build history to spot packages that should not be in the final stage:

bash
podman history "$IMAGE_ID" --no-trunc
output
CREATED BY                                                                                               SIZE
/bin/sh -c CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /server main.go                         101MB
/bin/sh -c #(nop) COPY file:… in .                                                                        2.56kB
/bin/sh -c #(nop) WORKDIR /src                                                                            0B
/bin/sh -c dnf install -y golang                                                                         475MB       FROM quay.io/rockylinux/rockylinux:10
KIWI 10.2.44                                                                                             244MB

The SBOM lists packages from every layer still present in the final filesystem:

  • Base OS packages — Rocky 10.2 RPMs from the 244 MB base layer
  • Build toolchain packagesgolang and dependencies from the 475 MB dnf install layer left in the final image

RPM package steps on RHEL-family nodes follow dnf syntax.

  • Application artifact — the server Go binary from the compile layer
  • Language runtime inside the binarystdlib at Go 1.26.5 listed as a gobinary component

A multi-stage build that copies only /server into distroless would drop most RPM rows from the SBOM. Deleted files in upper layers can still exist in image history even when they no longer appear in the inventory; SBOMs describe the final root filesystem, not every intermediate layer artifact.


Scan the stored SBOM

Rescan a saved CycloneDX file against the current vulnerability database without pulling the image:

bash
trivy sbom --severity HIGH,CRITICAL trivy-demo-3dcb33da.cdx.json
output
Detected SBOM format	format="cyclonedx-json"
Detected OS	family="rocky" version="10.2"
[rocky] Detecting vulnerabilities...	os_version="10" pkg_num=248

Report Summary

┌───────────────────────────────────────────┬──────────┬─────────────────┐
│                  Target                   │   Type   │ Vulnerabilities │
├───────────────────────────────────────────┼──────────┼─────────────────┤
│ trivy-demo-3dcb33da.cdx.json (rocky 10.2) │  rocky   │       34        │
└───────────────────────────────────────────┴──────────┴─────────────────┘

Total: 34 (HIGH: 34, CRITICAL: 0)

Scanning a stored SBOM:

  • Does not pull the image from a registry
  • Uses the current Trivy vulnerability database
  • Matches packages by identifiers recorded in the SBOM (name, version, ecosystem)
  • Cannot report components that were omitted from the SBOM

The same command works on SPDX JSON files when your pipeline stores that format instead.


Compare image scan and SBOM scan

Download the vulnerability database once so both scans use the same snapshot:

bash
trivy image --download-db-only
trivy version

Compare the stored SBOM and the tarball without updating the database again:

bash
trivy sbom \
  --skip-db-update \
  --severity HIGH,CRITICAL \
  trivy-demo-3dcb33da.cdx.json

trivy image \
  --input bloated.tar \
  --skip-db-update \
  --scanners vuln \
  --severity HIGH,CRITICAL

--skip-db-update is used only for this side-by-side comparison so both scans use the same database snapshot. For future rescans of the archived SBOM, omit the flag so Trivy can retrieve newer vulnerability data. Trivy 0.72 supports both --download-db-only and --skip-db-update for SBOM scanning.

SBOM scan output:

output
Detected SBOM format	format="cyclonedx-json"
Detected OS	family="rocky" version="10.2"
[rocky] Detecting vulnerabilities...	os_version="10" pkg_num=248

Report Summary

┌───────────────────────────────────────────┬──────────┬─────────────────┐
│                  Target                   │   Type   │ Vulnerabilities │
├───────────────────────────────────────────┼──────────┼─────────────────┤
│ trivy-demo-3dcb33da.cdx.json (rocky 10.2) │  rocky   │       34        │
└───────────────────────────────────────────┴──────────┴─────────────────┘

Total: 34 (HIGH: 34, CRITICAL: 0)

Direct image scan output:

output
Report Summary

┌──────────────────────────┬──────────┬─────────────────┐
│          Target          │   Type   │ Vulnerabilities │
├──────────────────────────┼──────────┼─────────────────┤
│ bloated.tar (rocky 10.2) │  rocky   │       34        │
├──────────────────────────┼──────────┼─────────────────┤
│ server                   │ gobinary │        0        │
└──────────────────────────┴──────────┴─────────────────┘

Total: 34 (HIGH: 34, CRITICAL: 0)

In this lab both paths report 34 HIGH findings for Rocky packages. Investigate differences when counts diverge:

Factor Effect
Scanner version Newer Trivy releases add detectors or change matching
Database timestamp trivy version shows the installed Trivy version and available vulnerability-database metadata, including UpdatedAt; stale DBs miss recent CVEs
Package metadata Red Hat extended version strings can trigger version-matching warnings
Layer context Image scans inspect live layers; SBOM scans trust recorded versions
Unsupported package types Components without PURL or ecosystem hints may appear in one scan only
SBOM format limits SPDX NOASSERTION licenses do not affect vuln matching, but missing packages do

Re-run trivy sbom on the archived file when the database updates, even if the image digest is unchanged. That models how auditors rescan historical releases.


Store SBOMs with releases

Use predictable file names that embed version and digest:

text
trivy-demo-1.0.0-3dcb33da.cdx.json
trivy-demo-1.0.0-3dcb33da.spdx.json

Record checksums for every stored SBOM file:

bash
sha256sum \
  trivy-demo-3dcb33da.cdx.json \
  trivy-demo-3dcb33da.spdx.json \
  trivy-demo-3dcb33da.spdx \
  > trivy-demo-3dcb33da.sbom.sha256

Store each release bundle with:

  • Image digest (sha256:3dcb33da…)
  • Tarball checksum (bloated.tar.sha256)
  • Cosign signature or attestation reference when signing is enabled
  • Vulnerability report JSON or SARIF from image scanning
  • Build provenance (CI run URL, commit, builder image digest)
  • Release approval ticket or change record

Attach Trivy metadata to every artifact:

  • Scanner version (0.72.0)
  • Vulnerability database UpdatedAt timestamp
  • Severity policy flags used during promotion

Attest the SBOM

Link the SBOM document to the image digest with a signed attestation so consumers can verify the inventory matches the promoted artifact. Sigstore and Cosign support SPDX and CycloneDX in-toto attestations; the exact cosign attest syntax belongs in sign and verify container images with Cosign.

At minimum, record in your release system which SBOM file hash corresponds to which image digest, even before you automate attestations.


Common misinterpretations

Mistake Why it is wrong
SBOM generation equals remediation Inventory documents packages; rebuilding the image fixes versions
Rescanning SBOM updates the image Only the vulnerability report changes; image bytes stay the same
Identical tag means identical SBOM Mutable tags can point at a new digest without you noticing
Missing SBOM row means safe Omitted components are invisible to trivy sbom
Raw CycloneDX and SPDX array counts must match Root artifacts and relationships are represented differently, so compare equivalent package entries rather than only array lengths
SBOM replaces vulnerability scanning You need both inventory and periodic rescans

Troubleshooting

Symptom Likely cause Fix
disables security scanning message SBOM format defaults to inventory only Add --scanners vuln when you need CVE rows embedded in CycloneDX
trivy sbom count lower than image scan SBOM missing packages or stale file Regenerate SBOM from the same digest; compare component lists
Version matching warnings for Go stdlib Red Hat extended version strings Watch for scanner updates; OS RPM findings still report
no space left on device on podman save Disk full on workstation Reuse a tarball only when its checksum and image-ID/digest mapping match the release
Empty licenseConcluded in SPDX Upstream license metadata unavailable Expected for some packages; not a scan failure
SBOM references bloated.tar not registry name Input was a tarball export Add release metadata mapping tarball name to registry digest

What's Next


References


Summary

Trivy turns a pinned container image digest into portable CycloneDX and SPDX inventories. Pin the local image ID before export, record the digest and tarball checksum, generate both formats from the same archive, and store the files beside vulnerability reports and provenance.

With a single vulnerability-database snapshot, the lab matched 34 HIGH findings from both the stored CycloneDX file and a direct image scan. Rescan archived SBOMs with trivy sbom when the database updates; SBOMs record the components detected in the scanned artifact—digest mappings, checksums, and signed attestations establish which released image the inventory belongs to. SBOMs do not replace image vulnerability scanning or signed attestations in your promotion pipeline.


Frequently Asked Questions

1. What is the difference between an SBOM and a Trivy vulnerability report?

An SBOM records the components detected in a specific image when the inventory is generated. A vulnerability report matches those components against the scanner database at scan time. Store the SBOM with the image digest, document checksum, and preferably a signed attestation so consumers can verify which artifact the inventory belongs to.

2. Can I scan a stored SBOM without pulling the image again?

Yes. trivy sbom reads CycloneDX or SPDX files from disk and applies the current vulnerability database. This is useful for auditors and delayed promotion gates, but it cannot find components that were never listed in the SBOM.

3. Should I generate CycloneDX or SPDX?

Choose the format your downstream tools consume. CycloneDX is common in cloud-native and security platforms. SPDX is widely used in open-source compliance workflows. Generate both from the same digest when partners require different formats.

4. Does generating an SBOM fix vulnerabilities?

No. SBOM generation only documents package names and versions. Remediation still requires rebuilding the image on an updated base or bumping dependencies, then generating a new SBOM for the new digest.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)