Sign in

CVEs & SBOM

A Zyxel NAS326 firmware image — 521AAZF4C0.bin, 59,389,563 bytes — unpacks to 6,789 files. The most serious fact about it is in none of them:

"firmware_version": "V5.21(AAZF.4)",
"firmware_cves":    ["CVE-2020-9054"],

That is the container header's own version string, matched by substring against a table of eight vendor firmware entries. CVE-2020-9054 is a pre-authentication OS command injection in weblogin.cgi, patched in V5.21(AAZF.7)C0; AAZF.4, .5 and .6 are vulnerable. No binary in the image had to be opened to establish it.

The 6,789 files answer a different question: which upstream components were bundled, at which versions, and which of those versions fall inside a published affected range. The mechanism throughout is version comparison, not code-shape analysis. Does the firmware's own HTTP daemon have a stack overflow? is Findings. Does the firmware ship an OpenSSL that someone else already found a bug in? is this.

QuestionAnswer
What is inventoriedUpstream components bundled in a binary or firmware image — name, version, the files evidencing each one, and the CVEs whose affected ranges contain that version
How versions are recoveredTwo producers. Five routes over an extracted firmware tree — path patterns, version banners, firmware header strings, package databases and lockfiles, web-library banners — plus a per-binary producer with four evidence kinds, the only route for a standalone binary
Where the CVE data comes fromA local index built from CVEProject cvelistV5, OSV.dev, CISA KEV and FIRST EPSS — never queried live at request time. The 24-hour refresh task is off unless an operator enables it
How a component reaches a CVERegistry identity goes straight to an OSV ecosystem key; a native library goes through a curated 78-row mapping table
What the posture isOver-report. The pipeline is advisory, not gating, and every finding carries the reason it fired
What it exportsA pinned openbinary.package-sbom-v1 document, rendered on demand to CycloneDX 1.6 or SPDX 3.0, and diffable against a vendor's claimed SBOM

The component inventory

Eighteen components came out of that image, seven of them carrying CVEs, eleven distinct CVE ids in total. All eighteen, with the five jQuery rows folded onto one line:

ComponentVersionEvidenceFilesCVEs
openssl1.0.2hbinary_version2CVE-2019-1559, CVE-2021-3711, CVE-2021-3712, CVE-2022-0778
busybox1.19.4binary_version4CVE-2018-1000517, CVE-2021-42378, CVE-2022-30065
openssh2.3.0binary_version2CVE-2020-14145, CVE-2021-41617
openssh6.7p1binary_version1CVE-2020-14145, CVE-2021-41617
zyshpath_pattern8CVE-2022-30525
firmware:CVE-2020-9054V5.21(AAZF.4)firmware_version1CVE-2020-9054
weblogin.cgipath_pattern1CVE-2020-9054
apache-httpdpath_pattern3250
cupspath_pattern41
sambapath_pattern10
bootstrap3.1.1web_lib_banner2
linux-kernel3.10.39binary_version2
jquery1.3.2, 1.5.1, 1.7.0, 1.10.2, 1.11.1web_lib_banner1 each
setuptools7.0package_manifest1

Every row has the same shape:

FieldWhat it holds
ComponentThe canonical component name, or a synthetic firmware:<CVE> name where the firmware-version route has no file to point at
VersionThe recovered version, or empty where the route asserts presence only
EvidenceWhich route produced the row: binary_version, path_pattern, firmware_version, web_lib_banner, package_manifest
FilesCount of matching extracted entries, not distinct installed copies
PathsUp to three example paths — usr/bin/openssl and usr/lib/libssl.so.1.0.0 for the OpenSSL row, usr/local/apache/cgi-bin/weblogin.cgi for the CGI
CVEsCurated CVE ids for that component, from the unpacker's own tables

Rows are ordered by CVE count descending, then file count, then name. firmware:CVE-2020-9054 is the synthetic case: the firmware-version layer has no file to point at, so the CVE it matched becomes the component name and the header string becomes the version.

The Files count and the 4 MiB scan window are both visible in this image's own rows. linux-kernel 3.10.39 reads 2 and quotes linux_kernel.bin twice. The banner scan runs over every extracted entry, executable or not, so one of the two files behind openssh 2.3.0 is not a program at all but the 149,946,368-byte decompressed rootfs image, which survives as its own entry and carries the string OpenSSH_2.3.0* at offset 2,582,571 — inside the window, since only the first 4 MiB of each entry gets string-scanned.

