Commit Graph
106 Commits
Author SHA1 Message Date
vulncheck 101445109f fix: ASB 2026 URL format, cvelistV5 fixed_version, macOS OS-CVE scan
- android-asb: from 2026 Google nests the bulletin under a year segment
  (/bulletin/2026/2026-01-01); older months stay flat (/bulletin/2025-10-01).
  fetch_asb_month now tries the year-nested URL first, then flat. Verified:
  2026-01 → nested 200/flat 404; 2025-xx → nested 404/flat 200.
- cvelistV5 scan: stop using lessThanOrEqual as fixed_version — that bound
  means the version is still affected (no published fix), so leave
  fixed_version empty (correct "no patch available", e.g. 7-Zip CVE-2026-58052
  ≤26.02). Only lessThan is a real fix target.
- app-cve OS scan: add macOS (cpe:2.3:o:apple:macos) alongside iOS/iPadOS so
  Mac assets get OS CVEs (e.g. Apple CVE-2026-43700, previously only Defender
  saw it).
2026-07-05 16:54:50 +02:00
vulncheck 59e4afbeb3 fix(android-asb): negative-cache empty/404 months to stop the refetch storm
Future ASB months Google hasn't published yet return 404. fetch_asb_month
only cached successful months, so every Android device re-fetched the same
404 months on every sync (the tester saw 2026-01…06 hammered dozens of times).

Now empty/404 results are cached too, with a 3-day TTL: months with CVEs stay
immutable/cached forever; empty months are skipped for 3 days, then retried
so a newly-published month is picked up. Legacy list-format cache entries
still read correctly.

(The 404s are expected — those months aren't published on source.android.com
yet; the 2025 months that exist already produced android-asb findings.)
2026-07-05 16:51:53 +02:00
vulncheck ed78810ad8 feat(intune): per-CVE Android detection from Google ASB
For Intune-managed Android devices, raise real-CVE findings for the months
the device is behind on patches. Device patch level (androidSecurityPatchLevel,
e.g. 2025-03-01) → every monthly Android Security Bulletin published after it
lists CVEs the device hasn't received. Fetched from source.android.com
(stable, static, per-month) and parsed (CVE + severity, severity carried
across rowspan rows); cached per month in a Setting.

Source 'android-asb' (real CVE ids → EPSS/KEV/CVSS enrichment applies). Wired
into the Intune sync alongside the patch-level-staleness headline finding.

Why not Samsung's SMR page: securityUpdate.smsb ignores the year/month query
param and loads the month via JS — a plain fetch returns the same latest month
regardless (verified: 2025-03/-04/-09 all identical). ASB is the upstream
source for the Google CVEs Samsung ships (the security-critical bulk).
Samsung-proprietary SVE CVEs aren't covered (their page is unscrapeable).

Volume guards: Critical + High only, last 12 months (tunable via _MAX_MONTHS
/ _WANT_SEV). Verified month math + the ASB parser against the live bulletins
(2025-09 → 109 CVEs, per-month distinct).
2026-06-30 10:48:06 +02:00
vulncheck c1fc992285 fix(app-cve-scan): build cvelistV5 index on manual scan + faster/robust build
Why the tester saw nothing: the manual "App CVE Scan" only LOADED the cached
index, it never built it — only the nightly did, and that build likely timed
out on the 557 MB download (120 s). So 7-Zip CVE-2026-58052 / Notepad++
CVE-2026-52885 (both in-registry, in-range) were never matched.

- Manual scan now builds the index when it's missing (same result as the
  nightly), then caches it.
- ZIP download timeout 120 s → 600 s.
- Index build pre-filters on raw bytes (only JSON-parse files mentioning a
  curated vendor) → ~99% fewer json.loads, build drops from minutes to ~a
  minute.

Verified the two CVEs' vendor/product (7-Zip/7-Zip, notepad-plus-plus) match
the registry and the installed versions (26.01 ≤ 26.02, 8.9.5 < 8.9.6.4) fall
in range — so they will now be detected once the index exists.
2026-06-30 10:40:06 +02:00
vulncheck 46594c153d feat(app-cve-scan): auto-suppress loose-CPE false positives via cvelistV5
Item 3 — the inverse of detection. Wazuh's CPE match over-reports CVEs across
product editions (a SQL Server 2019 / 15.x host carrying a CVE that only
affects 16.x/17.x). Now: for each open Wazuh finding, read the CVE's
cvelistV5 affected[] ranges scoped to the matched product, and if the
installed version is provably OUTSIDE every clean range → mark
status=false_positive (reversible via unmark, defer_reason explains why).

Conservative guards (never hide a real finding):
- Wazuh source only (the over-matcher); Nessus/app-scan/EOL untouched.
- Product match needs ≥2 shared significant tokens → only multi-word
  products (SQL Server, Visual Studio…) are ever scoped; single-token apps
  are left alone.
