SCADABLE

Generating an SBOM from your ESP-IDF build, and what esp-idf-sbom does not tell you

Espressif ships an official SBOM tool for ESP-IDF. It works. It also has gaps that matter the moment you have more than one device. A walkthrough plus the layer that goes on top.


Your ESP32 firmware needs an SBOM by September 2026. Here is the tool Espressif quietly shipped, and what it does not do.

If you ship a connected product into the EU and you have not started thinking about the Cyber Resilience Act, the calendar is shorter than it looks. Manufacturers are obligated to start reporting actively exploited vulnerabilities and severe incidents from September 11, 2026. The full set of essential cybersecurity requirements (including documentation that contains a software bill of materials) lands on December 11, 2027. The SBOM is the artifact you hand to a notified body, and the artifact you reference when you file a 24-hour early-warning notification to ENISA after a breach.

For ESP-IDF projects, the fastest way to start producing one is the official esp-idf-sbom tool. Espressif shipped it without much fanfare. It is functional, it is open source, and it has clear gaps that you will hit within an hour of running it on a real fleet.

Install and first run on a real project

esp-idf-sbom is a Python package that reads ESP-IDF build artifacts (build/project_description.json, the component manifest YAML files, the idf_component.yml lockfile) and produces an SPDX 2.2 document. It does not require a separate build. It does require that the project has already been built once.

# Activate your IDF environment first.
. $IDF_PATH/export.sh
 
# Install the tool.
pip install esp-idf-sbom
 
# Build, then generate.
cd ~/dev/gateway-esp
idf.py build
esp-idf-sbom create build -o sbom.spdx

The output is a single SPDX file. Roughly five thousand lines of YAML for a stock hello_world project, more for anything realistic. The interesting parts are the Package blocks, one per component, each with a CPE identifier, a version string, a license, and a relationship graph back to the parent project.

A representative slice for a project that pulls in mbedtls and lwip:

SPDXVersion: SPDX-2.2
DataLicense: CC0-1.0
SPDXID: SPDXRef-DOCUMENT
DocumentName: gateway-esp
DocumentNamespace: https://espressif.com/spdx/gateway-esp/0.3.0
Creator: Tool: esp-idf-sbom-0.18.0
Created: 2026-05-14T08:42:11Z
 
PackageName: mbedtls
SPDXID: SPDXRef-Package-mbedtls
PackageVersion: 3.6.2
PackageSupplier: Organization: Espressif Systems
PackageDownloadLocation: https://github.com/espressif/mbedtls
PackageLicenseConcluded: Apache-2.0
PackageLicenseDeclared: Apache-2.0
ExternalRef: SECURITY cpe23Type cpe:2.3:a:arm:mbed_tls:3.6.2:*:*:*:*:*:*:*
Relationship: SPDXRef-Package-gateway-esp DEPENDS_ON SPDXRef-Package-mbedtls

That is the shape. Component name, pinned version, license, and (critically for vulnerability scanning) a CPE identifier you can throw at NVD or OSV.

Reading the output, and what is missing

The first thing you notice is how many components show up. A minimal Wi-Fi firmware lists 80 to 120 packages. Most are IDF internals (esp_event, esp_system, nvs_flash), some are upstream (mbedtls, lwip, freertos), and a handful are your own components.

The second thing you notice is what is missing. If you pulled a third-party library in via idf_component.yml from the ESP Component Registry, it is captured. If you cloned a library into components/ by hand without an idf_component.yml, the tool sees it but reports no version, no license, and no CPE. It shows up as a phantom package with PackageVersion: NOASSERTION. From a CRA evidence perspective, NOASSERTION is worse than nothing. It documents that you knowingly shipped unidentified code.

The third gap is harder to spot. The SBOM does not distinguish between code that was compiled and code that was actually linked into the final binary. ESP-IDF's component manager pulls in transitive dependencies aggressively, and many produce object files the linker discards. They still show up in the SBOM, which overstates your attack surface and floods your vulnerability dashboard with noise.

Running the vulnerability check

The tool ships with a check subcommand that takes the generated SBOM, hits the NVD CVE feed, and prints anything it matches via the CPEs.

esp-idf-sbom check sbom.spdx

On a recent build of one of our reference gateways, the first run returned 47 CVEs. After ten minutes of triage, the real count was four. The other 43 broke down roughly like this:

  • 18 against xtensa-esp32-elf toolchain components and app_trace, which are build-time only and never run on the device.
  • 12 against IDF subsystems we had Kconfig-disabled (coap, asio, tinyusb).
  • 7 against versions of lwip and mbedtls that Espressif had backported fixes for in their fork, but where the upstream version string in the manifest still matched the vulnerable CPE.
  • 6 against freertos, all of which were against the upstream Amazon FreeRTOS lineage. ESP-IDF uses a heavily forked FreeRTOS and the SBOM tool has no reliable way to determine the base version, so it conservatively reports against the upstream CPE and you triage manually.

This is not a critique of the tool. It is doing exactly what an SBOM scanner is supposed to do. It is a critique of the workflow. If a developer runs this on their laptop once a week, the signal-to-noise ratio guarantees they stop reading by week three.