That same image carries the bytes OpenSSL 6 KB earlier at offset 2,576,873, well inside the window, and still produces no OpenSSL row: only printable runs that begin with a rule's prefix are tested, and that occurrence sits mid-run inside licence boilerplate. The OpenSSL row counts two real libraries and not three because of where the string sits in its run, not because the window ended.

Coverage counters

Counts at the end of the SBOM report how much of the image was actually opened, which is the only thing separating a partially covered image from a fully covered one:

"stats": {
  "files_extracted": 6789,
  "components_total": 18,
  "components_versioned": 13,
  "components_with_cves": 7,
  "components_vendor": 3,
  "unique_cve_ids": 11,
  "known_unknowns": {
    "cycle": 861, "no_extractor": 30, "depth_exceeded": 3,
    "encrypted": 0, "unsupported_compression": 0, "budget_truncated": 0,
    "total": 894, "bytes_skipped": 0
  }
}

The same counters read very differently on a second Zyxel image. V550ABOM8.1C0.bin — a VMG8825-T50K, less than half the NAS326's size — produces 17× the components and a worse coverage ratio:

CounterNAS326 521AAZF4C0.binVMG8825 V550ABOM8.1C0.bin
Image size, bytes59,389,56326,758,973
files_extracted6,7893,391
components_total18300
components_versioned13298
components_with_cves75
unique_cve_ids1110
known_unknowns.total8942,266

The 300 is not 300 answers. 286 of those rows are package_manifest rows with supplier opkg, read out of one OpenWRT package database, and opkg has no OSV ecosystem, so each falls back to name-based matching through the curated 78-row native table, where almost none of them appear; five rows carry a CVE. The 2,266 known unknowns against 3,391 extracted files is the other half of the same picture — two thirds as many recognised-and-not-descended entries as files actually written, against 894 of 6,789 for the NAS326.

Empty and absent CVE lists

An empty cves array in the SBOM means the curated tables inside the unpacker carried nothing for that component — the unpacker runs with no vulnerability database at all.

Component and version evidence

Two producers name components. Five routes run over an extracted firmware tree, with no vulnerability database loaded at all:

RouteKeys onYieldsCatalog size
Path patternsPath substrings in the extracted treePresence only6 substrings, 3 of them CVE-bearing
Version bannersA printable-run prefix per componentName and version7 rules over 6 components
Firmware versionThe version string in the container headerFirmware-level CVEs8 substrings
Package databases and lockfilesInstalled-package databases and language lockfilesExact (ecosystem, package, version)11 ecosystems
Web-library bannersBanner wording in .js, .mjs, .cjs and .css filesName and dotted version15 libraries

None of those five sees a standalone Mach-O, ELF or PE upload. A second producer runs inside a single binary — the only inventory route for an uploaded binary and for a curated package's members:

EvidenceKeys onYields
Install-nameMach-O LC_LOAD_DYLIB and its weak/lazy variantsLinked library name, version where the install-name carries one
Section bannerA string literal in a named section, quoted with its section and file offsetName and version. Two reserved section markers carry the registry routes: a Go binary's build-info graph yields full module paths, a Rust binary's crate graph yields bare crates.io names
SonameAn ELF DT_NEEDED entry matched against the curated Linux identity tableVerbatim soname plus canonical library name
Signature matchFunction-signature matches, Tier-1 high-confidence onlyName, the mined build's version, and the count of functions matched

Path patterns

Path patterns confirm a component exists and never say which version. Six substrings run against the extracted tree:

SubstringComponentCVEs
weblogin.cgiweblogin.cgi (Zyxel)CVE-2020-9054
/zyshzysh (Zyxel)CVE-2022-30525
/plutoipsec / pluto, supplier strongSwan / vendorCVE-2023-28771
/sambasambaPresence only
/apacheapache-httpdPresence only
/cupscupsPresence only

The key is coarse: /apache matched 3,250 paths in the NAS326 image — the entire web UI under usr/local/apache, not 3,250 copies of httpd. A patched build ships the same filename as an unpatched one, so a hit here is a prompt to look, not a conclusion.

Version banners

Version banners recover a version string from a file's own printable runs and compare it against a per-rule maximum. Seven rules cover six components:

ComponentRulesDetail
BusyBox1Maximum vulnerable version 1.34.0
OpenSSL1
dnsmasq1
OpenSSH1
Linux kernel1Version only, no CVEs attached
Dropbear2The banner is spelled Dropbear v in some builds and Dropbear_v in others

