Skip to content

CVE correlation

Since 2.0, enodia can tell you which known vulnerabilities affect the exact version each target reports — alongside the patch/lifecycle/branch axes, not instead of them. It matches against two public databases:

  • БДУ ФСТЭК — FSTEC’s (Russia’s) vulnerability database, bdu.fstec.ru.
  • NIST NVD — the US National Vulnerability Database, nvd.nist.gov.

Either works alone; with both configured, their findings are merged per CVE.

Since 2.2, four vendors’ own security data joins them where they publish it: MariaDB, Atlassian (Jira, Confluence, Bitbucket, Bamboo), PostgreSQL and nginx — see Vendors’ own data.

Since 2.1, ten Linux distributions are also matched per installed package against their vendors’ own security data — the Debian Security Tracker, vendor OVAL files and Alpine’s secdb (see Package-level CVEs for Linux distributions).

It’s all opt-in: a config without a cve: block behaves exactly as 1.x did, and each source works on its own.

enodia matches against local files only. check, collect and serve never download anything — the same closed-network reasoning as the two-phase design: the machine running check doesn’t need internet access for CVE matching, only a copy of the files. Since 2.2, one separate command fetches them, and only when you run it: enodia cve update. Or download them yourself, as described per source below — the files are the same either way.

Terminal window
enodia cve update # every cve.*.path in the active config
enodia cve update --from inventory.jsonl # also what that inventory's hosts need
enodia cve update --dry-run # list what would be fetched, download nothing

Into each configured cve.*.path it fetches what that entry reads:

  • БДУ — vulxml.zip. cve.bdu.path has to be a .zip here; the .xml and .tar.gz forms the lookup also accepts are your own repackaging, which update doesn’t produce.
  • NVD — this year’s file, last year’s, and any year not on disk yet; --all-years refreshes every year (NVD rebuilds all yearly files daily). cve.nvd.path has to be a directory.
  • Debian — the tracker’s .json.
  • OVAL, Alpine secdb, PostgreSQL’s per-major pages — one file per release, so the releases come from three places: files already in the directory, the inventories given with --from (whatever releases its hosts run), and --oval, --alpine and --postgresql. cve.oval.path and cve.alpine.path have to be directories; a cve.postgresql.path that is a file gets the main page only. Since 2.2.1 an OVAL or Alpine directory with no release from any of the three is an error (exit status 1) — name one or drop the key, since check refuses an empty directory anyway.
  • MariaDB, Atlassian, nginx and PostgreSQL’s main page — one file each.
Flag Fetches
--from <inventory> the OVAL releases, Alpine branches and PostgreSQL majors that inventory’s hosts need (repeatable)
--oval <release> one OVAL release: ubuntu:<codename>, rhel:<N>, almalinux:<N>, oracle-linux:<N>, astra-linux:<X.Y>, redos:<X.Y> (repeatable)
--alpine <branch> one Alpine branch’s secdb, e.g. v3.22 (repeatable)
--postgresql <major> one PostgreSQL major’s own security page, e.g. 13 (repeatable)
--all-years every NVD year, not only this one, last one and missing ones
--dry-run list what would be fetched, download nothing

Each file is requested If-Modified-Since its copy on disk, downloaded into .enodia-update/ beside it, loaded by the same code the CVE lookup uses, and only then moved over the old copy — a truncated zip or an HTML error page never replaces a working file. An unchanged file costs one request (MariaDB, PostgreSQL and Atlassian send no Last-Modified, so those are downloaded again and compared). Network errors, 429 and 5xx are retried twice. One failure doesn’t stop the rest; the exit status is 1 if any file failed. Run it from cron — the next check or serve cycle picks the new files up.

TLS is verified against the system’s trusted roots, plus what a cve.update block adds:

enodia.yaml
cve:
update:
ca_file: /etc/enodia/russian-trusted.pem # added to the system roots: PEM (one or many) or DER
ca_dir: /etc/enodia/ca # every certificate file in it, likewise
tls_skip_verify: false # true: verify nothing, for every download

bdu.fstec.ru needs this: its chain ends at the Russian Trusted Root CA, which almost no trust store carries, and the server doesn’t send its intermediate (see БДУ ФСТЭК below). Without either, the БДУ download fails with certificate signed by unknown authority and the others still go through. The Root CA and the 2024 Sub CA are published at http://nuc-cdp.digital.gov.ru/cdp/rootca_ssl_rsa2022.crt and http://nuc-cdp.digital.gov.ru/cdp/subca_ssl_rsa2024.crt; a file with both concatenated works as ca_file.

