Commit Graph
508 Commits
Author SHA1 Message Date
vulncheck 0845fbe5db fix(wazuh): give offline hosts a six-week grace window before their CVEs vanish
The asset sync fetched active agents only, and the sync-driven reconcile
inactivates every agent id the sync does not report. Since INACTIVE assets are
hidden from every CVE view, a host that was merely switched off lost all its
findings at the next sync — while Wazuh itself kept listing the agent and its
vulnerabilities. Holiday, sick leave or a spare laptop in a drawer looked
exactly like a decommissioned machine.

Root cause confirmed: not the 30-day soft-inactive window (which never got a
chance to apply here), but the connection-state filter feeding seen_ids.

- sync fetches ALL agents, paginated; disconnected ones stay in seen_ids
- last_seen now carries Wazuh's lastKeepAlive instead of "when we synced",
  so there is a real stamp to measure against
- per-agent status comes from status_for_last_seen(): ACTIVE inside the
  window, INACTIVE outside — replaces the binary connected/disconnected test
- reconcile_asset_lifecycle uses max(last_scan, last_seen): a host that keeps
  checking in but cannot be scanned while offline no longer ages out early
- window default 30 -> 42 days, one setting for both paths; migration 044
  rewrites only rows still holding the old default
- agents registered but never connected are skipped instead of imported

tests/test_asset_grace_window.py covers the window edges, UTC keepalive
parsing, the newest-stamp reconcile, and guards the sync against a
status filter being reintroduced.
2026-08-17 11:07:30 +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 4df65352ba feat(eol): flag Microsoft Teams classic as EOL since 2024-07-01
The lifecycle export does carry the classic client ("Microsoft Teams" /
release "Classic Teams Client", ends 2024-07-01), but Teams follows the
Modern policy: that date lives in "Release End Date" while the parser
reads the Extended/Retirement columns, which are empty on every Modern
row. So the row is dropped and a host still running classic Teams
produced no finding at all.

Reading Release End Date instead is not an option — for a supported
product it marks the end of a superseded release (every Windows 10
version row has one), which would report supported products as EOL.

So the date is fixed in code and matched on the two inventory names that
mean classic: "Microsoft Teams classic", and "Teams Machine-Wide
Installer" only at version 1.x, since the new client's bootstrapper
writes an entry of the same name with a 5-digit build.

The new client lists as plain "Microsoft Teams" and is now refused by
the export matcher outright: that string is the listing name for BOTH
releases, so the day Microsoft fills a retirement date on the classic
row, a current Teams would have read as CRITICAL EOL.
2026-08-17 10:24:40 +02:00
vulncheck 04d59d31e6 feat(euvd): link every CVE to its ENISA EUVD record
The EUVD link on the detail page only appeared when enrichment had
stored an euvd_id, which only happens for rows the EUVD KEV feed
covers. Every other CVE showed no EU record at all.

The EUVD resolves a CVE id to the same page as its own EUVD-ID
(/vulnerability/CVE-2026-68820 == /vulnerability/EUVD-2026-56468), so
the link needs no id mapping: use the EUVD-ID when we know it, the CVE
id otherwise. Label keeps the EUVD-ID in parentheses when present.
2026-08-17 10:16:42 +02:00
vulncheck 4daf28113e fix(kev): give EUVD-only exploited CVEs their product name back
An "Actively Exploited" row that only ENISA lists rendered as a bare CVE
id with no text: the EUVD parser kept the date, the exploited flag and the
EUVD-ID and threw away everything that says WHAT is affected. CISA rows
looked fine because CISA ships vendor/product/name — so the gap only showed
on the EU-only entries, exactly the ones nobody can recognise by number.

ENISA does carry it, nested: enisaIdProduct[].product.name (+ its vendor)
and a free-text description. Parse those, merge them like the other fields,
and let the KEV catalog fall back to them when CISA has nothing — CISA
still wins where both list a CVE, because its short vuln NAME beats a
paragraph on a one-line row. No CVE is dropped or filtered by any of this.

Caches written before these fields are refetched once, same as the pre-
`exploited` ones: an entry with no product would keep the blank rows for
another 24h. Descriptions are clipped to 300 chars — the cache is a single
JSON blob in a Setting row, and ~1700 full ENISA texts add ~0.5 MB to
every read for text no row ever shows.

Detail view now links the ENISA EUVD record under External References
whenever we know the EUVD-ID, so the EU-authoritative page and the
corresponding EUVD-ID are one click away instead of copy-paste.
2026-08-15 16:57:48 +02:00
vulncheck b469b5eca3 fix(deps): upgrade the auth stack, and free bcrypt from passlib
starlette was never pinned here — fastapi 0.115.6 held it below 0.42,
which is exactly where its 14 advisories live. So the starlette fix is a
fastapi upgrade (0.141.1), not a starlette one; pinning starlette
directly would only have fought the resolver.

passlib 1.7.4 has been unmaintained since 2020 and cannot initialise
against bcrypt 5 at all — it raises before it hashes anything. bcrypt is
now called directly, which drops the last reason to hold a crypto
primitive three majors back. The 72-byte truncation passlib did silently
is done explicitly, otherwise every existing hash stops verifying and
every long password turns a login into a 500.

lxml goes to 6.1.1 for its own sake: it parses Microsoft HTML in
m365_service, not the advisory feeds — those run through stdlib
ElementTree.