- If any relevant affected entry has no clean numeric range → abort (keep).
- If installed is inside ANY affected range → keep (it's real).

Nightly job runs it after the scan; manual POST /suppress-false-positives.
Verified on the tester's exact CVEs (installed 15.0.4013.40): CVE-2026-26116
(17.x-only) + CVE-2026-33120 (16.x-only) suppress; CVE-2024-29047 (covers
15.x) correctly kept.
2026-06-29 20:21:45 +02:00
vulncheck 61ce7497ff feat(app-cve-scan): cvelistV5 range-based detection for installed software
Catches CVEs the NVD-CPE scanner misses: fresh CVEs NVD hasn't CPE'd yet, or
ones filed under a different CPE product string than we curated (TeamViewer
lives under teamviewer:remote, not teamviewer:teamviewer). Matches directly
against cvelistV5 affected[].vendor/product + version ranges — the
authoritative MITRE feed we already cache as a ZIP.

- Curated product registry (name-regex → cvelistV5 vendor/product pairs):
  TeamViewer, Notepad++, Devolutions RDM, 7-Zip, Firefox, Chrome, VLC,
  PuTTY, WinSCP, Wireshark, FileZilla, Zoom. Unknown software ignored.
- build_product_index: one walk over the cached ZIP → {product_key:
  [{cve,start,lt,lte}]} for curated products only; cached in a Setting,
  rebuilt by the nightly job (the 557 MB walk happens once, not per scan).
- scan_asset: resolve installed software → indexed CVEs → version-range
  check → upsert (source 'app-scan', shared badge/cross-confirm/enrichment).
- Wired into run_app_cve_scan (loads cached index; skipped+logged if not
  built yet) and the nightly job (builds index first).

Verified against the real CVE JSON: TeamViewer CVE-2026-23572 (<15.74.5),
Notepad++ CVE-2026-52885 (<8.9.6.4), Devolutions CVE-2026-13372
(2026.2.5–2026.2.11) all detect at affected versions and correctly do NOT
match patched versions.
2026-06-29 15:05:20 +02:00
vulncheck d14ed835e4 fix(mobile-eol): clean device EOL text + correct Samsung A11/Active5 Pro
Description bugs (tester screenshot):
1. lowercase vendor ("samsung Galaxy Tab A8") — Intune reports manufacturer
   lowercase. Use a fixed vendor label (Apple iPhone / Apple / Samsung).
2. doubled model ("Samsung Galaxy Tab A8 Galaxy Tab A8 — EOL") —
   upsert_eol_vulnerability appends the release label itself, so product_name
   must be the VENDOR only. Now title reads "Samsung Galaxy Tab A8 — EOL" and
   the full device name is written back into the package column.

Mapping fixes from the tester (he resolved the two I left open, and caught a
wrong one):
- SM-X236 was mapped to Tab A9+ → it's Tab A11+ (5G). Corrected.
- SM-X230 → Tab A11+ (Wi-Fi), SM-X130/X135 → Tab A11, SM-X356 → Tab
  Active5 Pro. Added. All names verified against the live API.
2026-06-25 13:12:37 +02:00
vulncheck 428c3f2737 feat(mobile-eol): add older Samsung Tab A / Tab Active / XCover3-4 models
From the tester's missing-model list: Tab A 7.0/8.0 (2015/16), Tab A9+ 5G
(SM-X236), Tab Active / Active2 / Active3 / Active5, XCover3/4/4s. All 13
new release names verified against the live endoflife.date API; SM-G556 /
G525 / G736 were already mapped.

Two codes left out pending the marketing name (avoid a wrong-EOL mapping):
SM-X356 and SM-X230 — not confidently identifiable.
2026-06-25 11:16:57 +02:00
vulncheck d8c838fa09 fix(assets): source filter by scanner linkage; expand Samsung EOL table
assets: the source filter used the creation-time `source` enum, so an asset
first created by Wazuh/Manual and later matched by Nessus stayed off the
"Nessus" filter. Now filter by the actual per-scanner id columns
(wazuh_agent_id / nessus_host_uuid / intune_device_id|defender_machine_id),
so a merged asset shows under every scanner that sees it. MANUAL = no linkage.

mobile-eol: expand the Samsung SM-code table from ~30 to ~100 models —
S20→S25, Note10/20, Z Fold/Flip 2→7, full A-series, XCover, Tab S6→S10 +
Tab A. All 100 release names verified against the live endoflife.date API.
Apple was never table-bound (it fuzzy-matches the full live list). Unmapped
Samsung models still skip (no false-positive). Graduated severity (eoas→low,
eol-soon→medium, eol→high) already comes from eol_service.
2026-06-25 09:53:38 +02:00
vulncheck 98bd366ddf feat(intune): mobile device EOL/EOS + Android patch-level staleness
For Intune-managed phones/tablets, runs during the Intune sync (device dict
already in hand — no extra fetch):

1. Device-model EOL/EOS via endoflife.date, reusing eol_service
   (fetch/cache/EOLStatus/upsert → EOL- pseudo-CVE). Model→release mapping:
   - Apple: Intune reports the marketing name → fuzzy-match the endoflife
     release label/name (iPhone 15 Pro Max → 15-pro-max, iPad Air (5th
     generation) → air-5). Bare identifiers (iPhone15,3) are skipped.
   - Samsung: SM-code has no textual overlap with endoflife → curated
     SM-prefix table (S21–S25, A-series 5G, XCover, Tab S9/S10), matched by
     prefix so region suffixes are ignored. Unmapped models are skipped (no
     false-positive). All release names verified against the live API.

2. Android security-patch-level staleness from Intune's
   androidSecurityPatchLevel: age >=90/180/365d → low/medium/high pseudo-
   finding (ANDROID-PATCH-LEVEL-STALE, one per asset). The control instance
   that flags "patches not actually applied" without scraping any vendor
   bulletin. is_pseudo_cve recognises the new prefix.

Added androidSecurityPatchLevel to the managedDevices $select.
Deferred (not built): per-CVE Android bulletin scraping (Google ASB /
Samsung SMR) — brittle, no API.
2026-06-24 10:32:37 +02:00
vulncheck d460364590 fix(app-cve-scan): platform check kills cross-platform false-positives
Desktop Firefox on a Windows host was matching the Firefox-for-iOS CVE
(cpe:2.3:a:mozilla:firefox:*:...:iphone_os:*, target_sw=iphone_os). We
matched on vendor:product only and ignored the CPE platform field.

Now record each matching cpeMatch's target_sw (CPE index 9) and keep a CVE
for an asset only when target_sw is platform-neutral (*) or names the asset's
OS family (_os_family). Applied to both package and OS scans.

Cache key bumped to v2: → pre-fix rows (without target_sw) are ignored so the
stale FPs aren't served from cache; they re-fetch with the platform data.

Also: 503 backoff raised to >=3s × attempt over 4 tries (NVD 2.0 503s under
load even WITH a key — it's server-side, not auth), and the scan logs whether
NVD_API_KEY is present so a missing key is obvious in the logs.
2026-06-23 10:00:02 +02:00
vulncheck 3123263cf0 feat(app-cve-scan): iOS/iPadOS OS-level CVEs
Apple ships the precise OS version (e.g. 18.1.2) and NVD carries proper
version ranges for cpe:2.3:o:apple:iphone_os / :ipados, so it's the same
clean CPE-range check the desktop apps already use — no new machinery.

Reads asset.operating_system + asset.os_version (already synced from Intune),
so it covers Intune-only iPhones/iPads that have no syscollector packages.
Runs per asset regardless of package inventory; cached per (cpe, version) so
N devices on the same iOS build = one NVD query.

Android intentionally omitted: NVD only lists the base version (13/14/15)
without ranges → needs the Intune security-patch level + Android bulletin
parsing, a separate feature.
2026-06-22 22:19:05 +02:00
vulncheck 36a560d508 fix(app-cve-scan): kill NVD noise/503 storm + Graph 400/429 hammering
Three bugs from the first app-scan run:

1. Linux rpm/deb packages flooded NVD with junk queries
   (python:python:4.6.5-3.el8, epoch 1:3.2, 2.43.0.windows.1, ...) →
   503 storm + false-positives. Those packages are Wazuh's domain. Add
   _clean_version: only clean dotted-numeric versions reach the scanner
   ("7.0.2 (34567)" → "7.0.2"); epoch/release-tag versions are skipped.

2. A transient NVD 503/429 cached an EMPTY result for 7 days → real CVEs
   missed until TTL. _query_nvd_cpe now retries (3x backoff) and raises
   _TransientNVD on persistent 429/502/503/504; lookup_cves returns []
   WITHOUT caching so the next run retries. Permanent 4xx still cache empty.

3. v1.0 $expand=detectedApps 400s on this tenant → it 400'd once per
   device, every run. Flip _v1_detected_apps_ok off after the first 400
   and go straight to beta. _get now retries 429 honouring Retry-After.

NVD_API_KEY strongly recommended — keyless NVD is the main 503 source.
2026-06-22 22:15:34 +02:00
vulncheck 36976c6647 feat(app-cve-scan): built-in software→CVE scanner (OSV + NVD-CPE)
Maps installed software (Wazuh syscollector packages + Intune detectedApps)
to real CVEs via OSV and NVD-CPE with an own version-range check. Closes the
coverage gap for Intune-only / mobile devices that have no real scanner
(Intune managedDevices add every device but carry no CVE data; Defender TVM
only covers MDE-onboarded hosts with findings).

Design: curated + precise (low false-positives).
- Curated product registry (~24 common apps) name-regex → CPE/OSV; unknown
  app names are ignored (no CPE auto-guessing → no FP storm).
- NVD-CPE: query per product CPE, then verify the installed version actually
  falls inside each CVE's cpeMatch range ourselves (start/end incl/excl,
  exact-version equal, wildcard skipped); versionEndExcluding → fixed_version.
- OSV: precise server-side version match for language ecosystems.
- Results cached per (product_key, version) in app_cve_cache (TTL 7d) so the
  same version across N hosts = one query (and stays under NVD's rate limit).

Findings upsert as source 'app-scan' with real CVE ids → the normal
EPSS/KEV/CVSS enrichment + multi-source remediation apply, and they
cross-confirm with Wazuh/Nessus/Defender on the same (cve, asset).

Wiring:
- Migration 035 + AppCveCache model.
- Piggyback in intune_service._run_app_inventory (detectedApps already fetched).
- POST /api/v1/vulnerabilities/app-cve-scan (sync def → threadpool).
- Nightly scheduler job app_cve_scan_nightly (03:25 UTC).
- Frontend: app-scan source badge + filter + "App CVE Scan" button.

NVD_API_KEY recommended for scale (rate limit). Run alembic upgrade head.
2026-06-21 12:16:44 +02:00
vulncheck 7890391283 fix(defender): populate affected-software (Package) for Defender CVEs
Tester: Defender-sourced CVEs showed an empty Package/affected-software
column, although Defender's own UI lists the affected software (e.g.
"OpenSSL 1.1.1.0").

Cause: /api/machines/{id}/vulnerabilities returns CVE definitions only —
no per-device software. Now run_defender_sync additionally pulls the
tenant-wide SoftwareVulnerabilitiesByMachine assessment (one paginated
export), builds a (machineId, CVE) → "vendor name version" map, and sets
package_name on each Defender finding (and backfills it on an existing row
whose package was empty). Best-effort: if the tenant/plan doesn't expose
the export, behaviour is unchanged.
2026-06-16 13:29:43 +02:00
vulncheck c3a75e7a77 feat(risk): Asset Risk Dimensions — high-value-target scoring + exposure rebalance + URS
Tester: the port-based exposure score put nearly every Windows host at 100
(no separation), and the thing that actually matters — whether a host runs
a crown-jewel role enabling lateral movement / domain takeover — wasn't
captured.

- Migration 034 + model: assets.high_value_score (0-100) + risk_dimensions
  (JSON roles) + _updated_at.
- app/services/risk_dimensions_service.py: detect_risk_dimensions(ports,
  packages) → roles from syscollector ports (port + process) and installed
  packages: Domain Controller, ADCS/CA, backup servers, SW-distribution,
  Exchange, WSUS, MSSQL, DNS, DHCP, WinRM. Score = max(weight) + 0.3·rest
  (cap 100). risk_factor() maps it to a URS band (>=90→1.5 … else 1.0).
- exposure_service: rebalanced port weights — baseline Windows
  (SMB/MSRPC/NetBIOS/WinRM) now LOW; real remote-control/cleartext
  exposures (Telnet/VNC/RDP/FTP) stay HIGH. Risk detection runs in the same
  pass (reuses fetched ports + one get_packages call).
- urs_service: URS uses max(operator criticality factor, role factor) — a
  DC/ADCS host rises to critical weighting even at criticality=normal;
  operator can still set higher. criticality field untouched.
- assets API: high_value_score + risk_dimensions in the response + sortable;
  Assets page gets a "Risk" column with score + role badges.

Verified detection: DC(88+389)→100, SQL pkg+WinRM→79, plain Win→0,
Exchange+Veeam→100. Migration 034 required: alembic upgrade head.
Roles need Wazuh syscollector (ports+packages); Nessus/Intune-only → v2.
2026-06-16 13:23:16 +02:00
vulncheck d90044e696 feat(msrc): newest KB per build-branch+type; sensible M365-Apps handling
Tester-approved: show the newest MSRC KB per (build-branch + update-type)
— normally one entry, two on hotpatch hosts (Security Update + Security
Hotpatch Update); drop the superseded rest.

- msrc_service now stores the MSRC update type (SubType) in the fix row's
  `detail`, so the detail view can keep Security-Update vs Security-Hotpatch
  separately. (Re-run MSRC Enrich to populate it on existing rows.)
- get_vuln_remediations: dedup fixes to newest per (branch, type); for a
  Windows-OS finding narrow to the host's own build branch (1–2 entries).

Special case — Microsoft 365 Apps findings: the MSRC KBs are perpetual/MSI
Office builds that never match the installed Click-to-Run channel build, so
branch-filtering is skipped (it would hide everything). Instead the deduped
per-Office-version set + the aka.ms pointer are shown, and a clear
"Update via Office channel to build <channel build>" hint is prepended —
the actually-actionable fix for C2R installs.

Verified: Windows 10.0.26100.32690 → only the 26100 SU + 26100 Hotpatch;
M365 example 7 KBs → 4 (newest per Office line + aka.ms).
2026-06-15 15:08:06 +02:00
vulncheck 355adad829 feat(m365): fill real CVE title + description from cvelistV5
Tester: M365 findings showed the synthetic placeholder description; the
CVSS-correction cascade (by design, for efficiency) never touches
title/description. Fill them separately from the authoritative source.

apply_real_cve_metadata(db, cve_ids): fetches the real CVE title + English
description from CVE.org cvelistV5 raw and writes them onto the M365
findings (first_detected_by='m365_check'), keeping a trailing note that
the finding originated from the Microsoft 365 Apps source (so the
provenance stays clear). Runs after CVSS-correction + enrichment in both
run_m365_check and run_m365_for_packages.

Verified live: CVE-2026-45456 → title "Microsoft Outlook and Word Remote
Code Execution Vulnerability" + the real type-confusion description.
2026-06-13 13:22:16 +02:00
vulncheck c2cad0088d feat(intune): Phase 3 — Defender for Endpoint TVM real CVEs
Optional per-device CVE feed from Microsoft Defender for Endpoint (TVM),
toggled by `defender_tvm` in the Intune settings card. Reuses the same
Entra app (tenant/client/secret) but a separate API + scope.

- app/integrations/defender_client.py: client-credentials token for scope
  https://api.securitycenter.microsoft.com/.default; get_machines() +
  get_machine_vulnerabilities() (paginated); test_connection().
- app/services/defender_service.py: run_defender_sync — match Defender
  machine → asset by computerDnsName (pin defender_machine_id), upsert
  REAL Vulnerability rows (source='defender', cross-confirm via add_source
  with wazuh/nessus/intune), then VULNERABILITY_DETECTED audit + EPSS/KEV/
  date enrichment. Real CVE ids → cvelistV5/MSRC/OSV remediation applies.
- intune_service.run_intune_sync runs the Defender pass when enabled.
- Settings card: "Defender TVM CVEs" toggle (needs Vulnerability.Read.All
  on WindowsDefenderATP + Defender licensing).

Needs Application permission Vulnerability.Read.All (WindowsDefenderATP) +
admin consent. No migration (defender_machine_id added in 033).
2026-06-13 10:40:57 +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 5418d6a9d3 feat(intune): Microsoft Intune/Graph inventory source — Phase 1 (devices + OS-EOL)
Third inventory source next to Wazuh/Nessus: pulls Intune managed devices
via Microsoft Graph (app-only client-credentials) → assets + OS-level EOL.

- Migration 033: INTUNE assetsource label + assets.intune_device_id /
  defender_machine_id (+ indexes). Model updated.
- app/integrations/graph_client.py: client-credentials token cache (mirrors
  wazuh_client), paginated managedDevices, get_detected_apps (phase 2),
  test_connection. Plain httpx, no msal dependency.
- app/services/intune_service.py: run_intune_sync — find-or-create asset
  (intune_device_id → hostname → auto-create) mirroring nessus_sync,
  source=INTUNE, refresh OS/version, OS-EOL via eol_service.check_os_eol,
  id-keyed lifecycle reconcile.
- asset_lifecycle.reconcile_intune_by_seen_ids (mirrors the Nessus one).
- app/routers/intune.py: POST /api/v1/integrations/intune/test (admin),
  /sync (editor, fire-and-forget 202). Registered in main.py.
- scheduler: intune_sync_nightly (sync def) at 02:10.
- Settings: encrypted intune_config (PROTECTED_SETTING_KEYS); settings PUT
  now MERGES secret subfields (blank/"***set***" keeps the stored secret)
  so JSON configs can be edited without re-typing secrets; client_secret
  added to redaction subfields.
- Frontend: "Microsoft Intune (Graph API)" settings card (tenant/client/
  secret + Test/Sync), intune/defender source badges, Asset type field.
- .env.example documents the Entra app registration + permissions.

Migration 033 required: alembic upgrade head. detectedApps→EOL/M365 is
phase 2 (toggle present, off by default until the per-package helpers land).
2026-06-13 10:34:51 +02:00
vulncheck 9714fb72a2 feat(m365): real CVSS/metrics + richer description at CVE check-in
Tester (follow-up to Plan P 45524f7): M365 Apps CVEs were created with a
placeholder severity=medium / cvss=None and a thin description. Pull the
real metrics straight away at check-in and clarify the description.

- After an M365 check creates/updates findings, run the CVSS-correction
  cascade (vulnrichment → cvelistV5 → NVD) for the touched CVE ids, then
  enrich_vulnerabilities (EPSS/KEV/EUVD + NVD dates). These are real CVE
  ids, so they resolve like any other. The override path never touches
  `description`, so the M365 source note is preserved.
- Description rewritten: states the affected product, installed vs fixed
  build + "update via the Office channel", explains the detection source
  (Wazuh syscollector build vs MS365 release notes — not in NVD/Wazuh),
  and notes metrics come from MSRC + Vulnrichment/cvelistV5 with a pointer
  to the MSRC remediation section.

No migration. CVSS/severity now populated on the next M365 Enrich run.
2026-06-13 10:05:09 +02:00
vulncheck 933a47a10e feat(osv): add OSV.dev as an additional CVE remediation source
Tester idea: use the OSV.dev aggregator to broaden Linux/cross-ecosystem
coverage. Design question (fallback vs overwrite) → neither clobbers:
OSV is added as its OWN source (own UI block, source='osv'), never
overwriting vendor rows, and it also fills gaps for distros/ecosystems the
vendor providers don't cover (Debian, SUSE, Alpine, Rocky, AlmaLinux, npm,
PyPI, Go, ...).