It names the files the same way the manual commands below do (v3.22-main.json, 13.html, …), so the two can be mixed. The hosts it contacts are listed on the Privacy page.

One file, FSTEC’s full export (about 33 MB zipped):

Terminal window
curl -fL --cacert ru-chain.pem \
-o /var/lib/enodia/cve/bdu/vulxml.zip \
https://bdu.fstec.ru/files/documents/vulxml.zip

bdu.fstec.ru uses a certificate from Russia’s national CA (Минцифры), which isn’t in the usual system trust stores — a plain curl fails with a certificate error. The server also doesn’t send its intermediate certificate, and curl (unlike a browser) won’t fetch a missing one on its own, so installing only the root isn’t enough. Build a bundle from the root and the intermediate the site’s certificate names:

Terminal window
curl -fsS -o root.crt https://gu-st.ru/content/lending/russian_trusted_root_ca_pem.crt
curl -fsS -o sub.crt http://nuc-cdp.digital.gov.ru/cdp/subca_ssl_rsa2024.crt
{ cat root.crt; echo; cat sub.crt; } > ru-chain.pem

Checked live on 2026-09-23. If it stops working, the intermediate has most likely been rotated: the site certificate’s own Authority Information Access field names the current one (openssl s_client -connect bdu.fstec.ru:443 | openssl x509 -noout -ext authorityInfoAccess). curl -k also gets the file, but skips verifying what you’re about to feed into your security report.

One file per year, nvdcve-2.0-<year>.json.gz, 2002 through the current year. Put the ones you want in one directory:

Terminal window
mkdir -p /var/lib/enodia/cve/nvd && cd /var/lib/enodia/cve/nvd
for y in $(seq 2002 "$(date +%Y)"); do
curl -fsSLO "https://nvd.nist.gov/feeds/json/cve/2.0/nvdcve-2.0-$y.json.gz"
done

The current year’s file is updated daily; older years change rarely. Each file has a .meta sidecar (nvdcve-2.0-<year>.meta) with its size and sha256 — note the hash is of the uncompressed JSON, not the .gz.

One file, the tracker’s full JSON export (about 80 MB), for debian targets:

Terminal window
curl -fsSL -o /var/lib/enodia/cve/debian.json \
https://security-tracker.debian.org/tracker/data/json

.json.gz and .json.zip copies work too.

One file per distribution release in your fleet, all in one directory, for ubuntu, linuxmint, rhel, rocky-linux, almalinux, oracle-linux, astra-linux and redos targets:

Targets File
Ubuntu, Linux Mint (its Ubuntu base) https://security-metadata.canonical.com/oval/com.ubuntu.<codename>.usn.oval.xml.bz2
RHEL and Rocky Linux https://security.access.redhat.com/data/oval/v2/RHEL<N>/rhel-<N>.oval.xml.bz2
AlmaLinux https://security.almalinux.org/oval/org.almalinux.alsa-<N>.xml.bz2
Oracle Linux https://linux.oracle.com/security/oval/com.oracle.elsa-ol<N>.xml.bz2
Astra Linux SE 1.7, 1.8 https://dl.astralinux.ru/astra/oval/<1.7|1.8>_x86-64/oval-definitions-alse-<1.7|1.8>.xml
RED OS 7.3, 8.0 https://redos.red-soft.ru/support/secure/<7.3|8.0>/redos.xml
Terminal window
mkdir -p /var/lib/enodia/cve/oval && cd /var/lib/enodia/cve/oval
curl -fsSLO https://security-metadata.canonical.com/oval/com.ubuntu.noble.usn.oval.xml.bz2
curl -fsSLO https://security.access.redhat.com/data/oval/v2/RHEL9/rhel-9.oval.xml.bz2
curl -fsSL -o redos-8.0.xml https://redos.red-soft.ru/support/secure/8.0/redos.xml

Files are taken as published, .xml or .xml.bz2. Which release a file is for is read from its content, never from its name — so the two RED OS files, both published as redos.xml, only need distinct names on disk. Two files are refused on purpose, with an error naming the file to use instead:

  • Rocky Linux’s own OVAL (org.rockylinux.rlsa-<N>.xml) — it holds a small fraction of Rocky’s advisories and doesn’t pass OVAL schema validation. Rocky rebuilds Red Hat’s packages with the same versions, so Rocky hosts are matched against Red Hat’s file.
  • Ubuntu’s oci. variant — it checks the dpkg status file with regular expressions instead of packages.