The CSV upload read the whole body into memory with no cap, while feed
downloads have had one for a while. Same 16 MB limit, same shape.

OSV against the pinned set: 62 advisories → 0.
2026-08-15 09:47:30 +02:00
vulncheck 9e81436391 chore(frontend): drop next-auth, lift next/axios out of their advisories
next-auth was never imported — the backend issues the JWTs itself — so a
package carrying a critical advisory sat in the tree doing nothing.
Removing it beats upgrading it.

next 16.1.5 → 16.3.1 also clears postcss, sharp, nanoid, form-data,
follow-redirects and uuid, which came in transitively. eslint-config-next
moves in lockstep with next, otherwise it pins the old toolchain.

npm audit --omit=dev: 9 findings (1 critical) → 0.
2026-08-15 09:47:19 +02:00
vulncheck 57ed0b1534 feat(kev): alert on exploited CVEs that are open in our own environment
Actively-exploited CVEs were shown from CISA KEV only, on a dashboard
somebody had to be looking at. ENISA's EUVD exploited catalog was already
being fetched for enrichment but never surfaced as a KEV source.

- kev_service merges CISA KEV + ENISA EUVD (exploited only — an EU-Critical
  entry is a priority list, not an exploitation claim) into one entry per
  CVE carrying both sources.
- Inventory impact is counted honestly: active assets only (matching the
  vulnerabilities list), open findings apart from remediated ones, and an
  asset with both an open and a closed row counted once, as open. That gap
  is what made the badge say 64 where the filtered view listed 62.
- kev_alert_service mails the CVEs with open findings here — hostnames, IPs,
  counts, sources — once per CVE, again when more systems are affected.
  Hourly, plus straight after each threat-intel refresh.
- Dashboard pins what is open here above the newest listings; Advisories
  gains source filters, an alert panel and its config.

EUVD entries now carry an explicit `exploited` flag; caches predating it
cannot answer the question for an entry that is also EU-Critical, so they
are treated as stale and refetched once.
2026-08-15 09:37:16 +02:00
vulncheck 0bf32528e3 fix(msrc): give an MSRC-only finding its real text, and its KB next morning
Two defects on the same pair of findings — CVE-2026-6727 (MSRC only) and
CVE-2026-70304 (cross-confirmed), both detected 12.08. and both still wrong
on 14.08.

No Remediation block: the CVRF ingest ran weekly (Sun 04:40) while the scans
that create the findings run nightly, and the ingest only stores remediations
for CVE ids already in the vulnerabilities table. A Patch-Tuesday CVE
therefore waited up to seven days for its KB. The nightly MSRC scan now
chains a two-month catch-up ingest after itself, so the CVEs it (and the
app scan before it) just created get their fixes the same night; Sunday keeps
doing the full 18-month backfill.

Wrong description: msrc_scan_service creates its rows with a placeholder
("MSRC reports ... is fixed in build ...") because the fixed-build index
carries no flaw text, and the app-scan backfill only fills an EMPTY
description — so for a CVE no other scanner ever saw, the placeholder was
permanent. The monthly document does carry the text, in the Note titled
"Description"; the ingest now writes it over the placeholder, and only over
the placeholder. Existing rows heal on the next run, no migration.

While there: _list_recent_docs now sorts the /updates index by
CurrentReleaseDate before taking the tail. At 18 months a misplaced entry was
survivable; at two it is not. build_product_index reuses it instead of
slicing the raw index, which also stops non-monthly entries getting through.
2026-08-15 09:18:01 +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 65f21e5470 fix(eol): a Microsoft update keeps the EOL product it names after "for"
The tester's host runs SQL Server 2008 R2 and SQL Server 2012, both years past
end of life, and every EOL finding on it was retracted overnight as "MS
lifecycle no longer matches this installed product (re-evaluated — not
end-of-life)". Nine critical findings to zero, with the software still
installed.

Two causes, both of them a name the sweep refused to look at:

1. The listing had to PREFIX the inventory name (the SAP-GUI fix), and none of
   this host's evidence is shaped that way. A Microsoft update names its parent
   product AFTER the connector: "Service Pack 1 for SQL Server 2008 R2
   (KB2528583)", "Hotfix 7493 für SQL Server 2012", "SQL Server Browser for SQL
   Server 2012". Every component that names the product as a prefix is an
   add-on the sweep already refuses (Native Client, RsFx Driver, Setup), so the
   updates were the only evidence left, and they matched nothing.

   What follows "for" may now claim a listing — but only when Microsoft is the
   vendor. That is what tells the two cases apart: SAP's client is vendor "SAP
   SE", a service pack for SQL Server is "Microsoft Corporation". An unknown
   vendor changes nothing, the strict prefix rule stands, and an operating-
   system listing is never claimed by a package (an app "for Windows 10" is not
   Windows; the OS has its own EOL path).

2. A package with no version was skipped before any source saw it — and the
   engine's own entry, "Microsoft SQL Server 2012", carries no version at all.
   The name-only sources need none, so a versionless entry now reaches them;
   endoflife.date, which resolves the release FROM the version, still does not
   get a vote on one.

The vendor is stored on the finding, because the nightly revalidation re-asks
the resolver from the ROW, not from the inventory. Without it every finding
would be re-judged under a stricter rule than the one that created it and close
the same night it appeared — the open→patched flap, with nothing changed on the
host. Findings written before this change carry no vendor, so revalidation
leaves those names alone rather than closing them on an answer it cannot have.