- linux_remediation_service.fetch_osv(): GET api.osv.dev/v1/vulns/<CVE> →
  per-ecosystem fixed versions (skips commit-hash "fixes") + filtered
  advisory references (USN/RHSA/DSA/GHSA/errata/SUSE/Alma...).
- enrich_cve_linux() now stores the matching vendor provider AND OSV as
  separate sources per CVE.
- GET /vulnerabilities/{id}/remediations triggers on-demand enrichment for
  ANY non-Windows host (was Ubuntu/RHEL only), so OSV covers Debian/SUSE/
  etc.; cached in cve_remediations, off the event loop.
- UI labels the block "via OSV.dev (aggregator)".

Verified live: CVE-2023-48795 → OSV yields Debian DSA / GHSA / FreeBSD
advisory links the vendor providers don't. No migration (reuses 032).
2026-06-13 10:01:45 +02:00
vulncheck ac16f99be7 feat(linux): Ubuntu USN + RHEL/CentOS errata remediation enrichment
Tester feature (step 2 of multi-source enrichment): add Linux distro fixes
alongside the Nessus scanner solution and MSRC, reusing cve_remediations.

Unlike MSRC (monthly bulk doc), the Linux trackers are queryable per-CVE,
free and unauthenticated — so we fetch on demand when the CVE detail opens
for a Linux host, then cache into cve_remediations (repeat views instant,
re-fetch refreshes).

