Software Bill of Materials (SBOM) Formats: SPDX and CycloneDX from Zero¶
This guide assumes no prior knowledge. It explains what a Software Bill of Materials is, why SBOMs have gone from niche to mandatory in a few years, the identifiers that make them useful, and then takes you field by field through the two dominant formats: SPDX and CycloneDX. It finishes with VEX, tooling, validation, signing and distribution, quality, and a hands-on lab.
A companion document, Software Composition Analysis (SCA): A Complete Guide from Zero, covers the tools that produce and consume SBOMs.
Table of Contents¶
- What an SBOM is
- Why SBOMs suddenly matter
- Minimum elements: what an SBOM must contain
- SBOM types by lifecycle stage
- Identifiers: purl, CPE, hashes and friends
- SPDX in depth
- SPDX 3.0: the redesign
- CycloneDX in depth
- SPDX vs CycloneDX
- VEX: telling consumers what actually matters
- Generating SBOMs
- Validating and scoring SBOMs
- Signing, attesting, and distributing SBOMs
- Consuming SBOMs
- SBOM quality problems
- Hands-on lab
- Talking about SBOMs in an interview
- Glossary
- Further reading
1. What an SBOM is¶
A Software Bill of Materials (SBOM) is a formal, machine-readable inventory of the components that make up a piece of software, along with information about those components and the relationships between them.
The name comes from manufacturing. A bill of materials for a car lists every part — engine, bolts, chips — and which supplier made it. When an airbag supplier recalls a batch, the manufacturer can instantly find every car containing it.
Software is the same. When a vulnerability like Log4Shell is announced, an organization with SBOMs for all its products can answer "which of our products contain log4j-core between 2.0 and 2.14.1?" with a database query in minutes. Without SBOMs, the same answer takes weeks of manually inspecting repositories, build servers, and container images — and is still incomplete.
The food-label analogy is also common: an SBOM is the ingredient list for software. It doesn't tell you whether the food is good for you, but you can't make that judgement without it.
What an SBOM contains¶
At minimum:
- The software it describes (the "subject" or "root"): name, version, supplier.
- Components: each library, package, or file — name, version, supplier, unique identifiers, hashes, license.
- Relationships: which component contains or depends on which.
- Metadata about the SBOM itself: who created it, when, with which tool.
Optionally, much more: vulnerabilities, build provenance, services, hardware, AI models, cryptographic assets, and so on.
What an SBOM is not¶
- Not a vulnerability report. An SBOM lists ingredients. Vulnerabilities are determined by matching those ingredients against vulnerability databases — which changes daily, while the SBOM for a given release doesn't. (CycloneDX can carry vulnerability data, but that's an addition, not the core purpose.)
- Not a guarantee of security — just a prerequisite for managing it.
- Not a lockfile. A lockfile is ecosystem-specific and serves the build tool. An SBOM is a standardized, cross-ecosystem format for exchanging information between organizations and tools.
2. Why SBOMs suddenly matter¶
SBOMs existed for years in license compliance circles (SPDX dates from 2010). Several events turned them into a mainstream requirement.
Supply chain attacks. SolarWinds (2020) showed attackers compromising a vendor's build system to push malicious updates to thousands of customers. Log4Shell (2021) showed how hard it is to find a vulnerable component. The xz-utils backdoor (2024) showed how a trusted open-source dependency could be subverted.
United States — Executive Order 14028 (May 2021), "Improving the Nation's Cybersecurity," directed federal agencies to require SBOMs from software suppliers. The NTIA then published the Minimum Elements for an SBOM (July 2021), and CISA took over stewardship of SBOM work. Federal secure software development attestation requirements followed.
United States — FDA medical devices. Since 2023, premarket submissions for "cyber devices" must include an SBOM as part of cybersecurity documentation.
European Union — Cyber Resilience Act (CRA). In force since December 2024, the CRA requires manufacturers of "products with digital elements" sold in the EU to draw up an SBOM in a commonly used, machine-readable format covering at least the top-level dependencies. Vulnerability reporting obligations begin in September 2026, and most other obligations apply from December 2027. This affects practically anyone selling software or connected devices into the EU.
Germany — BSI TR-03183 provides detailed technical guidance on SBOM content and formats, and is frequently cited as a reference for CRA-style requirements.
Customer demand. Enterprise procurement and security questionnaires increasingly ask "can you provide an SBOM for this product?"
Internal value. Even with no regulator involved, SBOMs give you fast incident response, license compliance, dependency hygiene, and a portfolio-wide view of what you run.
3. Minimum elements: what an SBOM must contain¶
The NTIA's 2021 Minimum Elements remain the most-cited baseline. They're grouped into three areas.
Data fields (for each component)¶
| Field | Meaning | Example |
|---|---|---|
| Supplier name | Entity that creates/defines/identifies the component | The Apache Software Foundation |
| Component name | Name assigned by the supplier | log4j-core |
| Version | Version identifier | 2.17.1 |
| Other unique identifiers | Identifiers for lookup | pkg:maven/org.apache.logging.log4j/[email protected], CPE, SWID |
| Dependency relationship | That upstream component X is included in software Y | my-app DEPENDS_ON log4j-core |
| Author of SBOM data | Who created the SBOM | Acme Corp Build System |
| Timestamp | When the SBOM was assembled | 2026-10-01T12:00:00Z |
Automation support¶
The SBOM must be in a machine-readable, interoperable format. NTIA named three: SPDX, CycloneDX, and SWID tags. In practice, SPDX and CycloneDX dominate; SWID survives mainly as an identifier type.
Practices and processes¶
- Frequency — a new SBOM for every new build/release.
- Depth — at least top-level components plus enough detail to find transitive dependencies; ideally the full graph.
- Known unknowns — explicitly state where the dependency information is incomplete rather than silently omitting it.
- Distribution and delivery — SBOMs must be available to those who need them.
- Access control — terms for who can access them.
- Accommodation of mistakes — tolerance for errors and a process to correct them.
Updates since 2021: CISA has worked on revising the minimum elements. A 2025 CISA draft proposed raising the baseline with fields such as component hashes, license information, the name of the generating tool, and the generation context (before, during, or after build). Expect the baseline to keep tightening, and treat hashes and licenses as expected rather than optional.
4. SBOM types by lifecycle stage¶
An SBOM's content depends heavily on when and how it was generated. CISA's "Types of SBOM Documents" defines six:
| Type | Generated from | Strengths | Weaknesses |
|---|---|---|---|
| Design | Planned architecture, before code exists | Early license/risk review | Speculative; may not match reality |
| Source | Source repo, manifests, lockfiles | Easy; good dependency graph | Misses things added at build (base images, downloaded binaries); may include dev/test deps |
| Build | The build process itself | Most accurate for what was compiled; can include hashes of produced artifacts | Requires build integration |
| Analyzed | Inspecting a finished artifact (binary, image) after the fact | Works without source; finds what's really shipped | Heuristic; may miss or misidentify components |
| Deployed | Inventory of software installed in an environment | Shows real configuration | Environment-specific |
| Runtime | Instrumenting running systems | Shows what's actually loaded/executed | Requires instrumentation; snapshot in time |
Both formats can record this: CycloneDX via metadata.lifecycles, SPDX 3.0 via software_sbomType.
Practical guidance: generate a build SBOM for each release (most authoritative), and complement it with an analyzed SBOM of the final container image or binary to catch anything the build didn't know about.
5. Identifiers: purl, CPE, hashes and friends¶
An SBOM is only useful if consumers can unambiguously identify each component, because that's what links it to vulnerability and license databases. Names alone are useless: there are many packages named utils, and requests on PyPI is unrelated to request on npm.
5.1 Package URL (purl)¶
purl is the most important identifier in modern SBOMs. Format:
pkg:<type>/<namespace>/<name>@<version>?<qualifiers>#<subpath>
Only type and name are required; real-world SBOMs should always include a version.
| Ecosystem | Example purl |
|---|---|
| npm (scoped) | pkg:npm/%40angular/[email protected] |
| npm | pkg:npm/[email protected] |
| PyPI | pkg:pypi/[email protected] |
| Maven | pkg:maven/org.apache.logging.log4j/[email protected] |
| Cargo | pkg:cargo/[email protected] |
| Go | pkg:golang/github.com/spf13/[email protected] |
| NuGet | pkg:nuget/[email protected] |
| RubyGems | pkg:gem/[email protected] |
| Debian | pkg:deb/debian/[email protected]~deb12u2?arch=amd64&distro=debian-12 |
| RPM | pkg:rpm/redhat/[email protected]?arch=x86_64 |
| Alpine | pkg:apk/alpine/[email protected]?arch=x86_64 |
| Container | pkg:oci/nginx@sha256%3Aabc123...?repository_url=docker.io/library/nginx |
| GitHub repo | pkg:github/andrew/[email protected] |
| Generic | pkg:generic/[email protected]?download_url=https://... |
Why purl wins: it encodes where the package came from (ecosystem and, for distros, which distro), so a vulnerability database keyed on package names and ecosystems (OSV, GHSA, distro trackers) can match it directly. Note how the distro qualifier on the Debian example matters: Debian backports fixes, so the distro's version must be compared against Debian's own advisories.
5.2 CPE (Common Platform Enumeration)¶
cpe:2.3:a:apache:log4j:2.14.1:*:*:*:*:*:*:*
│ │ │ │
│ vendor product version
part (a=application, o=OS, h=hardware)
CPE is what NVD uses to say which products a CVE affects. It's useful for commercial software and things without a package manager, but it maps badly onto libraries: vendor/product strings are inconsistent, many packages have no CPE, and one CPE may cover many artifacts. Include CPEs when known (they help NVD-based matching), but don't rely on them alone.
5.3 Hashes¶
Cryptographic hashes (SHA-256 preferred; SHA-1 and MD5 are legacy) of component files or archives. Hashes let a consumer verify that the component they have is the exact one described, and they're the basis of file-signature matching. Both formats support multiple hash algorithms per component.
5.4 Other identifiers¶
- SWID tags (ISO/IEC 19770-2) — XML tags for installed software, mostly in commercial/enterprise software asset management.
- SWHID (SoftWare Hash IDentifier) — content-based identifiers from Software Heritage, e.g.
swh:1:cnt:.... - OmniBOR / gitoid — content-addressable identifiers derived from git object hashing, aimed at describing build artifact graphs.
- bom-ref / SPDXID — identifiers internal to a document, used to wire up relationships. They're not globally meaningful.
5.5 License identifiers and expressions¶
Both formats use the SPDX License List short identifiers (MIT, Apache-2.0, GPL-3.0-only, BSD-3-Clause) and SPDX license expressions:
MIT OR Apache-2.0 # choose either
GPL-2.0-only AND BSD-3-Clause # both apply
GPL-2.0-or-later WITH Classpath-exception-2.0 # license plus exception
LicenseRef-acme-commercial # custom license defined in the document
This is one of SPDX's most widely adopted contributions — even CycloneDX documents, package registries (npm, PyPI, crates.io), and source file headers (SPDX-License-Identifier: MIT) use it.
6. SPDX in depth¶
6.1 Background¶
SPDX (originally Software Package Data Exchange, more recently styled System Package Data Exchange) is a Linux Foundation project started in 2010, originally focused on license compliance — giving companies a standard way to exchange information about the licenses and copyrights of the open-source components they ship.
Key milestones:
| Version | Date | Notes |
|---|---|---|
| 1.0 | 2011 | First release; package-focused |
| 2.0 | 2015 | Relationships, multiple packages per document |
| 2.2 / 2.2.1 | 2020 | 2.2.1 published as international standard ISO/IEC 5962:2021 |
| 2.3 | 2022 | Added fields for security use cases (purl-friendly external refs, primary package purpose, dates) |
| 3.0 | April 2024 | Major, breaking redesign with profiles |
| 3.0.1 | December 2024 | Patch release; the current 3.x version as of this writing |
In practice, SPDX 2.3 is still what most tools emit and consume; SPDX 3.0 adoption is growing. You should understand both.
6.2 SPDX 2.x structure¶
An SPDX 2.x document has these sections:
- Document creation information — about the SBOM itself.
- Package information — one block per package (component).
- File information — optional, per file.
- Snippet information — optional, for parts of files.
- Other licensing information — definitions of custom
LicenseRef-licenses. - Relationships — how elements relate.
- Annotations — reviewer comments.
Every element has an SPDXID like SPDXRef-Package-serde used in relationships.
6.3 Special values¶
SPDX distinguishes between "we checked and there's nothing" and "we don't know":
NOASSERTION— the creator makes no assertion (didn't determine it, or chose not to say).NONE— the creator determined there is no value (e.g. no license, no download location).
This is how SPDX handles "known unknowns." An SBOM full of NOASSERTION is valid but low quality.
6.4 Serialization formats¶
SPDX 2.x can be written as tag-value (.spdx), JSON (.spdx.json), YAML, RDF/XML, and a spreadsheet format. JSON is now the most common for machine exchange; tag-value is easiest to read.
6.5 A complete SPDX 2.3 example (tag-value)¶
A tiny Rust application, demo-app, depending on serde and anyhow:
## Document Creation Information
SPDXVersion: SPDX-2.3
DataLicense: CC0-1.0
SPDXID: SPDXRef-DOCUMENT
DocumentName: demo-app-1.0.0
DocumentNamespace: https://example.com/spdx/demo-app-1.0.0-3f1c9a2e-7b4d-4c55-9e1a-2d0f6b8c1a77
Creator: Organization: Example Corp
Creator: Tool: syft-1.x
Created: 2026-10-01T12:00:00Z
## The application itself
PackageName: demo-app
SPDXID: SPDXRef-Package-demo-app
PackageVersion: 1.0.0
PackageSupplier: Organization: Example Corp
PackageDownloadLocation: NOASSERTION
FilesAnalyzed: false
PackageLicenseConcluded: MIT
PackageLicenseDeclared: MIT
PackageCopyrightText: Copyright 2026 Example Corp
PrimaryPackagePurpose: APPLICATION
ExternalRef: PACKAGE-MANAGER purl pkg:cargo/[email protected]
## Dependency: serde
PackageName: serde
SPDXID: SPDXRef-Package-serde
PackageVersion: 1.0.210
PackageSupplier: NOASSERTION
PackageDownloadLocation: https://crates.io/api/v1/crates/serde/1.0.210/download
FilesAnalyzed: false
PackageChecksum: SHA256: c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a
PackageLicenseConcluded: MIT OR Apache-2.0
PackageLicenseDeclared: MIT OR Apache-2.0
PackageCopyrightText: NOASSERTION
PrimaryPackagePurpose: LIBRARY
ExternalRef: PACKAGE-MANAGER purl pkg:cargo/[email protected]
## Dependency: anyhow
PackageName: anyhow
SPDXID: SPDXRef-Package-anyhow
PackageVersion: 1.0.89
PackageSupplier: NOASSERTION
PackageDownloadLocation: https://crates.io/api/v1/crates/anyhow/1.0.89/download
FilesAnalyzed: false
PackageLicenseConcluded: MIT OR Apache-2.0
PackageLicenseDeclared: MIT OR Apache-2.0
PackageCopyrightText: NOASSERTION
PrimaryPackagePurpose: LIBRARY
ExternalRef: PACKAGE-MANAGER purl pkg:cargo/[email protected]
## Relationships
Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-Package-demo-app
Relationship: SPDXRef-Package-demo-app DEPENDS_ON SPDXRef-Package-serde
Relationship: SPDXRef-Package-demo-app DEPENDS_ON SPDXRef-Package-anyhow
(Checksums and versions here are illustrative.)
6.6 Field-by-field explanation¶
Document creation information
| Field | Purpose |
|---|---|
SPDXVersion |
Spec version, e.g. SPDX-2.3 |
DataLicense |
License of the SBOM data itself — always CC0-1.0 in SPDX 2.x |
SPDXID |
Always SPDXRef-DOCUMENT for the document |
DocumentName |
Human-readable name |
DocumentNamespace |
A unique URI for this document; generate a new one (usually including a UUID) for every SBOM. Other documents can reference elements in this one via the namespace |
Creator |
One or more of Person:, Organization:, Tool: |
Created |
UTC timestamp |
Package information
| Field | Purpose |
|---|---|
PackageName, PackageVersion |
Identity |
SPDXID |
Document-local ID, must start with SPDXRef- |
PackageSupplier / PackageOriginator |
Who distributed it / who originally created it |
PackageDownloadLocation |
Where it came from (URL, VCS URL, NONE, NOASSERTION) |
FilesAnalyzed |
Whether file-level information is included. If true, a PackageVerificationCode (a hash computed over the package's files) is required |
PackageChecksum |
Hash(es) of the package archive |
PackageLicenseDeclared |
License the authors state |
PackageLicenseConcluded |
License the SBOM creator concluded applies |
PackageLicenseInfoFromFiles |
Licenses found in files (when files analyzed) |
PackageCopyrightText |
Copyright notices |
PrimaryPackagePurpose |
APPLICATION, LIBRARY, FRAMEWORK, CONTAINER, OPERATING-SYSTEM, FIRMWARE, FILE, SOURCE, etc. (2.3+) |
ExternalRef |
External identifiers: category PACKAGE-MANAGER (purl), SECURITY (cpe23Type, advisory, fix, url), PERSISTENT-ID (swh, gitoid) |
BuiltDate, ReleaseDate, ValidUntilDate |
Lifecycle dates (2.3+) |
Relationships — the heart of the dependency graph. Common types in SPDX 2.3:
| Relationship | Meaning |
|---|---|
DESCRIBES |
The document describes this element (the root) |
CONTAINS / CONTAINED_BY |
Physical containment (an image contains a package; a package contains files) |
DEPENDS_ON / DEPENDENCY_OF |
Dependency |
DEV_DEPENDENCY_OF, TEST_DEPENDENCY_OF, BUILD_DEPENDENCY_OF, RUNTIME_DEPENDENCY_OF, OPTIONAL_DEPENDENCY_OF |
Scoped dependencies |
STATIC_LINK, DYNAMIC_LINK |
Linking — relevant for license analysis |
GENERATED_FROM / GENERATES |
Build outputs vs sources |
BUILD_TOOL_OF |
A compiler or build tool used |
VARIANT_OF, PATCH_FOR, COPY_OF |
Modified or copied components |
DESCENDANT_OF, ANCESTOR_OF |
Fork lineage |
Files and snippets — SPDX's license-compliance heritage shows here. You can describe individual files (FileName, FileChecksum, LicenseInfoInFile, FileCopyrightText) and even byte or line ranges within a file (SnippetByteRange, SnippetLineRange, SnippetFromFileSPDXID) — exactly the evidence a snippet scanner like FossID or Black Duck produces.
6.7 The same SBOM in SPDX 2.3 JSON (abbreviated)¶
{
"spdxVersion": "SPDX-2.3",
"dataLicense": "CC0-1.0",
"SPDXID": "SPDXRef-DOCUMENT",
"name": "demo-app-1.0.0",
"documentNamespace": "https://example.com/spdx/demo-app-1.0.0-3f1c9a2e-7b4d-4c55-9e1a-2d0f6b8c1a77",
"creationInfo": {
"created": "2026-10-01T12:00:00Z",
"creators": ["Organization: Example Corp", "Tool: syft-1.x"]
},
"documentDescribes": ["SPDXRef-Package-demo-app"],
"packages": [
{
"name": "demo-app",
"SPDXID": "SPDXRef-Package-demo-app",
"versionInfo": "1.0.0",
"supplier": "Organization: Example Corp",
"downloadLocation": "NOASSERTION",
"filesAnalyzed": false,
"licenseConcluded": "MIT",
"licenseDeclared": "MIT",
"copyrightText": "Copyright 2026 Example Corp",
"primaryPackagePurpose": "APPLICATION",
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": "pkg:cargo/[email protected]"
}
]
},
{
"name": "serde",
"SPDXID": "SPDXRef-Package-serde",
"versionInfo": "1.0.210",
"supplier": "NOASSERTION",
"downloadLocation": "https://crates.io/api/v1/crates/serde/1.0.210/download",
"filesAnalyzed": false,
"checksums": [
{ "algorithm": "SHA256", "checksumValue": "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" }
],
"licenseConcluded": "MIT OR Apache-2.0",
"licenseDeclared": "MIT OR Apache-2.0",
"copyrightText": "NOASSERTION",
"primaryPackagePurpose": "LIBRARY",
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": "pkg:cargo/[email protected]"
}
]
}
],
"relationships": [
{
"spdxElementId": "SPDXRef-DOCUMENT",
"relationshipType": "DESCRIBES",
"relatedSpdxElement": "SPDXRef-Package-demo-app"
},
{
"spdxElementId": "SPDXRef-Package-demo-app",
"relationshipType": "DEPENDS_ON",
"relatedSpdxElement": "SPDXRef-Package-serde"
}
]
}
Note the field name differences between tag-value and JSON (PackageVersion vs versionInfo, ExternalRef vs externalRefs). Tools handle this; just be aware when reading files by hand.
7. SPDX 3.0: the redesign¶
SPDX 3.0 (April 2024; 3.0.1 in December 2024) is a substantial, breaking rewrite. The goal: support many use cases beyond license compliance — security, build provenance, AI, datasets — without making every document carry everything.
7.1 Key changes¶
Element-based graph model. Everything is an Element with a globally unique spdxId (a URI). Packages, files, people, organizations, tools, relationships, annotations, license expressions, vulnerabilities — all are elements. A document is essentially a graph of elements.
Relationships are first-class elements. In 2.x a relationship was a property on a package. In 3.0, each relationship is its own element with its own ID, from, to (a list), and relationshipType. This allows relationships to carry metadata and be referenced, e.g. by a VEX assessment.
Profiles. A profile is a set of classes and properties for a use case. A document declares which profiles it conforms to (profileConformance). SPDX 3.0 defines:
| Profile | Purpose |
|---|---|
| Core | Foundational concepts every document uses |
| Software | Packages, files, snippets, SBOMs |
| Security | Vulnerabilities and VEX-style assessments |
| Licensing (Simple and Expanded) | License expressions and detailed license modelling |
| Build | Build provenance: inputs, outputs, environment, parameters |
| AI | AI systems/models (training info, limitations, energy use, etc.) |
| Dataset | Datasets (collection process, sensitivity, preprocessing) |
| Lite | Minimal subset for basic license compliance exchange, aimed at supply chains that need a simple format |
CreationInfo shared object. Instead of repeating creation metadata, elements reference a shared CreationInfo object.
JSON-LD serialization. The canonical serialization is JSON-LD (JSON with linked-data context). It reads like JSON but follows the SPDX RDF model, so the same data can be processed as a graph.
Licenses via relationships. Instead of licenseDeclared fields, a package is linked to a license expression element through hasDeclaredLicense / hasConcludedLicense relationships.
Namespaced property names. Properties outside Core are prefixed with their profile (software_packageVersion, security_..., simplelicensing_...), and class names likewise (software_Package, software_Sbom).
7.2 Illustrative SPDX 3.0.1 JSON-LD¶
Simplified and trimmed to show the shape — always validate real documents with official SPDX tooling and the published schema.
{
"@context": "https://spdx.org/rdf/3.0.1/spdx-context.jsonld",
"@graph": [
{
"type": "CreationInfo",
"@id": "_:creationinfo",
"specVersion": "3.0.1",
"created": "2026-10-01T12:00:00Z",
"createdBy": ["https://example.com/spdx/agent/example-corp"]
},
{
"type": "Organization",
"spdxId": "https://example.com/spdx/agent/example-corp",
"name": "Example Corp",
"creationInfo": "_:creationinfo"
},
{
"type": "SpdxDocument",
"spdxId": "https://example.com/spdx/demo-app-1.0.0/document",
"creationInfo": "_:creationinfo",
"profileConformance": ["core", "software", "simpleLicensing"],
"rootElement": ["https://example.com/spdx/demo-app-1.0.0/sbom"]
},
{
"type": "software_Sbom",
"spdxId": "https://example.com/spdx/demo-app-1.0.0/sbom",
"creationInfo": "_:creationinfo",
"software_sbomType": ["build"],
"rootElement": ["https://example.com/spdx/demo-app-1.0.0/pkg/demo-app"],
"element": [
"https://example.com/spdx/demo-app-1.0.0/pkg/demo-app",
"https://example.com/spdx/demo-app-1.0.0/pkg/serde"
]
},
{
"type": "software_Package",
"spdxId": "https://example.com/spdx/demo-app-1.0.0/pkg/demo-app",
"creationInfo": "_:creationinfo",
"name": "demo-app",
"software_packageVersion": "1.0.0",
"software_packageUrl": "pkg:cargo/[email protected]",
"software_primaryPurpose": "application"
},
{
"type": "software_Package",
"spdxId": "https://example.com/spdx/demo-app-1.0.0/pkg/serde",
"creationInfo": "_:creationinfo",
"name": "serde",
"software_packageVersion": "1.0.210",
"software_packageUrl": "pkg:cargo/[email protected]",
"software_primaryPurpose": "library"
},
{
"type": "Relationship",
"spdxId": "https://example.com/spdx/demo-app-1.0.0/rel/1",
"creationInfo": "_:creationinfo",
"from": "https://example.com/spdx/demo-app-1.0.0/pkg/demo-app",
"relationshipType": "dependsOn",
"to": ["https://example.com/spdx/demo-app-1.0.0/pkg/serde"]
},
{
"type": "simplelicensing_LicenseExpression",
"spdxId": "https://example.com/spdx/demo-app-1.0.0/license/mit-or-apache",
"creationInfo": "_:creationinfo",
"simplelicensing_licenseExpression": "MIT OR Apache-2.0"
},
{
"type": "Relationship",
"spdxId": "https://example.com/spdx/demo-app-1.0.0/rel/2",
"creationInfo": "_:creationinfo",
"from": "https://example.com/spdx/demo-app-1.0.0/pkg/serde",
"relationshipType": "hasDeclaredLicense",
"to": ["https://example.com/spdx/demo-app-1.0.0/license/mit-or-apache"]
}
]
}
7.3 2.x vs 3.0 at a glance¶
| Aspect | SPDX 2.3 | SPDX 3.0 |
|---|---|---|
| Model | Document with sections | Graph of elements |
| IDs | SPDXRef-* local IDs + document namespace |
Global URIs (spdxId) |
| Relationships | Properties | First-class elements |
| Scope | Software + licensing, some security | Profiles: software, security, licensing, build, AI, dataset, lite |
| Serialization | Tag-value, JSON, YAML, RDF/XML, spreadsheet | JSON-LD (canonical), other RDF serializations |
| Licenses | Package fields | Relationships to license elements |
| Adoption (as of 2026) | Ubiquitous | Growing; check tool support |
| ISO standard | 2.2.1 = ISO/IEC 5962:2021 | — |
8. CycloneDX in depth¶
8.1 Background¶
CycloneDX was created in 2017 within the OWASP community, from the start with security and supply-chain risk as the primary use case (it grew out of the OWASP Dependency-Track ecosystem). It is designed to be lightweight, easy to generate, and easy to parse.
CycloneDX is now an Ecma International standard, ECMA-424, developed by Ecma's Technical Committee 54 together with OWASP.
| Version | Release date |
|---|---|
| 1.0 | March 2018 |
| 1.2 | May 2020 |
| 1.3 | May 2021 |
| 1.4 | January 2022 — added vulnerabilities (VEX) |
| 1.5 | June 2023 — added ML-BOM, formulation, lifecycles |
| 1.6 | April 2024 — added CBOM (cryptography), attestations; basis of ECMA-424 |
| 1.7 | October 2025 — latest as of this writing |
Most tooling targets 1.5 or 1.6, with 1.7 support arriving across the ecosystem. Check what your consumers accept before emitting the newest version.
8.2 Beyond software: the "xBOM" family¶
CycloneDX positions itself as a full-stack BOM standard. One schema supports:
- SBOM — software
- SaaSBOM — services, endpoints, data flows
- HBOM — hardware
- ML-BOM / AIBOM — machine learning models and datasets (with model cards)
- CBOM — cryptographic assets (algorithms, keys, certificates, protocols) — important for post-quantum migration planning
- MBOM — manufacturing
- OBOM — operations / runtime environment
- VDR / VEX — vulnerability disclosure and exploitability
- CDXA — attestations (claims and evidence against standards/requirements)
8.3 Formats, media types, and file names¶
- Serializations: JSON, XML, and Protocol Buffers.
- Media types:
application/vnd.cyclonedx+json,application/vnd.cyclonedx+xml; a version parameter may be appended (; version=1.6). - Conventional file names:
bom.json,bom.xml, or*.cdx.json,*.cdx.xml.
8.4 Top-level structure¶
bom
├── bomFormat "CycloneDX"
├── specVersion "1.6"
├── serialNumber urn:uuid:... (unique per BOM)
├── version 1, 2, 3... (revision of this BOM)
├── metadata timestamp, tools, authors, supplier, the subject component, lifecycles, licenses
├── components[] the inventory (can nest)
├── services[] external/internal services the software uses
├── dependencies[] the dependency graph (by bom-ref)
├── compositions[] completeness assertions ("known unknowns")
├── vulnerabilities[] VEX / VDR data
├── externalReferences[]
├── formulation[] how it was built (pipelines, tasks, workflows)
├── annotations[]
├── declarations attestations (1.6+)
├── definitions standards/requirements (1.6+)
├── properties[] name/value extensions
└── signature optional enveloped signature
serialNumber + version identify a specific BOM and its revisions: if you correct an SBOM for the same release, keep the serial number and increment version.
8.5 A complete CycloneDX 1.6 example (JSON)¶
The same demo-app:
{
"$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"serialNumber": "urn:uuid:3f1c9a2e-7b4d-4c55-9e1a-2d0f6b8c1a77",
"version": 1,
"metadata": {
"timestamp": "2026-10-01T12:00:00Z",
"lifecycles": [{ "phase": "build" }],
"tools": {
"components": [
{ "type": "application", "name": "cargo-cyclonedx", "version": "0.5.x" }
]
},
"authors": [{ "name": "Example Corp Build System" }],
"supplier": { "name": "Example Corp", "url": ["https://example.com"] },
"component": {
"type": "application",
"bom-ref": "pkg:cargo/[email protected]",
"name": "demo-app",
"version": "1.0.0",
"purl": "pkg:cargo/[email protected]",
"licenses": [{ "license": { "id": "MIT" } }]
}
},
"components": [
{
"type": "library",
"bom-ref": "pkg:cargo/[email protected]",
"name": "serde",
"version": "1.0.210",
"scope": "required",
"purl": "pkg:cargo/[email protected]",
"hashes": [
{ "alg": "SHA-256", "content": "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" }
],
"licenses": [{ "expression": "MIT OR Apache-2.0" }],
"externalReferences": [
{ "type": "vcs", "url": "https://github.com/serde-rs/serde" }
]
},
{
"type": "library",
"bom-ref": "pkg:cargo/[email protected]",
"name": "anyhow",
"version": "1.0.89",
"scope": "required",
"purl": "pkg:cargo/[email protected]",
"licenses": [{ "expression": "MIT OR Apache-2.0" }]
}
],
"dependencies": [
{
"ref": "pkg:cargo/[email protected]",
"dependsOn": ["pkg:cargo/[email protected]", "pkg:cargo/[email protected]"]
},
{ "ref": "pkg:cargo/[email protected]", "dependsOn": [] },
{ "ref": "pkg:cargo/[email protected]", "dependsOn": [] }
],
"compositions": [
{
"aggregate": "complete",
"assemblies": ["pkg:cargo/[email protected]"]
}
]
}
8.6 Key fields explained¶
Component type — application, framework, library, container, platform, operating-system, device, device-driver, firmware, file, machine-learning-model, data, cryptographic-asset.
bom-ref — a document-unique identifier used by dependencies, compositions, and vulnerabilities to refer to components. Using the purl as the bom-ref is a common convention, but any unique string works.
group — the namespace (Maven groupId, npm scope).
scope — required (needed at runtime), optional, or excluded (e.g. test-only). Useful for filtering dev dependencies out of customer-facing risk.
licenses — either a list of license objects (id for SPDX IDs, name for unknown licenses, optional text, url, and acknowledgement of declared vs concluded in 1.6+) or a single SPDX expression.
hashes — alg (SHA-256, SHA-512, SHA3-256, BLAKE3, legacy SHA-1/MD5) and content.
supplier, manufacturer, authors, publisher — the people and organizations involved.
pedigree — ancestry and modifications: ancestors (the upstream it came from), variants, commits, patches (with references to the issues they fix). This is how you say "this is OpenSSL 3.0.11 with our backported patch for CVE-X" — critical for avoiding false positives.
evidence — how the component was identified: identity (field, confidence, methods such as manifest-analysis, hash-comparison, binary-analysis), occurrences (file locations), callstack, licenses, copyright. This is where SCA tools put their detection evidence.
components nesting — a component can contain sub-components (e.g. a container image component containing OS packages), describing physical containment.
dependencies — a flat list where each entry is { ref, dependsOn[] }. This is the dependency graph, separate from containment.
compositions — explicit completeness statements, CycloneDX's answer to "known unknowns." aggregate can be complete, incomplete, incomplete_first_party_only, incomplete_third_party_only, unknown, not_specified, and others. If you only scanned the lockfile and not vendored code, say incomplete.
services — external APIs and services the software calls: endpoints, authentication, data classification, trust boundary crossings. Useful for SaaS architectures.
properties — free-form name/value pairs for tool-specific data (namespaced by convention, e.g. syft:package:foundBy).
formulation — describes how the software was built (workflows, tasks, steps, inputs, outputs), overlapping with build-provenance concepts.
9. SPDX vs CycloneDX¶
9.1 Comparison¶
| Aspect | SPDX | CycloneDX |
|---|---|---|
| Steward | Linux Foundation | OWASP + Ecma International TC54 |
| Formal standard | ISO/IEC 5962:2021 (v2.2.1) | ECMA-424 |
| Origin focus | License compliance | Security / supply-chain risk |
| Current versions | 2.3 (widely used), 3.0.1 | 1.6 (widely used), 1.7 |
| Serializations | Tag-value, JSON, YAML, RDF/XML (2.x); JSON-LD (3.0) | JSON, XML, Protobuf |
| File/snippet detail | Very strong (files, snippets, verification codes) | Supported via components of type file and evidence |
| License modelling | Richest (concluded vs declared, file-level, expanded licensing profile) | Good; SPDX IDs and expressions, declared/concluded acknowledgement |
| Vulnerabilities / VEX | Security profile in 3.0; external refs in 2.x | Native vulnerabilities array since 1.4 |
| Services / SaaS | Limited | Native services |
| Hardware, ML, crypto | 3.0 AI and Dataset profiles | HBOM, ML-BOM, CBOM |
| Build provenance | 3.0 Build profile | formulation |
| Completeness statement | NOASSERTION/NONE values |
compositions |
| Tooling ecosystem | Broad, strong in compliance and Linux distros, Kubernetes, Docker BuildKit attestations | Broad, strong in AppSec tools, Dependency-Track, language plugins |
9.2 Which should you use?¶
Honest answer: support both if you can, and pick based on your consumers.
- Choose SPDX when license compliance is the priority, when exchanging with partners/OSPOs that expect it, when you need file/snippet-level license detail, or where ISO standardization is valued in procurement.
- Choose CycloneDX when vulnerability management is the priority, when using Dependency-Track, when you need VEX in the same document, services, crypto inventories, or ML models.
- Regulations generally accept either. The EU CRA, NTIA/CISA guidance, and BSI TR-03183 recognize both formats (BSI specifies minimum versions).
Most generators (Syft, Trivy, Black Duck, FossID, Code Insight, Microsoft sbom-tool, cdxgen) can emit one or both. Generating both from the same scan is cheap.
9.3 Conversion¶
Converters exist (for example, the CycloneDX CLI and protobom-based tools), but conversion is lossy because the models differ: file/snippet detail, relationship subtleties, VEX, and tool-specific properties may not survive. Prefer generating each format natively from the source data rather than converting.
10. VEX: telling consumers what actually matters¶
10.1 The problem¶
You give a customer an SBOM. They run it through a scanner and get 200 CVEs. Many don't actually affect your product: the vulnerable function isn't used, the feature is disabled, or you've patched it. The customer files 200 support tickets.
10.2 The solution¶
VEX (Vulnerability Exploitability eXchange) is a machine-readable statement from the supplier about whether a specific product is affected by a specific vulnerability. Each statement links:
- a product (and version),
- a vulnerability (CVE ID),
- a status, and
- for "not affected," a justification; for "affected," an action statement.
CISA's minimum requirements define four statuses:
| Status | Meaning |
|---|---|
not_affected |
No remediation required |
affected |
Action is recommended |
fixed |
This version contains a fix |
under_investigation |
Not yet known |
Standard justifications for not_affected:
component_not_presentvulnerable_code_not_presentvulnerable_code_not_in_execute_pathvulnerable_code_cannot_be_controlled_by_adversaryinline_mitigations_already_exist
10.3 VEX formats¶
- CycloneDX VEX — the
vulnerabilitiesarray, either in the SBOM or as a separate BOM document referencing the SBOM's components. - OpenVEX — a minimal, standalone JSON format focused only on VEX.
- CSAF 2.0 VEX profile — the OASIS Common Security Advisory Framework; heavier, used by many large vendors for advisories.
- SPDX 3.0 Security profile — VEX assessment relationships between vulnerabilities and elements.
10.4 CycloneDX VEX example¶
{
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"serialNumber": "urn:uuid:9a1b2c3d-1111-2222-3333-444455556666",
"version": 1,
"vulnerabilities": [
{
"id": "CVE-2021-44228",
"source": { "name": "NVD", "url": "https://nvd.nist.gov/vuln/detail/CVE-2021-44228" },
"ratings": [{ "severity": "critical", "method": "CVSSv31", "score": 10.0 }],
"analysis": {
"state": "not_affected",
"justification": "code_not_reachable",
"detail": "The JNDI lookup feature is disabled and the JndiLookup class is removed from the shipped classpath."
},
"affects": [
{ "ref": "urn:cdx:3f1c9a2e-7b4d-4c55-9e1a-2d0f6b8c1a77/1#pkg:maven/org.apache.logging.log4j/[email protected]" }
]
}
]
}
CycloneDX's own vocabulary for analysis.state is: resolved, resolved_with_pedigree, exploitable, in_triage, false_positive, not_affected. Its justifications include code_not_present, code_not_reachable, requires_configuration, requires_dependency, requires_environment, protected_by_compiler, protected_at_runtime, protected_at_perimeter, protected_by_mitigating_control. Responses for affected states include can_not_fix, will_not_fix, update, rollback, workaround_available.
The affects.ref above uses a BOM-Link (urn:cdx:<serialNumber>/<version>#<bom-ref>) to point at a component in a different CycloneDX document — that's how a standalone VEX document references an SBOM.
10.5 VEX and VDR¶
- VEX — "is this product affected by this vulnerability?" Primarily communicates non-impact, to reduce noise.
- VDR (Vulnerability Disclosure Report) — a supplier's complete list of known vulnerabilities and their status for a product (NIST SP 800-161 references it). CycloneDX supports both.
10.6 Why VEX separates from the SBOM¶
The SBOM for release 1.0.0 is fixed once built. VEX statements change constantly as new CVEs appear and are analyzed. Many organizations therefore publish the SBOM once and maintain a separate, frequently updated VEX feed for that release.
11. Generating SBOMs¶
11.1 General-purpose generators¶
Syft (Anchore):
# Directory (source SBOM)
syft dir:. -o spdx-json=sbom.spdx.json -o cyclonedx-json=sbom.cdx.json
# Container image (analyzed SBOM)
syft nginx:1.27 -o cyclonedx-json=nginx.cdx.json
# Choose a specific format version where supported, e.g.
syft dir:. -o [email protected]=sbom.cdx.json
Trivy (Aqua):
trivy fs --format cyclonedx --output sbom.cdx.json .
trivy fs --format spdx-json --output sbom.spdx.json .
trivy image --format spdx-json --output image.spdx.json nginx:1.27
cdxgen (CycloneDX project) — broad multi-language CycloneDX generator, can also produce more advanced BOM types:
cdxgen -o bom.json .
Microsoft SBOM tool — generates SPDX, used in Microsoft's own build pipelines:
sbom-tool generate -b ./build-output -bc . -pn demo-app -pv 1.0.0 -ps "Example Corp" -nsb https://example.com/sbom
11.2 Ecosystem-native (build SBOMs)¶
Build-integrated plugins produce the most accurate dependency graphs:
| Ecosystem | Tool |
|---|---|
| Maven | cyclonedx-maven-plugin (mvn org.cyclonedx:cyclonedx-maven-plugin:makeAggregateBom); SPDX Maven plugin |
| Gradle | CycloneDX Gradle plugin |
| npm | @cyclonedx/cyclonedx-npm; npm sbom --sbom-format cyclonedx (npm 10+, also supports spdx) |
| Python | cyclonedx-py (from environment, requirements, Poetry, Pipenv) |
| Rust | cargo-cyclonedx (cargo cyclonedx --format json); cargo-sbom (SPDX); cargo-auditable (embeds dependency info into binaries) |
| Go | cyclonedx-gomod |
| .NET | CycloneDX dotnet tool; Microsoft sbom-tool |
| Containers | docker buildx build --sbom=true (BuildKit generates an SPDX attestation attached to the image); Docker Scout |
11.3 Commercial SCA tools¶
Black Duck, FossID, Revenera Code Insight, Snyk, Mend, Sonatype, JFrog, FOSSA, and others export SPDX and/or CycloneDX — usually enriched with curated license conclusions and vulnerability data. Many can also import SBOMs from third parties for analysis.
11.4 Platform features¶
GitHub can export a repository's dependency graph as SPDX. GitLab produces CycloneDX reports from dependency and container scanning jobs.
11.5 Where to generate in a pipeline¶
source ──▶ build ──▶ package/image ──▶ sign ──▶ publish ──▶ deploy
│ │ │ │ │
source build analyzed attest store SBOM +
SBOM SBOM SBOM SBOM to push to
(MR check) (release) (final image) artifact Dependency-Track
11.6 GitLab CI example: generate, validate, attest, upload¶
stages: [build, sbom, publish]
variables:
IMAGE: "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"
sbom:generate:
stage: sbom
image:
name: aquasec/trivy:latest
entrypoint: [""]
script:
- trivy image --format cyclonedx --output sbom.cdx.json "$IMAGE"
- trivy image --format spdx-json --output sbom.spdx.json "$IMAGE"
artifacts:
paths: [sbom.cdx.json, sbom.spdx.json]
reports:
cyclonedx: [sbom.cdx.json]
sbom:attest:
stage: publish
image: alpine:3.20
needs: [sbom:generate]
id_tokens:
SIGSTORE_ID_TOKEN:
aud: sigstore
script:
- apk add --no-cache cosign
# Keyless signing using the GitLab OIDC token; attaches an attestation to the image
- cosign attest --yes --type cyclonedx --predicate sbom.cdx.json "$IMAGE"
sbom:upload:
stage: publish
image: curlimages/curl:latest
needs: [sbom:generate]
script:
- >
curl -sf -X POST "$DT_URL/api/v1/bom"
-H "X-Api-Key: $DT_API_KEY"
-F "projectName=$CI_PROJECT_NAME"
-F "projectVersion=$CI_COMMIT_SHORT_SHA"
-F "autoCreate=true"
-F "[email protected]"
12. Validating and scoring SBOMs¶
A file that looks like an SBOM can still be invalid or nearly useless. Check two things.
12.1 Schema validity¶
Does it conform to the format's schema?
# CycloneDX CLI
cyclonedx validate --input-file sbom.cdx.json
# SPDX (Python tools)
pip install spdx-tools
pyspdxtools -i sbom.spdx.json # parses and validates
Both formats publish JSON Schemas (CycloneDX also XSD and protobuf definitions), so any JSON Schema validator works as a basic check.
12.2 Quality and conformance¶
Is it useful?
- sbomqs (Interlynk) — scores SBOMs on completeness, identifiers, licenses, structural quality, and conformance with frameworks like NTIA and BSI.
- NTIA conformance checker (SPDX project) — checks NTIA minimum elements in SPDX documents.
- Manual sanity checks: Does every component have a version? A purl? A supplier? Hashes? Is there a dependency graph or just a flat list? Are dev/test dependencies distinguished? Is the root component declared? Are there hundreds of
NOASSERTIONvalues?
sbomqs score sbom.cdx.json
Make validation a CI step: an SBOM that fails schema validation should fail the pipeline.
13. Signing, attesting, and distributing SBOMs¶
An SBOM is a security claim, so consumers need to trust it.
13.1 Integrity and authenticity¶
- Detached signatures — sign the SBOM file (GPG, cosign
sign-blob, X.509). - Enveloped signatures — CycloneDX supports JSON Signature Format (JSF) and XML signatures inside the document.
- Attestations — wrap the SBOM as the predicate of an in-toto attestation signed with Sigstore/cosign, and attach it to the container image in the registry. Consumers verify with:
cosign verify-attestation --type cyclonedx \
--certificate-identity-regexp '.*' \
--certificate-oidc-issuer https://gitlab.com \
registry.example.com/demo-app:abc123
(Use a strict identity pattern in practice, matching your actual project and pipeline.)
13.2 SBOM vs provenance¶
Don't confuse them:
- SBOM — what's inside the artifact.
- Provenance (e.g. SLSA provenance) — how and where it was built: which source commit, which builder, which parameters.
Both are often published as attestations on the same image. SPDX 3.0's Build profile and CycloneDX's formulation bring provenance-like data into the SBOM formats themselves.
13.3 Distribution options¶
- Attached to OCI images as attestations/referrers in the registry.
- Published alongside release artifacts (
demo-app-1.0.0.cdx.jsonnext to the tarball). - Customer/vendor portals with access control.
- Pushed to an SBOM management platform (Dependency-Track, commercial SCA servers).
- Discovery mechanisms such as Transparency Exchange API (TEA) work in Ecma TC54 aim to standardize how consumers find SBOMs/VEX for a product.
13.4 Storage and retention¶
Keep an SBOM for every released version for as long as that version is supported (and often longer, for regulatory and incident-response reasons). The CRA, for example, ties documentation retention to product support periods. Index SBOMs by product, version, and artifact digest.
14. Consuming SBOMs¶
Producing SBOMs is half the story. When you receive SBOMs from suppliers (or ingest your own):
- Validate schema and quality; reject or flag poor SBOMs.
- Verify signatures/attestations.
- Ingest into a management platform (Dependency-Track, commercial SCA, or a database).
- Normalize identifiers (purl), deduplicate components across products.
- Enrich with vulnerability data (OSV, NVD, GHSA, vendor advisories), license data, EPSS/KEV.
- Apply VEX from the supplier to suppress non-applicable findings.
- Apply policy and alert on violations.
- Monitor continuously — rerun matching when new vulnerabilities are published.
- Answer questions fast — "Where do we run log4j-core < 2.17.1?" across the whole portfolio.
# Quick consumer-side checks with free tools
grype sbom:./supplier.cdx.json
trivy sbom ./supplier.spdx.json
15. SBOM quality problems¶
Real-world SBOMs are often poor. Common problems and remedies:
| Problem | Effect | Remedy |
|---|---|---|
| Missing versions | Can't match vulnerabilities | Generate from lockfiles/build, not manifests |
| Missing purls | Consumers can't identify components | Use generators that emit purls; avoid name-only components |
| Flat list, no dependency graph | Can't find remediation paths or assess transitive risk | Include dependencies (CycloneDX) / DEPENDS_ON relationships (SPDX) |
NOASSERTION everywhere |
Valid but uninformative | Enrich with license and supplier data |
| Generated at the wrong stage | Source SBOM misses base image; analyzed SBOM misses build-only info | Combine build and analyzed SBOMs; record lifecycle |
| Dev/test dependencies mixed in | Noise for customers | Use scope or SPDX dependency relationship types |
| Different tools, different answers | Inconsistent inventories | Standardize on tools per stage; document which tool and version (in metadata) |
| No completeness statement | Consumers assume completeness | Use compositions or NOASSERTION honestly |
| Stale SBOM | Doesn't match shipped artifact | Generate per build; tie to artifact digest |
| Unsigned | Can't trust it | Sign/attest |
| Vendored / C/C++ code missing | False sense of completeness | Add signature/snippet scanning or manual declarations |
16. Hands-on lab¶
Work through this on any Linux machine with Docker. It produces genuine experience with both formats.
16.1 Setup¶
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b ~/.local/bin
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b ~/.local/bin
pip install --user spdx-tools
# Trivy: see https://trivy.dev, or use the aquasec/trivy container
# CycloneDX CLI: download a release binary from github.com/CycloneDX/cyclonedx-cli
16.2 Generate both formats for a real project¶
mkdir -p ~/sbom-lab && cd ~/sbom-lab
cargo new demo-app && cd demo-app
cargo add serde --features derive
cargo add anyhow
cargo generate-lockfile
syft dir:. -o spdx-json=sbom.spdx.json -o cyclonedx-json=sbom.cdx.json
16.3 Read them by hand¶
jq '.packages | length' sbom.spdx.json
jq '.components | length' sbom.cdx.json
jq '.components[] | {name, version, purl, licenses}' sbom.cdx.json | head -40
jq '.relationships[] | select(.relationshipType=="DEPENDS_ON")' sbom.spdx.json | head -20
jq '.dependencies[0]' sbom.cdx.json
Find in each file: the root component, a transitive dependency, its purl, its license, and its dependency relationship. Compare how the two formats express the same facts.
16.4 Validate¶
cyclonedx validate --input-file sbom.cdx.json
pyspdxtools -i sbom.spdx.json
Then deliberately break each file (remove a required field, misspell bomFormat) and see how validators report errors.
16.5 Compare generators¶
trivy fs --format cyclonedx --output trivy.cdx.json .
cargo install cargo-cyclonedx && cargo cyclonedx --format json
cyclonedx diff sbom.cdx.json trivy.cdx.json --component-versions
Why do the component counts differ? Look at build dependencies, dev dependencies, and the root component.
16.6 Container: analyzed SBOM¶
syft python:3.11-slim -o cyclonedx-json=python-slim.cdx.json
jq '[.components[] | .purl | split("/")[0]] | group_by(.) | map({type: .[0], count: length})' python-slim.cdx.json
You'll see both pkg:deb (OS packages) and pkg:pypi/pkg:generic components.
16.7 Vulnerabilities from SBOM only¶
grype sbom:./python-slim.cdx.json --only-fixed
No access to the image is needed — the SBOM alone is enough to match vulnerabilities. This is exactly how continuous monitoring platforms work.
16.8 Write a VEX statement¶
Pick one CVE from the Grype output, write a CycloneDX VEX document (use the example in Section 10.4) marking it not_affected with a justification, then pass it to Grype:
grype sbom:./python-slim.cdx.json --vex ./vex.cdx.json
Observe the finding being suppressed. (Grype also supports OpenVEX documents.)
16.9 Score quality¶
sbomqs score python-slim.cdx.json
sbomqs score sbom.spdx.json
Look at which categories score low and why.
16.10 Bonus: Dependency-Track¶
Upload python-slim.cdx.json into a local Dependency-Track (see the SCA guide), explore the component inventory, license view, vulnerabilities, and the audit trail when you mark findings as not affected. Then export a VEX from Dependency-Track and compare it with the one you wrote.
17. Talking about SBOMs in an interview¶
Be honest about what you've actually done. If your experience is from building a lab pipeline with open-source tools, describe it concretely — generating, validating, signing, and monitoring SBOMs is the same work regardless of which commercial product sits in the middle.
"What is an SBOM and why does it matter?" A machine-readable inventory of software components, identifiers, licenses, and relationships. It enables fast vulnerability response (Log4Shell), license compliance, and is required or expected by EO 14028-driven federal procurement, the FDA for medical devices, and the EU Cyber Resilience Act.
"What are the NTIA minimum elements?" Supplier, component name, version, other unique identifiers, dependency relationship, SBOM author, timestamp — plus automation support and practices (frequency, depth, known unknowns, distribution, access control, accommodation of mistakes).
"SPDX or CycloneDX?" Both are recognized standards (ISO/IEC 5962 and ECMA-424). SPDX originated in license compliance and has the richest licensing and file/snippet model; 3.0 adds profiles for security, build, AI, and datasets. CycloneDX originated in security, has native VEX, services, CBOM, ML-BOM. Choose based on consumers; generate both if practical; avoid lossy conversion.
"What's a purl and why is it important?" A package URL encoding ecosystem, namespace, name, version, and qualifiers. It's how SBOM components are matched to vulnerability databases like OSV and GHSA precisely; CPE alone is ambiguous for libraries.
"What is VEX?" A supplier's machine-readable statement of whether a product is affected by a vulnerability, with justification. It reduces false positives for consumers. Formats: CycloneDX VEX, OpenVEX, CSAF VEX, SPDX 3.0 Security profile.
"How would you implement SBOMs in a CI/CD pipeline?" Generate a build SBOM per release and an analyzed SBOM of the final image; validate schema and quality; sign/attest with cosign and attach to the image; store per version; push to Dependency-Track or the SCA platform for continuous monitoring; maintain VEX; gate releases on policy.
"What makes a bad SBOM?"
Missing versions and purls, no dependency graph, NOASSERTION everywhere, wrong lifecycle stage, unsigned, stale, no completeness statement.
"SBOM vs SLSA provenance?" SBOM is what's inside; provenance is how it was built. They're complementary.
18. Glossary¶
| Term | Meaning |
|---|---|
| Attestation | Signed statement about an artifact (e.g. in-toto) |
| BOM-Link | CycloneDX URN referencing a component in another BOM |
| bom-ref | CycloneDX document-local component identifier |
| CBOM | Cryptography Bill of Materials |
| CDXA | CycloneDX Attestations |
| Composition | CycloneDX completeness assertion |
| CPE | NVD product identifier |
| CRA | EU Cyber Resilience Act |
| CSAF | OASIS Common Security Advisory Framework |
| DocumentNamespace | Unique URI for an SPDX 2.x document |
| ECMA-424 | Ecma standard for CycloneDX |
| EO 14028 | 2021 US executive order driving SBOM adoption |
| Evidence | CycloneDX field recording how a component was identified |
| HBOM | Hardware Bill of Materials |
| ISO/IEC 5962:2021 | International standard for SPDX 2.2.1 |
| JSON-LD | Linked-data JSON; SPDX 3.0's canonical serialization |
| ML-BOM / AIBOM | Bill of materials for ML models and datasets |
| NOASSERTION / NONE | SPDX "unknown/not stated" vs "determined to be empty" |
| OpenVEX | Lightweight standalone VEX format |
| Pedigree | CycloneDX ancestry and modification data |
| Profile | SPDX 3.0 use-case module (Software, Security, Build, AI, etc.) |
| Provenance | Record of how an artifact was built (e.g. SLSA) |
| purl | Package URL |
| SaaSBOM | Bill of materials for services |
| serialNumber | Unique URN identifying a CycloneDX BOM |
| SPDXID / spdxId | SPDX element identifier (local in 2.x, URI in 3.0) |
| SWID | ISO/IEC 19770-2 software identification tags |
| TEA | Transparency Exchange API (SBOM/VEX discovery) |
| VDR | Vulnerability Disclosure Report |
| VEX | Vulnerability Exploitability eXchange |
19. Further reading¶
- SPDX specifications — https://spdx.dev/use/specifications/
- SPDX License List — https://spdx.org/licenses/
- CycloneDX specification and guides — https://cyclonedx.org
- CycloneDX specification repository — https://github.com/CycloneDX/specification
- Package URL specification — https://github.com/package-url/purl-spec
- CISA SBOM resources — https://www.cisa.gov/sbom
- NTIA, The Minimum Elements for a Software Bill of Materials (2021)
- CISA, Types of Software Bill of Materials (SBOM) Documents (2023)
- CISA, Minimum Requirements for Vulnerability Exploitability eXchange (VEX) (2023)
- OpenVEX — https://github.com/openvex
- OASIS CSAF — https://oasis-open.github.io/csaf-documentation/
- Sigstore / cosign — https://www.sigstore.dev
- SLSA — https://slsa.dev
- OWASP Dependency-Track — https://dependencytrack.org
- sbomqs — https://github.com/interlynk-io/sbomqs
- EU Cyber Resilience Act — Regulation (EU) 2024/2847
- BSI TR-03183 (Germany) — technical requirements for SBOMs
- Companion guide: Software Composition Analysis (SCA): A Complete Guide from Zero