Each rule is a prefix plus a maximum vulnerable version. The scan walks the printable runs of every extracted file, executable or not, takes the first run beginning with the prefix, keeps the alphanumerics and dots that follow — a hyphen and everything after it is dropped — and requires the result to contain a dot and be at least three characters long. Comparison is dotted-numeric with a trailing release letter folded in as one more component, so 1.0.2h compares as (1, 0, 2, 8) and sorts below 1.0.2k at (1, 0, 2, 11).

Matching the banner is not the same as being vulnerable. A bill of materials is not a vulnerable-only list: BusyBox v1.35.0 — past the 1.34.0 cut — still emits a component row, with an empty CVE list.

Firmware version strings

Firmware version matching is the most decisive route and the coarsest key: a device running NAS326 V5.21(AAZF.4) has CVE-2020-9054 whatever any individual binary looks like. Eight substrings run against the version string in the container header:

SubstringDevice familyCVEs
AAZF.4, AAZF.5, AAZF.6NAS326CVE-2020-9054
7.3.245.300VMG8825CVE-2024-40891, CVE-2025-0890
V4.6, V4.70, V4.71, V4.72ZyWALL and USGCVE-2023-28771

The key is a substring test, not a range: nothing parses the version or orders it against a boundary, so V4.6 fires on a V4.65 as readily as on the V4.60–V4.69 line it was written for, and "up to but not including" cannot be expressed at all. The VMG8825 fixture's extracted stamp is 7.3.245.300_v007, where the 7.3.245.300 entry matches as a prefix and containment does work only on the trailing _v007. Every entry needs a published vendor advisory naming a version range, which is why this layer grows one vendor at a time.

Package databases and lockfiles

Package databases and lockfiles bypass curation entirely, because they already name the exact (ecosystem, package, version) triple the advisory feed keys on. Eleven ecosystems are parsed from the extracted tree: dpkg status, Alpine apk installed, opkg status, rpmdb.sqlite, and the language lockfiles for npm, PyPI, Maven, RubyGems, Packagist, Go and Cargo.

An Alpine row keeps its -rN revision, a dpkg row keeps its epoch, a pom.properties gives the Maven group-and-artifact coordinate the Maven feed indexes — so the row goes straight to the feed with no mapping table in between and no coverage ceiling. Which of the eleven actually reach a feed is in Supplier and ecosystem coverage below.

Web-library banners

Web-library banners read pinned JS/CSS distributions out of .js, .mjs, .cjs and .css files in firmware web UIs. The scan is not a header window — it covers the same first 4 MiB every other extracted file gets. The catalog has fifteen entries — jQuery, jQuery UI, Bootstrap, AngularJS, Vue, React, Lodash, Underscore, Moment, Handlebars, Backbone, Knockout, Ember (emitted under its npm name, ember-source), Mustache, Prototype — each keying on that library's own banner wording plus a three-part dotted version:

The NAS326 image reports five jQuery versions — 1.3.2, 1.5.1, 1.7.0, 1.10.2, 1.11.1 — plus a Bootstrap 3.1.1. Nothing in the firmware declares any of them; they are dropped files with no registry behind them, which is why their supplier reads cdn. Four of the five jQuery hits are real files named for the version they carry: jquery-1.3.2.min.js, jquery-1.5.1.min.js, jquery-1.10.2.min.js, and a bare jquery.js at 1.11.1.

Signature identity

Signature identity names a statically linked library that carries no banner at all, through function-signature matches. The version then reflects the build the signature was mined from, which can differ from the build actually linked, so this evidence ranks below anything read directly out of the bytes. See Fingerprinting.

Known false positives

Four failure shapes are measured in the two Zyxel images. The first two are the same shape: a string naming another program's version, read as this file's own. The last two are version comparison producing a CVE list nothing supports.

Phantom openssh 2.3.0 — a pattern table read as a banner. sbin/ssh-keygen in this image contains no self-identifying banner; its first OpenSSH_-prefixed printable run is an entry in OpenSSH's own bug-compatibility pattern table:

$ strings -a sbin/ssh-keygen | grep OpenSSH_ | head -3
OpenSSH-2.0*,OpenSSH-2.1*,OpenSSH_2.1*,OpenSSH_2.2*
OpenSSH_2.3.0*
OpenSSH_2.3.*

The token stops at *, 2.3.0 parses, and a component that was never installed appears next to the real openssh 6.7p1 recovered from sbin/ssh. First-match-wins on a printable-run scan has no way to prefer a real banner over a pattern string that a program carries in order to recognise other programs.