app/services/linux_remediation_service.py:
  - provider_for_os(): Ubuntu -> ubuntu; CentOS/RHEL/Rocky/Alma/Oracle/
    Fedora -> redhat (CentOS/Alma rebuild RHEL, so the RHSA + fixed NVR is
    the actionable fix).
  - fetch_ubuntu(): ubuntu.com/security/cves/<CVE>.json → per-release fixed
    package versions (kind=fix), USN advisories, mitigation.
  - fetch_redhat(): access.redhat.com securitydata → RHSA advisory + fixed
    package NVR + errata link, mitigation/statement.
  - enrich_cve_linux(): pick provider by host OS, replace cached rows.

GET /vulnerabilities/{id}/remediations now triggers this enrichment
(off the event loop via asyncio.to_thread) on first view for a Linux host.
UI groups it under "via Ubuntu USN" / "via Red Hat / CentOS / Alma".

Verified live: CVE-2024-6387 (regreSSHion) returns per-release openssh
fixed versions + USN-6859-1 + the LoginGraceTime mitigation.

No migration (reuses cve_remediations from 032).
2026-06-10 13:31:39 +02:00
vulncheck 29f828678b feat(msrc): Microsoft (MSRC) per-CVE remediation enrichment
Tester feature (step 1 of multi-source enrichment): augment the Nessus-
only scanner remediation with authoritative Microsoft data, for Windows
OS *and* MS products (Office/365, .NET, SQL, Exchange, ...).

