Only what you built: which CVEs matter when part of a big component is compiled, and how to record "not affected, not built"
Why a firmware SBOM over-reports CVEs for big components such as ESP-IDF, Zephyr and Linux, how to check your build configuration, and how to record a "not affected" decision with evidence instead of a hunch.
By Declarable · Checked on 11 October 2026 · Guide version 1.2.0
You list "Linux 6.6" in your SBOM and a scan returns two hundred CVEs. Your image compiles a few dozen of the kernel's thousands of options. Most of those CVEs describe code that is not in your firmware. This guide is about telling the two groups apart, and writing down the difference so that you can stand behind it later.
It is not a way to make alerts go away. A "not affected" entry is a statement you make about your product, and someone may check it.
Why a component match over-reports
A vulnerability record names a software project and a version range, for example "Linux kernel before 6.6.30". A matcher compares that with the component in your SBOM. It sees the component name and version. It does not see which parts of the component you built.
Big components are made of many parts that you choose between at build time:
- Linux has drivers, file systems, network protocols and architecture code, each switched on or off in a kernel configuration.
- Zephyr has subsystems (networking, Bluetooth, USB, file systems) and modules, each controlled by Kconfig symbols.
- ESP-IDF is a set of components (Wi-Fi, Bluetooth, TLS, HTTP server and many more). Your project's
sdkconfigand the components it requires decide what is linked. - Yocto images are built from recipes, and many recipes have optional features chosen through
PACKAGECONFIG.
A CVE in the Bluetooth host stack matches "the Zephyr version you ship" whether or not your firmware has Bluetooth. The match is correct about the version. It says nothing about whether the vulnerable code is in your image. Our reading (argument): that is the gap between a match and a vulnerability in your product.
The opposite mistake is as bad. Treating every match as real buries the ones that matter. Treating every match as "probably not compiled" skips the ones that do.
How to tell from the build
The question for each match is narrow: is the code the CVE describes in the image you ship? Answer it from files your build produced, not from memory.
- Find the affected code. Read the advisory or the fix commit. Note the source file, the feature, or the configuration option it names. If the advisory names only a version, not a feature, you cannot rule the code out from the configuration, and the honest state is "still to decide".
- Find the switch that controls it. For Kconfig-based builds (Linux, Zephyr) this is a
CONFIG_...symbol. The file that decides it is the generated.configfor the build you shipped, not the defaults in the source tree. In ESP-IDF it issdkconfig, which uses the sameCONFIG_...style. In Yocto it is the recipe'sPACKAGECONFIGand the resulting configuration for that image. - Read the resolved value. In a
.configorsdkconfig, an option that is on readsCONFIG_NAME=y(or a value), and an option that is off appears as# CONFIG_NAME is not setor is absent. A symbol can be turned on by another symbol that selects it, so read the final file, not the line you edited. - Check the code really is out. A symbol that is off usually means the file is not compiled. Some code is built anyway because another option pulls it in, or because the symbol guards only part of the file. If you are unsure, look at the build output: the list of object files or the linker map shows what went in.
- Keep the evidence with the release. Save the
.config,sdkconfigor image manifest for that release. A decision tied to a file you no longer have is not evidence.
Two cases look similar and are not the same:
- The code is not compiled. The vulnerable source is not part of the image. The VEX justification for this is
code_not_present. - The code is compiled but cannot be reached. It is in the image, but nothing in your product can call it in the affected way. The justification for this is
code_not_reachable. It needs a much stronger argument, such as a call graph or a reachability analysis, because you are claiming something about how the product behaves, not about what was built.
If you are choosing between them for a configuration switch, code_not_present is the honest one only when the code is really absent from the binary.
What counts as evidence
Evidence is something another person can open and check:
- the configuration line, quoted with the file name and release (for example,
# CONFIG_BT is not setin the.configof release 2.4.0); - the build's own list of compiled sources, or the linker map showing the symbol is not there;
- a recipe's
PACKAGECONFIGresult for the image, from the build output.
These are not evidence:
- "We don't think we use it."
- "The feature is disabled in the product." (A feature can be hidden from users and still be compiled in.)
- "The upstream project says it is a niche driver."
The test we suggest (argument, not a legal rule): if a colleague or a market surveillance authority asked "how do you know?", could you hand them a file and a line number? If not, leave the entry as "under investigation" until you can.
Recording "not affected"
The standard way to say this is VEX, the Vulnerability Exploitability eXchange. CISA's "Minimum Requirements for Vulnerability Exploitability eXchange (VEX)" describes the status not_affected and expects a justification to go with it. CycloneDX's VEX has a justification field with values that include code_not_present and code_not_reachable. A well-formed entry holds four things:
- the vulnerability (the CVE or other identifier);
- the product and release it is about;
- the status,
not_affected, and the justification (code_not_present); - a plain sentence of detail, which is where the configuration line goes.
For example: "CVE-0000-0000 is in the Bluetooth host code. Release 2.4.0 builds with # CONFIG_BT is not set (file .config, kept with the release)."
Three habits make these entries hold up:
- Decide per release. A configuration change in release 2.5.0 that turns the option on makes your 2.4.0 decision no longer true for 2.5.0. Re-check on each release, and reopen any carried decision whose configuration option changed.
- Write the sentence for a stranger. It should stand alone, without your team's knowledge.
- Do not publish a guess. An entry you cannot support is worse than an open one, because it tells your customers something that may be false.
What this does not settle
Article 14 is a separate question. Triage decides whether a match is real for your product. Article 14 of the Cyber Resilience Act is a reporting duty: for an actively exploited vulnerability in a product you make available, you notify within set deadlines, and the first, the early warning, is due without undue delay, and at the latest 24 hours after you become aware. Recording "not affected, not built" is the outcome of your check, not a way around that clock. If an actively exploited vulnerability sits in a component you ship, decide quickly, and with evidence, whether your image includes the vulnerable code; if you cannot show that it does not, treat it as affected and follow the Article 14 guide. Our reading (argument): a rushed "we think it is not compiled" is the case where the lack of evidence hurts most.
Other places the code may be. A component can reach your image by more than one route. A vendored copy, a second library that bundles the same code, or a binary blob from a chip vendor may carry the same vulnerability under a different name. Your configuration answers for the copy it controls and for no other.
The legal weight is not fixed. The CRA asks you to identify and document vulnerabilities and components (Annex I, Part II(1)) and to address vulnerabilities without delay (Part II(2)). It does not prescribe VEX or any particular format, and says nothing about "not compiled" as a category. Our reading (argument): a documented, evidenced decision is the way to show that you did the identifying and documenting.
Where Declarable helps
Declarable writes a "not built" sentence next to a match in only two cases, and in both the sentence comes from your build, not from a guess:
- Zephyr. You upload the
.configof your build beside the SBOM. For a Zephyr subsystem that is its own component in the SBOM, and whose Kconfig symbol the.configshows is off, the match carries a sentence saying so. The.configis a separate upload; the SBOM alone does not tell us this. - Yocto. For a
kernel-module-*recipe, when the build's list of compiled sources has no file named after that module, the match carries a sentence saying the recipe appears not to have been compiled.
Declarable does not look inside a component. It does nothing for ESP-IDF, nothing for a plain Linux kernel component, and nothing for one file or one option inside a component. The two examples at the top of this guide are exactly those cases: a kernel CVE in a driver you did not build matches the kernel component, and a Bluetooth CVE with CONFIG_BT off matches Zephyr as a whole when Bluetooth is not its own component. For those, the check of your build is yours to do, as the steps above describe.
If the build says nothing about a component, Declarable says nothing about it either, which is not the same as "it was compiled". The decision, and the check behind it, stay with you.
What happens to the match. The first time a vulnerability matches a component, it waits for a person to decide it. Declarable closes nothing on its own. You can then mark the entry "not affected" with the justification "code not present", and the sentence from your build is kept as the note. The VEX file prepared from your decisions carries the status and the justification.
What happens on the next release. When you upload the next release, your decision and its note are carried forward onto the new release and marked "Check it still holds". The carried decision keeps its status, so a "not affected" arrives as "not affected". Declarable does not compare the new build's configuration with the old one. When a release changes the build configuration, for example by turning on the option that your decision depends on, reopen the carried decision and check it against the new .config or image manifest before you rely on it.
A short checklist
- Read what the CVE is about, not only the version range.
- Find the configuration switch that controls it.
- Read the resolved value in the
.config,sdkconfigor image manifest of the build you shipped. - Check the code is really absent, not only disabled at the top level.
- Record
not_affectedwithcode_not_presentand the configuration line in the detail. - Keep the file with the release, and re-check on the next release.
- If the vulnerability is being actively exploited, handle the Article 14 decision first.
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
- CISA, Minimum Requirements for Vulnerability Exploitability eXchange (VEX): https://www.cisa.gov/resources-tools/resources/minimum-requirements-vulnerability-exploitability-exchange-vex · checked
- CycloneDX, Vulnerability Exploitability eXchange (VEX) capability and the analysis justification values: https://cyclonedx.org/capabilities/vex/ · checked
- Zephyr documentation, Kconfig: configuration options and the generated .config: https://docs.zephyrproject.org/latest/build/kconfig/index.html · checked
- ESP-IDF documentation, Project Configuration (sdkconfig and Kconfig): https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/kconfig.html · checked
- Yocto Project documentation, PACKAGECONFIG (variable glossary): https://docs.yoctoproject.org/ref-manual/variables.html#term-PACKAGECONFIG · checked
- Yocto Project security manual, vulnerabilities and CVE_PRODUCT: https://docs.yoctoproject.org/security-manual/vulnerabilities.html · checked
- Linux kernel documentation, Kconfig language and the generated .config: https://docs.kernel.org/kbuild/kconfig-language.html · checked
- Regulation (EU) 2024/2847, Article 14(2)(a): early warning due without undue delay and in any event within 24 hours of becoming aware (EUR-Lex): https://eur-lex.europa.eu/eli/reg/2024/2847/oj/eng · checked
- Declarable code behind the Zephyr sentence: reads the uploaded .config, one component per subsystem (packages/engine/src/zephyr/hook.ts, subsystems.ts): https://github.com/Saddamdev0/declarable/blob/develop/packages/engine/src/zephyr/subsystems.ts · checked
- Declarable code behind the Yocto sentence: kernel-module recipes checked against the compiled sources (packages/engine/src/yocto/compiled-sources.ts): https://github.com/Saddamdev0/declarable/blob/develop/packages/engine/src/yocto/compiled-sources.ts · checked
- Declarable code behind carried decisions: status and note carried, "Check it still holds" reason, no configuration comparison (apps/web/lib/vuln/rescan.ts, carryOnto): https://github.com/Saddamdev0/declarable/blob/develop/apps/web/lib/vuln/rescan.ts · checked
Declarable prepares documentation for you to review. It is not legal advice and not certification. You sign your own declaration of conformity.