Commit Graph
24 Commits
Author SHA1 Message Date
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 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 e8e86396cb fix(eol): cap the severity of rapid-release "end of life"
Firefox and Chrome ship every four weeks, and the previous version stops
receiving patches the day the next one lands. By endoflife.date's definition
every install except the very newest is end-of-life — the tester's host moved
Firefox 150 to 152 and the finding came straight back as EOL-FIREFOX-152 at
CVSS 9.0, on its way to critical (9.8) as the day counter climbed.

That put "one release behind on a browser" above Exchange Server 2016, which
really is abandoned. Both were HIGH, and the browser was overtaking it purely
by age.

The finding stays — an unpatched browser is worth knowing about — but it is
capped at medium for these two, and the days-past-EOL escalation no longer
applies to them: on a four-week cycle that counter measures release cadence,
not growing risk. The text says so too ("pending update, not an abandoned
product") and names the current release. For these products the CVE scan is
the precise signal anyway; it says which hole is open rather than only that
the version is stale.

Scoped to a named set, so anything on a real support lifecycle — Windows,
Exchange, Office, SQL Server, Node, Java — keeps the full weight.
2026-08-10 14:22:32 +02:00
vulncheck 153809a3f1 fix(eol): detect Exchange 2016, and close findings when software is uninstalled
EXCHANGE SERVER 2016 STILL LOOKED SUPPORTED

Fixing the slug was necessary but not sufficient. endoflife.date names the
msexchange releases 2016 / 2019 / subscription while syscollector reports
15.1.2507.6, so prefix-matching a version against a year can never hit — the
same shape as Office and SharePoint, which is why those are already year-keyed.
Exchange is now too.

That alone still misses the plain "Microsoft Exchange Server" entry sitting
next to "…2016 Cumulative Update 23" in the same inventory: same install, no
year in the name to key on. So a build number is also matched against the
major.minor of each release's latest build — 15.1 IS 2016, 15.0 is 2013.
Deliberately returns nothing when two releases share it: 2019 and Subscription
Edition are both 15.2, and guessing would put a supported SE host on a release
that went end-of-support in 2025.

UNINSTALLED SOFTWARE KEPT ITS EOL FINDING

Supersede closes the old release when a newer one appears, which is why
Chrome 150 -> 151 has always worked. It cannot cover an uninstall: nothing
newer shows up, so nothing supersedes the finding. Since supersede was
restricted to single-release products, side-by-side software had no route out
at all.

The inventory answers it — a product the scan did not see is not installed —
but only when there IS one. An empty package list is a failed or not-yet-
populated fetch, never proof that a machine runs no software; closing on it is
exactly the mistake the app scan made with a re-registered Wazuh agent, so the
same precondition applies here.

Only findings this scan owns are retracted, leaving mobile/Intune EOL findings
alone. Wired into both the GUI check and the nightly job so the two agree,
counted as eol_uninstalled_closed.
2026-08-10 13:59:31 +02:00
vulncheck e0a56b9053 fix(eol): stop the supersede/reopen fight on side-by-side products
Supersede assumed "a product runs exactly one release per asset". True for a
browser or an OS, false for most Windows components: the tester's host carries
Visual C++ 2008, 2010, 2012, 2013, 2015 and 2022 redistributables at the same
time, all genuinely installed and several genuinely EOL.

So the two halves of the EOL check fought each other inside a single scan.
Processing the 2008 entry closed the 2013 finding as "superseded — asset moved
to a newer release"; processing 2013 reopened it and closed 2008; and so on
for every pair, every night. The change history filled with 50 alternating
rows, the audit log with hundreds, and the "newer release" a 2013 product was
supposedly superseded by was 2008 — five years older. Nothing on the host had
changed at any point.

Supersede now applies only to products a device runs one of (browsers, the OS,
Exchange, SharePoint). For anything installed side by side, an older release
still being present is the normal state, not a leftover, and its finding stays
open — which is also the correct answer, because that software really is
installed and really is end-of-life.

Not addressed here: nothing closes an EOL finding when a side-by-side product
is actually uninstalled. That needs the EOL check to reconcile against the
inventory the way the app scan does, rather than inferring removal from the
presence of a different release.
2026-08-10 09:51:30 +02:00
vulncheck fdd1019fbd fix(eol): correct the endoflife.date slugs — seven pointed at nothing
The product mapping was hand-written and never checked against the actual
catalogue, so a quarter of it was wrong. Every EOL run asked for ms-office,
exchange-server, java, visual-cpp, adobe-acrobat, apache and iis, and got 404
back each time — visible in the log as "not in catalog", invisible everywhere
else. The scan simply found no lifecycle data and moved on, which is why
Microsoft Exchange Server 2016 came out looking supported.

Checked against the live list (462 products) rather than guessed:

  exchange-server -> msexchange           (the reported Exchange 2016 case)
  ms-office       -> office
  apache          -> apache-http-server
  java            -> oracle-jdk

The other three do not exist there at all and never will: endoflife.date has
no record for Adobe Acrobat, the Visual C++ redistributables, or IIS. Their 18
mapping entries are removed rather than left to 404 on every run — IIS ships
with Windows Server and is covered by that entry, the other two depend on the
Microsoft export or a vendor page. Same reasoning as the existing Edge note.

tools/check_eol_slugs.py verifies the whole mapping against the live catalogue
and exits non-zero when a slug disappears, so this cannot rot again unnoticed.
2026-08-10 09:34:48 +02:00
vulncheckandClaude Opus 5 fdec6a7446 chore(eol): drop the dead Microsoft Edge mapping
endoflife.date carries no record for Edge — the 'microsoft-edge' slug 404s,
and no entry in their catalogue matches 'edge' at all — so every EOL check
spent a request to find that out.

Nothing is lost: Edge follows the Modern Lifecycle Policy and has no
end-of-life date while it stays current, which makes 'is this version too old'
a patch question. The CVE scan already answers that one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 21:16:01 +02:00
vulncheck da325bca77 fix(eol): close stale MS-lifecycle FPs from any path; stop the export refetch storm
1) The Edge→'Azure Stack Edge' false positive survived every nightly run.
   c3cd62c added a reconcile, but it lives in run_eol_for_packages while
   the EOL-check button has its own loop and never called it — so a
   finding produced by a since-corrected name match stayed open forever.
   New revalidate_ms_lifecycle_findings() re-asks the resolver for every
   OPEN EOL-MS-LIFECYCLE finding and closes the ones that no longer
   match, independent of which path ran. Called at the end of the EOL
   check; audited with the host name.