MSRC's per-CVE endpoint 404s, so we ingest the monthly CVRF documents
(api.msrc.microsoft.com/cvrf/v3.0/cvrf/{YYYY-Mon}, ~4 MB each) and extract
per-CVE remediations:
  - fixes:   KB number + FixedBuild + download URL (Remediations Type 2/3)
  - workarounds / mitigations (containment): Notes "Workarounds" /
    "Mitigations", HTML stripped to text — covers the "no KB yet, only
    containment" case the tester called out.

- Migration 032 + model: cve_remediations (CVE-level, source-tagged).
- app/services/msrc_service.py: refresh_msrc() pulls the last N monthly
  docs (default 18, setting msrc_months_back), stores rows only for CVE
  ids already in the DB (keeps it relevant). Re-parse replaces a CVE's
  rows so MS revisions (containment-only -> KB later) self-update.
- Endpoints: GET /vulnerabilities/{id}/remediations (scanner + external,
  grouped by source) and POST /vulnerabilities/msrc/refresh (fire-and-
  forget background thread). Weekly scheduler job (Sun 04:40).
- UI: CVE detail now renders a Remediation block per source ("via scanner"
  / "via Microsoft (MSRC)") with KB + download links, workarounds, and
  mitigation/containment. "🛡️ MSRC Enrich" button on the vuln list.

Verified parse against the live 2026-May CVRF doc (KB+build+catalog link
per Windows build). Migration 032 required: alembic upgrade head.

Step 2 (Linux: Ubuntu USN / CentOS errata) reuses cve_remediations next.
2026-06-10 13:29:25 +02:00
vulncheck eea9f5aa94 feat(ai): EOL-aware remediation prompt (upgrade plan, not "apply patch")
Tester: AI remediation for EOL/EOS findings was generic and sometimes
hallucinated a patch that doesn't exist (the product is out of support).

For pseudo-CVE findings (cve_id starts with EOL- / NESSUS-PLUGIN-), the
prompt now switches to an end-of-life system prompt: state the EOL/EOS
risk, name the supported target release + timeline, give OS-specific
upgrade/replace commands and download location, list interim compensating
controls / containment while the migration is pending, and a verification
step — explicitly told NOT to suggest applying a non-existent patch.
Real-CVE findings keep the existing patch-focused prompt.
2026-06-10 13:19:26 +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 3e382a489d fix(eol): stop short product names matching unrelated lifecycle listings
Tester false-positive: the evergreen Microsoft Edge browser (148.x/149.x)
was flagged EOL as "Azure Stack Edge" (end 2024-03-31).

Cause: the lifecycle-export matcher was bidirectional (rn in target OR
target in rn). "Microsoft Edge" normalises to just "edge" ("microsoft" is
stripped), and "edge" is a substring of the unrelated listing
"azure stack edge" — so the reverse direction matched.

Fix: one direction only — the LISTING name must be contained in the
product name. Verified against the live April-2026 export:
  Microsoft Edge / Edge WebView2  -> no match (was Azure Stack Edge)
  SQL Server 2014 Management Objects -> SQL Server 2014 (kept)
  Exchange Server 2016               -> kept
  a real "Azure Stack Edge" device   -> still matches