The four gaps Espressif has acknowledged

If you read through the esp-idf-sbom issues and discussions on GitHub, the maintainers are open about what the tool does not yet do well. Four themes show up repeatedly.

Toolchain noise clutters the output. Components like xtensa-esp32-elf and app_trace are part of the build environment and never end up on the device. They appear in the SBOM because the tool walks the full component dependency graph, not the linked binary. There is an open discussion about adding a --runtime-only filter, but at the time of writing it is not shipped.

Components built but not linked into the binary still appear. Same root cause as the toolchain noise. The component manager has a much wider view of "what is in this project" than the linker does. Until the SBOM tool consults the final ELF's symbol table to filter out unused code, the SBOM overstates what is actually executing on the chip.

FreeRTOS base version cannot be determined. ESP-IDF's FreeRTOS is a fork that has diverged from upstream over years. The tool emits a CPE for upstream FreeRTOS as a best effort, but cannot tell which patches have been backported. Every upstream FreeRTOS CVE will light up your SBOM whether or not you are actually vulnerable. This requires manual triage, every time, for every release.

The tool runs per build, on a developer machine. This is the gap that turns into a real problem fast. The SBOM is produced the moment Anna runs idf.py build on her laptop. There is no built-in concept of a build server, a release artifact registry, or a fleet view. Anna's SBOM lives in Anna's build/ directory until she copies it somewhere. Bob, who built the same release tag from the same git SHA on his machine, gets an SBOM that should be byte-identical but in practice differs because his IDF patch version or his pip cache resolved a different transitive dependency.

The first three gaps are tractable engineering problems and Espressif will probably close them. The fourth is structural. It is not really a tool problem. It is a workflow problem.

The harder gap: SBOM-per-machine versus fleet posture

Suppose you do everything right. You wire esp-idf-sbom create into your CI pipeline. Every release tag produces a deterministic SBOM as a build artifact. You store them in S3 keyed by version. You feel responsible.

Then a customer asks you on a Tuesday morning, "you just disclosed CVE-2025-XXXXX in your release notes for v1.4.2. We have 312 of your gateways deployed across our four sites. How many are exposed?"

The SBOM you generated at build time cannot answer that. It tells you v1.4.2 was vulnerable and v1.4.5 patched it. It does not tell you that 47 of those 312 gateways are still on v1.4.2 because three sites run a slower rollout cadence. It does not tell you which of those 47 sit in regulated environments where a 24-hour CRA notification clock starts the moment exploitation is confirmed.

A build-time SBOM is necessary but not sufficient. The missing piece is the join between "what is this firmware made of" and "where is this firmware running, right now, in production."

What the next layer looks like

The shape of the system that closes this gap is not subtle. You have probably already sketched it in your head.

  1. Ingest the SBOM at build time, not at scan time. CI publishes the SBOM as a release artifact alongside the binary. The SBOM is signed (or at least hashed) so you can prove provenance later.
  2. Store one SBOM per release version, indefinitely. The CRA reporting window is years long. A vulnerability disclosed in 2029 may apply to a version you shipped in 2026.
  3. Match against multiple feeds, not just NVD. OSV.dev is faster than NVD for ecosystem-level disclosures. The CISA Known Exploited Vulnerabilities catalog is the highest-priority subset that triggers the CRA active-exploitation reporting clock. Match against both.
  4. Render the per-namespace fleet posture. For each customer, for each release version still in production, show how many devices are on that version and which CVEs apply. This is the view that answers the Tuesday-morning question in three clicks.
  5. Gate the OTA rollout on the result. A new release should not deploy to a tenant whose security policy excludes a CVE class until a build that closes those CVEs is available. Vulnerability management stops being a quarterly audit ritual and becomes a continuous deployment guard.

None of those steps require novel research. They require infrastructure work that is not the reason anyone gets into hardware. You start with a ten-line CI invocation and end with a multi-tenant security database, an OSV ingester, a release registry, and a deploy gate. That is six months of platform work for a feature that is not your product.

This is the shape of work we picked up at SCADABLE because we kept watching customers try to build it themselves and run out of runway. We treat per-release SBOM ingest, OSV plus KEV matching, per-namespace posture views, and OTA gating as platform concerns so the firmware team can stay on the firmware. If you read our earlier post on certs and rotation, the philosophy is the same. The undifferentiated heavy lifting that every connected-product company has to do to pass an audit should be a platform concern, not a per-customer rebuild.

In the meantime, run esp-idf-sbom create on your next release. The output is imperfect, the noise is real, the workflow gap is structural. The alternative (no SBOM at all, then a CRA deadline you cannot meet) is worse. Generate the artifact. Store it. Get familiar with the format. The work that lands in 2027 goes faster if you started in 2026.

Talk to a founder

If you're staring down a CRA deadline and trying to figure out how the SBOM you generate at build time turns into evidence you can hand a regulator after deploying to a fleet, that's the conversation we have every week. Book a 20-minute slot with me directly.

Book a quick chat with Ali