2) The endless GET .../lifecycle/products/export/ run: the in-process
   memo was only set on SUCCESS, so once the fetch or the xlsx-link
   lookup failed, every one of the thousands of scanned packages
   re-fetched the page — which is also why the UI eventually gave up with
   'Backend connection failed' while the backend kept going. Failures are
   now memoised too (negative cache), so a broken source costs one
   request per hour instead of one per package.

3) EOL dialog text was stale: it named only endoflife.date and said
   nothing about the MS-lifecycle export, the built-in exotics, the OS
   check, auto-closing, or that Intune-only devices are covered elsewhere.

4) CVSS-correction dialog: one numbered source per line, as requested.
2026-07-27 12:10:31 +02:00
vulncheck a74c229264 fix(scan): stop closing multi-stream CVEs across branches; audit every reopen
1) FALSE NEGATIVE (regression from the Wazuh-override I added earlier):
   _inventory_confirms_fixed compared installed vs fixed_version without
   checking they belong to the same servicing branch. Multi-stream CVEs
   store only ONE stream's fix, so the compare was meaningless and closed
   still-live findings — tester: MySQL 8.0.23 vs stored fix 7.6.34 (8.0.23
   sits inside the separate 8.0.0–8.0.42 affected range), Suricata 8.0.0 vs
   stored fix 7.0.12 (inside 8.0.0–8.0.1). Both were auto-resolved wrongly,
   then reopened by the next Wazuh sync. Now the majors must match — same
   lesson as the Windows build-line guard: only compare within one release
   line. Edge/Notepad++ single-stream cases still close as before.