Existing wrong EOL-MS-LIFECYCLE-Azure_Stack_Edge rows on Edge hosts won't
be recreated; dismiss them once via the detail-page Dismiss button.
2026-06-10 08:52:14 +02:00
vulncheck 65e2c222ce fix(nessus): specific remediation beats generic on cross-confirmed CVEs
Tester: a cross-confirmed (Wazuh + Nessus) CVE showed the generic
remediation "Install the patches listed below." instead of the specific
Nessus plugin solution ("Upgrade to Paessler PRTG ... 18.2.40.1683 or
later", plugin 277614).

A CVE can match several Nessus plugins on one host; a catch-all plugin's
generic solution was overwriting / blocking the real plugin's fix because
the precedence relied on a title-based specificity heuristic.

Now remediation precedence is text-based: a non-generic solution always
replaces an empty/generic one, and a generic solution never overwrites a
specific one. _is_generic_remediation() flags catch-all phrases
("install the patches listed below", "apply the appropriate patch",
"n/a", "no known fix", ...). Existing wrong rows self-heal on the next
Nessus sync.
2026-06-08 10:42:29 +02:00
vulncheck 9021943f38 feat(ai): on-demand OpenRouter AI remediation on the CVE detail page
Tester feature request: generate OS-aware fix guidance per CVE via
OpenRouter (OpenAI-compatible).

- app/services/ai_service.py: calls OpenRouter /chat/completions via httpx
  (no new SDK dep). Config from env first, then settings table:
  OPENROUTER_API_KEY, OPENROUTER_MODEL (default openrouter/free),
  OPENROUTER_FALLBACKS (route=fallback). Builds an OS-aware prompt from the
  CVE + host (package, installed/fixed version, OS, scanner remediation)
  and asks for concrete commands + verification + mitigation. Maps 401/402
  to clear errors.
- POST /vulnerabilities/{id}/ai-remediation runs it via asyncio.to_thread
  (off the event loop). GET /ai-remediation/status reports whether a key
  is set so the UI hides the button when unconfigured.
- CVE detail: "🤖 AI Remediation" section with Generate/Regenerate button,
  shown only when configured.
- .env.example documents the OpenRouter keys.

Keyless by default = feature hidden; no behaviour change unless a key is set.
2026-06-06 09:48:51 +02:00
vulncheck 36cc29f69d feat(nessus): surface scanner remediation in its own CVE-detail section
Tester: Nessus already provides a per-finding remediation ("solution");
show it instead of burying it in the description blob.

- Migration 030: add vulnerabilities.remediation (TEXT).
- Nessus sync stores the plugin solution in the new remediation column
  (was appended to description); description now holds the synopsis only.
- API exposes remediation; CVE detail renders a "🛠️ Remediation (via
  scanner)" section below Affected Package when present.

Migration 030 required: alembic upgrade head.
2026-06-06 09:45:54 +02:00
vulncheck fbfa1d8e97 perf(enrich): ZIP date pass covers ALL missing CVEs in one run
The per-run cap (4000) was applied before the cvelistV5 ZIP pass, so a DB
with more undated CVEs than the cap stayed partially dated for several
nightly runs. During that partial state the "Newly Published" widget
showed a misleading subset — newest-published rows that had been dated
floated above genuinely-recent CVEs still sitting at NULL.

A local ZIP walk is cheap, so it should not be capped. Now the ZIP pass
runs over the entire missing set (dates a fresh DB completely in one run);
the per-run cap applies only to the slow per-CVE raw/NVD fallback for
whatever the ZIP didn't contain.
2026-06-03 15:46:43 +02:00
vulncheck 97cb7915c6 perf(enrich): bulk CVE-date backfill via cvelistV5 ZIP (shared cache)
Tester suggestion: do the date backfill like the CVSS-correction cascade —
download/parse the cvelistV5 ZIP snapshot instead of thousands of per-CVE
fetches when many CVEs need dating at once.

Hybrid with a threshold, reusing the CVSS cascade's SHARED disk cache
(/tmp/vulncheck-cvelistv5-cache.zip, 12h TTL):
  - missing > 200  -> one ZIP snapshot + local walk (cveMetadata
    datePublished/dateUpdated). Download-free when CVSS-correction already
    pulled the ZIP. Fresh-DB bulk fill drops from ~33 min to seconds.
  - missing <= 200 -> per-CVE raw GitHub (no 557 MB download for a handful
    of new CVEs on nightly runs).
Anything the ZIP didn't contain falls through to the per-CVE path, then
NVD as last resort.

Adds VulnOverrideService.load_cve_dates_via_zip() + a shared
_download_cvelistv5_zip() helper. Persistent date cache unchanged.
2026-06-03 15:33:24 +02:00
vulncheck df3d98ba68 perf(enrich): source CVE dates from cvelistV5 (no rate limit) before NVD
Tester suggestion: reuse the CVSS-correction cascade's authoritative
source (CVE.org cvelistV5) for the published/updated dates instead of
crawling the rate-limited NVD API one CVE at a time.

The NVD-only backfill needed a 6.5s sleep between requests without an API
key, so dating thousands of CVEs took hours (and, before the scheduler
thread-pool fix, froze the GUI the whole time).

Now the date backfill hits the official cvelistV5 raw JSON on GitHub first
(cveMetadata.datePublished / .dateUpdated — present for every published
CVE, GitHub raw has no aggressive rate limit, no sleep needed), and only
falls back to the NVD API per-CVE when cvelistV5 has no record. Per-run
cap raised to 4000 since the fast path no longer sleeps. Persistent cache
unchanged — each CVE still fetched once ever.

This gives both published_date and last_modified_date (the tester
specifically wanted dateUpdated) and drains a fresh DB in ~1-2 nightly
runs instead of weeks.

Verified live: cvelistV5 returns datePublished + dateUpdated for
CVE-2024-3094, CVE-2021-44228, CVE-2014-0160.
2026-06-03 14:29:08 +02:00
vulncheck d22ddb73b9 fix: dashboard full width, EOL-NESSUS homogenisation, published sort fallback
Tester screenshots — four issues:

1) Dashboard/assets wasted huge left/right gutters. Dropped the
   max-w-[1800px] cap on both pages -> content uses full available width
   (minus AppShell padding).

2) After a Nessus re-sync, a finding still showed BOTH the legacy
   EOL-NESSUS-{plugin_id} row AND the new slug-named row (e.g. Adobe
   Reader -> EOL-ADOBE-ACROBAT-...). The slug alias already resolves, but
   the old plugin-id row was never removed. _upsert_nessus_eol now deletes
   the legacy EOL-NESSUS-{plugin_id} row for the asset whenever the plugin
   resolves to a real product slug. Re-sync homogenises existing data.

3) "Newly Published" still looked unsorted: until the NVD published_date
   backfill drains, most rows have published_date NULL and were ordered by
   id (meaningless). Added a secondary sort on the CVE's own year+sequence
   so "newest CVE number first" holds even before backfill. Bumped the
   no-key NVD backfill cap 60 -> 150/run so dates fill faster.

