| Tested on | Rocky Linux 10.2 (Red Quartz) workstation |
|---|---|
| Package | trivy 0.72.0podman 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.
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:
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"image=localhost/trivy-demo:bloated
id=sha256:3dcb33da34eb0e1dc247da92bbac6114441b349c45292b0e65da8467e1cba300
digest=sha256:3dcb33da34eb0e1dc247da92bbac6114441b349c45292b0e65da8467e1cba300Record these fields in your release record alongside the SBOM files:
- Repository and tag (
localhost/trivy-demo:bloated) - Image ID and digest (
sha256:3dcb33da…) - Architecture (
amd64frompodman 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:
mkdir -p ~/trivy-sbom-lab
cd ~/trivy-sbom-lab
podman save "$IMAGE_ID" -o bloated.tar
sha256sum bloated.tar | tee bloated.tar.sha256Exporting 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:
trivy image --input bloated.tar --format cyclonedx --output trivy-demo-3dcb33da.cdx.json"--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=1Trivy 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:
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',[])))"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: 253Key CycloneDX fields in this file:
- metadata.component — root container reference (
bloated.tar) - components — OS packages, language libraries, and the
serverapplication 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:
trivy image --input bloated.tar --format spdx-json --output trivy-demo-3dcb33da.spdx.json"--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=1trivy image --input bloated.tar --format spdx --output trivy-demo-3dcb33da.spdxInspect the SPDX JSON document header:
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']))"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: 1035The tag-value file opens with document metadata and per-package blocks:
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: APPLICATIONSPDX 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:
podman history "$IMAGE_ID" --no-truncCREATED 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 244MBThe SBOM lists packages from every layer still present in the final filesystem:
- Base OS packages — Rocky 10.2 RPMs from the
244 MBbase layer - Build toolchain packages —
golangand dependencies from the475 MBdnf installlayer left in the final image
RPM package steps on RHEL-family nodes follow dnf syntax.
- Application artifact — the
serverGo binary from the compile layer - Language runtime inside the binary —
stdlibat Go 1.26.5 listed as agobinarycomponent
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:
trivy sbom --severity HIGH,CRITICAL trivy-demo-3dcb33da.cdx.jsonDetected 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:
trivy image --download-db-only
trivy versionCompare the stored SBOM and the tarball without updating the database again:
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:
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:
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:
trivy-demo-1.0.0-3dcb33da.cdx.json
trivy-demo-1.0.0-3dcb33da.spdx.jsonRecord checksums for every stored SBOM file:
sha256sum \
trivy-demo-3dcb33da.cdx.json \
trivy-demo-3dcb33da.spdx.json \
trivy-demo-3dcb33da.spdx \
> trivy-demo-3dcb33da.sbom.sha256Store 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
UpdatedAttimestamp - 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
- Sign and Verify Container Images with Cosign
- Allow Trusted Images with ImagePolicyWebhook
- Scan Kubernetes YAML with Kubesec and KubeLinter
References
- Generate and scan SBOMs with Trivy
- Trivy SBOM command reference
- Trivy SBOM attestations
- CycloneDX specification
- SPDX specification
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.

