Andrew Mercer
on this page

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

  1. What an SBOM is
  2. Why SBOMs suddenly matter
  3. Minimum elements: what an SBOM must contain
  4. SBOM types by lifecycle stage
  5. Identifiers: purl, CPE, hashes and friends
  6. SPDX in depth
  7. SPDX 3.0: the redesign
  8. CycloneDX in depth
  9. SPDX vs CycloneDX
  10. VEX: telling consumers what actually matters
  11. Generating SBOMs
  12. Validating and scoring SBOMs
  13. Signing, attesting, and distributing SBOMs
  14. Consuming SBOMs
  15. SBOM quality problems
  16. Hands-on lab
  17. Talking about SBOMs in an interview
  18. Glossary
  19. 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:

  1. Document creation information — about the SBOM itself.
  2. Package information — one block per package (component).
  3. File information — optional, per file.
  4. Snippet information — optional, for parts of files.
  5. Other licensing information — definitions of custom LicenseRef- licenses.
  6. Relationships — how elements relate.
  7. 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_present
  • vulnerable_code_not_present
  • vulnerable_code_not_in_execute_path
  • vulnerable_code_cannot_be_controlled_by_adversary
  • inline_mitigations_already_exist

10.3 VEX formats

  • CycloneDX VEX — the vulnerabilities array, 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 NOASSERTION values?
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.json next 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):

  1. Validate schema and quality; reject or flag poor SBOMs.
  2. Verify signatures/attestations.
  3. Ingest into a management platform (Dependency-Track, commercial SCA, or a database).
  4. Normalize identifiers (purl), deduplicate components across products.
  5. Enrich with vulnerability data (OSV, NVD, GHSA, vendor advisories), license data, EPSS/KEV.
  6. Apply VEX from the supplier to suppress non-applicable findings.
  7. Apply policy and alert on violations.
  8. Monitor continuously — rerun matching when new vulnerabilities are published.
  9. 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