4) Assets "Assigned To" select truncated to "Unas..." — widened
   maxWidth 140 -> 200px (minWidth 120).
2026-06-03 13:53:05 +02:00
vulncheck dc8ef025f5 feat(eol): Microsoft lifecycle export as EOL fallback for MS exotics (Plan O)
Tester: endoflife.date misses Microsoft "exotics" (and many server SKUs),
and Microsoft has no lifecycle API. The only machine-readable primary
source is the monthly Excel export linked from
learn.microsoft.com/lifecycle/products/export — whose download URL (GUID
+ month) changes every month.

New app/services/ms_lifecycle_service.py:
  - fetch_lifecycle_data(): scrape the export page for the current
    eos-product-listing .xlsx link, download it, parse
    ListingName/Release/EndDate via openpyxl, cache 24h in settings.
  - resolve_ms_lifecycle_eol(): match a syscollector product name to a
    listing; pick the latest NON-ESU end date (paid Extended Security
    Updates are an add-on most hosts lack, so a product is treated EOL
    when standard extended support ends). Returns a reusable EOLStatus.
  - Hardcoded exotics NOT in the export (tester-named): Silverlight and
    the old Visual C++ 2008-2013 redistributables, with fixed EOL dates.

Wired as a *fallback*: the EOL check (manual button + nightly job)
consults endoflife.date first and only falls back to MS-lifecycle for
names endoflife.date can't map. Findings upsert through the existing
EOL pseudo-CVE path (cve_id "EOL-MS-LIFECYCLE-...").

Verified against the live April-2026 export: SQL Server 2014 -> EOL
(2024-07-09), SQL Server 2019 -> not EOL (2030), Exchange 2016 -> EOL
(2025-10-14). Adds openpyxl==3.1.5.

No migration — uses existing vulnerabilities + settings tables.
2026-06-03 13:40:26 +02:00
vulncheck 45524f7f8a feat(m365): detect Microsoft 365 Apps CVEs not in NVD/Wazuh (Plan P)
Tester: M365 Apps security fixes never reach NVD and are invisible to
Wazuh's vulnerability detector — they only live on the Microsoft Learn
"Microsoft 365 Apps security updates" page. No Microsoft API exists.

New app/services/m365_service.py:
  - fetch_security_data(): parse that page into monthly releases
    (channel->build map + CVE list), cached 24h in settings.
  - parse_build("16.0.19929.20172") -> (19929, 20172); compares the last
    two dotted build segments numerically.
  - channel_for_product(): tester's rule — name contains "enterprise" ->
    Monthly Enterprise Channel, else Current Channel.
  - detect_missing_cves(): installed >= newest channel build -> UNAFFECTED;
    otherwise union the CVEs of every monthly section the host is behind.
  - upsert_m365_vulnerability(): real-CVE rows (enrichable like any CVE),
    placeholder severity refined by nightly enrichment / Correct-CVSS.
  - run_m365_check(): walk Wazuh-linked assets, collapse per-language
    duplicates, upsert.

Verified against the live page + tester's example: installed
16.0.19929.20172 vs MEC 19929.20162 -> UNAFFECTED (0 CVEs); an older
build -> the month's 15 CVEs. 92 releases parsed cleanly.

Wired up:
  - POST /api/v1/vulnerabilities/m365-check (synchronous, RequireEditor).
  - Nightly job m365_check_nightly at 03:20 UTC.
  - "M365 CVEs" button on the vulnerabilities page.

No migration — uses existing vulnerabilities + settings tables.
2026-06-03 13:34:42 +02:00
vulncheck 57e457e92f perf(enrich): confine rate-limited NVD date backfill to the nightly job
Follow-up to the published_date backfill. The NVD CVE API sleeps up to
6.5s per request without an API key, so running the backfill inline on
interactive / sync paths could hang them for minutes.

- NVD backfill now only runs in the nightly enrich_all_open_vulnerabilities
  job (and single-vuln enrich, <=1 lookup). Disabled on the "Refresh
  Threat Intel" button, the Wazuh full + per-agent syncs, and the Nessus
  sync — all pass use_nvd_dates=False to stay fast.
- Per-run lookup cap is now key-aware: 60/run without a key (~6.5 min),
  1500/run with a key (~18 min) — avoids a no-key install stalling the
  nightly job for the better part of an hour.
- Document NVD_API_KEY in .env.example (free key → ~10x faster fill,
  picked up automatically via env_file).
2026-06-03 12:28:51 +02:00
vulncheck 0cf0e5313f fix(enrich): backfill CVE published_date from NVD — fixes Newly-Published sort
Tester: "Newly Published" still wrong even in VIEW ALL.

Real root cause (not the sort SQL — that was already nulls-last desc):
published_date was NEVER populated. No ingest path wrote it — Nessus and
Wazuh imports only set detected_at, and enrichment only touched
EPSS/KEV/EUVD. So the column was 100% NULL and the sort had nothing to
order by → arbitrary order.

Fix: backfill published_date (and lastModified) from the NVD CVE API
inside the enrichment job.
  - fetch_nvd_cve_dates(): single-CVE GETs against services.nvd.nist.gov,
    persistent settings cache (dates are immutable → fetch each CVE once
    ever), capped NVD_MAX_LOOKUPS_PER_RUN=400 so a fresh DB drains the
    backlog over nightly runs instead of one rate-limit storm.
  - Honors NVD_API_KEY env for the 50/30s limit (else 5/30s, 6.5s sleep).
  - Only looks up vulns where published_date IS NULL — already-dated rows
    cost zero calls.
  - Toggle enrichment_nvd_dates_enabled (default on).