All URLs checked live on 2026-10-02.

Two files per Alpine branch in your fleet, main and community, for alpine-linux targets. They share names across branches, so save them under distinct ones:

Terminal window
mkdir -p /var/lib/enodia/cve/alpine && cd /var/lib/enodia/cve/alpine
for b in v3.20 v3.22; do
for r in main community; do
curl -fsSL -o "$b-$r.json" "https://secdb.alpinelinux.org/$b/$r.json"
done
done

One file, for mariadb targets: MariaDB’s own page “Security Vulnerabilities (CVE) Fixed in MariaDB Community Server”, saved as is in its Markdown source (about 320 KB):

Terminal window
curl -fsSL -o /var/lib/enodia/cve/mariadb.md \
https://mariadb.com/docs/server/security/cve/community-server.md

Checked live on 2026-10-09.

One file, for jira, confluence, bitbucket and bamboo targets: Atlassian’s vulnerability transparency export, the JSON this URL returns, saved as is (about 2.3 MB, no login):

Terminal window
curl -fsSL -o /var/lib/enodia/cve/atlassian.json \
https://api.atlassian.com/vuln-transparency/v1/products

For postgresql targets: the project’s security page saved as HTML. It names only the majors supported today — for an older major, save its own page (/support/security/<major>/) into the same directory:

Terminal window
mkdir -p /var/lib/enodia/cve/postgresql && cd /var/lib/enodia/cve/postgresql
curl -fsSL -o security.html https://www.postgresql.org/support/security/
curl -fsSL -o 13.html https://www.postgresql.org/support/security/13/

One file, for nginx targets: the advisories page saved as HTML:

Terminal window
curl -fsSL -o /var/lib/enodia/cve/nginx.html \
https://nginx.org/en/security_advisories.html

All three URLs checked live on 2026-10-09.

A cve: block in enodia.yaml — not settings.yaml, since it changes the evaluation, not just the display:

enodia.yaml
schemaVersion: 1
cve:
bdu:
path: /var/lib/enodia/cve/bdu/vulxml.zip
nvd:
path: /var/lib/enodia/cve/nvd
debian:
path: /var/lib/enodia/cve/debian.json
oval:
path: /var/lib/enodia/cve/oval
alpine:
path: /var/lib/enodia/cve/alpine
mariadb:
path: /var/lib/enodia/cve/mariadb.md
atlassian:
path: /var/lib/enodia/cve/atlassian.json
postgresql:
path: /var/lib/enodia/cve/postgresql
nginx:
path: /var/lib/enodia/cve/nginx.html
targets:
- id: gitlab-main
product: gitlab
address: https://gitlab.example.com
credentials: gitlab-token
Field Accepts
cve.bdu.path a .xml, .zip (the export as published), or .tar.gz/.tgz
cve.nvd.path a single .json, .json.gz or .json.zip file, or a directory of them
cve.debian.path the tracker’s export: .json, .json.gz or .json.zip
cve.oval.path one OVAL file (.xml or .xml.bz2), or a directory of them
cve.alpine.path one secdb .json file, or a directory of them
cve.mariadb.path MariaDB’s community-server.md, saved as is
cve.atlassian.path Atlassian’s vuln-transparency JSON, saved as is
cve.postgresql.path PostgreSQL’s security page as HTML, or a directory of such pages
cve.nginx.path nginx’s security_advisories.html, saved as is
cve.update TLS options for enodia cve update only: ca_file, ca_dir, tls_skip_verify

Relative paths resolve against the directory of the config file that names them, the same as credentials_file. The block is read from whichever config the run actually uses — --config, $ENODIA_CONFIG, or the default search paths. That includes check --from inventory.jsonl: an inventory collected inside a closed network is correlated wherever check runs, as long as a config with a cve: block is found there. With no config located at all, check --from still works, just without CVEs.

A configured path that doesn’t exist is an error, not a silent skip — check exits with stat ...: no such file or directory rather than producing a report that quietly has no CVEs in it. Since 2.1, enodia config validate checks that every configured path exists too, so a typo shows up there first (since 2.2, cve.update.ca_file and ca_dir too). Whether a file actually parses is still only found out when a run loads it — or when enodia cve update downloads it.