Verified against the live export: all nine SQL Server findings return with
Microsoft's own dates (2019-07-09 and 2022-07-12), and the SAP GUI, Edge,
ODBC/OLE DB drivers, Teams, Store for Business, Skype for Business and Windows
Admin Center controls stay clean.
2026-08-15 08:55:30 +02:00
vulncheck f14eb1b5b2 fix(eol): a Citrix stub keeps the release in its name
Intune lists published apps as "Microsoft Access 2010 / 1.0 / Delivered by
Citrix". The CVE scanner drops those rows because the version is a
placeholder, and the EOL sweep reused that same filter — so two products
years past end-of-support produced no finding at all.

The version is the untrustworthy half, not the name. A stub is now kept and
restricted to the name-only sources (the Microsoft lifecycle export and the
hardcoded exotics); the endoflife.date path, which derives the release from
the installed version, is skipped for it. A stub named just "Firefox" still
matches nothing, which is what the filter was written for.
2026-08-15 08:45:00 +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 8684263c13 fix(eol): a lifecycle listing must prefix the product name
"SAP GUI for Windows 8.00 64bit (Patch 17)" was reported CRITICAL, EOL for
3866 days, security support ended 2016-01-12 — Windows 8's date, on a client
SAP shipped in 2023 and supports until 2027.

Cause: the MS-lifecycle resolver matched a listing anywhere inside the product
name. Normalisation turns the name into "sap gui for windows 8 00 64bit patch
17", which contains the "Windows 8" listing verbatim (the dot in "8.00" becomes
a space, so the version starts with the token "8"). Every "<vendor> for
<Microsoft product>" install could inherit a Microsoft end-of-life date the
same way. The listing must now be a token-aligned prefix of the product name;
verified against the live export, this drops the SAP row and keeps every true
finding (Exchange 2016, SQL Server 2014/2019, Lync 2013, SharePoint 2016,
Windows Server 2016, System Center 2012 R2, Report Viewer 2012).

The same finding also cited the wrong source: description and source badge read
"endoflife.date" while the id read EOL-MS-LIFECYCLE-…, so the row named a
source that had never seen the product. Both now come from the slug.

Open false positives close themselves on the next EOL check —
revalidate_ms_lifecycle_findings re-asks the resolver per finding.
2026-08-13 13:34:41 +02:00
vulncheck 9ef1db5e9b fix(scheduler): sync Wazuh assets before the scheduled agent loop
The scheduled Wazuh scan read the asset list straight from the database
and asked Wazuh about whatever wazuh_agent_id each row already carried.
It never called get_agents(), so it could not learn about an agent id
that had changed.

A host that dropped off and re-registered comes back with a NEW agent
id. The row keeps the old one forever: every vulnerability lookup and
the nightly EOL sweep query an agent that no longer exists, so the asset
shows no CVEs and no EOL entries, and it never flips back from INACTIVE
to ACTIVE either — reconcile_asset_lifecycle only revives on a fresh
last_scan, and the failing lookup never writes one.

Pressing "Sync Data (Wazuh)" fixed it because the button calls
/assets/sync_wazuh first, which matches by hostname when the agent id
misses, rewrites the id and sets the status. The scheduled job now makes
the same call before it reads the asset list. A failing asset sync is
logged and does not abort the vulnerability scan.
2026-08-13 13:26:21 +02:00
vulncheck 042e5aa6c7 feat(advisories): make the per-feed item cap configurable
30 items per feed was a constant. It is now the setting
`advisory_feeds_max_items`, editable in the feed configuration panel, applied
to every feed and taking effect on the next refresh.

The range is 1–500 rather than unbounded, and that ceiling is load-bearing:
every item lands in the cached JSON of ONE settings row that the page reads
in full, so "no limit" means a row that grows forever and a page that stops
rendering. 500 per feed is a few hundred KB per source — a quarter of the BSI
advisories in one sitting, and still a page that loads.

The value is written through the generic settings API, which stores whatever
a text field produced, so the read path treats it as untrusted: "", "abc",
"50 ", '"120"', 0, -5, 999999, "inf", true, None and an unreachable database
all resolve to something usable — junk falls back to 30, out-of-range is
clamped — and it never raises, because a feed run must not fail over its own
limit. The editor rejects the same values up front with the bounds named,
rather than saving something that quietly becomes a different number.

Also bounds the feed download at 16 MB. The item limit is the operator's to
raise; the size of a stranger's response is not.
2026-08-13 12:14:40 +02:00
vulncheck c2cb0a5a3c refactor(eol): one sweep implementation instead of three
The EOL package loop existed three times — the eol-check endpoint, the
nightly job, and run_eol_for_packages — and had drifted the way copies do:
only the endpoint filtered published-app stubs out of the inventory, so the
nightly job could raise findings from placeholder versions the button would
never produce; the flap guard added yesterday had to be threaded into each
copy by hand; and the regression test could only cover a re-implementation
of the loop, not the loop itself.

Now all three callers go through run_eol_for_packages, which returns its
counters, and the test drives the same function production does. What the
callers legitimately differ in became two arguments:

  reconcile — retract findings this sweep did not re-confirm. Only for a
  caller holding the WHOLE inventory. Intune's detectedApps is partial, so
  on a co-managed host it would retract the Wazuh sweep's findings and the
  next Wazuh run would reopen them — the same flap, one layer up.

  seen_ids — findings the caller produced itself (the OS-level EOL row),
  which the package list can never re-confirm. The test asserts both
  directions, so a caller that forgets to seed it fails rather than quietly
  reopening every OS finding each night.