2) patched → open was never audited. Every scanner reopened findings
   inline, so only the positive direction (open → patched) appeared in the
   audit log and per-CVE Change History — the tester watched CVEs silently
   flip back to open with no record. New audit_events.reopen_if_patched()
   does the transition AND writes the status-change row; all 11 reopen
   sites (wazuh, nessus x2, defender, app-scan, msrc, m365, eol,
   mobile-eol, android) now go through it. Verified the resulting status is
   identical to the old branch logic for every status value — the only
   change is that the row now gets written.

3) Silence the pydantic v2 import warning (orm_mode → from_attributes).
2026-07-25 17:32:10 +02:00
vulncheck c3cd62c461 fix(eol): resolve stale MS-lifecycle findings the rescan no longer matches
run_eol_for_packages only ever ADDED findings; _supersede_old_eol fires
only when a replacement is created. So after the Edge→Azure-Stack-Edge
name-match fix, the wrong 'Microsoft Edge = EOL' finding stayed open
forever (no replacement is produced for a now-unmatched product).

Add a reconcile scoped to the EOL-MS-LIFECYCLE- prefix (package path
only; OS-level endoflife.date findings use other slugs and are
untouched): resolve open MS-lifecycle findings the current run didn't
re-produce. Guarded on a non-empty package list so a failed inventory
read can't mass-close.
2026-07-24 10:10:36 +02:00
vulncheck 9007cdc03a feat(eol): escalate severity by days-past-EOL + tag compliance frameworks
Adopts the endoflife.date/Wazuh EOL-risk model: a product that has been
past end-of-life for 1000+ days is now CRITICAL (cvss 9.8) rather than a
flat HIGH — the longer it runs unpatched, the higher the standing risk.
Titles carry the age ('EOL 1500d'). EOL findings also cite the control
failure (PCI-DSS 6.3.3, NIST 800-53 CM-8, HIPAA 164.312(a)(1)) so audit
reports can reference it. EOL-SOON / EOAS tiers unchanged.
2026-07-24 08:49:02 +02:00
vulncheck f5c2c58ab9 fix(eol): detect SharePoint end-of-life (Foundation/Server 2013-2019)
Neither the MS-lifecycle export nor the slug lookup knew SharePoint, so
Foundation/Server installs never produced an EOL finding — tester hit this on a
SharePoint Foundation 2013 box (EOL since 2023-04-11) with 2016/2019 going EOL
2026-07-14. endoflife.date does track it as 'sharepoint'; two gaps had to close:

- add the slug aliases (Server / Enterprise Server / Foundation / Designer)
- mark it year-keyed: releases are years ('2016') while the install reports a
  build ('16.0.5556.1005'), so prefix-matching the version against a cycle can
  never hit — the year comes from the product name (same shape as Office, and
  worse: 2016, 2019 and Subscription Edition ALL report 16.0.x, so the version
  alone cannot even tell the releases apart)

Subscription Edition is still supported and is skipped by the existing
wrapper-token guard, so it can't produce a false EOL. Dates verified against
the live endoflife.date API.
2026-07-14 12:04:40 +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
vulncheck 922341b378 feat(intune): Phase 2 — detectedApps feed EOL + M365 detection
Intune managed devices often run without a Wazuh agent, so their installed
software was invisible to EOL/M365 detection. Now the Intune sync reads
each device's detectedApps (Graph $expand=detectedApps) and runs them
through the existing detection.

- eol_service.run_eol_for_packages(db, asset, packages): source-agnostic
  per-package EOL (endoflife.date → MS-lifecycle/exotics fallback), same
  precedence as the eol-check endpoint.
- m365_service.run_m365_for_packages(db, asset, packages): source-agnostic
  M365-Apps CVE detection (build-vs-channel) + real-metric correction.
- intune_service: detectedApps enrichment now on by default (toggle
  "Detected apps (EOL/M365)" in the Intune settings card).