Phantom jquery 1.7.0 — a dependency line read as a component. The 1.7.0 row is not jQuery at all. It comes from jPlayer's playlist plugin, where the banner is a dependency line:

$ sed -n '10,16p' usr/local/dmsf/binary/resources/webbrowse/jPlayer/jplayer.playlist.min.js
 * Author: Mark J Panaghiston
 * Version: 2.3.0
 * Date: 20th April 2013
 *
 * Requires:
 *  - jQuery 1.7.0+
 *  - jPlayer 2.3.0+

The v is optional in the jQuery entry, so jQuery 1.7.0+ matches on the same terms as jQuery v1.7.0, and a stated requirement becomes a claimed component. The five entries that do require the v — AngularJS, Vue, React, Ember, Knockout — are narrower, and pay for it in banners that omit it.

CVE-2021-3711 on OpenSSL 1.0.2h — an upper bound with no floor. A banner rule carries an upper bound and no lower bound, so every CVE it lists attaches to every version at or below the cut. OpenSSL 1.0.2h therefore collects CVE-2021-3711, a buffer overflow in SM2 decryption — code that only exists in the 1.1.1 series. Three of the four CVEs on that row are correct for 1.0.2h; the fourth describes a function the binary does not contain.

openssl X9.42 — four CVEs against no version at all. The VMG8825 image emits this row, measured:

openssl  X9.42  binary_version  CVE-2019-1559, CVE-2021-3711, CVE-2021-3712, CVE-2022-0778

$ grep -ao 'OpenSSL X9.42[ -~]*' lib/libcrypto.so.1.1
OpenSSL X9.42 DH method

X9.42 is a Diffie-Hellman key-agreement standard, and the run is a method name inside the real OpenSSL library. X9.42 clears the emit gate because it contains a dot and is at least three characters, then clears the vulnerability gate for the opposite reason: the dotted-numeric parser reads X9, finds no leading digit, and returns an empty version — which compares below every rule maximum, so the rule's whole CVE list attaches. The same image's accurate OpenSSL row, 1.1.1n, sits past the 1.1.1k cut and carries none. The fabricated version is the one that scores, and nothing in it looks wrong: a component name, a version field, four real CVE ids.

Supplier and ecosystem coverage

A parsed component only becomes a finding if its supplier is one the rollup recognises: four OS-package suppliers and five language-registry suppliers, and nothing else.

SupplierParsed fromReaches the advisory feedIf not
Debiandpkg status, and a standalone .deb's controlYes — OSV ecosystem key, epoch preserved, keyed on the source package: a Source: field remaps libssl1.1 to openssl, which is what OSV keys Debian advisories on
Alpineapk installedYes — OSV ecosystem key, -rN revision preserved
UbuntuNothing — no parser ever emits an ubuntu supplierUnreachable, though the feed is ingested and the rollup would route to itA dpkg status is tagged Debian whatever distro it came from, so an Ubuntu-derived tree is matched against Debian advisories and the Ubuntu-specific ones are never consulted
opkg (OpenWRT)opkg statusNo — OpenWRT has no OSV ecosystemFalls back to name-based matching through the curated native table, keyed on the binary package name — the source remap is off here, since opkg advisories key on it
rpmrpmdb.sqliteNo — the distro/release qualifier the OSV rpm feeds require is not recoverable from an rpmdb aloneInventoried, never matched
npm, PyPI, Maven, RubyGems, PackagistLockfiles and manifests (pom.properties for the Maven coordinate)Yes — that registry's ecosystem key
Go, Cargo — from a firmware treeLockfilesNo — dropped by the firmware rollupInventoried, never matched
Go, Cargo — from a binaryThe binary's own embedded build graphYes — Go module path, crates.io name
cdnWeb-library banners in .js / .mjs / .cjs / .cssNo — a dropped file has no registry behind itInventoried, never matched
Native librarySoname, banner or signatureVia the curated 78-row tableNo table row and no registry produces no finding at all
AppleSystem frameworksNo — Apple publishes no mappable CVE/CPE applicabilityInventoried, excluded from matching

The vulnerability database

The vulnerability corpus is not bundled and is never queried live from an external service at request time. An operator fetches it once — about five minutes, dominated by the initial cvelistV5 git clone — and can enable an in-server task that refreshes it every 24 hours plus 0 to 10 minutes of jitter, single-flighted behind an on-disk lock, with a live-reload hook so a rebuilt index swaps in without a restart. That task is off by default: on a stock configuration the index is as fresh as the last hand-run ingest and no fresher.