Also adds last_modified_date column (migration 029) + exposes both in the
vulnerabilities API response, per tester ("published date und updated
date"). Index on published_date for the sort.

Migration 029 required: alembic upgrade head.
2026-06-03 12:07:54 +02:00
vulncheck ee3b0f18bb fix(lifecycle): id-keyed Nessus reconcile — fixes inactive-never-set
Tester on alembic 028 + rebuilt, asset INACTIVE still never set after
a reduced-scope Nessus sync.

Root cause: reconcile keyed on nessus_host_uuid. The single host in the
reduced scan had no host_uuid in its Nessus host_info, so the seen-uuid
set came back EMPTY → the fail-open guard ("empty set = upstream maybe
failed, don't deactivate anything") skipped the whole reconcile → the
dropped hosts from the previous scan stayed ACTIVE.

Fix: track the matched asset.id of every host touched this sync
(seen_asset_ids) and reconcile on that instead of the uuid set. A
uuid-less host still contributes its id, so the seen-set is non-empty
and the dropped assets get inactivated.

New reconcile_nessus_by_seen_ids():
  candidate = ACTIVE AND (source==NESSUS OR nessus_host_uuid NOT NULL)
              AND id NOT IN seen_asset_ids  → INACTIVE
  revive    = same set, INACTIVE, id IN seen_asset_ids → ACTIVE
  fail-open only when seen_asset_ids is truly empty (no host matched).
Logs "Nessus sync reconcile: N seen, M inactivated, K candidates" so
the next report is self-diagnosing.

Old uuid-keyed reconcile_missing_from_sync kept for the Wazuh path /
back-compat.
2026-06-03 12:02:21 +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 bab0092940 fix(lifecycle): legacy Nessus asset reconcile + diagnostic logging
Asset rows created before nessus_host_uuid was pinned (matched by IP
or hostname in older syncs) are silently skipped by
reconcile_missing_from_sync because id_field.isnot(None) excludes
them. After a reduced scan they stay ACTIVE forever, contradicting
the "sync-driven INACTIVE" promise in feedback 808246f.

- asset_lifecycle.reconcile_missing_from_sync: log a count of legacy
  unpinned ACTIVE assets so the next tester reproduction surfaces
  the root cause immediately.
- nessus_sync.reconcile_legacy_nessus_assets: one-shot helper that
  flips these rows to INACTIVE with an audit-log entry, safe to run
  multiple times.

Fixes feedback 2026-06-02 #3 (INACTIVE not flipping on reduced scan).
2026-06-02 14:34:52 +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
vulncheck 703728c69d fix(eol): slug-based pseudo-CVE id for Nessus EOL findings
New helper _slug_pseudo_cve resolves the plugin_name to an
endoflife.date slug via eol_service.resolve_product_slug, then emits
EOL-{SLUG}-{VERSION|P####} instead of EOL-NESSUS-{plugin_id}. Office
variants keep the year-based EOL-MS-OFFICE-YYYY id for the language-
pack dedup. Legacy _office_pseudo_cve retained as a fallback when slug
resolution fails.

Detection Sources panel (nessus + plugin_id) is untouched — the
plugin id remains queryable from the vuln.nessus_plugin_id column.

Fixes feedback 2026-06-02 #2b, #4. Existing rows still need a one-off
backfill (out of scope here).
2026-06-02 14:34:52 +02:00
vulncheck b162392384 fix(lifecycle): harden reconcile_missing_from_sync + audit mapping
Security review follow-up (post-commit hook):

- Empty seen_ids guard: when the upstream Nessus/Wazuh sync returns
  zero host ids, skip the reconcile and log a WARNING. The previous
  behaviour would have deactivated every active asset of that source
  on an upstream API failure.

- Backfill now Python-side: replaced the bulk .update() with
  synchronize_session=False (which desyncs pre-loaded Asset instances)
  with a fetch + ORM mutate. Also tightened the predicate to
  length(nessus_host_uuid) > 0 so empty strings are skipped.

- _audit_asset_status: explicit dict-based transition -> event-type
  mapping. Reason text is preserved in event_description so the
  audit UI can distinguish 'sync missed it' from 'operator
  decommissioned'.
2026-06-02 09:03:04 +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 7edbe8207f feat(eol): detect EOL on Nessus-only hosts via Unsupported Version plugins
The endoflife.date EOL sweep only covers Wazuh agents
(scheduler filters wazuh_agent_id IS NOT NULL) and Nessus's own
'Unsupported Version Detection' plugins (64784 SQL Server, 56998/97085
Office, ...) carry no CVE, so they were dropped by the non-CVE skip in
nessus_sync. Result: Nessus-only / synced hosts had zero EOL coverage.

Re-admit the Unsupported-Version plugin family (detected via
unsupported_by_vendor=true or the plugin name) as EOL pseudo-vulns with
cve_id EOL-NESSUS-{plugin_id}. The EOL- prefix makes is_eol_finding true
so they appear in the dashboard Newly EOL/EOS widget next to
endoflife.date findings. Severity/CVSS/installed+fixed version come from
the plugin itself. Dedup key (cve_id, asset_id); the EOL id is added to
the per-run seen set so the source-backfill doesn't immediately patch
the row it just created. New 'eol_created' stat in the sync summary.

Other non-CVE plugins (compliance/cipher/info) stay skipped.
2026-06-01 13:26:34 +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 f6ea2f1e17 feat(assets): network-exposure risk dimension from Wazuh ports (feedback #5)
Tester: use Wazuh IT-hygiene (open listeners) as an extra risk
indicator — a host exposing VNC/RDP/Telnet is network-vulnerabler
regardless of CVE count.

Schema (migration 026)
- assets.network_exposure_score (FLOAT 0-100, partial-indexed >0)
- assets.exposed_services (JSON [{port, proto, service, risk, ip}])
- assets.exposure_updated_at

Service (exposure_service.py)
- get_ports() added to WazuhClient (/syscollector/{agent}/ports).
- analyze_ports(): classifies LISTENING sockets against a risky-port
  table (Telnet 40, RDP/VNC/SMB 30-35, FTP/rsh 28-30, DB ports 22-26,
  SSH 8, …). Loopback-bound listeners excluded. Score = strongest
  listener at full weight + 40% of each additional, capped 100.
- refresh_all_exposure() walks Wazuh-linked assets, persists.

API + scheduler
- POST /api/v1/assets/refresh-exposure (on-demand).
- Nightly job 02:30 UTC.
- AssetResponse exposes network_exposure_score + exposed_services
  (JSON parsed to list) + exposure_updated_at.

Frontend
- Assets list: new Exposure column — coloured score badge (red ≥60,
  orange ≥30, yellow else) with the top services inline + full list
  in the tooltip.
- " Exposure" toolbar button triggers a refresh.

Informational only — does NOT auto-change asset criticality (operator
owns that). Surfaces the data so the operator can raise criticality
on heavily-exposed hosts.
2026-06-01 08:40:36 +02:00