Creating an SBOM: the software bill of materials for the Cyber Resilience Act

Two developers and a colleague analysing data together on several screens

Quality and security

At the end of 2021, the Log4Shell vulnerability in the widely used Java library Log4j became public. Many companies spent days searching: is the library in one of our products – and in which version? Anyone with a software bill of materials answered in minutes. Under the Cyber Resilience Act, this list becomes mandatory for manufacturers.

What belongs in an SBOM

The CRA defines the software bill of materials (SBOM) as a formal record of the details and supply chain relationships of the components included in the software of a product. In practice, it is a machine-readable list of all libraries, packages and modules. For each component, it contains at least:

  • name and version,
  • manufacturer or supplier,
  • a unique identifier, usually a package URL such as pkg:nuget/Newtonsoft.Json@13.0.3,
  • a checksum of the delivered file,
  • the licence,
  • the dependencies on other components.

What does not belong in it are vulnerabilities. Whether a flaw actually affects your product is recorded separately, for example in a VEX document (Vulnerability Exploitability eXchange).

What the CRA requires – and what it doesn't

Annex I, Part II, point 1 requires an SBOM in a commonly used, machine-readable format covering at the very least the top-level dependencies. It is part of the technical documentation and must be provided to the market surveillance authority upon reasoned request. You do not have to publish it. If you make it available to your customers, the user information must state where it can be found. The regulation does not yet prescribe a fixed format; the Commission may specify one in an implementing act.

A good benchmark already exists in the German BSI technical guideline TR-03183-2. Version 2.1.0 requires JSON or XML in CycloneDX from version 1.6 or SPDX from version 3.0.1. For each component it requires, among other things, creator, name, version, filename, licence, dependencies and a SHA-512 hash. Dependencies are resolved recursively, at least up to and including the first component outside your scope of delivery.

CycloneDX or SPDX?

 CycloneDXSPDX
Origin OWASP, standardised as ECMA-424 Linux Foundation, version 2.2.1 as ISO/IEC 5962:2021
Focus security and supply chain, VEX possible in the same format originally licences and compliance, security as well since version 3
Current version 1.7 3.0.1

Both formats satisfy the CRA; what matters is what your tools and customers process. For matching against vulnerability databases, for example with OWASP Dependency-Track, CycloneDX is common, while licence checks traditionally favour SPDX. Conversion is possible, but not always lossless.

Generating the SBOM in the build

An SBOM maintained by hand is wrong after the next package update. It belongs in the build – for exactly the version you ship. For .NET there is the CycloneDX command-line tool, for Python cyclonedx-py, and for SPDX, for example, Microsoft's sbom-tool:

# .NET: install the tool once, then run it in every release build
dotnet tool install --global CycloneDX
dotnet-CycloneDX Product.sln -o sbom -fn product.cdx.json -F Json -t -sn Product -sv 2.4.0

# Python: from requirements.txt
cyclonedx-py requirements requirements.txt --of JSON -o sbom/tool.cdx.json

-sv sets the product version, -t leaves out test projects. A small script then checks whether the SBOM is complete and fails the build if information is missing – for example licences the tool could not resolve:

import json
import sys

with open("sbom/product.cdx.json", encoding="utf-8") as file:
    components = json.load(file).get("components", [])

gaps = []
for component in components:
    name = component.get("name", "?")
    for field in ("version", "purl", "licenses", "hashes"):
        if not component.get(field):
            gaps.append(f"{name}: {field} missing")

print(f"{len(components)} components checked, {len(gaps)} gaps")
for line in gaps:
    print(line)
sys.exit(1 if gaps else 0)  # gaps make the build fail

What the tools don't find

Tools read package managers. Anything that bypasses them is missing: copied DLLs, C and C++ libraries, purchased modules, firmware, bundled runtimes. Record these once in a supplementary file that the build merges with the generated SBOM, for example with the merge command of the CycloneDX CLI. For CAD add-ins, the opposite applies: you do not ship the AutoCAD or Revit DLLs you build against. They do not belong in your SBOM as components, but they do belong in the documentation as a prerequisite.

And then the SBOM has to be used: archive it for each release with the technical documentation and match it against vulnerability databases continuously – new vulnerabilities also affect versions already running at your customers.

Note: This article gives a technical overview and is not legal advice.

Typical mistakes

  • Generating the SBOM from the source code instead of the build. It then does not describe what is actually shipped.
  • Recording only direct packages. Many vulnerabilities hide in transitive dependencies.
  • One SBOM for all versions. Every shipped version needs its own.
  • Writing vulnerabilities into the SBOM instead of a separate VEX document.
  • Forgetting copied DLLs and C++ libraries because no package manager knows them.

How we help

Sources: Regulation (EU) 2024/2847, Art. 3(39), Art. 13(24), Annex I Part II point 1, Annex II point 9, Annex VII and recital 77, EUR-Lex; BSI TR-03183-2, version 2.1.0; CycloneDX; SPDX. As of October 2026.

First meetingarrange nowFirst meetingarrange now