Every source can be pointed at an internal mirror instead of its upstream, which is what makes an air-gapped deployment possible: a mirror base replaces the canonical host, with the per-ecosystem paths unchanged.

FeedWhat it suppliesTransport
CVEProject cvelistV5The CVE records — descriptions, CVSS metrics, CWE ids, CPE applicabilityShallow git clone; subsequent pulls diff only the changed CVE JSON files
OSV.devAffected-version ranges per ecosystem — Linux, Debian, Ubuntu, Alpine, Go, crates.io, npm, PyPI, Maven, RubyGems, Packagist, NuGet, PubOne zip per ecosystem, spooled to disk, never buffered in RAM
CISA KEVKnown-exploited catalog membership, due dates, ransomware associationJSON, atomic-rename on update
FIRST EPSSExploit-probability score and percentile per CVEDaily csv.gz

Every feed URL is refused unless it is HTTPS; setting OPENBINARY_INSECURE_VULNDB=1 logs a warning and accepts plain HTTP, for an internal mirror that is not TLS-terminated. Three of the four fetches are size-capped:

FetchCap
OSV ecosystem zip2 GiB
CISA KEV50 MiB
EPSS archive100 MiB compressed, 200 MiB decompressed
cvelistV5 git cloneNone — and it is the largest transfer of the four. Bounded only by a stall guard: git aborts a transfer that stays below 1 KB/s for 60 seconds

Those caps are load-bearing. The OSV cap was 500 MiB; Ubuntu's archive grew past it to roughly 600 MB, the fetch aborted mid-stream on every cycle, and Ubuntu CVE coverage silently froze — a size limit that fires looks exactly like an ecosystem with no new advisories.

That is the reason every response carrying vulnerability data also carries the database's age in hours and an ok flag. The flag goes false if the last refresh failed or the index is more than 168 hours old, so a consumer can tell "no findings" from "nothing has been checked in a week".

Component-to-CVE matching

The posture is over-report: the pipeline is advisory, not gating, so a false positive is dismissable and a false negative is a breach. Every finding carries the reason it fired, and that reason is a query filter — as are minimum severity and known-exploited-only, for an operator who wants the short list:

ConfidenceMeaning
range_matchedAn affected range explicitly contains the component version
exactThe version equals an enumerated affected version in the record
range_unparseableA range was present and failed to parse — surfaced for audit rather than dropped
floor_onlyThe version is not exact, so no range can be confirmed against it. Two weaknesses land here: a floor (>= V) from an ELF symbol-version requirement or a .note.ABI-tag minimum, where the real release is that or newer; and an ABI major — a bare-major soname like libssl.so.3, or a signature-mined build number reflecting the build the signature came from rather than the one linked — which is not a floor in any useful sense. Both demote identically: the record names the component, and the CVE surfaces unconfirmed rather than as a fabricated range hit
name_onlyThe component has no version at all; the match is on name

The two matching routes

Registry identity — direct. Anything with an exact registry identity — a distro package, a lockfile entry, a Go module path from an embedded build graph, a crates.io name from a Rust binary's dependency graph — goes directly to its ecosystem key.

Native libraries — a curated table. Native libraries have no registry, so they go through a curated 78-row table: canonical name, aliases, CPE (vendor, product) pairs, optional OSV ecosystem, PURL template, SPDX license id, and an upstream citation per row. The OpenSSL row is the shape of all of them — aliases libssl, libcrypto, libssl3, libcrypto3 so a soname read from a DT_NEEDED entry lands on the project name, and two CPE pairs, (openssl, openssl) and (openssl, openssl-fips), because NIST splits the vendor across both.

Version comparison respects the ecosystem's scheme: PEP 440 for PyPI, Maven's ordering for Maven, dotted-numeric for the rest, and a demotion to range_unparseable when the comparison cannot be modeled.

Finding fields

Findings are deduplicated by canonical CVE id — DEBIAN-CVE-2024-6197 and CVE-2024-6197 collapse into one row, and the canonical record's metadata wins, because distro mirrors usually omit CVSS, KEV and EPSS entirely.

