← All posts
Tooling

Your Scanner Reads Manifests. Your Firmware Doesn't Have One.

Open-source scanners like Syft or Trivy do a great job with modern web stacks. Point them at a Node or Python project with a lockfile, and you’ll get a usable Software Bill of Materials (SBOM) in seconds. They’re reliable, well-maintained tools that usually give you everything you need.

Things get tricky when you run them against C or C++ firmware. The output isn’t wrong—it’s just thin. Most scanners rely on package manifests to prove what’s in a repo, and embedded codebases simply don't have them

That blind spot starts costing real money on September 11, 2026. That’s when Article 14 of the Cyber Resilience Act (CRA, Regulation (EU) 2024/2847) kicks in. From then on, manufacturers must report actively exploited vulnerabilities to ENISA (the EU Agency for Cybersecurity) and their national CSIRT on a strict clock: an early warning within 24 hours of becoming aware, a detailed notification within 72 hours, and a final report within 14 days of releasing a fix. The Commission’s own summary breaks down this reporting cascade best. Crucially, products already on the market count. The bottom line is simple: you can't report a vulnerability in a component you don't even know you're shipping.

Here are the three most common places those components go missing:

1. Vendored source with no manifest

A vendored dependency is a copy of upstream source sitting in third_party/, external/, or components/. There is no manifest entry for it. Sometimes the version is in a comment. Sometimes it's in a CHANGELOG nobody updated. Sometimes it is nowhere at all, and the only evidence is a version macro in a header.

A manifest-driven scanner looks for a manifest, finds none, and reports nothing. It is behaving correctly. The information it needs was never written down.

2. Static archives

A .a file is a container of ELF (Executable and Linkable Format) relocatable objects plus a symbol index. That's it. No version field, no license field, no package name. Nothing in the format carries provenance.

So if you link libimaging.a and that archive internally bundled zlib during its own build, your shipped binary contains zlib and no file in your repo says so. The failure runs the other direction too: the linker pulls only the objects it actually needs, so a dependency your build system declares may never reach the binary at all. Both errors matter to an auditor. One understates what you ship, the other overstates it.

3. Forked SDKs

ESP-IDF, STM32Cube, Zephyr, Yocto layers — teams fork them, pin them, and patch them locally. The version string still reads v4.4.2. What you're building is v4.4.2 plus however many local commits.

Look up a CVE (Common Vulnerabilities and Exposures) record against v4.4.2 and you get the upstream answer, which can be wrong in both directions. You may have already patched the issue locally. Or your fork diverged before an upstream fix you never pulled. Resolving this needs commit-level pinning, which is why we read git metadata directly rather than shelling out to gitcovered in an earlier post.

See it on your own repo

You don't need our tool to measure the gap. Three commands:

$ find . -type d \( -name third_party -o -name external -o -name vendor \) \
    -not -path '*/.git/*'
./third_party
./components/external

$ ar t build/lib/libimaging.a | head -4
jpeg_decode.o
png_read.o
zlib_inflate.o
tiff_dir.o

$ strings build/lib/libimaging.a | grep -iE '(zlib|libpng|libjpeg) [0-9]' | sort -u
zlib 1.2.11

Now compare that against your current SBOM. Every directory in the first result and every recognisable name in the third should appear in it. Count what doesn't.

What we do about it

bomwerk scan handles the lockfile ecosystems you’d expect—npm, PyPI, Go, Cargo, Maven, NuGet—and then keeps going where typical tools stop. It uses heuristics to fingerprint vendored trees through file layouts and version macros. It inspects binaries directly, searching static archives for embedded version strings and symbol patterns. And it leverages Git metadata to assign commit-level identity to forks.

Every component in the generated output comes with two crucial fields: where the evidence came from and our confidence level. Outputs are standard CycloneDX 1.6 or SPDX 3.0, pre-populated with the fields required by BSI TR-03183—the technical guideline from Germany's Federal Office for Information Security, and currently the most practical, field-level spec published.

Limitations, stated plainly

Heuristic identification isn't magic, and we won't pretend it is. If an archive is completely stripped—with no version strings and no recognizable symbols—it might be unidentifiable. When that happens, we flag it as an unknown component rather than making wild guesses. If a fork has no recoverable upstream lineage, we report it as exactly that.

This tool provides build-accurate visibility backed by transparent evidence and confidence levels—not magic 100% accuracy. Any security vendor telling you they have 100% binary identification accuracy is selling you snake oil.

As for environment support: the CLI runs natively on Linux and macOS today. A native Windows binary is on our immediate roadmap, though our .NET and C# scanners work right now regardless of which host OS you execute them on.

Scanning a C/C++ repo and not sure what your SBOM is missing? We'll scan one repo free and show you the diff against whatever you run today.

Get a free scan of one repo