Inventory filtering moved into the sweep, where no caller can forget it.
2026-08-13 11:56:02 +02:00
vulncheck 42702d0891 test(eol): lock down the sweep fixed point
Replays a real Exchange inventory through the actual state machine — upsert,
supersede, reconcile, against SQLite — three times, and asserts what the
tester asked for: the Subscription-Edition host carries no Exchange finding,
the 2016 host carries exactly one, and sweeps 2 and 3 write no status change
at all.

Verified to have teeth: it fails on the build the flapping audit log came
from (companion packages claim releases, findings supersede each other 14×
per run) and on yesterday's, where the add-on token list still let
"Exchange Server 2010 MAPI Client and CDO" through — 4 flips per run.

Offline and pinned: the endoflife.date catalogue is inlined, so the test
asserts our behaviour rather than what the feed says this week.
2026-08-13 11:49:02 +02:00
vulncheck 544d6c6eec fix(eol): only the product itself may claim its release year
Office, SharePoint and Exchange take their release from the year in the
inventory NAME, so every companion package that happens to carry a year
claimed that release was installed: "Microsoft Office 2007 Primary Interop
Assemblies", "Microsoft Exchange Server 2010 MAPI Client and CDO",
"Microsoft SharePoint 2013 Client Components SDK". Each produced a CRITICAL
EOL finding for a product the host does not run — the tester's Exchange SE
host, fully supported, carried an EOL-MSEXCHANGE-2007.