FieldRule
IdAlways the canonical form, since a distro-prefixed id resolves to no NVD page
SeverityBucketed from CVSS: v3 preferred, v4 promoted when its base score is higher
VectorThe CVSS vector string
EPSSScore and percentile
KEVCatalog hit with its ransomware flag
ComponentsEvery component that matched
DescriptionCapped at 4,096 characters
CWEThe id, when the record has one
ConfidenceWhy it fired — the table above

Sort order is severity, then KEV, then EPSS, then id.

Distro-range suppression

When a canonical record and a distro mirror describe the same CVE, the distro range is dropped and only the canonical one is consulted. The OSV Debian feed reports curl as still-unfixed in Debian 11 for CVEs that upstream curl patched years ago: the range arrives as introduced=0 with no fixed event, which reads as "every version is affected". Apple ships libcurl 8.7.1, built from upstream. Matching that against the Debian range produced a confirmed false positive on every macOS binary linking libcurl:

index: CVE-2023-28320        (cvelist)  curl, fixed in 8.1.0
       DEBIAN-CVE-2023-28320 (osv)      curl, introduced 0, no fix
component: curl 8.7.1
result:    no finding

The suppression is per-CVE and does not cross CVE boundaries: an unrelated DEBIAN-CVE-… still emits, and a distro CVE with no canonical sibling still emits — silence there would be wrong, because there is nothing better to check against.

Two channels do the suppressing, because half the distro advisories do not carry a canonical id in their own name. DEBIAN-CVE-2023-28320 reduces to CVE-2023-28320 by stripping a prefix; a vendor advisory wrapper — DLA-, DSA-, RHSA-, USN-, SUSE-SU-, ALAS- — cannot, since DLA-2085-1 names no CVE and the link exists only in the record's own alias list. The second channel reads those aliases and drops the wrapper when a canonical alias on it is also a candidate against the same component. A wrapper whose canonical CVE is not a candidate survives under its own id.

The wire schema

One pinned schema, openbinary.package-sbom-v1, carries the component data: field additions inside v1 are non-breaking, and a change to what an existing field means bumps to v2. A component appears once per name-and-version pair however many files carry it — the OpenSSL row's count of 2 is the number of matching files, and at most three of their paths are quoted, with the rest left to the package's file listing.

Name-and-version is the whole key, so one installed component reached by two evidence routes becomes two rows whenever the routes spell its version differently, and nothing merges them. The VMG8825 image, measured:

busybox  1.20.1     binary_version     3 CVEs
busybox  1_20_1-5   package_manifest   0 CVEs
openssl  X9.42      binary_version     4 CVEs
openssl  1.1.1n     binary_version     0 CVEs
zysh     —          path_pattern       1 CVE
zysh     2.0-7      package_manifest   0 CVEs

Three installed components, six rows. In every pair the row carrying the CVEs and the row carrying the accurate version are different rows, and a consumer counting components counts each twice.

On the vulnerability-finding side of the wire, a reachability hint — whether the vulnerable function is reachable from this binary's entry points — has a fully defined type that nothing ever fills in, so the field is omitted from every response the engine emits today.

SBOM exports and vendor-SBOM diffing

The same component data renders on demand to CycloneDX 1.6 or SPDX 3.0 JSON-LD, deterministically: the same components at the same timestamp produce byte-equal output, and the CycloneDX serialNumber is a UUIDv5 derived from the binary's SHA-256, so re-rendering an SBOM does not produce a document that diffs against its predecessor. PURL, CPE and SPDX license id are materialized from the curated table at render time and never persisted on the component, so a correction to a mapping row fixes every future export at once.

The reverse direction is the supply-chain primitive the format converters do not have: a vendor's claimed SBOM — CycloneDX 1.4 through 1.6, or SPDX 2.x/3.0 — can be diffed against what was derived from the bytes, sorting every component into confirmed, version-mismatch, undeclared, or unverifiable. Undeclared is the interesting bucket: components in the shipped binary that the vendor's own bill of materials does not mention.

Downstream consumers

Six consumers read the same component and finding data off that schema:

Limits

The other three security surfaces run on the binary's own code; this one runs on the inventory of what the binary bundles. A firmware audit needs all four. A bundled OpenSSL with a published CVE, the vendor's own HTTP daemon with a stack overflow, a binary whose recovered capabilities include process injection, and a dropped payload that scores Malicious are four different problems, and knowing one of them says nothing about the other three.

See Findings for the code-shape CWE catalog, Indicators for capability tagging, Malware for the verdict composer, Fingerprinting for the function-signature identity layer, Platform for the fleet rollups, and Architectures for what the unpacker can and cannot open.