Findings are CVE-level, so OSV/MSRC/Ubuntu remediation enrichment and the
normal EPSS/KEV/date enrichment apply automatically. No migration.
2026-06-13 10:37:22 +02:00
vulncheck 00d22fc318 feat(audit): initial VULNERABILITY_DETECTED event for sync-created findings
Tester: a CVE newly created by a sync appeared in the vuln list but left
NO initial audit entry ("new CVE detected on asset X at ...") — the audit
trail only began with the first status change (open -> patched etc.).
Confirmed: not intentional, simply never built; fails the revisionssicher
requirement.

- Migration 031: ALTER TYPE auditeventtype ADD VALUE
  'VULNERABILITY_DETECTED' (idempotent, autocommit block).
- New app/services/audit_events.py: audit_new_vulnerabilities() writes one
  System/Auto event per newly created finding:
  "New finding detected: CVE-X on <hostname> (severity=..., source=...)".
- Wired into every creation path:
    * Wazuh full sync (run_wazuh_vulnerability_sync)  -> source=wazuh
    * Wazuh per-agent sync (sync_agent_vulnerabilities, covers the
      scheduler loop + per-asset rescan)              -> source=wazuh
    * Nessus sync (incl. Nessus EOL pseudo-vulns via
      newly_created_vuln_ids)                         -> source=nessus
    * endoflife.date upsert                           -> source=eol_check
    * M365 Apps upsert                                -> source=m365_check
  Full-sync and per-agent paths are independent (no double events).
- Best-effort: audit failure never breaks a sync.

Migration 031 required: alembic upgrade head.
2026-06-10 08:58:38 +02:00
vulncheck 3693cfce24 fix: Newly-Published null-date flood + Adobe Reader EOL slug
Tester batch 2026-06-03 (screenshot).

#2 "Newly Published" table jumbled after a Nessus sync. Cause: the
published_date sort used COALESCE(published_date, detected_at).
Nessus imports old CVEs (2014-2023) with published_date NULL +
detected_at=today, so COALESCE made every freshly-imported old CVE
rank as "published today" and flood the top in arbitrary id order.
Now sorts by real published_date with nulls-LAST — null-date rows
sink instead of masquerading as newest.

#1 Adobe Reader EOL finding showed as EOL-NESSUS-56213 instead of a
product slug. Nessus plugin 56213 names the product "Adobe Reader"
(no "Acrobat"), so the acrobat-prefixed slug keys never matched.
Added adobereader / acrobatreader → adobe-acrobat aliases.

#3 (asset INACTIVE not set after a reduced-scope Nessus sync) — the
reconcile_missing_from_sync path + AssetSource.NESSUS re-tagging +
migration 028 already handle this in current dev; tester deploy is
likely behind. Needs: deploy latest + alembic upgrade, then re-test.
If still broken, the "asset sync-reconcile (nessus): N ACTIVE assets
have no nessus_host_uuid pinned" log line distinguishes upstream
(unpinned) from logic.
2026-06-03 11:09:10 +02:00
vulncheck 9385ca4e0d fix(eol): add VC++ Redistributable slugs + reject sub-component matches
- _PRODUCT_SLUGS extended with visual-cpp aliases (vcredist,
  microsoftvisualc..., msvcr, msvcp) so 2005/2008/2010 etc. flow
  through to endoflife.date's visual-cpp product.