БДУ and NVD are stream-parsed and the result is cached in the OS cache directory ($XDG_CACHE_HOME/enodia/cve, i.e. ~/.cache/enodia/cve by default on Linux; ~/Library/Caches/enodia/cve on macOS; %LocalAppData%\enodia\cve on Windows). The first run after a file changes parses it in full — about a minute for all of NVD plus БДУ; measured on 2026-09-23 with БДУ plus NVD’s 2026 file alone, 25 s. Every later run reads the cache: 0.2 s for the same data, an 11 MB cache. There is no TTL — the cache is keyed on the files themselves (size and modification time) and on enodia’s own product tables, so replacing a file, adding a year to the NVD directory, or upgrading enodia each trigger a rebuild on their own.

Parsed OVAL is cached the same way — about 11 s to parse Ubuntu noble’s, RHEL 9’s, AlmaLinux 9’s and Oracle Linux 9’s files together, most of it bzip2. The Debian tracker export (about a second to parse) and Alpine’s secdb (a few hundred KB) aren’t cached. With every source configured at once (БДУ, NVD, Debian, eight OVAL files, Alpine), upstream measured check at about 22 s cold and 3.4 s warm, peaking at 0.5–0.6 GB of memory — less if cve.oval.path holds only the releases you actually run.

enodia serve re-reads the cve: block and the files on every --interval cycle (cheaply, from the cache), so replacing the files from cron takes effect without restarting the server.

  • check — a CVES column in the compact and drift views: the number of distinct CVEs affecting that exact version. - means no findings — none affect that version, there’s no cve: block, or enodia doesn’t match the product (see below); the column itself is always there. lifecycle and fleet don’t carry the column.
  • export --format html — the same column with an info link opening a per-target list: one line per CVE, most severe first, with links to NVD, cve.org and, for БДУ findings, the bdu.fstec.ru page; БДУ’s Russian text when БДУ has the CVE, NVD’s English description otherwise; and the rating as colored badges, e.g. CRITICAL · CVSS 3.1 9.8. Package-level findings are one line per package instead — linux 6.12.107-1 → 6.12.111-1, linked to the advisory that carries the fix, with its CVE list folded underneath. It’s pure CSS — the default offline report still contains no JavaScript at all.
  • export --format json — every per-source finding in full under each assessment’s cves array: the source (bdu, nvd, or a vendor’s: mariadb, atlassian, postgresql, nginx), advisory ID, CVE IDs, title, the source’s own severity text, the matched product name or CPE, the version range, and a parsed CVSS rating. Unlike the table and the HTML list, which count one line per CVE, JSON keeps every source’s finding separately — the same CVE can appear once from БДУ and once per matching NVD CPE. Package-level findings (source debian, oval or alpine) also carry the installed and fixed versions — see Reporting.
  • export --format prometheus — no CVE data.

CVEs don’t affect severity or the exit code. SEVERITY is still computed from the patch/lifecycle/branch axes only, and --fail-on only knows those three axes too — a finding is a fact to look at, not a verdict enodia has made on your behalf. Whether and how a CVE should escalate severity is an open question upstream.

91 of the 123 products: 81 by product name against БДУ and NVD (six of them also against their vendor’s own data — see below), each vendor/product name checked verbatim against the real full exports, and 10 Linux distributions per installed package (see the next section). See each product’s own page under Product setup for its sources.

Not matched, each for a reason:

  • The other general-purpose Linux distributions (Fedora, CentOS Stream, Amazon Linux, openSUSE, …) — their CVEs are package vulnerabilities, a release number can’t say which packages have been patched since, and there’s no package-level source for them yet.
  • The BSDs and Oracle Solaris — NVD records their patch levels (FreeBSD’s -p5, OpenBSD errata) in a CPE field this matcher doesn’t read; matching on the release alone would flag a fully patched host with every CVE ever fixed in that release.
  • ESXi and vCenter — the same problem: almost all their entries are 7.0 + update_1-style literals.
  • TrueNAS — too few entries, versioned differently from what the probe reports.
  • No usable data in either source — Kitsu, Zou, postgres_exporter, Perforce Proxy, Perforce Helix Swarm, Supermicro BMC, LibreTranslate, TorrServer, and Euro-Office (a fork with no entries of its own).
  • PostHog — its NVD bounds are git commits, not versions.
  • generic — a hand-written parser has no product identity to look up.

Package-level CVEs for Linux distributions

Section titled “Package-level CVEs for Linux distributions”

A release number can’t tell which packages on a host have been patched since, so these ten distributions are matched per installed package instead. Their probes read the installed packages and the running kernel in the same SSH round trip as the version itself, and each package is checked against its distribution’s own security data:

Probe Source Key
debian Debian Security Tracker cve.debian.path
ubuntu Canonical OVAL cve.oval.path
linuxmint Canonical OVAL, for its Ubuntu base cve.oval.path
rhel, rocky-linux Red Hat OVAL cve.oval.path
almalinux AlmaLinux OVAL cve.oval.path
oracle-linux Oracle OVAL cve.oval.path
astra-linux Astra Linux OVAL (SE 1.7, 1.8) cve.oval.path
redos RED OS OVAL (7.3, 8.0) cve.oval.path
alpine-linux Alpine secdb cve.alpine.path

Only CVEs that already have a fix newer than what’s installed are reported — what an upgrade (and, for the kernel, a reboot) would close. CVEs the vendor hasn’t fixed yet are left out: they’re the same on every host of a release and nobody can act on them, so they’d bury the actionable ones.

One finding per package, not per CVE. A lagging kernel alone can carry over a thousand CVEs; a per-CVE list would be unreadable. Each finding names the package, its installed version, the version that closes every CVE in it, and the advisory carrying that fix (USN, RHSA, ALSA, ELSA, Astra bulletin, ROS, or the Debian/Alpine tracker page). The CVES column still counts CVEs, not packages.

Versions are compared by each package manager’s own rules — dpkg’s, rpm’s and apk’s ordering, checked upstream against apt_pkg, rpm and apk-tools on thousands of real version pairs each — plus AppStream module streams (a package is only matched against its own stream’s fixes), Oracle Linux’s architecture, FIPS and Ksplice variants, and the running kernel rather than whatever kernel packages happen to be installed. Every source was cross-checked upstream against oscap oval eval, dnf updateinfo, python3-apt or apk version -t on real hosts and containers, with identical results.

Proxmox VE gets package findings as a second target: an SSH debian target on the same host, next to its API proxmox one. Debian’s linux package is only matched against a running Debian kernel, so Proxmox’s own kernel isn’t mistaken for one.

GitLab, HashiCorp Vault, Nextcloud and MongoDB publish separate CVE lists for their community and enterprise editions. Their probes record the server’s own edition in extra.enterprise, and a community instance no longer sees enterprise-only findings — on real data, GitLab 19.2.2 CE sees 4 of NVD’s 9, Nextcloud 27.1.3 CE 11 of 23. When the edition is unknown (an older server that doesn’t report it), every finding is kept.

Since 2.2, a few more products’ versions say which line or edition a range applies to:

  • Jenkins — weekly (2.580) and LTS (2.568.3) releases get the same fix under different numbers, and both databases write a range for each. The version’s shape picks the line (two parts weekly, three LTS), so a fixed LTS is no longer flagged by the weekly bound of the same fix.
  • Splunk — only Splunk Enterprise ranges apply (splunkd’s own product_type says which); Splunk Cloud isn’t mapped.
  • pfSense — the probe reports Community Edition only, so pfSense Plus ranges never apply.
  • WAPT — the server’s own edition (community or enterprise) is passed through as is.
  • Kafka — a Confluent Platform build (7.6.1-ccs) gets no lookup: its own numbering would read as newer than every Apache Kafka bound.

iDRAC: both databases name each iDRAC generation as its own product, and their firmware numbers overlap (iDRAC7 and iDRAC8 both run 2.x, with different fixes). The generation is read from the probe’s extra.model, Redfish’s own model string: 11G is iDRAC6, 12G iDRAC7, 13G iDRAC8, 14G–16G iDRAC9, 17G iDRAC10. Without a model, only firmware 3.x and later is looked up — that can only be iDRAC9.

Synology DSM: a release is version, build and Update — Synology writes DSM 7.2.1-69057 Update 6, NVD and БДУ 7.2.1-69057-6. Since 2.2 the probe also records the Update in extra.update, and both sides are folded into one comparable version. An inventory collected before 2.2 has no extra.update and reads as Update 0: fixed Updates may be flagged, none are missed.

The ssh probe covers any SSH implementation, so it’s matched by banner: OpenSSH_… looks up OpenSSH, dropbear_… looks up Dropbear, and any other SSH stack gets no lookup rather than borrowing OpenSSH’s CVEs.

БДУ and NVD often describe a fix in one branch as an open-ended range (“before 11.4.10”), which then covers every older branch too — including fixed releases and branches that never had the bug. Four vendors publish the per-branch truth themselves, and enodia reads it next to БДУ and NVD with one rule on top: where the vendor’s data knows a CVE, its verdict wins — a БДУ or NVD finding whose CVEs the vendor covers and doesn’t flag for this version is dropped. CVEs the vendor doesn’t list still come from БДУ and NVD.