Naming the add-ons one at a time is a race nobody wins; there is always
another rollup. What separates the product from its companions is what
FOLLOWS the year: the product name ends there, or continues with an edition
or update qualifier ("2016 Cumulative Update 23", "Professional Plus 2016 -
de-de"). Any other word after the year belongs to a different product that
merely names this one. Both resolvers consult the same rule, so a companion
is refused whichever source would have matched it.

Second half of the same report: the status loop. These slugs are
single-release, so each companion superseded the others' finding once per
sweep — 2007 closes 2016, 2016 reopens and closes 2007, every run, with
nothing changing on the host. Supersede now leaves alone any finding the
same sweep already confirmed: the sweep saw both products in one inventory,
so neither replaced the other. Dropping the companions removes the cause;
this removes the mechanism.

The real entries are untouched — "Microsoft Exchange Server 2016 Cumulative
Update 23" and the plain "Microsoft Exchange Server" both still resolve to
release 2016 (EOL 2025-10-14), so the host's genuine finding stands.
2026-08-13 11:42:38 +02:00
vulncheck 610a745147 fix(eol): detect .NET and .NET Framework at all
".NET" normalises to "net" — _normalise_name strips the dot — so the
curated "dotnet" / "dotnetframework" slug keys could never match a real
inventory entry ("Microsoft .NET Runtime - 8.0.29 (x64)" normalises to
"microsoftnetruntime8029x64"). Every .NET and .NET Framework install was
skipped: no finding for the dead releases, none for the live ones either.

A prefix key cannot repair it — "microsoftnet" would also swallow
"Microsoft Network Monitor" — so both products are matched by regex, kept
tight enough that "Microsoft Visual Studio .NET 2003" and "Microsoft .NET
Micro Framework Porting Kit" keep falling through to the Microsoft export.

Second defect behind the first: modern .NET reports an MSI build in the
version field (9.0.18 ships as 72.72.55158), so no release prefix-matches
it even with the slug fixed. The release comes from the display name, the
same way app_cve_scanner_service already handles these products.

Developer-side packs (Targeting / Developer Pack, Reference Assemblies)
are excluded: they carry the runtime's name with an old version number and
are build inputs, not an installed runtime.

And the source rule: for these two products endoflife.date decides alone.
Microsoft's export carries no .NET row today and its lifecycle page agrees
with endoflife.date on .NET 9 (2026-11-11), but a future export row must
not be able to retire a supported release early.

Verified against the reported inventory: .NET 8/9 -> EOL SOON (2026-11-10),
.NET 10 -> supported to 2028-11-14, Desktop Runtime 6.0.36 -> EOL.
2026-08-13 11:29:47 +02:00
vulncheck b851b1c3c4 fix(intune): make the app-inventory probe tell 403 causes apart
The probe reported every failure as "app registration lacks the
permission", but detectedApps and managedDevices succeed on the same
token, so that permission is demonstrably granted. The 403 body is the
Intune service envelope, not an AAD denial — and _get truncates it at
200 chars, cutting off the part that names the cause.

Print the untruncated body, the token's roles claim, and a control
request against a route that certainly does not exist. Intune answers
unknown segments with the same generic 403, so only the contrast
between the three shows whether deviceInventories('ApplicationProperties')
is an ungranted route, a gated feature, or no route at all.
2026-08-12 14:24:16 +02:00
vulncheck b8be1c4af8 fix(intune): record the device check-in, not our sync time
asset.last_scan was stamped with datetime.now() on every sync, so the
dashboard reported fresh data while the underlying detectedApps snapshot
could be seven days old (Discovered apps refreshes weekly per device,
Win32/IME every 24h). An Edge reading 151.0.4129.59 here was already .78 on
the device, and the app-CVE scan ran on the stale version without a warning.

Store lastSyncDateTime — already selected, never used — in last_seen and
count devices whose check-in is >= 7 days old in the sync stats.

Add tools/probe_intune_app_inventory.py: Intune's App inventory replaces
Discovered apps and collects several times a day, but its Graph surface is
not in the beta reference yet, so the payload shape has to come from a real
tenant before any mapping is written.
2026-08-12 14:13:34 +02:00
vulncheck 1d7f647ec3 fix(gui): make URS rows open the asset like the compliance table
The hostname was styled as a link (blue, bold) but carried no handler, so
clicking it did nothing — the identical table below already routes through
openAsset.
2026-08-12 14:13:34 +02:00
vulncheck d87d781a64 fix(gui): recover the audit Asset column for rows written without a host
The column reads new_value.hostname. Writers fill it since c81d030, but
every row written before that stays empty forever — which is exactly
what the tester still sees on the EOL new-finding events — and any
writer that stores a blank hostname produces the same empty cell.

One extra query per page maps vulnerability id → hostname for the rows
that name no host, and merges it into the payload on the way out. No
migration, no stored rows touched, and a future writer that forgets the
field is covered too.
2026-08-12 13:05:35 +02:00
vulncheck d01b5867fa fix(scan): add-on packages must not produce EOL findings of their own
A host running Exchange Server Subscription Edition — fully supported —
still lists "Microsoft Exchange Server 2007 Standard Anti-Spam Filter
Updates", "Microsoft Exchange 2007 Enterprise Rules Updates", a dozen
language packs and a Lync bootstrapper in its inventory. Every one of
those matched a product: the Exchange ones resolved to the msexchange
slug with the year pulled straight out of the name, so the tester's SE
host carried CRITICAL EOL-MSEXCHANGE-2007 findings for a product that is
not installed, and "Microsoft Lync Server 2013, Bootstrapper
Prerequisites Installer Package" — which ships with an Exchange install
— was read as a Lync 2013 server.

The flapping followed from that. msexchange is a single-release slug, so
the 2007, 2010 and 2016 rows superseded each other in turn, one status
flip per add-on per run, filling the change history with alternating
open/patched entries while nothing on the host changed.

is_component_package() is consulted by BOTH resolvers, so a component is
refused whichever source would have matched it. The real product entries
sit in the same inventory ("Microsoft Exchange Server", "… 2016
Cumulative Update 23"), so the true 2016 finding survives — it is now
attributed to the server instead of to a Greek language pack. Findings
already open close themselves on the next sweep: the package is gone
from the inventory, which the reconcile reads as uninstalled.

Second bug in the same area: MS-lifecycle findings were keyed
EOL-MS-LIFECYCLE-{release}, and that export says "Original Release" for
almost everything. Every MS-lifecycle product on a host therefore shared
ONE row, whose title, package and description were overwritten by
whichever package the sweep touched last — the tester's Lync 2013
finding kept turning into Visual C++ 2012 and back, with a status change
logged each time. Keyed per product now; rows written under the old
shared id are closed by the revalidate pass with their own reason rather
than left as duplicates.
2026-08-12 13:05:28 +02:00
vulncheck b634e0658f feat(gui): link GitHub advisories from the CVE detail view
NVD and cvelistV5 have no record for a lot of the ids this scanner
produces, and the GLOBAL GitHub advisory database does not have them
either: github.com/advisories/GHSA-8c6v-7g3w-prrq is a 404 while
wazuh/wazuh's own advisory page serves it. Repo-published advisories
only exist on the repo.

So every CVE now offers a GitHub chip. When the affected product maps to
a repo that publishes its own advisories (the existing _REPO_MAP), the
link points at that repo — the detail endpoint upgrades it to the
advisory itself when the cached repo index knows the GHSA id, and falls
back to the repo's advisory search otherwise. Everything else gets the
global advisory search. All of them are search pages or verified
records, so none can 404.

Two things fixed in passing:

- The GHSA branch of the External References block was dead code: the
  block was guarded on /^CVE-/, so a GHSA-only finding (the ones added
  in 6304b66) showed no references at all.
- The `references` column has been written by the Nessus sync since it
  landed and was never rendered anywhere. Those vendor advisories are
  often the only page describing the flaw, and for a NESSUS-PLUGIN- row
  they are the only reference it can have — the block now renders for
  those rows too, with the catalogue links still restricted to real CVE
  ids so no NVD link is built from a plugin id.
2026-08-12 13:05:14 +02:00
vulncheck 6304b661f9 feat(scan): findings for advisories that carry no CVE number
4 of Notepad++'s 18 advisories and 10 of Wazuh's 60 have only a GHSA id. They
were skipped, on the reasoning that the pipeline keys on CVE numbers — which
made them invisible to every source we have, since NVD and cvelistV5 are keyed
on CVE numbers by definition. A published flaw with a version range is a
finding whether or not a number was ever assigned.

The GHSA id stands in as the identifier, the way NESSUS-PLUGIN- and EOL- ids
already do in this schema, and it fits the column with room to spare.

What such a finding does not get is threat intel. KEV, EPSS and the CVSS
enrichment all look up a CVE number and skip anything else on their own
(enrichment_service filters on the prefix), so it simply carries none — honest,
since none exists for it. The detail page swaps the NVD and CVE.org links,
which would 404, for the advisory itself.

Measured against the versions in the test environment, this adds nothing
today: at Wazuh 4.14.1 and Notepad++ 8.9.5 every matching advisory happens to
have a CVE id. The CVE-less ones are on other release lines (5.0.0-beta). So
this is coverage for the case rather than a fix for a current gap.

The same run shows the component filter doing its job: at 4.14.1 the agent
gets 1 finding and the manager 13, from the same 60 advisories.
2026-08-12 10:11:23 +02:00
vulncheck 7f36f9b6fb fix(scan): fix floor on the NVD path, advisory summaries, subsystem fallback
Three things from one screenshot, and the second one explains the first
report.

THE FIX FLOOR ONLY WORKED ON ONE PATH

2a831e4 derived ">26.5.2.2" from a lessThanOrEqual bound, but only in
cvelistV5. Node.js comes from there and reads "not announced — need newer than
24.14.0"; the Acrobat extension comes from NVD, where the same fact is spelled
versionEndIncluding, and read a bare "not announced". Derived on both paths
now, with the same rules: a real fix version wins, a commit hash is neither.

THE ADVISORIES NEVER PASSED THEIR SUMMARY ON

The GitHub summary was cached and then dropped, so those findings displayed
only the title we generate ("Wazuh Agent 4.14.2 — CVE-2026-44901") and nothing
about the flaw. Worse than cosmetic: the manager/agent prose check in _upsert
reads that text, and with nothing to read it could not do its job.

A SUBSYSTEM FALLBACK FOR ADVISORIES WITHOUT A PACKAGE

11 of wazuh/wazuh's 60 name no usable package ("Wazuh v4.14.1"), leaving the
component filter nothing to work with. The title usually says it anyway: the
cluster and its DAPI are how manager nodes talk to each other, an agent has
neither, while syscheckd and logcollector run on the endpoint. Used only when
the package field is silent, and only when exactly one side is named — a title
mentioning both is left to the version ranges.

Verified live: CVE-2026-44901 (manager) is dropped on agents and kept on
managers, CVE-2026-49392 (agent) the other way round. Notepad++ is untouched —
its advisories name no component, so nothing is filtered there.

Still open, and worth a decision: 4 of Notepad++'s 18 advisories and 10 of
Wazuh's 60 carry no CVE id at all. They are skipped, because the rest of the
pipeline keys on CVE numbers for enrichment, KEV, EPSS and cross-scanner
dedup. Covering them means giving a finding a GHSA id instead.
2026-08-12 10:03:40 +02:00
vulncheck 7088c552e2 fix(scan): a rate-limited advisory build must store nothing
The pagination guard stopped the fetch on a 403 but not the build: execution
fell through to _store, which wrote the truncated index and a fresh timestamp,
so a partial answer was served as authoritative for the full 24h TTL —
the same failure the guard was written against, one level up.

Nothing is stored now, and the last complete index is returned. Returning an
empty one would be worse than stale: every repo-advisory finding would go
untouched, which the reconcile reads as resolved. _stored() is the raw read,
load_index() keeps the TTL check on top of it.

Two more from the same review:

Commit hashes could still reach a fix version. _load_via_nvd harvests the same
NVD bounds as the scanners into FixCandidates and had no is_version check, so
"before 44bf114d2f49" survived on that path after both scanners rejected it.
It imports the scanner's rule rather than carrying a third copy.

A repo that answers with no advisories is recorded as such again; one that
never answered is not. The earlier `if not payload: continue` collapsed both
into "never checked".

The non-manager package rule is now NON_MANAGER_PKG_RE, one constant that
_upsert and the test share — the test compiled its own copy of the pattern and
would have stayed green if the production one changed. Covered by a new check
that stubs a 403 and asserts nothing is written.
2026-08-12 08:58:08 +02:00
vulncheck b968407f58 fix(scan): manager-side CVEs off every non-manager package, not just the agent
The guard named wazuh-agent only, so CVE-2026-25769 — cluster-protocol code
execution between master and worker — moved to the next package along and sat
on a dashboard host instead. A dashboard takes part in that protocol no more
than an agent does, and neither does the indexer.

The rule now covers agent, dashboard and indexer; manager and server keep
everything, since that is what the CVEs are about.

This is the fallback for NVD and cvelistV5, which have no component field. The
GitHub advisories state it outright and are matched on that — but this
particular CVE names its package "Wazuh v4.14.1" there, which identifies no
component, so the prose check is what has to catch it.
2026-08-12 08:39:49 +02:00
vulncheck be3c111a93 fix(gui): match the audit-log pager to Vulnerabilities and Assets
Audit logs had its own pagination bar: a "Rows per page:" label, 25/50/100/250
steps and full-word First/Prev/Next/Last buttons. The Vulnerabilities and Assets
tables use "Per page" with 50/100/250/500/1000 and compact «/‹ Prev/Next ›/»
controls. Same bar everywhere now; 1000 is the cap on /audit/logs.
2026-08-11 22:37:54 +02:00
vulncheck 41f5a422d4 feat(scan): use the component GitHub states, instead of inferring it
The tester pointed at the right field. NVD files agent, manager and dashboard
under one CPE and cannot express which is meant, so a manager flaw landed on
every endpoint. The GitHub advisories say it outright, in
vulnerabilities[].package.name: 41 of wazuh/wazuh's entries name
"wazuh-manager", 13 name the agent, one the dashboard, and two describe the
project's CI rather than any product.

That is stated fact, and it beats the prose heuristic on both counts.

It covers the direction the heuristic could not: CVE-2026-49392 is agent-only
and stayed open on the manager, because reading "this is only about the agent"
out of prose is far harder than spotting manager wording. The component field
answers both directions at once.

And it removes CVE-2026-67308 properly. That one is about GitHub Actions
workflows, so no amount of manager/agent wording applies — its component is
"wazuh/wazuh (CI workflows)", which corresponds to nothing installed anywhere
and now lands on no host.

Deliberately one-sided in the safe direction: an advisory that names no
component, or a package shape we do not recognise, keeps its finding. Only a
stated mismatch drops one.

The prose heuristic stays for NVD and cvelistV5, which have no such field.
Cache key bumped so the component is available immediately rather than after
the 24h TTL.
2026-08-11 14:03:41 +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 eef8f5d2d0 fix(scan): follow pagination on repo advisories
One page of 100 was fetched and the Link header ignored. wazuh/wazuh sits at
60 advisories and notepad-plus-plus at 18, so nothing is missing today — but a
repo crossing 100 would have lost everything past the first page without a
word, which is the failure mode this whole day was made of: a source that
answers 200 and quietly returns part of the answer.

Pages are followed via rel="next", capped at 20 so a malformed header cannot
loop. A multi-page fetch is logged, and hitting the rate limit stops the whole
build rather than storing a partial index as if it were complete.

Checked while answering where these advisories come from: the per-repo
endpoint carries the full history (2023 through 2026, no second page), while
the global advisory database returns exactly one Wazuh entry — it is organised
by package ecosystem and Wazuh is not a package in any of them. The per-repo
endpoint is the right source; the global one would add nothing.
2026-08-11 08:51:30 +02:00
vulncheck 9ea5e76d19 fix(scan): keep manager-side Wazuh CVEs off agent-only hosts
Wazuh files agent, manager and indexer under one CPE, so every server-side CVE
landed on every endpoint running the agent. CVE-2026-25769 is the clearest
case: code execution "on the master node", reached "from a worker node" over
the cluster protocol. An agent speaks 1514/1515 to a manager and is not part
of that protocol at all — the finding sat on a client workstation.

Findings whose description names the server side and nothing else are dropped
when the package is wazuh-agent. The check is deliberately timid: any text
that also mentions the agent is kept, because a flaw in how the manager
handles agent input is a real risk on both sides, and missing a genuine agent
CVE is the more expensive error.

It lives in _upsert, where all three sources converge, so NVD, cvelistV5 and
the GitHub advisories are covered by one rule. Findings already stored close
themselves on the next scan: the CVE is no longer re-detected, so the existing
reconcile marks it patched.
2026-08-11 08:50:00 +02:00
vulncheck b7fbeb7598 fix(gui): fill the audit Asset column for lifecycle events too
Third and last place this was missing. ASSET_DEACTIVATED and
ASSET_REACTIVATED wrote the bare new status ("inactive") into new_value, which
is not parseable, so the Asset column stayed empty on exactly the rows that
are ABOUT an asset — while the hostname sat in the description right beside
it.

Same JSON shape as the vulnerability events now, with the reason alongside the
host, so the column fills for all three kinds: findings detected, findings
changed, and assets deactivated or revived.
2026-08-11 08:13:04 +02:00
vulncheck 6382c6daa5 fix(sync): put an address in the IP column, not a hostname
The all-in-one fallback took the host straight out of the configured API URL,
so an installation addressed by name ended up with an FQDN in a column called
IP ADDRESS — which sorts and filters as text and is not what anything reading
that field expects.

It is resolved now, and passed through untouched when the URL already contains
an address. If DNS has no answer the field stays empty: better nothing there
than a value of the wrong kind.
2026-08-10 19:40:31 +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 901f6fc088 fix(scan): repo advisories — real versions only, and see rpm-packaged installs
Three defects the tester found within minutes of the GitHub source going live.

A COMMIT HASH IS NOT A FIX VERSION

Wazuh answers some advisories with "44bf114d2f49" instead of a release, and it
was passed through verbatim: the finding told him to upgrade to an upstream
commit. Only a dotted-numeric build is stored now. A fixed_version is written
once and never revised, so a wrong one outlives the finding — empty is
recoverable, wrong is not.

THE ADVISORIES NEVER REACHED A LINUX AGENT

This path read the raw version field. Fine for Notepad++, wrong for anything
shipped as an rpm/deb: a Wazuh agent reports "4.14.1-1" and comparing that
against "4.14.6" matched nothing. The advisories therefore only ever landed on
the cluster node, whose version is already clean, and quietly skipped every
agent — which is most of the estate. It now resolves the version the same way
the other scanners do.

NO IP ON AN ALL-IN-ONE NODE

A single-node install reports no address, because there is no cluster to
address, so the asset had an empty IP column. The configured API URL points at
exactly that machine and is used as the fallback — only when there is one node,
where the inference actually holds.
2026-08-10 18:58:19 +02:00
vulncheck c81d0309a5 fix(gui): fill the audit Asset column for new-finding events too
Status changes carry the host in new_value and render correctly; the
VULNERABILITY_DETECTED rows wrote no new_value at all, so the new Asset column
was empty for exactly the events there are most of. The hostname was already
in the description text, which is the part that gets truncated.

Those rows now carry the same JSON as the status changes — host, CVE, source
and severity — so the column fills for both kinds and the data is there for
filtering later.
2026-08-10 18:43:59 +02:00
vulncheck ff298ec23f feat(scan): read Wazuh advisories from GitHub, ahead of the CVE feeds
The tester was right that NVD alone leaves a hole, and it is bigger than
expected. Checked live against the repo: Wazuh 4.14.5 — the newest release NVD
knows any bound for — is affected by 10 GitHub advisories, three of them
critical, all fixed in 4.14.6. A host on 4.14.5 looks clean to NVD and to
cvelistV5 and is not. Several advisories carry no CVE id at all, so nothing
keyed on CVE numbers can ever see them.

No new service: github_repo_advisory_service already does exactly this for
Notepad++, so Wazuh is one line in its repo map, covering the agent, manager,
server, indexer and dashboard packages plus the synthesised "wazuh-server"
name from the cluster-node scan. The API needs no token for public advisories.

Its range parser did need one addition. It was written against Notepad++'s
free-text ("<= v8.9.6.4", "old versions - 8.8.1") and fell through to the
"below patched" rule for GitHub's own ">= 4.3.0" form. That happened to give
the right answer for 4.x, but it ignores the floor — a 3.x install would have
inherited an advisory that starts at 4.0.0. The form is now parsed properly,
with the optional upper bound.
2026-08-10 18:43:05 +02:00
vulncheck 5689365a60 fix(scan): strip the v from a node version before scanning
The cluster API reports "v4.14.1" on some builds. The sync strips the prefix,
but an asset synced before that still carries it — and _clean_version rejects
the entire string over that one letter, so the node resolved fine and was then
never actually scanned. Stripped at the point of use as well, so a stored
value that is not clean no longer costs the check.
2026-08-10 16:23:13 +02:00
vulncheck ee337dcfe2 fix: widen wazuh_agent_id — node ids did not fit, sync returned 500
assets.wazuh_agent_id was String(10), sized for a numeric agent id ("042").
Manager nodes are stored in the same column as "node:<name>", which is 11
characters for "node:master" alone and free-form beyond that, so the insert
was rejected on length and the whole Wazuh sync answered 500. Widened to 64
and the id is truncated to fit.

The failure also showed the block was not as contained as intended: the
guard around the node sync caught nothing, because the offending insert was
only validated at the agent loop's commit further down, outside it. It now
flushes inside the guard and rolls back on failure, so a problem with a node
costs the nodes and not the entire asset sync.
2026-08-10 16:20:06 +02:00
vulncheck adfc2cf898 feat(scan): sync Wazuh manager nodes as assets and scan their version
Detecting wazuh-agent covered the hosts that run one. Master and worker nodes
run none — they inventory other machines and nobody inventories them — so they
were never assets at all, and the heaviest Wazuh CVEs are precisely the ones
on the server. The tester has master and worker on 4.14.1 and saw nothing.

The cluster API reports its own version without an agent being involved, over
the session that already fetches agents, so no new credentials and no new
integration: /cluster/nodes during the Wazuh sync, falling back to
/manager/info on a standalone install where the cluster endpoint is off.

Each node becomes an asset with a "node:<name>" id. The scan reads that prefix
and, instead of asking syscollector for a package list that cannot exist,
takes the node's own version as its inventory — which then runs through the
same registry, ranges and reconcile as everything else. A node with no version
reported contributes nothing rather than a guess.

Dashboard and indexer are unaffected: those do carry agents and were already
covered by the wazuh-agent detection.
2026-08-10 16:00:27 +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 5436745d0e fix: give the remaining asset foreign keys a delete rule
Migration 040 fixed the two constraints pointing at vulnerabilities, which was
only half the chain — I checked what referenced findings and not what
referenced assets. So the delete got one step further and failed on the next
constraint instead:

    ForeignKeyViolation: update or delete on table "assets" violates foreign
    key constraint "notification_logs_asset_id_fkey"

This time every foreign key into the delete path was enumerated. Three had no
rule, each gets the one that fits: the notification log keeps its row with the
reference cleared (it records that a mail WAS sent, which stays true), while
scans and findings belong to their asset, are NOT NULL, and go with it. The
asset_groups association is included even though SQLAlchemy clears it itself —
the database should not depend on the application getting the order right.

Verified no foreign key into assets, vulnerabilities or scans is left without
a rule.
2026-08-10 15:37:03 +02:00
vulncheck a7b601001c feat(scan): report disabled browser extensions too
A disabled extension was skipped before any lookup, on the reasoning that code
which does not execute is not a live risk. Wrong call: the vulnerable code is
still in the user's profile, and a click, a policy push or a profile sync puts
it back in the browser. Latent, not absent.

It also made the tool inconsistent with itself. Wazuh reports every installed
Linux kernel, including ones the machine has never booted, and nobody argues
those should be hidden for not running right now — same situation, same answer.

The finding carries the state in its name ("… [disabled]") so whoever picks it
up can weigh it, and enabled/disabled are tracked separately, so an extension
present in one profile and switched off in another shows both.
2026-08-10 15:20:28 +02:00
vulncheck 9558129bc1 feat(gui): show the affected asset as its own audit-log column
Naming the host inside the description was not enough: that column truncates
at max-w-xs, and these rows are long — "Vulnerability EOL-CHROME-150 status
changed: open → patched (Superseded — asset moved to a newer chrome release
(EOL-CH…" cuts off before it. So the machine was still only reachable by
exporting the CSV and reading new_value by hand.

It has been in new_value's JSON all along, so the column reads it from there
and no backend change is needed. Rows written before the hostname fix show a
dash rather than a guess.
2026-08-10 14:39:57 +02:00