ac6-formation, un département d'Ac6 SAS
EN
EnglishFrench
go-up

ac6 ac6-formation

SBOM for embedded firmware: generating, checking and actually using it

An SBOM, or software bill of materials, is the exhaustive list of a product's software components with their versions and licences. On a typical embedded firmware that means between 200 and 800 entries, the overwhelming majority of which come from code you did not write. The Cyber Resilience Act makes it part of the technical file, but its real value lies elsewhere: without it you cannot answer the only question that matters when a CVE lands, namely "does this affect me".

The two formats, and which to choose

SPDX (ISO/IEC 5962) comes from the licence compliance world, under the Linux Foundation. It is verbose, very precise about licences and package relationships, and it is the format that embedded Linux build systems produce natively.

CycloneDX, from OWASP, comes from the application security world. It is more compact, oriented towards vulnerability analysis, and handles service dependencies and hardware components better.

In practice the choice rarely arises: you produce whatever your build system can generate, and convert if a customer demands the other. Both are accepted by authorities, and analysis tools read both.

What matters more than the format is having PURL or CPE identifiers on every component. Without them, automatic matching against vulnerability databases does not work, and your SBOM becomes an archive document rather than a tool.

Generating from Yocto

Yocto produces an SPDX SBOM natively, and on current releases the create-spdx class is inherited by default through INHERIT_DISTRO: there is nothing to enable. On an older release, or if your distro changed INHERIT_DISTRO, add it explicitly:

INHERIT += "create-spdx"

Two variables are worth knowing: SPDX_PRETTY for readable JSON, and SPDX_INCLUDE_SOURCES to include source files in the document.

The main output is an IMAGE-MACHINE.spdx.json file in tmp/deploy/images/MACHINE/, with additional files in tmp/deploy/spdx.

Two things to know. The SBOM describes what the build produced, not what is actually installed in the final image: a package that was compiled but not installed can still appear. And source archiving, through archiver, is a separate mechanism that must be enabled on its own if you want to rebuild identically in five years.

Generating from Buildroot

Buildroot offers make legal-info, which produces licenses.txt, a manifest and the source tree. That is not an SBOM in the strict sense: no standard format, no PURL identifiers, therefore no automatic CVE matching.

To get a usable SPDX or CycloneDX you need an external tool that scans the result, such as syft on the generated filesystem:

syft dir:output/target -o spdx-json > sbom.spdx.json
syft dir:output/target -o cyclonedx-json > sbom.cdx.json

This scan-the-filesystem approach has a structural weakness: it sees binaries, not provenance. A library statically linked into an executable disappears from the inventory. That is the flaw of any after-the-fact generation, and the reason an SBOM produced by the build beats one produced from the finished product.

The Zephyr case

Zephyr generates SPDX through its build tooling:

# in prj.conf, or on the command line
CONFIG_BUILD_OUTPUT_META=y

west build -b <board> <app> -d build
west spdx -d build                      # SPDX 2.3 by default
west spdx -d build --spdx-version 3.0   # for SPDX 3.0

The --init option, still found in many tutorials, is deprecated and will be removed in Zephyr 5.0: a build with CONFIG_BUILD_OUTPUT_META now requests the required information itself.

The output separates three documents: the application, the SDK, and the Zephyr kernel with its modules. That separation is useful, because Zephyr CVEs are published per module rather than for the whole tree.

Watch out for the classic gap: the Zephyr SBOM covers what is compiled into the image, but not the bootloader. If you use MCUboot you must generate and attach its own SBOM, otherwise a bootloader vulnerability will go unnoticed.

Producing the document achieves nothing without the loop that consumes it. The pattern that works:

  1. the build produces the SBOM on every run, tagged with the firmware version
  2. the SBOM is archived and versioned alongside the released artefact
  3. a scanner compares it daily against vulnerability databases
  4. every match triggers an applicability assessment

Step 4 is the one that gets skipped, and it is the expensive one. A CVE match is not a vulnerability: the affected code may not be compiled in, the function may not be reachable, the configuration may be out of scope. That is what VEX is for, a format that lets you formally state "this component is present, this CVE does not apply, here is why".

Without VEX, a team tracking three hundred components receives enough irrelevant alerts to stop reading them within two months.

The traps that make an SBOM worthless

TrapConsequence
Generated once, by handStale from the next build onwards, therefore wrong
No PURL or CPENo automatic matching possible
Produced by scanning the final binaryStatic libraries vanish
No exact version, such as "1.x"Impossible to decide whether the CVE applies
Bootloader missingA whole class of vulnerabilities stays invisible
Not archived with the releaseNo way to know what shipped in the 2025 firmware

The last point carries the most weight. The support obligation runs for five years minimum: you must be able to state, in 2031, exactly what was inside the firmware shipped in 2026. An SBOM that is not archived with the artefact is useless on the day it would have been useful.

What to put in place, in order

Enable generation inside the build system, not beside it. Check that every entry carries an exact version and a PURL or CPE identifier. Archive the SBOM with the signed binary, in the same artefact repository. Point a vulnerability scanner at it. And adopt VEX from the first wave of alerts, before the team learns to ignore them.

References

Going further

These mechanisms make sense once you have produced an SBOM on a real project, seen what is missing, and worked one CVE match through end to end. That is what our Cyber Resilience Act course and our embedded Linux and secure development courses cover.