Key Products Source
cve.mariadb.path mariadb MariaDB’s table of fixed CVEs
cve.atlassian.path jira, confluence, bitbucket, bamboo Atlassian’s per-release vulnerability data
cve.postgresql.path postgresql PostgreSQL’s security pages
cve.nginx.path nginx nginx’s security advisories

Without these keys the products are still matched against БДУ and NVD alone — with the overlap problem above.

MariaDB maintains five or six release series at once. On real fleet versions, БДУ’s and NVD’s ranges flagged the latest, fully patched releases of maintained series (10.11.19, 11.4.13), while the same two databases missed 9 of the 21 CVEs MariaDB itself lists for 10.11.8.

cve.mariadb.path adds MariaDB’s own table of fixed CVEs, which names the fixing release per series. CVEs the table doesn’t list (newer than your downloaded copy, БДУ-only, or without a CVE id) still come from БДУ and NVD.

How the table is read:

  • A series with its own fix is vulnerable from its first release up to that fix.
  • A series with no fix of its own that was still maintained when the CVE was fixed elsewhere is unaffected — MariaDB fixes every live series together.
  • A series that had already ended by then is flagged for every release, with the lowest fix in a newer series as the release to move to (FixStatus says so). This errs toward reporting on purpose, and only for ended series.

Atlassian’s export lists every Jira Software, Jira Core, Confluence, Bitbucket and Bamboo release (Server and Data Center) with the CVEs it is affected by and the release that fixes each — third-party dependency CVEs included, which NVD’s Atlassian entries never list. The probe can’t tell Server from Data Center, so both lists are read. Jira Service Management numbers its releases on its own and isn’t mapped; release candidates and EAPs are skipped.

A target is judged within its own major.minor branch: from an affected release to the next one listed as fixing it, or to the branch’s end when no fix follows. Jira 10.3.26 isn’t flagged for a CVE Atlassian lists only for 10.1 and 11.3. Since Atlassian lists releases one by one, its verdict holds only for a release it lists — a release newer than your copy of the file keeps БДУ’s and NVD’s findings. Upstream measured the newest release of each maintained branch with no Atlassian findings, while older ones gain many: Jira 10.3.12 went from 4 CVEs to 119, almost all dependencies fixed in later 10.3 releases.

The security page names, for every CVE, the supported majors it affects and the fix in each. Its verdict covers only the majors the saved pages name: the main page lists only majors supported today, so for an ended major (13, 9.6) save its own page into the same directory — without it, that major keeps БДУ’s and NVD’s findings. A major that had already ended before a CVE came out is flagged with no fix when the CVE reaches back to the oldest major still supported then, as with MariaDB’s ended series. packaging rows (an installer or RPM build) are tracked but not flagged. Upstream measured the current 18/17/16/15/14 releases going from up to 55 БДУ findings each to none.

Each advisory lists the vulnerable versions and, per branch, the first fixed release (1.31.3+, 1.30.4+): stable 1.30.5 is no longer flagged by a range written up to the mainline fix. Branches that never got the fix stay flagged; advisories for nginx/Windows only are skipped.

  • БДУ can over-report across branches. One БДУ entry often lists a separate range per maintenance branch, all sharing one lower bound, so a version that is already the fix on its own branch can still fall inside a sibling branch’s wider range (Confluence 8.3.3 against CVE-2023-22515 is the documented example; Synology DSM’s per-branch ranges do the same — DSM 7.2.1-69057 Update 8 gets 5 БДУ findings). NVD’s ranges for the same CVE carry their own lower bounds and don’t have this problem. enodia deliberately errs toward reporting a finding to double-check rather than silently missing a real one.
  • NVD entries with no version constraint at all are dropped. Measured against the full exports, they were almost all decades-old CVEs attached to current releases; the cost is the rare genuinely-unfixed CVE recorded that way.
  • Package-level coverage has gaps of its own. The Debian tracker only covers releases Debian’s security team still supports (bookworm, trixie, testing, sid) — older hosts get no package findings. Alpine edge has no numbered branch and gets none either. OVAL isn’t evaluated as a full interpreter: package signing keys aren’t checked, so a third-party package with a distribution package’s name is compared as if it were the distribution’s. Astra Linux’s kernel packages are compared as installed, not as running.
  • NVD’s multi-product conditions (“vulnerable only with library Y”) aren’t evaluated — a probe reports one product per target, so each vulnerable entry for a matched product counts on its own.