Firmware SBOMs that actually match CVEs: identifiers, CPE versus purl, and where C libraries hide
Why a firmware SBOM full of file names finds no vulnerabilities, how CPE and Package URL identifiers work, and where the C libraries in Zephyr, Yocto and ESP-IDF builds come from.
By Declarable · Checked on 11 October 2026 · Guide version 1.3.0
The Cyber Resilience Act asks you to "identify and document vulnerabilities and components" in your product, including with a software bill of materials (Annex I, Part II(1)). Producing an SBOM is the easy half. The hard half is an SBOM whose entries a vulnerability database can recognise. A firmware SBOM that lists mbedtls with no version and no identifier will match nothing, and "no vulnerabilities found" from such a file means only that nothing could be looked up. This guide explains why, and what to fix.
Why identifiers matter
A vulnerability record says which software it affects. A matcher compares that statement with each component in your SBOM. If the two do not use the same name for the same software, there is no match, whatever the version.
Names alone are unreliable. The Yocto Project's security manual explains the problem plainly: by default its sbom-cve-check uses the recipe name (BPN) as the product name when querying the CVE database, which can produce false positives (CVEs for a different program with the same name) and false negatives (real CVEs missed). Its fix is the CVE_PRODUCT variable, which names the component's vendor and product exactly as the NIST database records them; the manual's example maps the flex recipe to flex_project:flex westes:flex so that it matches only the right "flex".
The lesson applies to every firmware build system: a component needs an identifier that a vulnerability source uses, not just a name.
The two identifiers you will meet
CPE
NIST describes the Common Platform Enumeration as "a standardized method of describing and identifying classes of applications, operating systems, and hardware devices". The current version is CPE 2.3. A CPE name has fixed fields, including part, vendor, product and version, written as cpe:2.3:a:arm:mbed_tls:3.5.2:*:*:*:*:*:*:*. The US National Vulnerability Database (NVD) states which CPEs each CVE affects, so a correct CPE is the most direct route to NVD matches.
The catch: you need the vendor and product string the NVD actually uses, and it is not always obvious. Arm's TLS library is arm:mbed_tls, not mbedtls.
Package URL (purl)
The purl specification describes a purl as a URL made of seven components, written scheme:type/namespace/name@version?qualifiers#subpath. The scheme is always pkg; the type names the ecosystem, such as npm, pypi, maven or github. Mbed TLS from GitHub is pkg:github/Mbed-TLS/mbedtls@V3.5.2.
The OSV vulnerability format uses purl too: an OSV record identifies the affected package by ecosystem and name and, optionally, a purl.
Which one?
Both, where you can. They serve different databases. The Zephyr project's own module metadata accepts both forms side by side, and its example for Mbed TLS lists one of each. BSI TR-03183-2, the German technical guideline on SBOM content (national guidance, not binding), asks for CPE or purl identifiers on a component where they exist, and its format mapping also allows SWID.
Where C libraries live, and why they are hard
Language ecosystems like npm or PyPI have a registry, and the registry name is the identity. Embedded C does not. The same library reaches your image by many routes, and each one loses identity in its own way.
Vendored source. A library copied into your tree keeps no record of where it came from or which upstream version it was. It needs a manually maintained identifier, or it will be invisible to every matcher.
Zephyr modules. Modules arrive through the west manifest, pinned to a revision. Zephyr's west spdx writes a separate modules-deps document for module dependencies, and a module can declare its CPE and purl in a security: external-references section of its zephyr/module.yml. In Zephyr 4.4, if a module does not declare them, the SBOM entry has a name but nothing to match; the development branch adds a purl built from the module's git remote and pinned revision.
Yocto recipes. The OpenEmbedded build system produces SPDX output through the create-spdx class, which is on by default from Yocto 4.3 (earlier releases need INHERIT += "create-spdx"). The image-level file depends on the release: Yocto 5.1 and later write IMAGE.rootfs.spdx.json, while Yocto 5.0 and 4.3 write IMAGE.rootfs.spdx.tar.zst. Matching quality depends on CVE_PRODUCT being right in each recipe.
ESP-IDF components. Espressif's esp-idf-sbom tool generates SPDX (2.2 by default, with SPDX 3.0 JSON-LD and CycloneDX available), reads SBOM manifest files in component directories, and uses CPE identifiers to check components against the NVD with idf.py sbom-check.
Commit-pinned forks. When you pin a library to a git commit rather than a release, there may be no version string to compare. OSV's GIT range type handles this: affected ranges are expressed as full git commit hashes and evaluated against the repository's commit graph. Most CPE-based matching cannot do that.
Binary blobs. Radio stacks, Bluetooth controllers and Wi-Fi drivers are often shipped by the chip vendor as precompiled libraries. No build tool can read their identity from the build; they need a supplier, name and version written down from the vendor's release notes, and the vendor's own security advisories become a vulnerability source of their own.
A checklist for SBOMs that match
- Every component has a version. Use the released version, or the commit hash when you track a branch.
- Every component you can map has a CPE and a purl. Check CPE vendor and product strings against the NVD dictionary, not against the project's display name.
- Vendored code is declared, with the upstream it came from and the version it was copied at.
- Blobs are listed by hand with supplier, name and version, and their vendor advisories are on your watch list.
- The SBOM is regenerated per release, from that release's build, and kept with it.
- Components without an identifier are visible. Count them. They are searched by name, the weakest kind of match, and the scan labels them "Inferred" or "Unknown". An SBOM where a third of the components have no identifier is a third checked only by name, and your reports should say "checked by name only" or "not checked", never "no vulnerabilities".
- Matches get a decision, recorded per release: affected, not affected (with a reason, such as "the vulnerable function is not compiled in"), or fixed.
Where Declarable helps
A software bill of materials and a vulnerability record often spell the same library differently. Your SBOM may say mbedtls; CVE-2024-28960 is filed under different vendor and product pairs across public records: arm:mbed_tls, trustedfirmware:mbed_tls and mbed:mbedtls. A lookup for the exact string your SBOM wrote down finds none of them.
On import, Declarable folds spelling differences — case, underscores, hyphens and spaces — and then looks the component up under each vendor and product pair our alias table records for that name, not only the spelling the SBOM carries. Mbed TLS is one entry in that table, alongside other libraries that turn up in embedded firmware and are spelled inconsistently across feeds.
The table has a floor, not a ceiling. A name it does not cover is still searched, under the spelling your SBOM wrote. What the table adds is quality: a hit found by name alone is graded lower than one found by identifier or by a mapped vendor and product, and it is yours to check by hand. The free scan grades a match in three steps. "Strong": the package URL, the CPE or the vendor and product agree, and your version is inside the range the record gives. "Likely": the name agrees and your version is inside the range, or a CPE agrees with its vendor left blank and your version is inside the range. "Possible": the identifier or the name agrees but your version could not be compared with the record, for example a commit hash or a vendor suffix. An identifier alone is only "Possible" because it is half the claim: it says which software this is, not whether your build is affected. "Possible" matches are listed on the result but left out of its totals, so read them; an exact identifier can be behind one.
The free scan also labels how each component was identified, in four words. "Stated": the SBOM itself gives the component a package URL or CPE. "Mapped": identified by one of our checked mapping rows, not by the SBOM. "Inferred": guessed from the name and version; check it before relying on it. "Unknown": a name with no version and no package URL or CPE, and no mapping row. It is still searched by name, and because there is no version to compare, a name match on it is at most "Possible". A component with a name and a version but no package URL, no CPE and no mapping row is "Inferred", not "Unknown". That is why checklist items 2 and 6 above still apply — check the CPE yourself, and count the components the scan labels "Inferred" or "Unknown", because a name-only match is the weakest kind. A match the alias table finds still only starts a lookup; the decision on each match, checklist item 7, stays with you. Import does not say your inventory is complete or that a match is correct, only that it looked under more than one spelling.
Why this is a CRA question, not just a tooling one
Annex I, Part I(2)(a) requires, on the basis of the cybersecurity risk assessment and where applicable, a product to be made available "without known exploitable vulnerabilities". Part II(1) asks for vulnerabilities to be identified and documented, not only components. Part II(2) asks you to remediate without delay. Our reading (argument): you can only know about vulnerabilities your SBOM lets you find, so all three depend on matching that works.
What is settled and what is not
Settled (evidence): the Annex I requirements quoted above; how CPE, purl and OSV identify software, per their own specifications.
Read in our own code on 11 October 2026 (evidence, true of that version only): a name the alias table does not cover is still searched under its own spelling; "Strong", "Likely" and "Possible" are the free scan's match grades, set in the engine's matching code (match.ts) from the identifier and whether the version could be compared; "Stated", "Mapped", "Inferred" and "Unknown" are its identity labels.
Not settled: the CRA does not prescribe an SBOM format or a required identifier, and no EU implementing act on SBOM format had been adopted by 8 October 2026. In our reading (argument), BSI TR-03183-2 is the most detailed reference; it is a national guideline and not binding.
Sources
- Regulation (EU) 2024/2847 (Cyber Resilience Act), Official Journal text on EUR-Lex: https://eur-lex.europa.eu/eli/reg/2024/2847/oj/eng · checked
- Yocto Project security manual, vulnerabilities and CVE_PRODUCT: https://docs.yoctoproject.org/security-manual/vulnerabilities.html · checked
- Yocto Project documentation, creating a Software Bill of Materials: https://docs.yoctoproject.org/dev/dev-manual/sbom.html · checked
- NIST, Common Platform Enumeration (CPE) specifications: https://csrc.nist.gov/projects/security-content-automation-protocol/specifications/cpe · checked
- Package URL (purl) specification: https://github.com/package-url/purl-spec · checked
- OSV schema (Open Source Vulnerability format): https://ossf.github.io/osv-schema/ · checked
- Zephyr documentation, modules: the security section of module.yml: https://docs.zephyrproject.org/latest/develop/modules.html · checked
- Zephyr documentation, west spdx (Software Bill of Materials generation): https://docs.zephyrproject.org/latest/develop/west/zephyr-cmds.html · checked
- ESP-IDF documentation, SBOM generation and vulnerability check (idf.py sbom): https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-guides/tools/idf-sbom.html · checked
- BSI TR-03183, Cyber Resilience Requirements (Part 2: SBOM, version 2.1.0): https://www.bsi.bund.de/dok/TR-03183 · checked
- Zephyr source, west spdx module handling on the development branch (scripts/pylib/zspdx/walker.py): https://github.com/zephyrproject-rtos/zephyr/blob/main/scripts/pylib/zspdx/walker.py · checked
- Zephyr discussion #106620, 31 Aug 2026 reply: a complete SPDX/CycloneDX file still trips scanners on vendor-name mismatches: https://github.com/zephyrproject-rtos/zephyr/discussions/106620 · checked
A free tool to try next:
- SBOM check: Fix a Zephyr, Yocto or ESP-IDF SBOM and see its known vulnerabilities
Declarable prepares documentation for you to review. It is not legal advice and not certification. You sign your own declaration of conformity.