ESP32 Firmware SBOM: Finding Publicly Known Vulnerabilities for EN 18031 and the CRA
Both EN 18031 and the Cyber Resilience Act push you toward the same two artifacts: an inventory of what is inside your firmware, and a list of the publicly known vulnerabilities affecting it. That has always been painful for embedded software, but if you build on an ESP32, Espressif now ships a genuinely neat solution that does most of the work.

The Firmware SBOM Problem: Espressif's Answer
If you place a connected radio product on the EU market, two regulations now converge on the same demand. EN 18031, harmonized under the RED Directive and mandatory since August 2025, requires you to show your device ships with no publicly known exploitable vulnerabilities. The Cyber Resilience Act asks for the same outcome and names the artifact: products must be placed on the market without known exploitable vulnerabilities, and manufacturers must draw up a software bill of materials. Neither is satisfied by good intentions: both need you to know, concretely, every component inside your firmware and whether any of them carries a known flaw.
For application and container software, this is a solved problem. A mature ecosystem (Syft, Grype, Trivy, Dependency-Track) grew up around package managers and Linux images, where every dependency has a name, a version and a registry to look it up in. Firmware has none of that. Bare-metal C is vendored into the source tree, submodules are pinned to a git commit, and there is often no package manager and no registered identifier at all. The tooling that makes SBOMs trivial for a Docker image has historically had nothing to grip onto in an embedded build.
This is where one of the biggest names in IoT silicon quietly did the hard part for you. Espressif, whose ESP32 chips sit inside an enormous share of the world's Wi-Fi connected devices, ships an SBOM and vulnerability tool built directly into ESP-IDF. It generates the inventory from your real build and scans it against the National Vulnerability Database. It is not flawless, and this article is honest about where you still need to reach for a second database. But if you are shipping on an Espressif device, a job that is genuinely hard on most embedded platforms is largely done for you.
What EN 18031 and the CRA Actually Require
The two regimes arrive at the SBOM from opposite directions, and it helps to see exactly how. The CRA (Regulation (EU) 2024/2847) is blunt: Annex I, Part II, point 1 requires manufacturers to document components "including by drawing up a software bill of materials in a commonly used and machine-readable format covering at the very least the top-level dependencies of the products."
That phrase deserves care, because it is easy to read too much into it. "At the very least the top-level dependencies" is the SBOM's explicit minimum depth, not a statement that top-level is enough. The same Annex I obligation is broader than the SBOM clause: you must document the components contained in the product and handle vulnerabilities in all integrated components (Recital 34). A transitive library compiled or statically linked into your firmware is an integrated component, so if a known exploitable flaw lives there, listing only the direct dependency does not discharge the duty. In practice you want the fullest reliable inventory your tooling produces, excluding build-only or unreachable dependencies with a documented reason rather than stopping at level one. On ESP32 that burden is largely lifted: as we will see, Espressif's tool already emits the full dependency graph, not just the direct dependencies.
EN 18031 never uses the word SBOM, but it lands you in the same place. Its requirement is GEC-1: up-to-date software and hardware with no publicly known exploitable vulnerabilities. You cannot demonstrate that a device carries no known vulnerability without first knowing what it is made of. The SBOM is not a deliverable the standard names; it is the practical prerequisite for the claim it does demand.
And both regimes converge on one word: exploitable. The CRA requires products placed on the market "without known exploitable vulnerabilities": one "that has the potential to be effectively used by an adversary under practical operational conditions." Not present. Exploitable. That distinction is what turns an intimidating raw CVE count into a short, defensible list, and, as we will see, it is exactly the line Espressif's tool already draws for you.
Generating the SBOM from a Real ESP32 Build

Espressif builds this into ESP-IDF. The esp-idf-sbom tool reached version 1.2.0 in June 2026 and is marked production-stable. Build your project first, then generate the inventory:
$ idf.py build$ idf.py sbom-createThat writes an NTIA-compliant SPDX file into build/. To scan it for known vulnerabilities:
$ idf.py sbom-check --spdx-file build/hello_world.spdxThe standalone CLI gives you more control, and is what you want in CI:
$ pip install esp-idf-sbom$ esp-idf-sbom create -o sbom.spdx build/project_description.json$ esp-idf-sbom check sbom.spdxThe critical detail is the input: **project_description.json comes from your actual build**. The inventory describes the firmware you are about to ship, not a static catalogue of everything the SDK contains, which removes most of the noise generic scanners generate.
It also goes deep, not just wide. The SBOM is not a flat list of top-level components: it describes an SPDX package for the project application, the ESP-IDF framework, the toolchain, every component pulled into the build, and the git submodules and subpackages beneath them, linked by SPDX relationships. That is the full dependency graph, exactly the deeper-than-top-level coverage the CRA's broader obligations point toward.
Choosing an output format
Pass --format to control the serialization. Pick CycloneDX if you want VEX statements travelling alongside the inventory:
$ esp-idf-sbom create --format cyclonedx-json -o sbom.cdx.json build/project_description.jsonSupported values are spdx-tag-value@2.2 (default), spdx-json@2.2, spdx-json-ld@3.0.1 and cyclonedx-json@1.6. In CI the check exits non-zero when it finds a vulnerability or an invalid manifest, so a failing build is your signal to stop and triage.

