The CRA for Zephyr developers: what the law asks, what west spdx gives you, and what is left
A plain-English guide for teams shipping Zephyr firmware into the EU: the Cyber Resilience Act dates, the SBOM it asks for, what west spdx produces and lacks, and a short list of what to do next.
By Declarable · Checked on 11 October 2026 · Guide version 1.3.1
If you ship a connected product built on Zephyr and sell it in the EU, the Cyber Resilience Act (CRA, Regulation (EU) 2024/2847) applies to your firmware. This guide covers what the law asks of a firmware team, what Zephyr's own west spdx command already produces, the gaps between the two, and a short list of what to do. It is a documentation aid, not legal advice.
The dates that matter
The CRA applies in stages. Its reporting duty under Article 14 has applied since 11 September 2026; the rest of the Regulation, including the essential requirements, the technical documentation, the EU declaration of conformity and the CE marking, applies from 11 December 2027, while the rules on notified bodies in Chapter IV have applied since 11 June 2026 (Article 71(2), EUR-Lex text checked 8 October 2026).
The reporting duty is not limited to new products. Article 69(3) says the Article 14 obligations apply to every in-scope product placed on the market before 11 December 2027 as well. So a Zephyr device you shipped last year is already covered by the reporting rules today.
What the CRA asks of firmware
The essential requirements are in Annex I. Part I covers the product's properties; Part II covers how you handle vulnerabilities. Our reading (argument): for a firmware team, five points carry most of the work.
- Know what is inside (Annex I, Part II(1)). You must identify and document the components and vulnerabilities in the product, "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".
- Ship without known exploitable vulnerabilities (Part I(2)(a)). On the basis of your cybersecurity risk assessment and where applicable, a product must be made available on the market without known exploitable vulnerabilities.
- Fix without delay (Part II(2)). Vulnerabilities are addressed and remediated without delay, in relation to the risk each poses to your product, including by security updates, and where technically feasible security updates are provided separately from functionality updates.
- Be updatable (Part I(2)(c)). Vulnerabilities must be fixable through security updates, where applicable automatic and enabled by default, with a clear and easy-to-use opt-out.
- Distribute updates securely (Part II(7)).
The Regulation defines an SBOM as "a formal record containing details and supply chain relationships of components included in the software elements" (Article 3(39)). It does not name a format. The German BSI's technical guideline TR-03183-2, version 2.1.0, is national guidance and not binding; in our reading (argument) it is the most detailed reference available today. It maps its required fields to SPDX and CycloneDX and raised its minimum versions to CycloneDX 1.6 and SPDX 3.0.1.
What west spdx gives you
Zephyr ships an SBOM generator. Per the Zephyr documentation, west spdx "generates a Software Bill of Materials (SBOM) for a Zephyr build as a set of SPDX documents", recording source files, build artifacts, the relationships between them, and the licence and copyright of every file.
The steps are short:
- Enable
CONFIG_BUILD_OUTPUT_METAin your project. - Build with
west build. - Run
west spdx -d BUILD_DIR.
It writes four documents to BUILD_DIR/spdx/:
- app: the application source files used in the build;
- zephyr: the Zephyr source files used;
- build: the built output files;
- modules-deps: a reference entry for each module in the workspace, whether or not your build used it.
SPDX 2.3 is the default. The current Zephyr documentation, which follows the development branch, lists 2.2, 2.3 and 3.0 as choices through --spdx-version, with 3.1 marked experimental; the latest release on 8 October 2026, Zephyr 4.4.2, offers only 2.2 and 2.3. --analyze-includes adds the header files actually included, and, together with it, --include-sdk adds an sdk document for the toolchain headers.
That is a real head start: a file-level, build-accurate record that most firmware stacks do not produce out of the box.
What it lacks
Identifiers that a vulnerability database can match. A vulnerability feed matches components by an identifier, usually a CPE name (the scheme the US National Vulnerability Database uses) or a Package URL (purl). The Zephyr documentation lets a module declare these in a security: external-references: section of its zephyr/module.yml, in CPE 2.3 or purl form; its own example is cpe:2.3:a:arm:mbed_tls:3.5.2:*:*:*:*:*:*:* and pkg:github/Mbed-TLS/mbedtls@V3.5.2. These references go into the modules-deps document. In Zephyr 4.4, a module that does not declare them, including your own and many vendor HALs, appears without a matchable identifier. The development branch also adds a purl built from the module's git remote and pinned revision; our reading (argument) is that this helps only where a vulnerability source tracks that repository. Code copied into the tree rather than pulled in as a module will not be named as a component at all.
A release number for each module. A vulnerability lookup needs a version as well as a name. Zephyr's own west.yml pins every module to a commit id and not to a release tag, so a module often reaches the SBOM with no release number at all. A commit id identifies the code but is not a release number: a scanner cannot compare it with "affected before 3.6.2". Adding a name-only identifier therefore still leaves an empty or meaningless result, which is what a maker reported in zephyrproject-rtos/zephyr#106620 (open, no accepted answer when we checked). To get a number, read it from the module's own version file, from the upstream tag that points at the commit you pin, or from a vendor SDK manifest that pins tags. Write the number down beside the module; do not guess it from a commit.
A name the vulnerability database uses. The name in the SBOM is often not the name the database uses. A reply in the same discussion notes that even complete files trip scanners on vendor names, such as mbedtls against trustedfirmware or arm. The Mbed TLS example above shows the same gap. Firmware SBOMs that actually match CVEs explains why and how to check it.
A format version that matches today's references. The default output is SPDX 2.3. BSI TR-03183-2 v2.1.0 asks for SPDX 3.0.1 or CycloneDX 1.6 or later for a new or updated SBOM. If a customer or authority follows that guideline, you will want to generate or convert to one of those.
Binary blobs. Radio stacks, Bluetooth controllers and modem firmware often arrive as precompiled libraries. west spdx records files the build used; it does not know who supplied a blob, which version it is, or which advisories cover it. Each blob needs its supplier, name and version recorded by hand, or from your vendor's release notes.
Vulnerabilities. An SBOM lists components. It does not say which of them have known vulnerabilities, which of those affect your configuration, or what you decided. Annex I Part II(1) asks you to identify vulnerabilities as well as components, so you need a matching step and a record of your triage decisions.
Everything outside the SBOM. Article 14 reporting, a coordinated vulnerability disclosure policy, the support period, the technical documentation and the declaration of conformity are not produced by any build tool.
What your SBOM does not show
west spdx "records the source files that went into the build", collected "from the CMake file-API" (Zephyr documentation). Three things firmware teams ask about sit at its edge.
Code that is not a Zephyr module. The build treats a project as a module if it has a zephyr/module.yml, or both zephyr/CMakeLists.txt and zephyr/Kconfig. West finds modules from your manifest, and EXTRA_ZEPHYR_MODULES adds modules from outside the west workspace (Zephyr modules documentation). A module added through EXTRA_ZEPHYR_MODULES is still a module, so it can carry security: external-references; check that it does. Code that reaches the build any other way, such as a library your own CMake fetches, a vendor SDK on a local path or a prebuilt library you link, is not a module. Our reading (argument): its files may appear in the build documents, but it is not named as a component with a supplier and a version, so no vulnerability feed will match it. Record supplier, name and version for each by hand. Modules can describe their binary blobs, with version and checksum, in the blobs section of module.yml; that is a good place to start the list.
Modules that were not built. The module list can be wider than the build. In the Zephyr source we read (scripts/pylib/zspdx/walker.py, development branch), the modules-deps document gets one package for every module in the workspace metadata, with a comment that it is emitted "whether or not its code is built"; nothing there filters by what was compiled. A maker raised this in zephyrproject-rtos/zephyr#108287 (30 April 2026, no accepted fix when we checked). So do not read the module list as a list of what your firmware contains. A match against a module your build did not use needs the same "not affected, not built" decision and evidence as any other: see Only what you built.
Code Kconfig switches off. Because west spdx lists the files the build used, source files in a subsystem your Kconfig disables are not in the file-level documents, though the module that holds them can still be listed in modules-deps. A vulnerability feed, though, matches a module and its version, not a file (argument), so a CVE in mbedTLS or in Zephyr itself can match even when the affected code is not built in your configuration. When you decide one does not affect you for that reason, record the decision and the reason, and keep each release's build/zephyr/.config with its SBOM as the evidence.
MCUboot or a vendor bootloader. With sysbuild, each image has its own build directory, such as build/mcuboot and build/<app>, and the west spdx documentation says to "make sure you target the actual application which you want to generate the SBOM for". So one run describes one image. Our reading (argument): run west spdx on the MCUboot build directory as well, with CONFIG_BUILD_OUTPUT_META enabled for that image, and keep both sets with the release. A bootloader the chip vendor supplies as a binary, or that you build in a separate tree, is in neither: list it beside the SBOM with supplier, name and version.
Where Declarable helps
When you upload a Zephyr build, Declarable writes the upstream CPE and purl onto each module it knows, so that a vulnerability lookup can match it. If you include a west.yml that pins release tags, as many vendor SDKs do, it reads the module's version from that tag. It does not invent a release number from a commit id: what it does with a commit depends on how it reads that module's version. For a module it reads by commit, it carries the first seven characters of the commit id, which name the exact code, and the components page marks that version inferred. That is not a release number, so a vulnerability lookup does not treat it as one, and it raises no note. For a module it reads by release tag, a commit gives no version at all, because a commit id is not a release, and the upload carries a note that the module is pinned to a commit or a branch and not to a release tag. In that second case, pin a release tag in west.yml, or set the version by hand on the components page. Either way, if you know which upstream release the commit matches, check the vulnerability matches for that module yourself. It does not remove modules your build did not use, so check the matches for those yourself. The decision on each match stays with you.
Does a kernel CVE start the 24-hour clock?
Not on its own, whether the CVE is in the Zephyr kernel or in a module. Article 14(1) requires a manufacturer to notify "any actively exploited vulnerability contained in the product with digital elements that it becomes aware of", and Article 14(2)(a) sets the early warning "without undue delay and in any event within 24 hours of the manufacturer becoming aware of it". The CRA defines an actively exploited vulnerability as "a vulnerability for which there is reliable evidence that a malicious actor has exploited it in a system without permission of the system owner" (Article 3(42)). A published CVE says a flaw exists; it says nothing about exploitation, so a new CVE against Zephyr or mbedTLS does not by itself start a clock. What starts it is becoming aware of reliable evidence that a vulnerability contained in your product has been exploited. Our reading (argument): that evidence does not have to come from your own devices, so a credible report of in-the-wild exploitation of a flaw in code your firmware builds is the moment to treat as the start; a CVE in code your Kconfig does not build is, in our reading, not contained in your product. The Regulation does not define "becoming aware", so write down when and how you learned of each exploitation report and what you decided. Separately, every vulnerability you do ship still has to be addressed and remediated without delay, in relation to the risk it poses to your product (Annex I, Part II(2)), exploited or not. Article 14, step by step walks the reports and their deadlines.
What to do now
- Turn on west spdx in CI for every release build and keep the four documents with the release artefacts. A release's SBOM should be the one generated from that release's build.
- Add
security: external-referencesto the module.yml of every module you control, and check that the upstream modules you depend on (mbedTLS, LittleFS, LVGL, TF-M and so on) carry them. Where they do not, keep a mapping of module to CPE or purl, and record a release number for each module, since a pinned commit is not one. - List your blobs with supplier, name and version, and keep that list beside the SBOM.
- Decide your output format. If you need SPDX 3.0.1 or CycloneDX 1.6, generate it or convert it, and validate the result.
- Match against vulnerability data on every release and on a schedule, and record a decision for each match: affected, not affected (with the reason), or fixed.
- Get ready for Article 14 now, because it already applies: know who in the company holds the ENISA reporting role and how you would hit a 24-hour early warning. See our guide "Article 14, step by step".
- Pick and publish a support period, at least five years unless the product is expected to be in use for less (Article 13(8)). See "What the support period means".
What is settled and what is not
Settled (evidence): the dates in Article 71, the Article 14 duties and their reach back to products already on the market, and the SBOM requirement in Annex I Part II(1).
Not settled (as of 8 October 2026): no harmonised standard for the CRA is cited in the Official Journal, and the Commission has not adopted an implementing act fixing an SBOM format. The BSI guideline is a national reference, not an EU rule. We update this guide when either changes.
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
- CRA Article 3, definitions (EUR-Lex): https://eur-lex.europa.eu/eli/reg/2024/2847/oj/eng#art_3 · checked
- CRA Article 69, transitional provisions (EUR-Lex): https://eur-lex.europa.eu/eli/reg/2024/2847/oj/eng#art_69 · checked
- CRA Article 71, entry into force and application (EUR-Lex): https://eur-lex.europa.eu/eli/reg/2024/2847/oj/eng#art_71 · checked
- CRA Article 13, obligations of manufacturers (EUR-Lex): https://eur-lex.europa.eu/eli/reg/2024/2847/oj/eng#art_13 · checked
- Zephyr documentation, west spdx (Software Bill of Materials generation): https://docs.zephyrproject.org/latest/develop/west/zephyr-cmds.html · checked
- Zephyr documentation, modules: the security section of module.yml: https://docs.zephyrproject.org/latest/develop/modules.html · checked
- BSI TR-03183, Cyber Resilience Requirements (Part 2: SBOM, version 2.1.0): https://www.bsi.bund.de/dok/TR-03183 · checked
- Package URL (purl) specification: https://github.com/package-url/purl-spec · checked
- NIST, Common Platform Enumeration (CPE) specifications: https://csrc.nist.gov/projects/security-content-automation-protocol/specifications/cpe · 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 4.4.2 source, SPDX versions west spdx supports (scripts/west_commands/zspdx/version.py): https://github.com/zephyrproject-rtos/zephyr/blob/v4.4.2/scripts/west_commands/zspdx/version.py · checked
- CRA Article 14(1) and (2), reporting obligations of manufacturers (EUR-Lex): https://eur-lex.europa.eu/eli/reg/2024/2847/oj/eng#art_14 · checked
- Zephyr documentation, sysbuild (one build directory per image, MCUboot): https://docs.zephyrproject.org/latest/build/sysbuild/index.html · checked
- zephyrproject-rtos/zephyr discussion #106620, west spdx output with no PackageVersion and an empty cve-bin-tool result: https://github.com/zephyrproject-rtos/zephyr/discussions/106620 · checked
- zephyrproject-rtos/zephyr discussion #108287, SBOM lists workspace modules that are not enabled or built: https://github.com/zephyrproject-rtos/zephyr/discussions/108287 · checked
Building on this platform? Places to go next:
- The Zephyr page
- 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.