Commit Graph
59 Commits
Author SHA1 Message Date
vulncheck 0b71453c48 chore(release): drop personal attribution from comments and docstrings
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.
2026-08-26 15:34:05 +02:00
vulncheck 891979ee28 fix(wazuh): detect the manager, and stop the agent inheriting its CVEs
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.
2026-08-21 08:38:59 +02:00
vulncheck 6e449dba12 fix(vcenter): auto-close patched ESXi/vCenter CVEs, and stop leaking the sync lock
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.
2026-08-21 08:28:05 +02:00
vulncheck a099fef3b8 fix(firefox): take the CVE list from Mozilla's MFSA, not cvelistV5 ranges
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
2026-08-21 08:01:53 +02:00
vulncheck 1b1ff11ca1 fix(cvelistv5): force a fresh ZIP for the nightly index build
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.
2026-08-21 07:43:28 +02:00
vulncheck 7078f5e024 feat(vsphere): read vCenter + every ESXi host, and detect their CVEs
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.
2026-08-17 11:54:51 +02:00
vulncheck 630f34f450 fix(scan): keep beta-only and CI-only Wazuh flaws off installed agents
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.
2026-08-17 10:36:23 +02:00
vulncheck 36744f193e fix(scan): match Python the interpreter, not python-* modules
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.
2026-08-15 09:12:01 +02:00
vulncheck 4bb7bdeb62 feat(scan): detect Apache Tomcat, and its EOL, on both paths
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.
2026-08-15 09:05:40 +02:00
vulncheck a1a94d93bd fix(scan): detect Vim, VS Code and CPython from cvelistV5
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.
2026-08-13 16:28:03 +02:00
vulncheck 974de08b3c fix(autodesk): match AutoCAD by release, not by release year
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.
2026-08-13 14:04:46 +02:00
vulncheck 4927a1e2ac fix(scan): reject commit-hash bounds on the CPE path too
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.
2026-08-11 08:56:50 +02:00
vulncheck 991db2bc96 fix(scan): a commit hash is not a version bound
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.
2026-08-10 19:04:56 +02:00
vulncheck c31e5de70d feat(scan): detect Wazuh's own components
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.
2026-08-10 15:46:26 +02:00
vulncheck 93120f5dd6 fix(scan): a companion product is not the product it names
"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.
2026-08-10 14:18:56 +02:00
vulncheck 2a831e4678 feat(scan): report a fix floor when the advisory names no patched build
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).
2026-08-10 14:02:24 +02:00
vulncheck d90b8455f2 fix(scan): never store a release line as the fixed version
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.
2026-08-10 08:59:27 +02:00
vulncheck 949708745a fix(scan): apply the version-shape check to Teams as well
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.
2026-08-10 08:07:42 +02:00
vulncheck 20b7686aea fix(scan): see Firefox on Android; mark extensions as per-user
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.
2026-08-09 19:39:43 +02:00
vulncheck d61b7a86af fix: never auto-resolve from an empty inventory; unblock asset deletion
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.
2026-08-09 19:38:27 +02:00
vulncheck faedd83ae9 fix(scan): tell Adobe's three acrobat products apart by version shape
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.
2026-08-06 13:39:38 +02:00
vulncheck fc0359acef fix(scan): let Node.js reach the cvelistV5 path
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.
2026-08-06 10:09:17 +02:00
vulncheckandClaude Opus 5 3384a1e918 fix(scan): an inclusive bound names one release line, not everything below
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>
2026-08-05 13:12:40 +02:00
vulncheckandClaude Opus 5 3fd27d46e5 fix(scan): carry the CVE description, and keep iOS browsers out
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>
2026-08-05 09:54:32 +02:00
vulncheckandClaude Opus 5 9dc774e629 feat(scan): add the Autodesk viewers and wire up cvelistV5 for the family
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>
2026-08-04 12:09:31 +02:00
vulncheckandClaude Opus 5 f64902417e feat(scan): detect the Adobe Creative Cloud desktop apps
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>
2026-08-04 10:06:50 +02:00
vulncheckandClaude Opus 5 264d54c150 feat(scan): detect VMware Tools, and read wildcard version floors
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>
2026-08-04 08:35:22 +02:00
vulncheckandClaude Opus 5 f0ae234b85 fix(adobe): match the product, not the add-ons that carry its name
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>
2026-08-03 18:08:38 +02:00
vulncheckandClaude Opus 5 3e6c15cef6 feat(scan): cover Adobe Acrobat via cvelistV5, and stop Defender flapping
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>
2026-08-03 15:53:03 +02:00
vulncheckandClaude Opus 5 ddf0a373df feat(scan): detect Exchange Server Subscription Edition
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>
2026-07-31 14:09:45 +02:00
vulncheckandClaude Opus 5 793e840665 feat(scan): index CVEs that name a single exact version
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>
2026-07-31 13:52:09 +02:00
vulncheckandClaude Opus 5 ac9700b32c fix(scan): bump the cvelistV5 index key for the Checkmk pairs
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>
2026-07-30 11:24:27 +02:00
vulncheckandClaude Opus 5 8ef4a5ce0d feat(scan): detect Checkmk agent CVEs
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>
2026-07-30 11:24:05 +02:00
vulncheckandClaude Opus 5 6b3744e569 fix(severity): read the Chromium severity Google states in prose
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>
2026-07-30 09:06:15 +02:00
vulncheckandClaude Opus 5 06715d2b15 feat(sap): match SAP from cvelistV5 by release + patch-level range
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>
2026-07-29 15:01:03 +02:00
vulncheckandClaude Opus 5 f26f7baf94 feat(scan): detect Apple OS CVEs from cvelistV5
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>
2026-07-29 13:04:07 +02:00
vulncheck 5df35f43cf feat(scan): detect Oracle Java CVEs from cvelistV5
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.
2026-07-27 14:48:08 +02:00
vulncheck 7941d0403f fix(scan): never match Firefox ESR installs against release-train ranges
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.
2026-07-24 11:51:48 +02:00
vulncheck 97562abed0 feat(vendor): capture + show software vendor from Intune/Defender/Wazuh
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).
2026-07-23 14:06:05 +02:00
vulncheck 8db635effa fix(scan): tighten Firefox match to require the Mozilla vendor word
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.
2026-07-23 13:59:38 +02:00
vulncheck 3bb480c39f fix(scan): drop ESR false positive; correct Cisco PSIRT feed URL
- 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.
2026-07-23 13:27:20 +02:00
vulncheck 54edb38b3a feat(app-scan): modern .NET (8/9/10) CVE detection via cvelistV5
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.
2026-07-21 14:41:36 +02:00
vulncheck 948589d07f fix(scan): cross-source auto-resolve, stale installed version, Citrix stubs
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.
2026-07-21 14:22:50 +02:00
vulncheck b4d0caf8ad fix(app-scan): find newest Firefox CVEs + SharePoint 2013 (both were my bad calls)
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.
2026-07-16 09:38:25 +02:00
vulncheck d304463996 feat(app-scan): Windows OS CVEs, family-matched (Server AND Client)
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.
2026-07-16 09:32:42 +02:00
vulncheck 2519427b41 feat(app-scan): SharePoint 2016/2019/Subscription CVE detection via cvelistV5
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.
2026-07-16 09:29:24 +02:00
vulncheck c4abbaa1c1 revert(app-scan): turn the Windows OS scan back off — cross-release FPs
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.
2026-07-16 09:26:29 +02:00
vulncheck e19c8d9da7 fix(app-scan): stop cross-release Windows false positives
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
2026-07-14 15:36:10 +02:00
vulncheck 5703dfa806 feat(app-scan): Windows OS CVEs via cvelistV5 bounded ranges (Server AND Client)
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
2026-07-14 13:01:25 +02:00
vulncheck 7bb396a606 fix(eol/app-scan): supersede stale EOL releases + detect Android Chrome
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.
2026-07-10 15:32:52 +02:00