Hitting the public NVD API on every build is slow and rate-limited: sync a local mirror once, then scan offline:
$ esp-idf-sbom sync-db$ esp-idf-sbom check --local-db sbom.spdxThe mirror occupies roughly 900 MB, a fair trade for reproducible, offline-capable scans. Espressif also publishes an official GitHub Action, espressif/esp-idf-sbom-action, and runs a daily security dashboard over ESP-IDF releases as a useful independent cross-check.
What a Real Scan Looks Like
Espressif's docs walk through a project pinned to an outdated libexpat 2.6.0. Running the checker prints a report summary straight to the terminal:
Report summary┌───────────────────────────────────┬────────────────────────────────────────────────────────────────┐│ Date: │ 2024-11-13T14:20:23Z ││ Project name: │ project-expat_test ││ Project version: │ 6c23b9cdb268 ││ Vulnerability database: │ NATIONAL VULNERABILITY DATABASE REST API (https://nvd.nist.gov)││ Generated by tool: │ esp-idf-sbom (0.19.1) ││ Generated with command: │ esp-idf-sbom check sbom.spdx ││ Number of scanned packages: │ 32 │├───────────────────────────────────┼────────────────────────────────────────────────────────────────┤│ CRITICAL CVEs found: │ CVE-2024-45491, CVE-2024-45492 ││ Packages affect by CRITICAL CVEs: │ libexpat ││ Number of CRITICAL CVEs: │ 2 │├───────────────────────────────────┼────────────────────────────────────────────────────────────────┤│ HIGH CVEs found: │ CVE-2024-45490 ││ Packages affect by HIGH CVEs: │ libexpat ││ Number of HIGH CVEs: │ 1 │├───────────────────────────────────┼────────────────────────────────────────────────────────────────┤│ MEDIUM CVEs found: │ ││ Packages affect by MEDIUM CVEs: │ ││ Number of MEDIUM CVEs: │ 0 │├───────────────────────────────────┼────────────────────────────────────────────────────────────────┤│ LOW CVEs found: │ ││ Packages affect by LOW CVEs: │ ││ Number of LOW CVEs: │ 0 │├───────────────────────────────────┼────────────────────────────────────────────────────────────────┤│ UNKNOWN CVEs found: │ ││ Packages affect by UNKNOWN CVEs: │ ││ Number of UNKNOWN CVEs: │ 0 │├───────────────────────────────────┼────────────────────────────────────────────────────────────────┤│ All CVEs found: │ CVE-2024-45491, CVE-2024-45492, CVE-2024-45490 ││ All packages affect by CVEs: │ libexpat ││ Total number of CVEs: │ 3 │└───────────────────────────────────┴────────────────────────────────────────────────────────────────┘ Packages with Identified Vulnerabilities┌──────────┬─────────┬────────────────┬────────────┬───────────────┬───────────────────────────────────────────────────┐│ Package │ Version │ CVE ID │ Base Score │ Base Severity │ Information │├──────────┼─────────┼────────────────┼────────────┼───────────────┼───────────────────────────────────────────────────┤│ │ │ │ │ │ Status Analyzed ││ │ │ │ │ │ CVSS 3.1 ││ │ │ │ │ │ Vec. CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/... ││ libexpat │ 2.6.0 │ CVE-2024-45491 │ 9.8 │ CRITICAL │ CPE cpe:2.3:a:libexpat_project:libexpat:2... ││ │ │ │ │ │ Link https://nvd.nist.gov/vuln/detail/CVE-... ││ │ │ │ │ │ Desc. An issue was discovered in libexpat ││ │ │ │ │ │ before 2.6.3. dtdCopy in xmlparse.c... │├──────────┼─────────┼────────────────┼────────────┼───────────────┼───────────────────────────────────────────────────┤│ │ │ │ │ │ Status Analyzed ││ │ │ │ │ │ CVSS 3.1 ││ │ │ │ │ │ Vec. CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/... ││ libexpat │ 2.6.0 │ CVE-2024-45492 │ 9.8 │ CRITICAL │ CPE cpe:2.3:a:libexpat_project:libexpat:2... ││ │ │ │ │ │ Link https://nvd.nist.gov/vuln/detail/CVE-... ││ │ │ │ │ │ Desc. An issue was discovered in libexpat ││ │ │ │ │ │ before 2.6.3. nextScaffoldPart in... │├──────────┼─────────┼────────────────┼────────────┼───────────────┼───────────────────────────────────────────────────┤│ │ │ │ │ │ Status Analyzed ││ │ │ │ │ │ CVSS 3.1 ││ │ │ │ │ │ Vec. CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/... ││ libexpat │ 2.6.0 │ CVE-2024-45490 │ 7.5 │ HIGH │ CPE cpe:2.3:a:libexpat_project:libexpat:2... ││ │ │ │ │ │ Link https://nvd.nist.gov/vuln/detail/CVE-... ││ │ │ │ │ │ Desc. An issue was discovered in libexpat ││ │ │ │ │ │ before 2.6.3. xmlparse.c does not... │└──────────┴─────────┴────────────────┴────────────┴───────────────┴───────────────────────────────────────────────────┘ Newly identified vulnerabilities. Further analysis may be required for confirmation.Thirty-two packages scanned, three CVEs against one XML parser, matched on the component's platform identifier cpe:2.3:a:libexpat_project:libexpat:2.6.0. On a real product with dozens of components, an unfiltered scan is how teams end up staring at a hundred findings the week before launch, and this is where Espressif's tool behaves better than most.
The best feature is the vulnerability it refuses to report
From the tool's own docs: "only packages with direct or indirect relationship to the project package are examined." The example every ESP32 developer will recognise: if mbedtls is compiled through a dependency but not linked into the final binary, it appears in the SBOM but is not reachable from the root project, so it is recorded in the inventory yet not flagged as a vulnerability. That is exactly the line EN 18031's GEC-1 and the CRA's definition of exploitable both ask you to draw, and most general-purpose scanners do not.
A few flags let you move that line deliberately, depending on how wide a net you want:
--rem-unusedis the one you will reach for most. It drops the components that never reach the final binary from the SBOM entirely, so the inventory you ship describes only what is actually in the firmware. This is the flag that keeps your bill of materials honest and your scan focused.--check-all-packagesgoes the other way, widening the vulnerability check to every package in the inventory rather than only the reachable set. Useful when you deliberately want the full picture, not just the shipped one.--extended-scanalso searches the descriptions of CVEs that NVD has not finished analysing. Genuinely useful as an early warning, but too noisy to stand alone as your compliance evidence.
One Database Is Not Enough, and the Fix Is Already in Your SBOM

The honest limitation, stated plainly: the built-in checker queries the National Vulnerability Database only, matching by CPE, the identifier you saw in the report above. It does not query OSV, the GitHub Advisory Database, or OSS Index.
That matters more in 2026 than it did in 2023. NIST itself confirmed in April 2026: "Starting in early 2024, the NVD developed a significant backlog of unenriched CVEs... we have been unable to clear that backlog." A CVE that never receives CPE data cannot be matched by a CPE-based scanner, and this bites embedded work hardest, where libraries are often vendored into the source tree or pinned to a git commit with no registered CPE at all.
The good news: the tool already emits the identifier that solves this
Alongside every CPE, esp-idf-sbom emits a Package URL (PURL), for example pkg:github/madler/zlib@<version>. A PURL points at the upstream source repository, and that is the key that unlocks the databases CPE cannot reach. OSV (osv.dev) matches by git commit range, so it can catch a vulnerable copy of a C library vendored into your tree with no CPE whatsoever. The GitHub Advisory Database carries first-party maintainer advisories, sometimes ahead of NVD.
| Source | Matches by | Catches | Blind spot |
|---|---|---|---|
NVD (built into | CPE product identifier | Well-known components with a registered CPE and a clean version string | Unenriched CVEs; vendored code; forks under a different vendor name |
OSV | Git commit range, PURL, ecosystem package | Vendored and submoduled C/C++ with no CPE at all | Depends on advisory patch-link quality; needs git history |
GitHub Advisory Database | Ecosystem package, GHSA identifier | First-party maintainer advisories, sometimes ahead of NVD | No native C/C++ ecosystem; mirrors CVEs otherwise |
This is not a workaround: it is the intended use of a portable, machine-readable bill of materials. Generate once with Espressif's tool, then query several databases with the identifiers it already gave you.
From a Hundred Findings to a Short, Defensible List

A raw scan tells you which components contain a vulnerability. Neither regulation asks that question; both ask whether it is exploitable in the product you are shipping. Triaging that down is where the SBOM-to-CVE pipeline earns its keep. For each finding, ask three questions in order: is the vulnerable code compiled into this build? If so, is the affected feature enabled in your configuration? If so, is it reachable by an adversary under practical conditions? A flaw in a TLS 1.3 handshake path is not exploitable in firmware that never negotiates TLS 1.3.
Write the verdict down in a format a machine can read
A decision you cannot show is a decision you did not make. VEX, a Vulnerability Exploitability eXchange statement, records the outcome in a structured form. Every finding gets one of four statuses: not_affected, affected, fixed, or under_investigation. Where you claim not_affected, you must supply one of five standard justifications:
component_not_present: the component is not in the shipped product.vulnerable_code_not_present: the component is included, but the vulnerable code is not.vulnerable_code_not_in_execute_path: the vulnerable code is present but never executed.vulnerable_code_cannot_be_controlled_by_adversary: it executes, but an attacker cannot reach or influence it.inline_mitigations_already_exist: built-in protections prevent exploitation.
esp-idf-sbom emits not_affected VEX statements in its CycloneDX output and supports per-CVE exclusion lists with documented reasons in SPDX. So the justification you reasoned through in a design review travels with the bill of materials, machine-readable, ready for a market surveillance authority or downstream integrator. If you have filled an EN 18031 vulnerability table, these categories will look familiar: VEX is the same reasoning in a form a machine can consume.
Frequently Asked Questions
Does EN 18031 require me to produce an SBOM?
No. Software bill of materials and SBOM do not appear in EN 18031-1, -2 or -3. The standard requires GEC-1: no publicly known exploitable vulnerabilities. You cannot demonstrate that without an inventory of your components, so an SBOM is the natural way to produce the evidence: it just is not the thing being demanded.
Does the CRA require a complete dependency graph?
Not as a blanket rule, but "top-level is all you need" is the wrong takeaway. Annex I Part II(1) sets the SBOM's explicit minimum depth at "at the very least the top-level dependencies." Separately, though, the CRA obliges you to document the components contained in the product and to handle vulnerabilities in all integrated components (Recital 34). A transitive library bundled into your firmware is an integrated component, so if a known exploitable flaw lives there, listing only the direct dependency will not discharge your duty. The defensible position: put every direct dependency in the SBOM as the minimum, then inventory the shipped transitive components too, excluding build-only or unreachable ones with a documented reason.
Must the SBOM use SPDX or CycloneDX?
The CRA says only "a commonly used and machine-readable format." Article 13(24) lets the Commission specify a format by implementing act; none has been adopted. Both formats satisfy the requirement, and esp-idf-sbom emits both.
Do I have to publish my SBOM?
No. Under the CRA it belongs in your technical documentation (Annex VII), provided to a market surveillance authority on reasoned request. Making it available to users is explicitly optional.
The scanner flagged mbedtls but we never link it. Is that a bug?
Almost certainly not; by default it should not be flagged at all. esp-idf-sbom only checks packages reachable from the root project, so a component compiled through a dependency but never linked appears in the SBOM and is skipped by the vulnerability check. vulnerable_code_not_present is the VEX justification that records the finding honestly.
When do the CRA obligations actually apply?
It entered into force on 10 December 2024. Reporting obligations (Article 14) apply from 11 September 2026, and the main obligations, including the Annex I essential requirements, from 11 December 2027. EN 18031 under the RED Directive has been mandatory since 1 August 2025, so for most connected radio products this work is already in scope.
From Scan Results to Compliance Evidence
What Espressif's tool cannot do alone is see the CVEs NVD has not enriched, or the vendored library that never had a CPE. That is no reason for pessimism: the SBOM it produces already carries the PURLs those other databases need. Generate once, query broadly, judge honestly, and write the verdict down where a machine can read it. That is the engineering half. Turning those triaged findings into technical documentation that survives a market surveillance review is the compliance half, and it is where RedComply lives.
RedComply structures the whole EN 18031 assessment, including the vulnerability handling section GEC-1 drives. Import your findings, record each as remediated, mitigated, accepted, or not exploitable, and let the platform carry those justifications through to your test plan and Declaration of Conformity. The AI assistant reads your uploaded evidence (scan reports, advisories, design notes) and helps you write justifications that reference the actual document.
Structured vulnerability tables aligned with EN 18031's own remediation categories
A Knowledge Base that keeps scan reports and vendor advisories beside the assessment they support
Decision trees and test plans that inherit your vulnerability verdicts instead of asking for them twice
Declaration of Conformity generation from the documentation you have already completed
Run the scan with Espressif's tool. Bring the verdicts to RedComply and let the documentation build itself. Visit redcomply.com to see how the vulnerability handling section fits into a full EN 18031 assessment.
