Comments across the codebase credited one individual by role and, in places,
described that person's own machines: which SQL Server versions a host ran,
which devices were enrolled, what a particular dashboard showed, how many
findings sat open on which server. In a public repository that reads as a
profile of someone's unpatched estate.
The observations are why the code looks the way it does, so they stay. Every
CVE id, version, build number, count and date is preserved, as are the
verbatim quotes that motivated specific sort and filter rules — only the
attribution changes, to "field report", "observed", "a host". A local
variable in tests/test_autodesk_year.py was renamed for the same reason; its
value and every assertion around it are byte-identical.
PROJECT_OVERVIEW.md additionally loses a subtitle naming the kind of
organisation this was built for, and a support section pointing at an
internal team, both replaced with neutral wording.
Comments, docstrings and markdown prose only: 74 files, 200 lines, one-for-one
swaps. detect_changes reports 104 touched symbols and zero affected execution
flows, and all 55 test scripts pass. Nothing here needs re-testing.
Four separate defects made the same product invisible on the one host it
runs on. The tester's two reports are the visible tip of it.
1. GHSA index kept vulnerabilities[0] only. An advisory carries one block
per affected package, each with its own range. GHSA-mvh4-g699-984j
(CVE-2026-54085) lists wazuh-agent ">= 4.2.0" and wazuh-manager
"> 4.2.0" — only the agent was ever indexed, so the manager side was
never seen. 7 of wazuh/wazuh's 74 advisories are shaped this way.
2. The range parser understood only a "< x" upper bound, and read
patched_versions with a bare _vt(). Wazuh writes ">= 4.0.0,
<= 5.0.0-beta2" (CVE-2026-74039), ">= 4.14.4", "≥ 4.8.0",
"4.10.4, 4.14.5" and "from 3.5.0 to 4.3.10." — every one of those
returned False. A ">= x" range has no upper bound of its own and falls
back on the patched value, so an unreadable patched value silenced the
whole advisory: 24 of the 74 were invisible. A 4.14.1 manager goes from
23 findings to 39, with nothing lost.
3. cvelistV5 states the component too — CVE-2026-74039 is filed under
product "wazuh-manager" — but every wazuh-* package resolves to the one
key "wazuh", so the record landed on the agents. The only guard was a
prose heuristic, and that description names neither side. That is the
reported false positive: an agent carrying a manager-only API DoS.
4. All three scan paths dedup on (product key, version). A manager host
runs wazuh-manager AND wazuh-agent at the identical version, so
whichever syscollector listed first took the key and the other was
never scanned. With the agent first, the manager guard in _upsert then
dropped the manager-side CVEs as well.
Hypothesis that held: the component is stated fact in both feeds, and
every path was inferring it from prose instead of reading it.
Verified against the live wazuh/wazuh feed (74 advisories) and the
cvelistV5 record for CVE-2026-74039. GHSA index cache key bumped to v3 to
force a rebuild; the stale agent finding closes itself through the
existing app-scan reconcile on the next scan.
Two bugs, one visible symptom: a hypervisor patched during the day kept every
CVE its old build had, and the manual sync answered "another sync was already
running" forever.
1. The vSphere pass never reconciled. The app scan's auto-resolve sits behind
`if packages:` — a hypervisor runs no agent, has no package inventory, and
so never reached it. Nothing else ever revisited these findings: the new
version and build were written onto the asset (correct in the GUI) while
the findings from the old build stayed open, run after run. The reconcile
now lives in scan_asset_vmware, where the verdict is made, so both callers
(vCenter sync + nightly app scan) get it. Same contract as every other
reconcile: retract our own source, mark patched only once no scanner still
claims it, and close nothing at all when this pass had no verdict — no
build on the asset, no index, or a bound the catalog cannot resolve.
It commits itself: the nightly scan commits an asset only when the scan
FOUND something, and a host whose findings were all just closed found
nothing — its cleanup would have been rolled back and redone every night.
2. The advisory lock leaked across the connection pool. pg_try_advisory_lock
is SESSION scoped — it belongs to the postgres backend, i.e. to the pooled
connection — but both syncs took it on their ORM Session and then commit
once per host, and a committed Session hands its connection back to the
pool. Any concurrent request could take it, after which the sync ran on a
different backend: the closing pg_advisory_unlock found nothing, returned
false, and the lock stayed held for the life of that pooled connection.
Reproduced against postgres: run 1 released it by luck, run 2 unlocked the
wrong backend, run 3 was skipped, one lock held. From then on EVERY vCenter
sync — manual and nightly — was skipped, so nothing rescanned the hosts and
nothing could ever be closed. The lock now gets a connection of its own
(database.advisory_lock), and a failed unlock invalidates that connection
instead of returning it to the pool still locked. Intune/Defender had the
same pattern and the same fix.
Of the 23 CVEs in mfsa2026-74 (announced 18.08.) TrueVuln had found two on
20.08. Not a cache problem — the yml was fetched 19.08. 15:39 — the MFSA index
was only ever used to fill in severity and description, never to find a CVE.
Detection came from cvelistV5, which states Firefox INVERSELY: no affected
range, only one "unaffected" floor per maintained train (115.39 / 140.14 /
153.1 / 154). Nothing in the record marks which floor is the release train and
which are ESR, so _ranges_from_affected only inverts the single-floor shape —
anything else is skipped rather than flag a regular Firefox 121 for an ESR-only
advisory. CVE-2026-74975 and -74989 have one floor; the other 21 have two to
four and produced no range at all. Whether such a CVE was found then came down
to NVD having published a CPE for it, which is why two assets on the same
Firefox build reported different CVE sets.
Mozilla's own advisory says it in one line — fixed_in: [Firefox 154] — so the
MFSA index now decides the Firefox ranges and cvelistV5 keeps everything else:
- mozilla_advisory_service: derive ff_fix (mainline, non-ESR/iOS/Thunderbird)
and esr_fix per CVE; merge fixed_in across advisories instead of letting the
first file listed win, so the regular-vs-ESR verdict no longer depends on
alphabetical order; parse the indented " - Firefox 137.0.2" form (5 of 178
advisories, previously an empty fixed_in); never overwrite a good index with
an empty one and serve it stale when a rebuild fails.
- cvelistv5_scan_service: _merge_mozilla_mfsa replaces the Firefox entries for
every CVE Mozilla has ruled on, drops the ESR/iOS/Thunderbird-only ones, and
spares an install sitting at or above its own ESR train's fix. Index setting
bumped to v28 so the cache rebuilds.
Verified against all 178 announce/2025+2026 advisories and the live cvelistV5
records: all 23 mfsa2026-74 CVEs now resolve to "below 154", identically for
153.0.4 and 147.0.4. Regression test: tests/test_firefox_mfsa_source.py
The 15 Chrome CVEs from stable 151.0.7922.169 were committed to cvelistV5 on
2026-08-18 at 20:43 UTC but only surfaced in TrueVuln two nights later.
Confirmed cause: the nightly index build (03:20 UTC) shares
/tmp/truevuln-cvelistv5-cache.zip with the threat-intel refresh, whose
IntervalTrigger(24h) is anchored to app startup rather than the clock. On a
container started in the late afternoon that job re-downloads the ZIP around
17:00-20:00, so at 03:20 the file is only ~10h old — inside the 12h TTL. The
index build therefore never downloaded anything: it rebuilt, night after
night, from an afternoon snapshot taken before Google published. A simulation
of the real cron times and TTLs puts worst-case detection latency at 34.2h.
The matching logic was never at fault — the real CVE-2026-76044 record flags
150.0.7871.125 and 151.0.7922.139 and clears .169/.170. Only the index was
blind.
Nightly build now passes force_fresh=True, bypassing the TTL. On-demand
callers (manual scan, FP suppression) keep the 12h cache so the GUI never
triggers a 557 MB download. A forced refresh that fails falls back to the
on-disk ZIP instead of skipping the walk.
Worst-case latency drops 34.2h -> 23.8h, at a cost of up to 2 ZIP downloads
per day instead of ~1.4. No job timing changed.
A hypervisor runs no agent. Wazuh cannot reach it, Intune does not know it,
and Nessus only sees it if someone scoped a credentialed scan at it — so the
machines whose compromise takes every VM on them down were the ones with no
vulnerability coverage at all.
The connector reads the vCenter appliance and every ESXi host in its
inventory over the vSphere SOAP API (pyVmomi): hostname, management IP,
hardware model, product version and BUILD. One PropertyCollector pass, so a
500-host estate is one round trip. Read-only rights are enough.
The build is the point. NVD carries nothing usable — CVE-2026-47876 and
CVE-2026-59310 both sit there with no configuration — and the bounds
cvelistV5 does state are build identifiers, not versions:
ESX 8.0 lessThan "ESXi80U3k-25595708"
vCenter 8.0 lessThan "8.0 U3k"
_is_version rejects both, so _ranges_from_affected dropped the entries and
every vSphere CVE was invisible. They are now indexed verbatim and resolved
to build numbers: ESXi bounds carry one inline, vCenter bounds name a release
whose build comes from Broadcom KB 326316 (seeded in full, re-read weekly,
merged never replaced). A bound that resolves to no build produces no
verdict, and neither does a host with no build recorded.
Comparing builds alone is wrong in both directions. CVE-2025-22224 names two
fixes for the 8.0 line at once — U3d (24585383) for 8.0.3 and U2d (24585300)
for 8.0.2 — so the U2d host is patched despite the higher number existing.
And vCenter 8.0 U2f shipped four days AFTER the U3k fix on the older update
line, with a higher build, because Broadcom ships async patches there. So the
decision is scoped to the update line first, then still checked against the
build.
What must never resolve is asserted in the tests: the same advisories file
"VMware Cloud Foundation (vCenter Server)" and "vSphere Foundation" with the
SUITE's version numbers, and matching those would compare a vCenter 8.0.3
appliance against a VCF 5.x range.
EOL comes from endoflife.date (esxi / vcenter), keyed by major line — which
is also the honest granularity, one end-of-support date per line. 7.0 ended
2025-10-02, 6.7/6.5 in 2022. Both slugs are single-release, so an upgrade
retires the old finding. The OS patterns are shared with the CVE pass so a
machine cannot be a hypervisor for its CVEs and something else for its dates.
Checked against all 189 vSphere CVE records currently in cvelistV5: 64 bounds
indexed, none unresolvable, and every current release comes out clean while
each one behind gets its own line's fix named.
Migration 045 adds AssetSource.VCENTER, assets.vmware_uuid (the pin) and
assets.vmware_build.
Two Wazuh CVEs landed on every host running the agent, a Windows agent on
4.14.5 among them, and neither describes anything installed there.
CVE-2026-67308 is shell injection in Wazuh's GitHub Actions workflows —
the CI, not the product — bounded by the fixing commit. The commit-hash
guard already rejected that bound on the affected entry, but the record
states the same commit twice, and the "unaffected" half feeds the
inverse-record rule (Mozilla's "X and up are fixed"), which never checked
its bound. The hash walked back in there and "44" outranked every 4.x.
CVE-2026-67307 lets an enrolled agent spoof cluster attribution in
inventory-sync messages. Wazuh's own advisory names 5.0.0-beta1, fixed in
5.0.0-beta3; the CNA record says "0 → < 5.0.0-beta3", which read literally
covers everything ever shipped. A prerelease upper bound names the
prerelease train, so an open floor becomes that train's start: 4.x below
it and the finished 5.0.0 above it are out, the betas between stay in.
Both are auto-closed on the next app-scan run.
The python entry in both registries matched any name containing the word,
so PyPI/distro modules resolved as CPython: "python-dotenv 1.1.1" was
compared against CPython's ranges and flagged with every CVE up to 2.7.15
(CVE-2017-1000158, CVSS 9.8). Same for python-dateutil, python3-pip,
pythonnet.
The name now has to END at the interpreter, optionally with its release
("python3", "python3.11", "Python 3.13.7 (64-bit)"); a trailing "-" or
letter means a different product on a different version line.
Tomcat had no entry in either registry, so neither path ever looked at it:
resolve() returned None on the cvelistV5 side and there was no CPE row on the
NVD side. A Windows install ("Apache Tomcat 9.0 Tomcat9 (remove only)",
9.0.107) was invisible to both — every Tomcat CVE, on every host, had no
source at all.
The reference case is CVE-2026-34486, a regression in the fix for
CVE-2026-29146 that lets the EncryptInterceptor be bypassed. The ASF names
three EXACT versions there (11.0.20, 10.1.53, 9.0.116) and nothing else, which
_ranges_from_affected already reads as three closed [v, v] ranges — so 9.0.107
is correctly NOT flagged for it, while the older ranges it genuinely sits
inside (CVE-2026-29146: 9.0.0.M1 .. <=9.0.115) do match.
The floors are what keep the release lines apart. Every ASF record is stated
per line, and each floor is that line's first milestone, written either
"9.0.0.M1" or "11.0.0-M1" — _vtuple reads both as 9.0.0.1 / 11.0.0.1, so a 9.0
host can never fall inside the 11.0.18 bound. Vendor appears both as "Apache
Software Foundation" and as plain "Apache" (CVE-2020-1938), so both are paired.
Kept narrow: the name is anchored and the satellites are excluded. Tomcat
Connectors (mod_jk), Tomcat Native and TomEE are separate products with their
own version lines, and comparing an mod_jk 1.2.x against a 9.0.x range would
be nonsense.
EOL: the slug had to go ABOVE "apache" in the map. The resolver walks it in
insertion order and takes the first anchored hit, so "Apache Tomcat 9.0 …"
starts with "apache" and every Tomcat was being checked against the Apache
HTTP Server lifecycle — a different product on a 2.4.x version line that no
Tomcat build can ever match. endoflife.date keys the releases by line, which
the reported version prefixes directly: 9.0 is supported until 2027-03-31 (so
the host above gets no EOL finding), 8.5 died 2024-03-31, 8.0 in 2018, 7 in
2021.
Index key bumped to v26 so the cached index is rebuilt with the new product
instead of serving the old one for another 26 hours.
None of the three had a cvelistV5 registry entry, so resolve() returned
None and the path never looked at them — while NVD carries no CPE and no
affected block for any of the reported CVEs, so the CPE path could not see
them either. Fifteen CVEs on live hosts had no source at all.
Vim CVE-2026-73070/-73072/-73074/-73076/-73077/-73078 on 9.0.0969.
The bound is stated inside the version field ("< 9.2.0842"),
the operator form the range parser already reads.
VS Code CVE-2026-47285/-58650/-59113/-69278/-69306/-69320/-70335/-70336
on 1.132.0, all stated as 1.0.0 .. <1.132.1.
CPython CVE-2025-15366. The Windows installer reports an MSI build
(3.13.7150.0) while the real version sits in the name — read as
a build it is ABOVE every 3.13.x bound, so no range ever
matched. name_ver takes the version from the name, which is
what the CPE registry entry already does for Python.
Kept narrow so nothing new is over-matched: the names are anchored
(Neovim, Visual Studio Community 2022 and the Python launcher do not
resolve) and the pairs are exact, so Microsoft's VS Code EXTENSIONS —
their own products, with their own version lines — stay out.
Both paths get the products: the cvelistV5 entries cover what NVD has not
enriched, and the two new CPE registry rows cover the older records NVD
did enrich (every pre-2020 Vim record names vendor "n/a" and is not
matchable from the CNA block) and cross-confirm the fresh ones later.
Index key bumped to v25 so the cached index is rebuilt with the new
products instead of serving the old one for another 26 hours.
Two halves of the same blind spot, both on a stock AutoCAD LT 2026
(build 25.1.60.0):
MISSED. The cvelistV5 registry never carried year_ver, so that path
compared the MSI build 25.1.60.0 against "2026.0.0 lessThan 2026.1.2"
and matched nothing. It is the only path that can see these CVEs at
all — CVE-2026-7405, -7406 (ADSK-SA-2026-0012) and -17550 sit at NVD
with vulnStatus "Received" and zero cpeMatch entries, so the CPE path
is blind to them by construction. Four confirmed CVEs were invisible.
NEVER CLEARS. The year came from the product name and the build was
thrown away, so every 2026 install scanned as a bare "2026" — below
every 2026.x fix bound forever. A host patched to 2026.1.2 keeps all
seven of its autocad_lt findings with no version it could reach to
drop them. Three of those seven carry a bare-year CPE at NVD with no
range at all.
Both are fixed by resolving the build to the release Autodesk actually
bounds by: 25.1.60.0 -> 2026.0.0, 25.1.172.0 -> 2026.1.2. The table is
keyed by the build's major.minor, which has to agree with the year in
the name — that one check keeps it off products numbered some other
way, so Navisworks Freedom 2025 (22.0.1411.23) still falls back to the
bare year instead of landing on an AutoCAD row.
Hypothesis that held: the year was doing two jobs it cannot do at
once — pick the release line, and say where inside it the host sits.
The hash rule went into cvelistV5 only, so the same CVE could still arrive
through NVD after the other path had stopped producing it — the tester still
saw CVE-2026-67308 on a Wazuh agent, bounded by "before 44bf114d2f49" and
advising an upgrade to an upstream commit.
The rule now lives in the CPE scanner, where both paths use it, and applies to
every NVD bound as well as to the fix version. cvelistV5 delegates rather than
keeping its own copy, so the two cannot drift apart and let a CVE back in
through whichever is laxer. Pinned by a test asserting both agree.
Note for the tester's case: this particular CVE is about Wazuh's GitHub
Actions workflows, so the manager-only filter does NOT catch it — that one
keys on cluster and manager wording, and this text mentions neither. It is the
hash rule that removes it, on every host.
CVE-2026-67308 is a shell-injection flaw in Wazuh's GitHub Actions workflows —
their CI, not the product — and the record bounds it with the commit that
fixed it: "before 44bf114d2f49". Read as digits that becomes (44, 114, 2, 49),
which every installed 4.x sits below, so it landed on every Wazuh host as a
critical finding about a pull-request workflow nobody runs. The fix field then
advised upgrading to an upstream commit.
Bounds that are hashes are dropped where ranges are built, so an entry left
without a usable bound is skipped entirely rather than matching everything
below a number that was never a version.
The test for "is this a version" has to be narrow, because plenty of real ones
carry letters: a hash is hex, seven characters or more, with no separator.
Checkmk's 2.4.0p12 and 5.0.0-beta1 have separators; Teams' 25060212643 is all
digits. Only things like 44bf114d2f49 and deadbeef fail it.
Verified against the pair the tester reported: CVE-2026-67308 now yields no
range at all, while CVE-2026-25770 (3.9.0 up to 4.14.3, a genuine privilege
escalation) still matches his 4.14.1 and stops at 4.14.3. Index key to v24.
The scanner watches every other product on these hosts and was blind to the
one doing the watching. NVD carries 54 CVEs for Wazuh, several of them remote
code execution on the manager, and neither detection path resolved the name at
all — a Wazuh 4.14.5 server reported nothing about itself.
Two things had to line up.
The CPE names: NVD files 41 of the 54 under wazuh:wazuh, while the dashboard
and the older Kibana app have their own names and their own CVEs, so the
dashboard entry queries all three. cvelistV5 names the product per component
and states ranges as ">= 4.2.0, < 4.7.2".
The version: these ship as rpm/deb, so syscollector reports "4.14.5-1".
_clean_version rejects that outright — deliberately, to keep distro packages
out of the scanner — which is what made the components invisible even once the
names matched. A pkg_ver flag trims the packaging release suffix, leaving the
upstream version the advisories talk about. Both paths share
_effective_version, so both registries carry the flag; without it on the
cvelistV5 side that path still dropped the version.
Helper packages are excluded by name (wazuh-agent-config-helper and friends
are not the agent). Index key to v23; diagnose_scan gains a check for the
trimmed version, so the container can be verified directly.
"Veeam Explorer for Microsoft Teams" 13.3.2.3 collected four Teams CVEs whose
bounds are 25122.x — Veeam's own software, matched purely because its name
contains "Microsoft Teams". Checking the rest of the registry against the same
shape found four more: Chrome findings on "Citrix Workspace app for Google
Chrome", Firefox findings on "Kaspersky Plugin for Mozilla Firefox", and so on.
Backup tools, plugins and connectors are all named this way.
The product being described stands FIRST, so a match that lands only in the
"…for <product>" tail is dropped. Matching itself still runs on the full name,
which matters: the Firefox entry rejects "Firefox for iOS" itself (WebKit, not
Gecko, different CVEs), and trimming the name before matching hid that from it
— caught by test_mobile_browsers.
Teams is anchored to the start of the name as well, so a product that merely
mentions it later cannot match even without a "for". Both scanner paths share
the rule, so they cannot disagree about the same install.
Adobe writes CVE-2026-48294 as "lessThanOrEqual 26.5.2.2", so no fixed build
exists to show and the finding read "not announced" — which hid something the
tester rightly pointed out twice: if 26.5.2.2 and older are affected, anything
above it is not. That is a valid statement, and an admin can act on "newer
than 26.5.2.2" even when the vendor named no build.
Stored as ">26.5.2.2" and rendered as "not announced — need newer than
26.5.2.2". The prefix carries both facts at once, which is why it is not
written as a bare version: fixed_version also drives the "patch available"
badge, and lessThanOrEqual means the CNA named no patched release, often
because none has shipped. has_fix reads the prefix and does not claim one
exists. A later record with a real lessThan replaces the floor.
No schema change: the existing column carries it, and nothing compares
fixed_version numerically (only equality, in has_fix).
CVE-2026-21710 lists Node 20.20.1, 22.22.1, 24.14.0 and 25.8.1 as concrete
entries and then adds "4.0 lessThan 4.*" and "5.0 lessThan 5.*" for the
ancient lines. The wildcard was carried through as a fix target, so a host on
24.13.1 was advised to upgrade to "4.*" — a release line, not a build anyone
can install.
It sticks, too: a fixed_version is filled in once and never overwritten
(vuln_override_service), so a wrong one outlives the finding. Empty is
recoverable, wrong is not — so wildcards are dropped at the source, and the
ones already written are cleared on the next scan and counted as
fix_versions_cleared.
why_match on the tester's host reported 0 matching NVD rules for
CVE-2025-49731, yet the finding kept reappearing — so it was not the CPE path
producing it, it was cvelistV5, which had no shape check.
The record names three builds: 1.0.0.2025112902 (Android), 7.10.1 (iOS) and
the MSRC build 25060212643. A host on the current 26183.1903.4892.4448
compares below every one of them, so a fully patched machine kept a Teams CVE
open. The MSRC scanner already refused to compare across build shapes (that is
what the "Teams build shapes are not compared" check covers); this path did
not, and it re-created the finding on every scan, which is why it survived the
reconcile — a CVE detected this run is never stale.
Same rule as Adobe, so the strict list is now a named set rather than a prefix
test on one key. The old Teams numbering (1.6.00.4472) shares its shape with
the mobile bounds and still compares normally, so genuinely outdated classic
clients are unaffected.
Index key bumped to v22; diagnose_scan gains a check for this exact host.
Android reports the app by its store id, org.mozilla.firefox. The cvelistV5
registry matched "mozilla firefox" with a space, so that path missed Firefox
on every Android device while the CPE path saw it fine — the two sources
disagreed on the same install. Anchored to the start so it stays a package id
match rather than a substring hit; Chrome already had com.android.chrome.
Browser extensions now carry "(User)" the way Windows ARP marks a per-user
install ("Microsoft Visual Studio Code (User)"). An extension lives in the
browser profile, so it is updated or removed per user and will not be found in
Programs and Features — worth saying in the finding itself.
Three findings from the tester, all reproducible.
EMPTY INVENTORY CLOSED EVERYTHING
The auto-resolve rule was documented as "only safe when we had a real
inventory this run (packages non-empty)", but the code only ever checked the
asset status. A Wazuh agent that re-registers under a NEW id keeps its old
id's syscollector data until it rescans, so /syscollector/<new-id>/packages
answers 200 with an empty list — a live, healthy asset reporting no software.
Every app-scan-only finding on it then counted as "no longer detected" and was
closed, only to be reopened by the next scan that saw the inventory again.
That is the flapping behind several of the "was closed, now open again"
reports.
An asset that HAS an inventory source and returns nothing from it is a fetch
we cannot trust, not a host running no software. Its findings are now left
alone and the skip is logged and counted (resolve_skipped_no_inventory). The
asset matching itself was fine — the sync falls back to the hostname and
carries the new agent id over.
ASSETS WITH NOTIFICATIONS COULD NOT BE DELETED
notification_logs and ai_analyses referenced vulnerabilities with no ON DELETE
rule, so deleting an asset failed with a ForeignKeyViolation once anything had
been mailed about one of its findings — which is why some assets deleted fine
and others returned 500. Migration 040 gives each the rule that fits: the
notification log keeps its row with the reference cleared (it records that a
mail WAS sent, which stays true), the AI analysis goes with the finding it
describes.
DELL RVTOOLS AND VEEAM ONE
Both are cvelistV5-only — DSA-2026-325 (CVE-2026-64993) and the six CVEs in
Veeam KB4892 are all still awaiting NVD enrichment, so the CPE path is blind
to them. Veeam's advisory covers the SERVER only ("Veeam ONE 13.0.2.6723 and
all earlier version 13 builds", fixed in 13.1.0.7034), so the name pattern
excludes the client and console — otherwise every workstation with the console
installed would be flagged.
Dell states its bound as "4.8.1 or later", which is prose, not a version. The
comparison survived it by accident (only digits are read) but the sentence was
carried through and shown as the fixed version, so _clean_bound trims exactly
that shape and leaves anything else verbatim.
Index key bumped to v21.
Adobe files three different things under the same acrobat / acrobat_reader
CPEs, and nothing in the record separates them except how the version is
written:
desktop Acrobat/Reader 26.001.21771 3 parts
Acrobat Reader T5 in Edge 127.0.2651.105 4 parts, Chromium numbering
browser extension 26.7.1.0 4 parts, its own numbering
CVE-2024-41879 says "Acrobat Reader versions 127.0.2651.105 and earlier" and
carries cpe:microsoft:edge as its second configuration — it is the PDF engine
built into Edge, not the desktop application. Compared as plain numbers
26 < 127, so it was reported on every desktop Acrobat AND on the extension.
CVE-2024-20721, -20709 and -39379 (bound 120.0.2210.91) did the same, as did
the CVE-2026-479xx block, which is why the extension work looked like it had
regressed: those findings were never extension CVEs to begin with.
A bound only counts now when it is written in the same shape as the install:
same number of parts, and the leading number on the same side of 100 (which
is what separates Chromium's three digits from the extension's two). Both
sides must agree, otherwise the bound describes a different product and the
two are not comparable — every caller already drops a match it cannot compare.
Scoped to Adobe on both paths (NVD reads it off the CPE criteria, cvelistV5
off the curated key), because part counts differ legitimately elsewhere:
"8.6" against "8.6.1" must keep matching.
The genuine extension finding CVE-2026-48294 (up to 26.5.2.2) is unaffected,
and so are real desktop CVEs — both pinned in tests/test_adobe_product_split.py.
The July-2026 Node releases have no NVD configuration at all — CVE-2026-56846
and -56848 sit there with no CPE — so the CPE path is structurally blind to
them and a host on 24.13.1 was reported clean. cvelistV5 carries both records
in full, but the scanner had no registry entry for Node, so it never looked.
The bound logic was already right (both were in diagnose_scan since the
inclusive-bound fix); nothing ever fed it.
vendor "nodejs" / product "node", one entry per release line with
version == lessThanOrEqual, so each line keeps its major as the floor and
"up to 24.18.0" cannot swallow an already-patched 22.x host.
Index key bumped to v20 (new entry → rebuild). diagnose_scan gains four checks:
both Adobe schemes, Autodesk years untouched, and Node reaching this path.
A record whose `version` equals its upper bound was treated the same way
regardless of WHICH bound it is, and the two mean different things.
version == lessThan is a zero-width, impossible range. Chrome emits it, NVD
reads it as an open floor, and that is right: a Chrome CVE fixed in
151.0.7922.72 does affect 150.x. Unchanged.
version == lessThanOrEqual is inclusive, so the entry names ONE release line.
Node states CVE-2026-56846 as two entries — 24.18.0 and 22.23.1 — and its
description says "affects Node.js 24.x and 22.x". Opening the floor made "up
to 24.18.0" swallow everything older: a host on 22.23.2, already patched and
covered by the OTHER entry, matched through the 24.x range, and so did 20.x,
which is not affected at all. The major version is now kept as the floor,
confining each entry to its own line.
This also closes the false negative that surfaced it: Node 24.13.1 was
reported unaffected because the entry was read as the single version 24.18.0
rather than the line it describes.
Index key bumped to v19 — cached ranges carry the old floors.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
App-scan findings showed only the title we generate — "Adobe Acrobat (64-bit)
26.001.21691 — CVE-2026-48294" — which says what MATCHED, not what the
vulnerability is. The CNA's own text was sitting unused in both sources. It is
now taken from the cvelistV5 record and from NVD's descriptions block,
whichever path found the CVE, and backfilled onto existing rows that have none
(fill-only, so a source with better text keeps the last word).
Second: iOS browser builds were resolving to the desktop CPEs.
org.mozilla.ios.Firefox matched mozilla:firefox, which is wrong for the same
reason iOS Chrome was already excluded — Apple mandates WebKit there, so the
Gecko and Blink CVEs behind those ranges do not apply to it. Android is
unaffected by this: org.mozilla.firefox and com.android.chrome share engine,
version numbers and fixes with desktop, and NVD keeps them under one CPE with
a neutral target_sw, so they stay mapped as before. Verified: no separate
firefox_for_android CPE exists (0 CVEs).
Firefox ESR keeps its existing exclusion — NVD tracks it as its own
firefox_esr CPE.
Index key bumped to v18; cached entries carry no description.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
DWG TrueView and Navisworks Freedom were blind spots — viewers, installed far
more widely than the authoring tools and just as exposed, since both parse
untrusted CAD files (CVE-2025-1658/-1659/-1660 are DWFX parsing bugs).
cvelistV5 is not optional for Autodesk: the three CVEs from ADSK-SA-2026-0009
(CVE-2026-16463, -16465, -17550) carry no NVD data at all, so the CPE path
cannot see them at any point. Both paths are wired up for all four products.
Two version notations occur and both compare correctly as numbers: older
records say "2026 .. <2026.1", newer ones "2027.0.0 .. <2027.1.0". AutoCAD and
AutoCAD LT are listed separately in the records but carry IDENTICAL ranges,
verified across CVE-2025-5046, -8894 and CVE-2026-17550; kept apart in case
they ever diverge.
Language packs are the trap here. They ship as separate inventory entries with
the SAME version as the application — "AutoCAD LT 2022 Language Pack" at
24.1.51.0, and Navisworks Freedom 2025 has twelve of them across Deutsch,
Italiano, Français, Português, Español, Korean and more. Each would have
collected the full CVE set of its product, exactly like Adobe's dictionary
pack. Excluded, along with Material Library and Single Sign On Component.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Illustrator, Photoshop, InDesign and Bridge were blind spots — nothing looked
at them at all, on software that ships dozens of code-execution CVEs per
patch day (APSB26-42, -51, -40, -32, -39, -89).
Both sources carry them well, so both paths are wired up. cvelistV5 names two
of the four with a "Desktop" suffix ("Photoshop Desktop", "InDesign Desktop")
and two without ("Illustrator", "Bridge"), which is not a pattern — each name
is listed. Verified against CVE-2026-34661, -27289, -27283 and -34630.
The inventory names carry a YEAR the CVE records never mention ("Adobe
Illustrator 2026" at version 30.1), so the year is ignored rather than parsed
— the version field is what gets compared. Two InDesign generations can sit on
one host and both resolve to the same product, each judged on its own version.
Anchored, and the helper components that ship alongside are excluded:
AdobeNotificationClient, Adobe Refresh Manager and AdobeAcrobatDCCoreApp are
not the products and carry unrelated version numbers — the same trap the
dictionary pack sprang last commit.
Index key bumped to v16.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
VMware Tools is the clearest case for running both sources. NVD enriched
CVE-2025-41244 with a CPE and marked CVE-2025-41246, -41239, -22247 and
-22230 "NOT SCHEDULED" — the CPE path can never see those four, on software
that sits on every virtualised Windows endpoint. cvelistV5 has all of them,
under three spellings: VMware/Tools, VMware/VMware tools, and n/a/VMware
Tools. All three are mapped; both paths are wired up.
Adding the product alone would not have worked, because VMware writes its
affected versions as placeholders — "13.x.x.x", and "12.x.x, 11.x.x" naming
TWO release lines in a single field. Parsed as digits the second becomes
(12, 11), which is not a version: a 12.4 host compares BELOW that floor and
drops out of its own range, and so does an 11.3 host. Both would have been
reported unaffected while the record says the opposite.
_wildcard_floor now reads the concrete part in front of each `x` and takes the
lowest — the floor of the whole statement. Versions without a wildcard are
untouched, so nothing else changes. This is general, not VMware-specific;
any CNA writing "9.x" gets read correctly now.
Index key bumped to v15.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Adobe entries added in 2dc60be and 3e6c15c matched the product name
anywhere in the string, so they also matched every add-on that mentions it:
"Asian Language And Spelling Dictionaries Support For Adobe Acrobat Reader",
language packs, font packs. Those ship their own version numbers that look
exactly like old Reader builds — the dictionary pack is 23.008.20421 — so each
one collected the full set of Reader CVEs. The tester saw CVE-2026-48373
listed eight times on one host, and not once for the Reader itself.
The name must now START with the product, which drops the "Support For …"
add-ons, plus an explicit exclusion for the packs that do lead with it
(language pack, dictionaries, spelling, font pack). Same pattern as the
Exchange language packs and the Teams add-in — a vendor putting its product
name inside an accessory's name is the recurring shape here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two findings from the same report.
Adobe had no cvelistV5 entry at all — only the CPE path, which meant current
versions depended entirely on NVD having published a CPE yet. cvelistV5 names
the product "Adobe" / "Acrobat Reader": no DC suffix, and no Reader-vs-Acrobat
split, because Adobe stopped shipping them apart — APSB26-63 covers both and
links the same release notes for either. NVD meanwhile keeps the older _dc
spellings alive in parallel, which is why 2dc60be made the CPE path query both
names. Verified against CVE-2026-47965 / -47911 / -47961: affected up to and
including 26.001.21651, and the bound resolves correctly against that build.
Wazuh only detects ancient Reader builds (wazuh/wazuh#29960), so these two
paths are the entire coverage for current versions.
Separately, Defender findings flapped open and closed within one sync. The
auto-resolve ran per MACHINE, but several Defender machines can map to one
asset — a re-imaged or dual-registered device keeps its old machine entry. The
machine that no longer lists a CVE closed the finding; the one that still
lists it reopened it a minute later; next sync the same again (tester:
CVE-2026-66313, patched 13:41, open 13:42, patched 15:00). The CVE sets are
now unioned per asset and resolved once, after every machine has been asked.
Index key bumped to v14.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A complete blind spot: Wazuh does not detect it (wazuh/wazuh#36200), so an
Exchange SE host reported nothing at all. Both of our sources carry the data —
cvelistV5 states "15.02.0.0 .. lessThan 15.02.2562.043" for CVE-2026-42897, and
NVD has the matching CPE — so this only needed a registry entry on each path.
The leading zeros that make this awkward elsewhere are irrelevant here: Wazuh
reports 15.2.2562.27 and the record says 15.02.2562.043, and both sides are
parsed as numbers rather than compared as strings. That difference is exactly
what the Wazuh issue describes as the cause on their side.
The name regex is ANCHORED and exact, which is the whole risk of this change.
One Exchange host lists five kinds of row — the product itself, "Microsoft
Exchange Server", a dozen language packs (all still on the RTM build), and
"Hotfix Update for Exchange Server Subscription Edition (KB5066373)" whose
version field is the literal "1". Only the product entry carries the real
build; the KB row would compare below every fix ever published and flag the
host permanently.
Index key bumped to v13. tests/test_exchange_se.py pins all five row types
plus the version boundary.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A record stating one affected version and no bound at all was dropped
entirely, so the CVE never entered the index and nothing but Defender could
report it. CVE-2026-14266 is exactly that shape: "7-Zip 20.01, status
affected", nothing else.
They were dropped to avoid over-matching, which is a real risk — an unbounded
floor swallows every version above it. A single exact version has no such
problem when it is read as the closed range [v, v]: it matches that one
version and nothing else, which is precisely what the record claims. 20.01
hits, 20.01.0 hits (same version, written differently), 19.00 and 26.01 do
not.
Index key bumped to v12; the cached index has none of these entries.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
New pairs change what the index CONTAINS, and a cached index built by the
previous version has no checkmk bucket at all — so the scan would have kept
serving a stale index and found nothing until the key changed anyway.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Checkmk agent was invisible to every path, and the reason was one
character: it numbers patches inside the version string (2.4.0p12), which the
dotted-numeric rule in _clean_version rejected outright, so the version was
discarded before any source was consulted. Wazuh does not detect it either
(wazuh/wazuh#35646), which left it a complete blind spot.
Nothing else had to change — _vtuple already reads 2.4.0p12 as (2,4,0,12), so
the comparisons were correct all along. Both sources state their bounds in the
same notation (lessThan "2.4.0p13"), and both are now wired up: the cvelistV5
pairs for the branch-bounded ranges, the NVD CPE registry entry as the second
route.
The exclusion that made the rule strict stays intact: Linux distro versions
(1:3.2, 4.6.5-3.el8, 2.43.0.windows.1) are still rejected — they belong to
Wazuh and pushing them at NVD produces noise. tests/test_checkmk_version.py
covers both halves, including that an older release branch answers only for
itself.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Chrome CVE records carry no metrics block whatsoever — Google puts the
severity in the description instead: "… (Chromium security severity:
Critical)". Every fresh Chrome finding therefore landed on the neutral
'medium' placeholder, so a Critical sandbox escape sorted level with a Low UI
glitch, both in the queue and in the daily digest mail. For a team that
triages by severity that is worse than no data.
The severity word is now read from the description when no metrics block
exists. No score is invented — only what the vendor stated, and a real CVSS
always wins over the prose. Once NVD publishes a score, the existing
CVSS-sync takes over as before.
Existing rows heal on the next scan: the upsert already lifts a finding off
the medium placeholder when a source reports a better severity.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
NVD enumerates one CPE per SAP patch level, which can only ever describe the
levels that existed when the record was written — a host on a newer level
falls through. cvelistV5 states the bound instead ("7.70 PL0" ..
"7.70 PL11"), so it keeps answering as SAP ships more levels.
The records contradict themselves, and the resolution is the whole point of
this change: CVE-2023-32113 carries BOTH "<= 7.70" (the entire release) and
"7.70 PL0".."7.70 PL11". A host on 7.70 PL26 is affected by the first
statement and patched by the second. The patch level is the precise one, so
it decides for its own release, and the release-wide bound only answers for
releases that have no patch-level entry at all. Entries are therefore weighed
per CVE, not row by row.
Both vendor spellings are matched — records days apart say "SAP SE" and
"SAP_SE".
Index key bumped to v10 (new sap bucket). tests/test_sap_cvelistv5.py pins
the contradiction, the fallback, and the unreadable-level case against the
verbatim version objects.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Apple OS findings could only ever come from NVD, and NVD's Apple enrichment
lags: CVE-2026-28911 sat at "UNDERGOING ENRICHMENT" with no configuration at
all, so the CPE path had nothing to match and the cvelistV5 path refused
every non-Windows asset outright. Both iPhones and Macs therefore showed
these CVEs only where Defender happened to report them — with no package or
installed version, since Defender carries neither.
cvelistV5 has the data on day one (Apple / macOS / lessThan 14.8.8), so the
OS scan now reads it there too.
Apple states each parallel release train as its own ZERO-floor range on the
same CVE — CVE-2026-64721 carries lessThan 14.8.8, 15.7.8 AND 26.6 — so a
Sonoma 14.7 host matches all three at face value and the finding would claim
"fixed in 26.6", an upgrade that host will never receive. The major version
picks the train, the same role _win_family plays for Windows builds.
tests/test_apple_os_train.py pins that behaviour.
Index key bumped to v9; the cached index has no apple-os bucket.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Java was in no scanner registry at all — a complete blind spot. NVD leaves
these CVEs 'Awaiting Enrichment' (no CPEs, no affected block), so cvelistV5
is the only structured source, confirmed across CVE-2026-60526 / -21925 /
-47057 / -62574.
Three things Java needs that no other product did:
- Identity via BOTH routes the records use: the vendor/product block
('Oracle Corporation' / 'Oracle Java SE') and the CPE product
(oracle:java_se, plus the older oracle:jre / oracle:jdk — Java SE IS the
JRE, and the same CVE can be filed either way).
- Version from the NAME, not the field: Wazuh/Intune report 'Java 8 Update
441' with an ARP build of 8.0.4410.7, which maps to nothing. Parsed to
(feature, update); '1.8.0_471' and '8u491' spellings both handled.
- Bare affected versions get indexed. Oracle states 'version: 8u491,
status: affected' with no range, so the normal range extractor yielded
nothing for these records.
Matching rule is CUMULATIVE — installed <= affected, within the same
feature release. Oracle only names the current supported update, but older
ones carry the same flaw; validated against Defender TVM, which reports
CVE-2026-62574 (affected 8u491) on hosts running 8u102 and 8u191. Java 11
is never matched by an 8u CVE.
Verified against the live CVE-2026-62574 record: 8u102/8u191/8u441/8u491
all flagged, newer safe. Index bumped to v8 so the new key is picked up.
Tester: CVE-2026-16395 flagged on 'Mozilla Firefox 52.2.1 ESR' — the
cvelistV5 record carries only 'unaffected 153, lte *' (release train),
and per mfsa2026-69/-70 no ESR branch is affected at all. Both matchers
(cvelistV5 registry + curated CPE registry) resolved the ESR package to
the plain firefox key, so ESR builds were judged against release-train
ranges — structurally wrong (NVD tracks ESR as its own firefox_esr CPE;
Mozilla ships separate ESR advisories).
Guard both resolve() paths: a package whose name carries the ESR word
never resolves to the firefox key. ESR patch state would need the MFSA
fixed_in data (already stored in the mozilla_advisory index) — until
that's wired, no match beats a false positive. Existing FP rows
auto-close on the next app-scan via the stale reconcile.
Answers 'is vendor readable via the Intune/Defender API?' — yes:
- Intune detectedApps.publisher was being dropped in _map_apps; now kept
as the package vendor.
- Defender softwareVendor was folded into the package label; now stored
separately.
- Wazuh syscollector already carries vendor; now threaded through.
New vulnerabilities.package_vendor column (migration 038, idempotent),
populated at the package-finding chokepoints (app_cve_scanner._upsert +
cvelistv5 scan_asset via pkg vendor, defender _upsert_cve) and shown in
the Affected Package card on the CVE detail page.
Scope: single-package findings. Per-package (vulnerability_packages) and
Nessus/m365 vendor left as follow-up — Nessus rolls vendor into the
plugin name and m365/OS vendor is implicit (Microsoft).
Drop the bare-"firefox" alternative from the product key regex so a
stray "…Firefox…" inside another product's name can't resolve to the
Firefox CVE key. Windows ARP / Wazuh inventory always carry the "Mozilla"
prefix (incl. "Mozilla Firefox ESR"), so no real install is lost.
- cvelistv5: inverse-unaffected fix bound now only for records with
exactly ONE unaffected entry. Firefox ESR CVEs carry two (e.g.
CVE-2026-16361: 115.38 lte 115.* + 140.13 lte *), and Mozilla writes
the ESR floor as an unbounded 'lte *', so a 'below X' rule wrongly
flagged regular Firefox 121/152. Multi-train records now skipped.
- advisory feeds: DEFAULT cisco-psirt URL corrected to
psirtrss20/CiscoSecurityAdvisory.xml (old rss.x?i=44 is DTD-refused);
saved configs auto-migrate the stale URL on load.
The July .NET CVEs (CVE-2026-47304/50649/50525/50659) were invisible to the
app scan because cvelistV5's registry had no .NET entry. Added per-release keys
(dotnet-8/9/10 -> '.NET 8.0'/'9.0'/'10.0'), same reasoning as SharePoint: the
record floors are generic release floors (8.0.0 .. 8.0.29), so a shared key
would cross-match releases. The real semantic version lives in the ARP NAME
('Microsoft .NET Runtime - 8.0.16 (x64)') and bumps with every monthly patch;
the version field is an MSI build -> the scan now supports name_ver entries via
the existing _effective_version mechanic. Matches Runtime/Host/Desktop-Runtime;
SDKs excluded (8.0.1xx numbering never falls in the runtime range, runtime is
installed alongside anyway).
.NET FRAMEWORK stays out deliberately, with data this time: its ARP version
(4.8.04084) is STATIC across monthly patches — only file versions change — so
comparing it to fix builds like 4.8.4803.0 would flag every install forever.
Framework patch state is file/KB-based; Defender TVM covers it and those
findings already flow in via the Defender sync. The record's ampersand ranges
('2.0.50727.9069 & 3.0...') parse to no-match, verified harmless.
Index bumped to v7. Verified against the live records: 8.0.16 -> affected by
8.0.0..8.0.29; 8.0.29 -> clean; SDK/Framework/Targeting-Pack names don't
resolve.
Three tester-reported issues:
1. Cross-confirmed findings never auto-resolved. Each reconcile (app-scan /
defender / msrc) skipped findings any OTHER scanner also reported, so a
Chrome CVE seen by app-scan AND Defender was closed by neither — a patched
host (150.0.7871.125 installed, fix .115) kept an open finding forever.
All three reconciles now use the Nessus-backfill contract instead: drop
YOUR OWN source when no longer detected, mark patched (audit-logged) only
once no source is left. A finding another scanner still reports stays open
under that scanner. Verified both directions in a self-check.
2. Installed version went stale. The app-scan upsert only filled
package_version when empty, so the row kept its first-ever version
(tester: Firefox showed 'Installed: 150.0.3' while 152.0.5 was on the
box). Re-detection now refreshes the version.
3. Citrix published-app registry stubs ('Firefox 1.0', vendor 'Delivered by
Citrix') describe software NOT installed on the box and produced
ancient-CVE false positives (CVE-2008-2798 against Firefox 1.0). Packages
whose vendor names Citrix are skipped in both scanners; the stub vendor
field is the discriminator, real installs keep their real vendor.
The tester proved two of my 'structurally impossible' claims wrong by patching
around them locally. Both were wrong, and for the same reason as before —
checking too narrow a slice of the data.
1. Newest Firefox CVEs were never indexed. Mozilla states the INVERSE: no
affected range at all, only 'version 152.0.6, lessThanOrEqual *,
status: unaffected'. _ranges_from_affected skipped every non-affected entry,
so the CVE vanished. 'X and up are fixed' == 'below X is affected', so a
lone unaffected floor now yields a fix bound. Guarded: only when the entry
declared no affected range of its own AND there is exactly one floor —
several floors mean several branches (release vs ESR) and picking one would
over- or under-report.
2. SharePoint 2013 does have real fix ranges. I concluded otherwise from six
RECENT MSRC docs — of course a product EOL since 2023-04-11 is absent there.
The CVE records from its supported years carry real builds
(CVE-2023-23395: 15.0.0 .. 15.0.5537.1000) and an unpatched farm is behind
all of them. That is exactly what the tester's Nessus reports.
Verified against the live records, both directions:
Firefox 147.0.4.0 / 152.0.5 -> affected; 152.0.6 / 153.0 -> clean
SharePoint Foundation 2013 @ 15.0.4569.1506 -> flagged by CVE-2023-23395
and CVE-2022-35823
regression: WS2016 @ 14393.5000 -> flagged, @ 14393.9234 -> clean
Index bumped to v6 so it rebuilds once.
Re-enables the OS scan that c4abbaa turned off, with the guard that was missing.
Two independent checks, each one learned from a real false positive:
1. FAMILY — the entry's product name must belong to the asset's Windows family.
Windows 11 24H2 and Windows Server 2025 share build line 10.0.26100 but keep
separate revision sequences, so CVE-2026-41089 (Server 2025 only, fix .32860)
matched a fully-patched 24H2 client at .8655. The index now keeps the product
name to make this possible.
2. RELEASE-BOUNDED — the range's floor and fix must sit on one build line. Older
records use a generic 10.0.0 floor that swallows every lower build
(CVE-2021-26432 put a 17763 fix on a 14393 host).
Together the range only answers 'patched or not' within the host's own release.
Verified end-to-end against the real records, both directions:
Win11 24H2 @ 26100.8655 -> EMPTY (the reported FP)
WS2016 @ 14393.9234 -> EMPTY (patched; also rejects the 2021 record)
WS2025 @ 26100.32690 -> CVE-2026-41089 (.32860), CVE-2026-47291 (.32995)
Win11 23H2@ 22631.5000 -> CVE-2026-47291 (.22631.7219) only
WS2016 @ 14393.5000 -> CVE-2026-47291 (.14393.9234) only
Index bumped to v5 (entries carry the product name) so it rebuilds once.
SharePoint was simply absent from the curated registry — the app scan never
looked for it, which is why Nessus found those CVEs and we did not. cvelistV5
carries usable numeric ranges for it, verified across 82 recent CVEs rather
than one sample:
Microsoft SharePoint Enterprise Server 2016 16.0.0 .. 16.0.5556.1005
Microsoft SharePoint Server 2019 16.0.0 .. 16.0.10417.20153
Microsoft SharePoint Server Subscription Edition 16.0.0 .. 16.0.19725.20384
One key PER RELEASE, not a shared 'sharepoint' key: every range uses a generic
16.0.0 floor and all three releases report 16.0.x, so a shared key would let a
2016 install (16.0.5456) fall inside the 2019 range — the same cross-release
false positive the Windows OS scan just produced. The release is decided by the
product NAME; the range then only answers 'patched or not' within it.
2013 stays out: Microsoft publishes no fixes for it, so no range exists. Its
EOL finding is the signal there — this changes nothing for a 2013-only host.
Identifying a Windows release by its build LINE is not sound, so the whole
premise of 5703dfa fails. Windows 11 24H2 and Windows Server 2025 both sit on
10.0.26100 but keep separate revision sequences:
CVE-2026-41089 affects Windows Server 2025 ONLY, fix 10.0.26100.32860
a fully-patched 24H2 client reports 10.0.26100.8655
-> 8655 < 32860, so the client matched a Server-only CVE
The same-line floor/fix guard from e19c8d9 was necessary but not sufficient: it
only rejects the old generic-floor records, not two releases sharing a line.
Disabling rather than attempting a third in-place fix: the FPs are in the
tester's data now, and I have already been wrong three times here by
generalising from too few records. Windows products are no longer indexed at
all, so the findings self-heal via the app-scan auto-resolve on the next run.
Package scanning is untouched.
Re-enabling needs the entry's product NAME in the index, matched against the
asset's OS family, verified against CVE-2026-41089 (client must not match) and
CVE-2026-47291 (each must match its own line). Written up in scan_asset_os.
Regression from 5703dfa. A Server 2016 host (10.0.14393.9234) was flagged with
CVE-2021-26432 'fixed in 10.0.17763.2114' — a Server 2019 build. My claim that
cvelistV5 ranges are release-bounded came from checking ONE modern record, and
older ones are not:
CVE-2021-26432 version=10.0.0 lessThan=10.0.17763.2114 (generic floor)
CVE-2026-47291 version=10.0.14393.0 lessThan=10.0.14393.9234 (release-bounded)
A '10.0.0' floor makes the range swallow every lower build, so a 14393 host fell
inside a 17763 fix. The OS scan now requires the floor and the fix to sit on the
same build line and skips the rest — such an entry carries no release info at
all, and the OS string alone can't supply it. Costs nothing in practice: Windows
servicing is cumulative, so a host behind on an old CVE is already flagged by
that line's newer ones. Existing false positives self-heal on the next scan via
the app-scan auto-resolve.
Also:
- scheduler job renamed to '(Windows OS)' — it covers client too, not just Server
- dashboard: AI Audit History hidden from read-only (AI endpoints are editor+)
- dashboard: 'Export Report' had no onClick at all (dead for every role) — now
links to the reports page
- compliance: the Impact-CSV upload card is admin-only (the import endpoint is
RequireAdmin), so it no longer 403s silently for other roles
Corrects bf529ed, which claimed Microsoft products cannot use the CPE/cvelistV5
path. That was wrong, and the reasoning behind it was a sampling error: the
'no ranges' check looked at CVE-2013-3900 and the 'lessThan: publication' check
at CVE-2021-24072 — both ancient. Modern MS records carry real numeric ranges.
Re-verified on the CVE the tester cited:
CVE-2026-47291 cvelistV5: 20/20 affected entries have a numeric lessThan,
0 'publication'
NVD: 22/24 microsoft cpeMatch have versionEndExcluding
CVE-2026-45467 (SharePoint, current): lessThan 16.0.5556.1005 etc.
So there is no Server/Client split and no reason to special-case Microsoft.
Implementation reads cvelistV5, not NVD, for one concrete reason: NVD flattens
these to an END bound only, so a 1607 host (14393.x) sits inside the 22H2 range
(endExcluding 19045.7417) and false-positives. cvelistV5 keeps the range bounded
(10.0.22631.0 .. 10.0.22631.7219), and those bounds select the host's release by
themselves — no build->release table, Server and Client fall out for free.
- pattern products in the index (one key for the whole 'Windows 10|11|Server'
family) so a new release doesn't need a registry row; anchored so SharePoint,
Teams and '.NET Framework on Windows Server 2016' can't be swallowed
- scan_asset_os() flags straight from asset.os_version; runs for every Windows
asset, no software inventory required
- index bumped to v4 (entries changed) so it rebuilds once
Two Chrome-on-mobile issues:
1. Duplicate EOL findings (EOL-CHROME-149 AND -150 on one iPad running only
150). A product runs one release per asset, so upsert_eol_vulnerability now
supersedes any other open EOL-<slug>-* on the same asset (marks it patched +
audit-logs it). Generic — also cleans up OS major upgrades (Server 2016->2019
etc.). Fixes the tester's 'why two EOL' question.
2. Android Chrome went undetected: Intune reports it by package id
'com.android.chrome', which didn't match the 'google chrome' registry regex.
Add it to both scanners (curated NVD-CPE + cvelistV5). Android shares Chrome's
version numbers and security fixes with Desktop (per Google), so the same
google:chrome ranges apply. Bare 'Chrome' (iOS, WebKit-backed) stays
unmatched by design.