- _WRAPPER_TOKENS adds nativeclient, setupsupportfiles, setup,
  premium, clicktorun, subscription — these name a tracked product
  but have a different lifecycle, so they were producing false EOL
  matches (e.g. SQL Server 2008 R2 Native Client inheriting SQL
  Server's EOL date).

Fixes feedback 2026-06-02 #6 (Coverage Gap missing EOL entries).
2026-06-02 14:34:52 +02:00
vulncheckandvulncheck 808246f0a9 fix(feedback-2026-06-01): sync-driven INACTIVE, EOL naming, sortable assets
Tester feedback round 2026-06-01 — 8 issues grouped into 4 fixes.

**A. Sync-driven INACTIVE reconciliation** (highest priority)
- AssetSource.NESSUS enum value added; pre-existing MANUAL assets with
  a nessus_host_uuid re-tagged in 027 migration.
- New `reconcile_missing_from_sync()` helper in asset_lifecycle flips
  ACTIVE assets of a given source (WAZUH/NESSUS) to INACTIVE when their
  id (wazuh_agent_id or nessus_host_uuid) is missing from the latest
  sync. Vice-versa reactivates INACTIVE assets that re-appear.
- Hooked from nessus_sync.run_nessus_sync (after per-host loop, before
  commit) and from POST /api/v1/assets/sync_wazuh (after the agents
  loop). Both return assets_inactivated/assets_reactivated counts in
  their response.
- INACTIVE-asset CVEs now hidden from list_vulnerabilities and
  get_dashboard_statistics by default (new
  `include_inactive_assets` query param opts back in).
- AuditEventType gains ASSET_DEACTIVATED + ASSET_REACTIVATED so the
  transitions are filterable in the audit UI.

**B. EOL pseudo-CVE naming for Nessus + endoflife.date**
- nessus_sync: new `_office_pseudo_cve()` returns
  EOL-MS-OFFICE-{YEAR} for Office variants, else EOL-NESSUS-{pid}.
  `_normalise_office_pkg()` collapses MUI/Proofing strings to "MS
  Office". Title suffix "OSX MUI..." trimmed for Office.
- nessus_sync: end-of-pass sweep collapses pre-existing
  EOL-NESSUS-{pid} Office rows into the unified EOL-MS-OFFICE-{year}
  anchor (status=patched + audit).
- eol_service: _PRODUCT_SLUGS now maps microsoftoffice[proofing/osxmui
  /osxmuigerman/formac] -> ms-office. _YEAR_KEYED_SLUGS includes
  ms-office. _pseudo_cve_id produces EOL-MS-OFFICE-{year} for free.
- eol_service: existing-row branch bumps detected_at so the
  sort_by=detected_at widget ranks freshest finding first.

**C. Asset view icon asymmetry + dashboard panel fix**
- assets/page.tsx: coverage-gap icon now shows for any asset with
  wazuh_agent_id OR nessus_host_uuid.
- nessus_sync._find_or_create_asset: backfills ip_address on existing
  assets (UUID / hostname match) so the "Launch targeted Nessus scan"
  button (already gated only on asset.ip_address) lights up.
- frontend types + backend AssetResponse expose nessus_host_uuid +
  source.
- page.tsx CVE-id cell: `break-all` -> `whitespace-nowrap` so the
  free L/R dashboard space isn't wasted on character-wrapped ids.

**D. Assets table sortable columns**
- list_assets: new sort_by / sort_order with whitelist-driven ORDER BY.
  Last-scan sort uses nulls_last(). Joins Policy/User only for those
  columns. Unknown sort_by falls back to hostname asc.
- assets/page.tsx: sortBy/sortOrder state, handleSort toggler, SortArrow
  indicator, clickable headers with cursor-pointer + hover style, and
  a useEffect re-fetch on change. Mirrors the pattern in
  vulnerabilities/page.tsx.

**Out of scope (flagged)**
- `delete_asset` remains a hard delete with cascade. The user's
  "previously-deleted asset not re-created by Scan+Sync" expectation is
  intentional design — the auto-create gate (auto_create_assets) and
  the new INACTIVE flip cover the rest of the use cases.
- DECOMMISSIONED AssetStatus is still unreachable through automation.
- Vulnerability-count column in assets table stays non-sortable (it
  is a Python-side count, not a SQL column).

**Migration**
- 027_add_nessus_source_and_audit_events.py: ALTER TYPE for both
  enums + UPDATE backfill. Run `alembic upgrade head` once on prod.
2026-06-02 08:58:51 +02:00
vulncheck afd03cefac fix(eol): skip third-party wrappers + anchor slug match (Veeam FP)
'Veeam Explorer for PostgreSQL 13' (v13.1.5.2) was matched to the
postgresql endoflife slug because resolve_product_slug did an
unanchored substring scan ('postgresql' appears in the name) and the
tool's own version 13.x prefix-matched PostgreSQL release 13 -> bogus
HIGH 'EOL-POSTGRESQL-13' finding. The version is Veeam's, not Postgres'.

Two guards: a _WRAPPER_TOKENS denylist (veeam, explorer-for, connector,
odbc, jdbc, driver, ...) that skips name-dropping tools outright, and
the substring scan is now prefix-anchored so a product name must START
with a known key. 'Microsoft SQL Server 2016 Express' and the Office
year match still resolve; '... for PostgreSQL' no longer does.
2026-06-01 13:22:08 +02:00
vulncheck f2a2f644e1 fix(eol+ui): Office year-keyed EOL match + table overflow clipping
eol_service: Office (and other year-keyed products) report a build
number from Wazuh syscollector (16.0.4266.1001) while endoflife.date
keys releases by year (name/label '2016'). The numeric prefix match in
_pick_release never matched, so Office 2016 EOL went undetected. Add a
year-from-product-name resolver (_YEAR_KEYED_SLUGS, _extract_year,
_pick_release_by_year) used before the numeric fallback. SQL Server and
other numeric-versioned products are unaffected.

dashboard: long EOL pseudo-CVE ids (EOL-WINDOWS-SERVER-2016) plus
whitespace-nowrap forced the 'Newly EOL/EOS' widget wider than its grid
column, scrolling CPR/Flags out of view. CVE cell now break-all.

assets: 9 columns at px-6 padding (~432px) overflowed max-w-7xl and
clipped the Actions column. Reduced table padding to px-3.
2026-06-01 13:16:06 +02:00
vulncheck c952a42bc2 fix(eol): three-tier severity + EOL-SOON; nessus Essentials import fallback
Two tester findings.

1. Windows Server 2016 wrongly flagged as EOL (HIGH) though it still
   gets monthly CUs until 2027-01. Cause: is_eoas (active-support
   ended 2022) was treated the same as is_eol → MEDIUM/critical-ish
   finding even while security patches still flow.

   New three-tier model in _build_eol_status():
     - is_eol      : security support ENDED (eolFrom past, no ESU) →
                     HIGH, cvss 9.0, title "EOL".
     - is_eol_soon : eolFrom within EOL_SOON_DAYS (90) but future →
                     MEDIUM, cvss 5.5, title "EOL SOON (Nd)".
     - is_eoas only: mainstream support ended, security patches still
                     flow → LOW, cvss 3.0, title "end-of-active-support".
   Server 2016 (eoas 2022, eol 2027-01) now → LOW today, flips to
   MEDIUM "EOL SOON" ~90d before Jan 2027, HIGH after.
   _days_until() handles eolFrom given as a bool (endoflife quirk).
   Both check_eol + check_os_eol share _build_eol_status now.

2. Nessus "Scan + Import" failed with "Server disconnected without
   sending a response" on launch. That's the Nessus Essentials (free)
   API-launch limitation — it drops the connection and the scan never
   enters 'running'. The job now catches the launch NessusAPIError: if
   the scan already has completed/imported results it imports those
   (flagged via job.launch_warning + a clear stage message); otherwise
   it fails with an actionable hint ("this edition may not allow
   API-triggered scans — launch in the Nessus UI then Sync now").

eol-check loops (router + scheduler) updated to also surface
is_eol_soon findings.
2026-06-01 08:19:19 +02:00
vulncheck 99ecf2abb4 feat(eol): OS-level EOL detection (Windows Server 2008 R2, Ubuntu, etc.)
Plan E — extend endoflife.date detection from installed packages to
the OS itself. Tester confirmed via PowerShell that windows-server
2008-r2 is EOL on endoflife.date; Wazuh syscollector packages don't
carry the OS so we read asset.operating_system + os_version.

Service (eol_service.py)
- resolve_os_to_eol(os_name, os_version) maps Wazuh OS strings to
  (endoflife_slug, release_codename):
    "Microsoft Windows Server 2008 R2"  → (windows-server, 2008-r2)
    "...Server 2016 Datacenter"         → (windows-server, 2016)
    "Microsoft Windows 10 Pro"          → (windows, 10)
    build >= 22000                      → (windows, 11)
    "Ubuntu" 22.04.3 LTS                → (ubuntu, 22.04)
    Debian / RHEL / CentOS              → numeric major
- check_os_eol() matches the codename against release.name (exact,
  then prefix) — NOT the numeric _pick_release used for packages,
  because OS releases use codenames not versions.

Endpoint + scheduler
- /vulnerabilities/eol-check now runs the OS check per asset BEFORE
  the package loop (so an asset whose package fetch errors still
  gets its OS evaluated). New stat: os_eol_findings.
- eol_check_nightly mirrors the same OS-first logic.

OS EOL findings reuse the EOL-* pseudo-CVE machinery: severity HIGH
(EOL) / MEDIUM (EOAS), cve_id EOL-WINDOWS-SERVER-2008-R2, fixed_version
= latest supported release.
2026-05-31 13:59:19 +02:00
vulncheck 8ad2c592f8 fix(eol): in-process memo + UPSERT cache to kill duplicate-key race
Tester report: EOL check 500'd with
  duplicate key value violates unique constraint 'ix_settings_key'
  Key (key)=(eol_cache_mssqlserver) already exists.

Cause: a single asset has many SQL-Server-related packages (engine,
setup bootstrap, VSS writer, …) all mapping to the same slug
'mssqlserver'. _cache_get returned None for each in quick succession
(autoflush quirk on this session), each path added a Setting row,
the eventual flush hit the unique constraint.

Fix
- _PROCESS_MEMO dict in module scope — first fetch per slug per
  backend-process is the only one that talks to httpx + the DB. All
  subsequent lookups for the same slug return the in-RAM dict.
- _cache_put rewritten as PostgreSQL ON CONFLICT DO UPDATE upsert
  (sqlalchemy.dialects.postgresql.insert) so even races between
  concurrent requests can't duplicate-key.
- Failed cache write rolls back its own attempt instead of poisoning
  the outer transaction.
2026-05-27 12:00:29 +02:00
vulncheck 373dc9d813 feat(eol): endoflife.date EOL detection — pseudo-CVE per finding
Tester request: close the unsupported-software gap Wazuh has vs
Nessus plugin 64784 (Microsoft SQL Server Unsupported Version
Detection). PowerShell prototype against endoflife.date API
confirmed the approach.

Backend
- New app/services/eol_service.py
  * Hand-curated product slug map (Microsoft / Mozilla / Adobe /
    runtimes / databases / Linux distros).
  * resolve_product_slug() normalises Wazuh/Nessus product names and
    matches against the map.
  * fetch_product() hits api/v1/products/{slug} with 24h cache in
    the settings table (negative-caches 404s).
  * check_eol() picks the longest-prefix release for the installed
    version, evaluates eolFrom / eoasFrom / eoesFrom against today.
  * upsert_eol_vulnerability() writes one pseudo-CVE per (asset,
    product, release) with cve_id of the form
    EOL-{SLUG}-{RELEASE} so re-runs converge.
- Vulnerability.is_pseudo_cve also matches EOL-* now.
- Vulnerability.is_eol_finding new property + API field.

API + scheduler
- POST /vulnerabilities/eol-check (editor) — optional asset_id query,
  walks syscollector packages, upserts EOL pseudo-CVEs. Returns
  stats {assets_scanned, packages_checked, products_unmapped,
  eol_findings_total, eol_findings_new, errors[]}.
- New scheduler job eol_check_nightly at 03:15 UTC.

Frontend
- Vulnerability type gains is_eol_finding.
- List page renders purple "EOL" badge instead of amber "NON-CVE"
  for endoflife rows.

Out of scope (later iteration):
- OS-level EOL (Windows / Ubuntu releases) — would parse Asset.os
  + os_version separately.
- Per-package detail (VulnerabilityPackage child rows) for EOL
  pseudo-CVEs — current row already carries name/version/latest.
- Settings-UI for the product-slug mapping table.
2026-05-27 11:31:27 +02:00