Commit Graph
42 Commits
Author SHA1 Message Date
vulncheckandClaude Opus 4.8 9e5b46979e fix(vulns): exclude EOL / Nessus-plugin pseudo-CVEs from published_date view
COALESCE(published_date, detected_at) made every EOL row ride the
detected_at fallback to the top of the "Newly Published" list — a
tester-reported false chronological order. Skip rows whose cve_id
starts with EOL- or NESSUS-PLUGIN- when sort_by=published_date and the
caller did not explicitly search=EOL-.

Fixes feedback 2026-06-02 #5a.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 13:46:46 +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 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 3cdd2ba5da feat(ui): deep-link VIEW ALL, EOL/exploit filters, breakdown rows, nessus-only patched fix
Four small UI/UX wins from tester feedback bundled.

A) Dashboard VIEW ALL deep-links
- Each widget passes a viewAllHref matching its filter:
  Recent Critical → /vulnerabilities?in_any_catalog=true&sort_by=updated_at
  Newly Published → /vulnerabilities?sort_by=published_date
  Newly EOL/EOS   → /vulnerabilities?search=EOL-&sort_by=detected_at
- "VIEW ALL >" now lands the operator on the matching filtered view
  instead of the unfiltered firehose.

B) EOL + Public-Exploit filter toggles on vulns list
- New `EOL` checkbox (purple) pushes `search=EOL-` to backend so the
  list scopes to endoflife pseudo-CVEs.
- New `PUBL-EXPL` checkbox (orange) client-side-filters the current
  page to rows with exploit_db_count > 0 OR pocs_github_count > 0
  OR metasploit_module_count > 0. NOTE: total count becomes the
  filtered-page length while the toggle is on — backend filter
  param can land in a follow-up if needed.

C) Priority breakdown shows public-exploit catalog counts
- Vuln-detail "Priority Score Breakdown" gets three new informational
  rows under the headline exploit_bonus:
    ↳ Metasploit modules N
    ↳ Exploit-DB entries N
    ↳ GitHub PoCs        N
  Tooltip: "Already counted in exploit_bonus via max() — listed for
  transparency". Addresses tester confusion that only the
  highest-weighted source appeared in the breakdown.
- TypeScript PriorityBreakdown type extended with the three counts
  and the three new exploit_source labels.

D) Nessus-only patched → PENDING_VERIFICATION loop
- update_vulnerability_status only triggers verify_patch_with_rescan
  when wazuh actually reports the vuln (wazuh in source_list).
  Nessus-only rows (sources=["nessus"]) skip the rescan path and
  mark PATCHED directly — they cannot be re-verified by Wazuh's
  syscollector since the agent doesn't know that CVE exists.
  Tester report: "Bei nessus only scan bleibt es im PENDING
  VERIFICATION stehen. Workaround: bulk-patch".
2026-05-31 13:48:07 +02:00
vulncheck e656701293 feat(exploit-intel): public exploit catalogs (EDB / PoC-in-GitHub / Metasploit)
Plan M — close the "exploit-db is only a link, not in the score"
gap. Three new enrichment sources mapped per-CVE, written to
dedicated columns, displayed as badges, weighted in the priority
breakdown.

Schema (migration 025)
- vulnerabilities.exploit_db_ids (JSON list) + exploit_db_count
- vulnerabilities.pocs_github_urls (JSON list) + pocs_github_count
- vulnerabilities.metasploit_modules (JSON list) + metasploit_module_count
- vulnerabilities.exploit_intel_updated_at
- partial indexes on the *_count columns where > 0 for fast
  "show me CVEs with exploits" filters.

Service (app/services/exploit_intel_service.py)
- fetch_exploit_db_cve_map(): downloads gitlab.com/exploit-database/
  exploitdb/files_exploits.csv (cached 24h), parses CVE refs from
  the `codes` column → {cve: [edb_id, ...]}.
- fetch_pocs_github_cve_map(): walks nomi-sec/PoC-in-GitHub yearly
  folders via the GitHub contents API, builds {cve: [repo_url, ...]}.
- fetch_metasploit_cve_map(): downloads rapid7's
  modules_metadata_base.json, extracts CVE refs per module.
- refresh_all_exploit_intel(): bulk writer, supports only_open + per-
  source toggles, marks/clears counts so a removed reference doesn't
  leave a stale flag behind.

Priority weighting
- New exploit signals folded into the existing max() block of
  exploit_bonus:
    metasploit_module_count > 0 → 4.5
    exploit_db_count        > 0 → 4.0
    pocs_github_count       > 0 → 2.5
  exploit_source label expanded with metasploit / exploit_db /
  poc_github so the breakdown UI shows which source drove the score.

API + Scheduler
- POST /api/v1/vulnerabilities/exploit-intel/refresh (editor) —
  on-demand pull with `only_open`, `fetch_pocs`, `fetch_msf` flags.
- New nightly job at 03:45 UTC (between vulnrichment 03:00 + URS
  04:00 so the score boost lands before URS recomputes).

Frontend
- Three list-row badges (MSF n / EDB n / PoC n) coloured red →
  orange → yellow, ordered by weight, with tooltips.
- Vuln-detail page gets a "Public Exploit Catalogs" card under
  Threat Intelligence — Metasploit module paths, clickable EDB-id
  links to www.exploit-db.com, GitHub PoC links.
- "Exploit Intel" toolbar button next to EOL Check.

Out of scope (Plan N or later):
- GHSA integration (GitHub Security Advisory Database).
- Nuclei template count.
- Per-source toggles in Settings UI.
2026-05-27 18:49:13 +02:00
vulncheck e85737b199 feat(dashboard+ui): EOL widget, status-change tooltip, header clarity
Three tester asks bundled (D dropped — current EOL scoring fine).

A) Status tooltip on vulns list
- Backend: _attach_last_change() bulk-loads the latest
  VULNERABILITY_UPDATED audit per page in one query (O(N), not N+1)
  and exposes it as `last_change: {timestamp, username, old_status,
  new_status, reason, source}` on the response.
- Frontend: hover on the Status badge shows the audit line plus a
  small inline source tag underneath ("admin" / "bulk" /
  "auto·nessus" / "auto·wazuh") so the operator can distinguish
  manual patches from scanner auto-patches at a glance, without
  opening the detail page.

B) EOL / EOS dashboard widget
- Dashboard grid expanded from 2 → 3 columns.
- New "Newly EOL / EOS" widget fed by
  GET /vulnerabilities?status=active&search=EOL- sorted by detection.
- Same row component as the existing widgets so style + click-
  through behaviour matches.

C) Header text clarification
- "Recent Critical CVEs" subtitle now reads
  "sorted by local re-score" instead of the ambiguous
  "sorted by last update" — caller was confused that it wasn't
  using upstream CVE-5 dateUpdated.

D) EOL scoring (noted — no change)
- Pseudo-CVEs already pick CVSS 9.0 (EOL/no patches coming) or
  6.0 (out-of-active-support, security patches still flow). priority
  + cpr derived from those via refresh_scores().

Out of scope (separate plans):
- Plan M: search_vulns integration (GHSA / exploit-db / PoC-in-
  GitHub / Metasploit / Nuclei) for richer enrichment.
- Plan N: direct exploit-db badge + score weighting.
2026-05-27 18:32:16 +02:00
vulncheck a332860aec fix(eol): verify_ssl default False to match Wazuh sync/rescan paths
Self-signed Wazuh certs are the norm — sync + rescan default to
verify_ssl=False. EOL check was overriding to True which broke
every get_packages() call with CERTIFICATE_VERIFY_FAILED.
2026-05-27 11:56:55 +02:00
vulncheck c6194c8c4e fix(eol): import read_setting_value from app.auth.setting_crypto
Forgot the import in run_eol_check + eol_check_nightly. Other call
sites (sync, rescan) import it inline — same pattern now.
2026-05-27 11:53:40 +02:00
vulncheck de16b06ba0 fix(eol): use read_setting_value + WazuhClient(base_url=) signature
500 on /vulnerabilities/eol-check — JSONDecodeError because the
raw settings row holds a Fernet-encrypted ciphertext, not bare
JSON. Other Wazuh callers (sync, rescan) use read_setting_value()
which decrypts first. Mine bypassed it.

Also fix WazuhClient kwargs: real signature is
WazuhClient(base_url=, username=, password=, indexer_url=, ...).
My code passed api_url= which was silently dropped → connect
attempt with None base_url would have failed even after the JSON
fix.

Same fix applied to scheduler.eol_check_nightly.
2026-05-27 11:52:02 +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
vulncheck 53baebed19 feat(audit): auto-stamp manual-status-change reason + per-vuln bulk log
Tester report: comment/reason field on the status-change modal is
optional → manual patches via single-select OR multi-select bulk
update produced audit-log entries with no rationale, making it
impossible to distinguish them from scanner auto-patches.

Backend
- _auto_reason(new_status, username) helper builds
  "Manually set to {status} by {username} on {YYYY-MM-DD HH:MM UTC}"
  whenever the operator submitted no comment.
- update_vulnerability_status now falls back to auto_reason when
  both `reason` and `defer_reason` are blank.
- bulk_update_vulnerabilities gains a `reason` field on
  BulkVulnerabilityUpdateRequest. Falls back to auto_reason for the
  status component. Emits a per-vuln log_vulnerability_change entry
  (source="manual_bulk") so each row's Change History panel shows
  WHO bulk-changed it, plus the existing one-line summary audit row
  for the admin firehose.

Net effect
- Bulk-patch 50 CVEs → 50 per-vuln audit entries tagged "manual_bulk"
  + 1 summary entry. Audit log + Vuln-detail Change History both
  populated.
- Single-patch with empty comment → entry is now
  "Manually set to patched by alice on 2026-05-27 11:39 UTC"
  instead of bare status diff.
- Scanner auto-patch entries unchanged (source="nessus_sync" /
  "wazuh_sync" — already distinct).
2026-05-27 11:01:20 +02:00
vulncheck 763e04a6c4 feat(override): canonical CVE-metadata propagation + sibling inheritance
Tester report: "Same CVE shows different CVSS depending on sort
order (PRIO vs CPR desc)". Cause: per-(cve_id, asset_id) rows drift
apart over time — override service touched some assets but not the
ones detected later by a fresh scan.

Strategy
- Define _CANONICAL_FIELDS = CVE-intrinsic columns that should NEVER
  differ between sibling rows of the same CVE:
    cvss_score, cvss_vector, severity,
    exploitation_status, exploitation_source,
    ssvc_technical_impact, ssvc_automatable.
- fixed_version EXPLICITLY excluded — Plan I (multi-stream picker)
  writes per-package fixes that legitimately differ between hosts
  running different release streams (Firefox ESR 115 vs 140 vs 150).

Propagation paths
- propagate_canonical_to_siblings(db, vuln):
  every override touch now mirrors the new canonical values onto
  all other vuln rows sharing the cve_id. Refreshes their
  priority_score + cpr_score so sort order converges.
- apply_canonical_from_siblings(db, vuln):
  every fresh sync insert (Wazuh + Nessus) pulls the freshest
  sibling's canonical values so the new row starts at the correct
  CVSS instead of Wazuh's 10.0 placeholder.

Backfill endpoint
- POST /vulnerabilities/canonicalize-cve-metadata (editor) — one-off
  pass over existing data. For every CVE with > 1 row, pick the
  source-pinned / most-recent vuln and propagate. Cheap; one query
  per distinct CVE.

What this fixes in practice
- New Wazuh agent reports an already-overridden CVE → row starts with
  the canonical 7.3 score, not 10.0.
- Override re-run on one asset propagates the corrected CVSS to all
  10 hosts that share the CVE.
- Sort-by-PRIO / sort-by-CPR now keeps all instances of a CVE
  adjacent in the listing.
2026-05-26 16:06:21 +02:00
vulncheck 74eeb66aac feat(audit): revisionssicher status-change trail + UI history panel
Tester asked for "echte Revisionssicherheit" — WHO patched WHAT,
WHEN, WHY — and reported that Nessus auto-patched CVEs (rows that
disappeared from a follow-up Nessus scan) were not visible in any
status anymore: not OPEN, not PATCHED, just gone-feeling.

Backend
- VulnerabilityUpdateRequest gains a status-agnostic `reason: str`
  field (defer_reason kept for legacy callers).
- log_vulnerability_change() now persists a JSON payload in
  audit_logs.new_value: {status, reason, source, cve_id}. user_id
  is nullable so automated transitions are clearly marked.
- Both auto-patch paths now write audit-log entries:
  * Nessus stale_nessus_vulns loop → "Nessus rescan {scan_id} no
    longer reports this CVE on {hostname}"
  * Wazuh stale_wazuh loop (both per-agent + full-sync) → same
    pattern with the agent_id.
- /audit/logs accepts resource_type + resource_id filters and lets
  editors query a single resource (admins still see everything).
  old_value + new_value added to the response so the frontend can
  parse the JSON payload.

Frontend
- Status-change modal sends `reason` (status-agnostic) instead of
  always overwriting defer_reason. Deferred status keeps the legacy
  fields too.
- Vuln detail page renders a "Change History" panel below Timeline:
  per-entry old→new transition, source tag (manual / nessus_sync /
  wazuh_sync), free-text reason, username + timestamp. Nessus auto-
  patched CVEs now have a clear paper trail.

No migration — audit_logs schema already had old_value / new_value
columns; we're just populating them with structured JSON now.
2026-05-26 08:57:11 +02:00
vulncheck e144ef6586 feat(vulns): status filter groups (active / closed) with SOC default
Tester asked whether the existing 'All Statuses' filter pulling in
FP / accepted / deferred rows was intended. It was, but the SOC
workflow always starts from 'show me what still needs action'.

Backend (GET /vulnerabilities ?status=...)
- 'active' (NEW) → open + pending_verification + patch_failed
- 'closed' (NEW) → patched + false_positive + accepted_risk + deferred
- 'all'         → no filter
- literal enum   → unchanged
- absent param   → defaults to 'active' (was 'open')

Frontend
- Dropdown is now optgroup'd: Groups / Active states / Closed states.
- Default state changed from 'open' to 'active' so the list shows
  the full actionable surface at first load.
- Send the literal value to the backend (including 'all') instead of
  treating 'all' as "omit param".
2026-05-25 19:46:09 +02:00
vulncheck bb5f4cc1ec fix(vulns): materialise priority_score + cpr_score for global SQL sort
Tester reported "Spaltensortierung gilt nur teilweise für die
aktuell angezeigten CVES." Cause: priority/cpr were computed in
Python from calculate_priority_breakdown() at response time. The
list endpoint paginated FIRST in SQL by an inaccurate proxy then
re-sorted only the current page by the real score → page 1 looked
fine, pages 2+ were out of order.

Schema (migration 024)
- Add indexed columns `priority_score` + `cpr_score` (FLOAT) to
  vulnerabilities.

Model
- `Vulnerability.refresh_scores()` recomputes + persists both
  columns. Cheap, no I/O.

Write paths now call refresh_scores():
- Wazuh sync (sync_agent_vulnerabilities) — both insert + update branches
- Nessus sync (run_nessus_sync) — both merge + create branches
- Enrichment (refresh_threat_intel_enrichment) — per-vuln after EPSS/KEV/EUVD
- Override service (_apply_single_override) — after the source pin

Sort
- /vulnerabilities?sort_by=priority|cpr now SQL-sorts on the
  materialised columns (NULLs last on desc, first on asc, with
  id tiebreaker). Whole filter result is in correct order across
  pages.

Backfill
- POST /vulnerabilities/recompute-scores (editor) — one-off pass
  over all rows. Run once after the upgrade so existing data
  picks up scores. Nightly Vulnrichment + URS jobs would converge
  the rest naturally.

Status filter UX (open question to operator)
- Tester also wondered if "All Statuses" should include closed
  states (false_positive / accepted_risk / deferred). Current
  behaviour matches the dropdown label literally and is left
  unchanged here — separate decision.
2026-05-25 19:38:06 +02:00
vulncheck 527c708814 feat(vulns): per-package detail table + UI for multi-package CVEs
Tester reported CVE-2023-48795 hitting PuTTY 0.73 AND WinSCP 6.1.2
on the same host — both joined into a single
`vulnerabilities.package_name` string ("PuTTY..., WinSCP...") with
ONE `fixed_version`. Per-package fix tracking impossible.

Schema (migration 023)
- New table `vulnerability_packages` (vuln_id, package_name,
  package_version, fixed_version, source, first_detected_at,
  last_seen_at). Unique on (vuln_id, package_name).
- Backfill creates one child per existing vuln carrying the joined
  string verbatim (no comma-split — joined names may contain commas).
- ON DELETE CASCADE + ORM passive_deletes so asset deletes propagate.

Sync (Wazuh)
- sync_agent_vulnerabilities collects per-package rows in a dict
  keyed by package_name and upserts VulnerabilityPackage children
  after the parent insert/update.
- last_seen_at updated each sync; future enhancement can prune
  packages Wazuh stopped reporting (mirror parent-row stale logic).

Override service
- _apply_single_override propagates Vulnrichment/NVD/cvelistV5
  fixed_version into every child package row that has none AND
  whose installed_version differs from the proposed fix
  (Ghostscript inclusive-bound case).

API
- VulnerabilityResponse gains `packages: List[PackageInfo]` and
  `has_fix_any: bool`. _build_vuln_response emits per-package data.
- Forward refs resolved with model_rebuild().

Frontend
- VulnerabilityPackage type + Vulnerability.packages + has_fix_any
- PATCH AVAILABLE badge on list page now uses has_fix_any +
  fixed_version != installed_version check (no more false positives
  when supposed fix equals affected version).
- Detail page renders a per-package table (Installed / Fixed in /
  FIX indicator / source tag) when child rows exist, falls back to
  legacy single-package summary for pseudo-CVEs.

No breaking changes — parent columns (`package_name`,
`package_version`, `fixed_version`) stay for legacy queries. Nessus
sync still writes only the parent row; per-package Nessus support
can land as a follow-up.
2026-05-25 19:08:01 +02:00
vulncheck f30bce470f fix(api): expose exploitation_status + exploitation_source in vuln response
Tester showed CVE-2024-29506 with 'Exploit (ssvc) +2.0' in the
Priority Breakdown (so exploitation_status='poc' in DB) yet no
POC pill on the vulns-list main page next to KEV/EUVD.

Root cause: VulnerabilityResponse Pydantic schema declared
exploitation_status and exploitation_source, but
_build_vuln_response dict never populated them. Pydantic silently
nulled both → API always returned exploitation_status=null → the
frontend pill guard `vuln.exploitation_status && != 'none'` was
always false → no badge rendered. The SSVC fields ssvc_technical_
impact + ssvc_automatable WERE in the response (added in 70840e0),
which is why TI-TOTAL and AUTO pills did render — but the simpler
POC/ACTIVE/WIDESPREAD pill from the original SSVC enrichment never
showed.

Two lines added to _build_vuln_response. Also adds exploitation_
source to the Pydantic schema so the frontend can show 'corrected
via vulnrichment' vs 'via nvd' / 'cvelistv5' tooltips down the
line.

Pure response-shape fix — no DB or migration. After deploy +
hard-reload tester sees POC pills next to KEV/EUVD on every CVE
that Vulnrichment classified as poc/active/widespread.
2026-05-22 08:26:23 +02:00
vulncheck 68572542ee fix: populate fixed_version + fqdn/short hostname asset dedup
Two colleague-reported issues addressed in one commit.

A) PATCH AVAILABLE badge never lit up
   Schema column, API field, and frontend pill were all in place
   since commit 0e83548, but no sync code ever wrote fixed_version
   into the DB. Result: column NULL for every row, badge silent.

   Wazuh sync (query_vulnerabilities_from_indexer):
   - Tries vulnerability.fix → vulnerability.fixed_version → package.fix
   - Falls back to regex over vulnerability.condition / package.condition
     ("Package less than X.Y.Z" pattern, common in Wazuh 4.x feeds)
   - Emits fixed_version on every result dict
   Both router paths (run_wazuh_vulnerability_sync and
   sync_agent_vulnerabilities) now persist it on create AND backfill
   it on update when the column was previously NULL.

   Nessus sync (NessusClient.plugin_fixed_version):
   - Reads vuln_information.fixed_version / fix_version when present
     (Windows MS-Bulletin plugins always carry it)
   - Falls back to regex over solution + plugin_output text:
     "Fixed version : X", "Upgrade to ... version X", "Upgrade to X"
   nessus_sync writes it on new vulns and backfills empty existing
   rows on merge.

B) Duplicate assets — Nessus FQDN vs Wazuh short hostname
   _find_or_create_asset matched hostnames with strict .ilike(),
   so 'host01.umgebung.local' (Nessus) never matched 'host01'
   (Wazuh) → second asset created, scoring split, dashboard misleading.

   Match order now:
   1) nessus_host_uuid pin
   2) exact hostname.ilike(full_form)
   3) short form (everything before the first '.') against
      Asset.hostname
   4) reverse — asset stored as FQDN, Nessus reports short →
      Asset.hostname.ilike('{short}.%')
   5) IP exact
   6) auto-create with the SHORT hostname so the next Wazuh sync
      consolidates onto the same row

   Existing duplicate rows stay — operator can merge via SQL or
   delete the Nessus-only asset once everything points at the
   Wazuh asset via nessus_host_uuid pinning.
2026-05-22 08:23:11 +02:00
vulncheck e71341bcdd feat(dashboard): split recent vulns into Critical + Newly Published widgets
Colleague asked for more than 5 entries and proposed splitting the
Recent Vulnerabilities table into two focused widgets so 'what
changed risk recently' and 'what just got published' don't fight
for the same 5 rows.

Backend:
- sort_map gains 'updated_at' (Vulnerability.updated_at) so the
  Critical widget can sort by 'risk-relevant DB write timestamp'
  — picks up Vulnrichment corrections, KEV listings, manual status
  changes etc.

Frontend (page.tsx):
- new renderVulnWidget() helper renders a compact 6-column table
  (CVE / Sev / CVSS / PRIO / CPR / Flags) instead of the previous
  wide 7-column one. Used twice on the dashboard:
    'Recent Critical CVEs'  — CVSS ≥ 8 OR KEV OR EUVD, top 10,
                              sort_by=updated_at desc
    'Newly Published CVEs'  — top 10, sort_by=published_date desc
- Flags column surfaces KEV / EUVD / SSVC status (WIDESP/ACTIVE/POC)
  pills inline so the operator can see actionable indicators at a
  glance without opening the detail page.
- Side-by-side on desktop (lg:grid-cols-2), stacked on mobile.

Both widgets dedup by cve_id client-side and pull a wider window
(80 entries) so 10 distinct CVEs land even when a hot CVE hits many
assets. Click on a row jumps to the filtered Vulns list.

Increased from 5 → 10 entries per widget (configurable via the loop
guard if 15 is preferred — set both to 15 and the limit=80 fetch
still covers worst case).
2026-05-21 21:36:26 +02:00
vulncheck a5c5361cea fix: wazuh sync scan-row + ssvc badges + compliance disconnected diag
Three colleague-reported issues addressed in one commit.

A) 'Sync Data (Wazuh)' button left no entry in /scans history
   while the Nessus button did (since commit 4bca41e). Mirrored the
   pattern: run_wazuh_vulnerability_sync now opens a per-agent Scan
   row (scan_type=WAZUH, status RUNNING→COMPLETED/FAILED) before
   fetching vulns and closes it after the source-aware backfill,
   recording vulnerabilities_found = len(active_cves). Skip-backfill
   branch records 0 + error_message explaining why.

B) Vulns-list SSVC badge only fired for exploitation_status != 'none'.
   ~99% of Vulnrichment-curated CVEs have exploitation_status='none'
   (CISA flags 'no known exploitation' for most), so colleague's
   495 SSVC-source-pinned CVEs showed zero badges. Added two more
   pills surfacing the actually-interesting SSVC dimensions:
     TI-TOTAL  ssvc_technical_impact='total' (attacker → full takeover)
     AUTO      ssvc_automatable='yes' (reliable mass exploitation)
   These render alongside the existing POC/ACTIVE/WIDESPREAD pill.

C) Compliance refresh appears to ignore disconnected agents — likely
   not a code filter but Wazuh returning empty /sca/{agent_id}
   responses for offline agents. Added per-asset logging when
   policies_synced=0 and an assets_no_data counter in the stats so
   the operator can see how many agents Wazuh actually has SCA data
   for. No code change to the filter — disconnected agents are still
   queried, just transparently reported as data-less if Wazuh has
   nothing on them.
2026-05-20 15:13:07 +02:00
vulncheck 70840e0d0a feat(ssvc): persist + display technical_impact and automatable
CISA Vulnrichment scores each CVE on three SSVC decision points:
Exploitation, Technical Impact, Automatable. We were already
persisting Exploitation (exploitation_status), but the other two
were parsed and thrown away — exactly the signal the tester wanted
to use to spot 'attacker takes total control + mass-exploitable'
CVEs at a glance.

Adds:

- Migration 013 (idempotent): two new nullable + indexed columns
    ssvc_technical_impact  VARCHAR(16)  -- partial | total
    ssvc_automatable       VARCHAR(8)   -- yes | no

- Model: matching SQLAlchemy columns on Vulnerability.

- vuln_override_service:
    * VerifiedCVEData gains both fields.
    * _parse_vulnrichment_record extracts both from the SSVC
      'options' list (alongside Exploitation).
    * _apply_single_override writes them when present, so the same
      'Correct CVSS' run also fills the SSVC enrichment.

- /api/v1/vulnerabilities response (VulnerabilityResponse +
  _build_vuln_response): exposes both fields.

- Frontend types + detail page: new SSVC sub-block under Detection
  Sources card renders Technical Impact + Automatable with red
  emphasis for 'total' and 'yes' (the high-risk values).

Frontend list column for these will follow once we have CPR bonus
weighting (next commit), so the operator sees the score uplift
alongside the badge in one motion.
2026-05-17 11:50:18 +02:00
vulncheck 5b18310cf8 feat(override): async job + status endpoint for vulnrichment correction
The synchronous /override/vulnrichment endpoint blocks the browser
request for the full duration of the correction — 23 minutes on the
tester's instance, well past every reasonable client timeout. Result:
'backend connection failed' modal and zero feedback about whether
the work actually ran.

New endpoints (sync one kept untouched for single-CVE backward compat):

  POST /override/vulnrichment/start
       Returns {job_id, status: 'queued', poll_url} immediately.
       Spawns a daemon thread that does the heavy lift.

  GET  /override/vulnrichment/status/{job_id}
       Returns the live snapshot: status, stage, total, done,
       updated/checked/not_found, error if any.

  GET  /override/vulnrichment/jobs
       Recent jobs ringbuffer for an admin overview.

State lives in an in-memory dict (override_jobs.py) — survives
requests, not backend restarts. That's fine; a restart would have
killed the worker thread anyway and the user re-clicks. Ringbuffer
caps at 50 entries, oldest finished gets evicted.

Worker owns its own SessionLocal() so it does not collide with the
trigger request's DB session. Stages narrate what's currently
happening ('downloading vulnrichment ZIP snapshot', 'fetching N
CVEs from github raw', etc.) so the frontend can show a meaningful
progress message even though we don't get per-CVE callbacks from
inside correct_vulnerability_scores.

Frontend modal that polls the status endpoint comes in the next commit.
2026-05-17 11:41:49 +02:00
vulncheckandClaude Opus 4.7 5e35630abf fix(sort): natural-numeric cve_id sort + published_date option (dashboard)
The previous cve_id sort used Postgres lex collation, which placed
CVE-2026-8401 above CVE-2026-35440 because '8' > '3' character-wise.
Tester reported the correct expectation: the higher numeric suffix
should win regardless of length.

Two fixes:

1. sort_by=cve_id now splits the ID on '-' and casts year + number to
   int, then orders numerically. Pseudo-CVEs (NESSUS-PLUGIN-*) drop
   to the end and tiebreak on the raw string, so the column is still
   sortable in a mixed dataset.

2. New sort_by=published_date uses coalesce(published_date,
   detected_at) — semantically the most defensible "newest CVE" sort,
   since CVE-IDs are not chronological (assigned in batches by CNAs).
   Falls back to detected_at when CISA enrichment has not stamped a
   real published_date yet.

The dashboard 'Recent Vulnerabilities' widget switches to
sort_by=published_date — what users actually mean by 'newest'.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 11:11:58 +02:00
vulncheck 03eef00f31 security: harden auth, secrets, headers and email rendering
Closes 10 findings from the automated security scan (1 critical, 4 high,
5 medium). Operator action required before redeploy — see deploy notes
in chat or README.DEV.md.

Critical:
- TOTP/LDAP Fernet key (AUTH_PROVIDER_CRYPTO_KEY) is now env-only.
  Removed the DB fallback that co-located the key with the ciphertext
  it protects.

High:
- Rate limiter no longer trusts X-Forwarded-For from arbitrary peers.
  TRUSTED_PROXY_CIDRS gates which direct peers may rewrite the client
  IP, and ProxyHeadersMiddleware trusted_hosts is narrowed from "*"
  to FORWARDED_ALLOW_IPS.
- TOTP codes are single-use within their 90s validation window.
  In-memory replay cache keyed on (user_id, code).
- JWTs carry a jti claim; logout revokes both access and refresh JTIs,
  refresh rotates (revokes the presented token), and get_current_user
  rejects any revoked JTI. In-memory store with TTL = token exp.
- Sensitive setting values (wazuh_config, smtp_config, nessus_config)
  are encrypted at rest with an enc:v1: prefix. All read sites go
  through read_setting_value(); legacy plaintext rows still readable
  until next write. GET responses redact secret subfields so admins
  cannot accidentally exfiltrate stored credentials.

Medium:
- Email template rendering HTML-escapes all dynamic values. The "rows"
  variable is whitelisted as pre-escaped HTML. Severity CSS class is
  whitelisted to prevent attribute breakout via crafted package data.
- Request logging redacts sensitive query parameters (token, password,
  code, mfa_token, ...). Validation-error handler no longer logs or
  returns the offending request body.
- /health returns only {"status":"healthy"} — environment and version
  no longer leak to unauthenticated callers.
- SETUP_ADMIN_TOKEN comparison uses hmac.compare_digest.
- Settings PUT denylists auth_provider_crypto_key (env-only) and
  refuses to store the "***set***" redaction placeholder back into
  protected configs.
2026-05-16 09:25:22 +02:00
vulncheckandClaude Opus 4.7 b3d09b3b96 fix(api): expose multi-scanner fields in VulnerabilityResponse schema
Tester reported the VPR score never showed up on the vuln detail page
even though the list view rendered it after the previous commit.
Root cause: the detail endpoint declares response_model=
VulnerabilityDetailResponse → Pydantic strips any dict keys not
listed on the parent VulnerabilityResponse schema. _build_vuln_response
correctly emitted sources/cross_confirmed/first_detected_by/
nessus_plugin_id/nessus_vpr_score/exploitation_status, but the schema
silently dropped them. The list endpoint has no response_model so it
passed through untouched — which is why the badge appeared there but
not in detail.

Adds the missing fields to VulnerabilityResponse so detail + PATCH
responses now include them. is_pseudo_cve and assigned_group_name
were also missing and are added in the same pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 16:50:14 +02:00
vulncheckandClaude Opus 4.7 3d9706a506 fix(sync): correct variable name in per-agent rescan (raw_vulns)
Mass-patch guard from 662742f copied the variable name from
run_wazuh_vulnerability_sync() (where the var is wazuh_vulns) into
sync_agent_vulnerabilities() — but that function uses raw_vulns
internally. Result: every scheduled per-agent rescan crashed with
'NameError: name wazuh_vulns is not defined' since yesterday.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 16:13:58 +02:00
vulncheckandClaude Opus 4.7 662742f8e2 fix(sync): skip backfill when scanner returns 0 findings (mass-patch guard)
After fixing an unreachable Wazuh manager, a sync run came back with an
empty agent vulnerability list (indexer warming up) and the
source-aware backfill happily marked 1500+ findings as patched because
every CVE was 'no longer reported'. Same vector exists in Nessus sync
when a host's response is partial.

Guard added in all three sync paths:
- run_wazuh_vulnerability_sync (bulk per-agent loop)
- sync_agent_vulnerabilities (per-agent rescan)
- run_nessus_sync (per-host loop)

If active_cves AND raw findings list are both empty for the
agent/host, log a warning, touch last_scan, and continue — backfill
is skipped. A scanner reporting a host as truly clean from one
moment to the next is far rarer than an API hiccup, so the default
must be 'don't wipe history'.

Recovery for vulns already wiped today (sources=[], status=patched,
recent patched_at) is handled with a one-off UPDATE; the README's
verification block stays valid post-fix.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 08:37:15 +02:00
vulncheck 3074820737 feat: CVSS score override service with CISA Vulnrichment support
- Add VulnOverrideService to correct erroneous Wazuh CVSS scores (e.g. 10.0 placeholder)
- Add CISA Vulnrichment integration for verified CVSSv3.1 scores and SSVC exploitation status
- Add new API endpoints:
  - GET /override/check - find incorrect scores
  - POST /override/nessus/{asset_id} - override from Nessus
  - POST /override/vulnrichment - override from CISA (all CVEs)
  - GET /override/stats - score error statistics
- Add exploitation_status (SSVC: none/poc/active/widespread) and exploitation_source columns
- Add 'Correct CVSS' button in vulnerabilities frontend
- Add SSVC badge display in vulnerability table
- Update Vulnerability model and types

Fixes: CVE-2026-8390 incorrect CVSS 10.0 → 7.3 HIGH
2026-05-14 20:36:51 +02:00
vulncheck e569b01e93 fix: wazuh_severity fallback in bulk sync + rate-limit MFA endpoints
- Bulk sync was calling map_severity(score) without wazuh_severity
  fallback — vulns without a CVSS score got severity=none instead of
  using Wazuh's own severity string (High/Medium/Low) as fallback.
  Per-agent sync (sync_agent_vulnerabilities) already did this correctly.

- mfa/setup, mfa/activate, mfa/disable had no rate limiting.
  With a stolen JWT an attacker could brute-force the password on
  mfa/disable (no per-user lockout tracks POST /auth/mfa/* attempts).
  Added: setup=5/min, activate=10/min, disable=5/min.
2026-05-14 17:50:46 +02:00
vulncheck 2f7f0f0714 fix(wazuh-bulk-sync): mirror source-aware fixes to bulk sync path
The bulk sync endpoint (POST /api/v1/vulnerabilities/sync) duplicates
the same logic as sync_agent_vulnerabilities and had the same bugs:

- db.rollback() on IntegrityError replaced with db.begin_nested()
  savepoint — full rollback was losing all prior inserts for an agent
- New vulns now set sources='["wazuh"]' and first_detected_by='wazuh'
- existing.add_source('wazuh') on merge path for cross-confirmation
- Wazuh no longer reopens accepted_risk/false_positive/deferred vulns
  (patched → open, but other manual statuses preserved)
- Patching logic is now source-aware: remove 'wazuh' from sources,
  mark patched only when source_list is empty. Nessus-only findings
  are not touched.
- Enrichment query scoped to newly_created_vuln_ids instead of
  'all vulns without enrichment_updated_at' which could be thousands
2026-05-14 17:46:48 +02:00
vulncheck 37698189e2 fix(wazuh-sync): source-aware backfill + savepoint + first_detected_by
Three bugs mirrored from the Nessus sync review:

1. db.rollback() on IntegrityError replaced with db.begin_nested()
   savepoint — the full rollback was wiping all previous inserts in the
   same agent sync run when a race-duplicate was encountered.

2. New Wazuh vulns now set sources='["wazuh"]' and
   first_detected_by='wazuh' explicitly. Previously only the SQLAlchemy
   Python default fired (sources) and first_detected_by was always NULL
   for Wazuh-created entries after migration 010.

3. Wazuh patching logic now source-aware: instead of bulk-marking every
   open vuln not in active_cves as PATCHED (which silently wiped
   Nessus-only findings), we now iterate wazuh-sourced open vulns,
   remove 'wazuh' from sources, and only flip status=patched when
   source_list is empty. Nessus-only findings are untouched.
   Also: existing.add_source('wazuh') called on merge path so
   cross-confirmation is tracked when Wazuh finds a Nessus-only CVE.
2026-05-14 17:44:17 +02:00
vulncheck 9ae16a3668 feat(nessus): import VPR, exploit_available, maturity, description, solution
Nessus plugin payloads carry more than just severity + CVE — we now
extract and store everything actionable:

- nessus_vpr_score (new column, migration 011): Tenable's VPR rating
  (0-10), independent from our own priority_score
- exploit_available (existing boolean): True when Nessus knows of a
  public exploit; only escalates, never overwrites Wazuh-confirmed True
- exploit_maturity (existing string): Unproven / PoC / Functional / High
- description: prose description + 'Solution:' section, only set when
  empty so hand-written notes survive
- references: see_also URL list as JSON, only when existing is null

Create path sets all fields directly; merge path backfills empties so
existing Nessus findings get enriched on the next sync. API response
adds nessus_vpr_score; frontend Vulnerability type mirrors it.
2026-05-14 17:24:08 +02:00
vulncheck 515c9e3084 fix(nessus): populate package_name from plugin_name + sortable source column
- nessus_sync: new findings now store plugin_name as package_name so the
  Vulns table's PACKAGE column shows the affected software for Nessus-only
  rows (was blank). Merge path backfills package_name / title on existing
  rows that were created before this fix.
- vulnerabilities router: sort_by='source' maps to first_detected_by,
  so users can group rows by scanner from the table header.
- Vulns page: Source column header is now clickable with the same
  hover-style and SortArrow as the other sortable columns.
2026-05-14 16:37:21 +02:00
vulncheck 0a67e5bd2a feat(nessus): Vulns list — source column, filters, false-positive workflow
Backend (app/routers/vulnerabilities.py):
- _build_vuln_response includes sources[], cross_confirmed,
  first_detected_by, nessus_plugin_id, is_pseudo_cve
- New query params on GET /api/v1/vulnerabilities:
    ?source=wazuh|nessus|manual    — filter by scanner attribution
    ?cross_confirmed=true          — only multi-scanner findings
  Implementation uses SQL LIKE on the sources JSON text (rows with a
  comma in the JSON list have >=2 sources, sufficient for our scale).
- New endpoints:
    PATCH /api/v1/vulnerabilities/{id}/false-positive
    PATCH /api/v1/vulnerabilities/{id}/unmark-false-positive
  Both audit-logged. False-positive marks set
  notification_suppressed=true automatically (no SLA-breach spam).

Frontend:
- frontend/types/index.ts: sources, cross_confirmed, first_detected_by,
  nessus_plugin_id, is_pseudo_cve fields
- frontend/app/vulnerabilities/page.tsx:
    * Filter bar gains a Source dropdown (all/wazuh/nessus/manual) and
      a Cross-confirmed checkbox
    * New 'Source' column between Status and Assigned-To with per-scanner
      badges (Wazuh green, Nessus purple, manual gray), ✓×2 emerald
      badge when cross-confirmed, NON-CVE amber badge for pseudo-CVE
      (Nessus EOL / Compliance / Cipher findings)
    * NoSymbolIcon button in Actions: click → prompt for reason →
      PATCH /false-positive. Re-click on a false-positive vuln reverts
      back to open.

This makes the Wazuh ∪ Nessus merged view actionable: admins see which
findings have multi-scanner confirmation and can mark Nessus-only false
alarms without affecting Wazuh-confirmed entries.
2026-05-13 23:14:28 +02:00
vulncheck e12f11134e feat(notifications): digest mode for new-vulnerability emails
Replaces the per-CVE inline email send during Wazuh sync with a batched
digest dispatcher that runs once at the end of the sync.

- Old behavior (now opt-in via notification_mode=single): one email per
  CVE per recipient. A 100-CVE sync with 3 assignees = 300 mails, often
  triggering SMTP rate limits (Gmail/Outlook/Proton ~20/min).
- New default (notification_mode=digest): one email per recipient with
  a styled HTML table listing every CVE relevant to them (severity
  counts at the top, sorted rows below, button to dashboard). 100 CVEs
  × 3 assignees collapse to 3 mails.

Implementation:
- email_service.dispatch_new_vuln_notifications(db, new_vulns):
  resolves recipients per vuln (vuln.assigned > group > asset.assigned
  > asset.groups), applies notification_min_severity, groups by email,
  and sends. Logs one NotificationLog per recipient.
- email_service.send_new_vulnerability_digest() renders new
  DEFAULT_DIGEST_TEMPLATE (responsive HTML, severity badges, table).
- email_service.get_notification_mode() reads notification_mode setting.
- Sync paths (run_wazuh_vulnerability_sync, sync_agent_vulnerabilities)
  now accumulate newly_created_vuln_ids and call the dispatcher once
  after the loop instead of mailing inline.
- Settings UI gains a 'Delivery Mode' dropdown next to the severity
  threshold.
2026-05-12 21:13:55 +02:00
vulncheck 34324996f8 feat(vulns): click-to-sort table headers + sticky Actions column
- Headers (CVE, Severity, CVSS, EPSS, KEV, EUVD, CPR, Priority, Status)
  clickable, toggle asc/desc, arrow indicator. Defaults to priority desc.
- Backend sort_by extended: epss, cpr (CVSS*EPSS proxy in SQL),
  kev, euvd, severity, status, cve_id. NULL values sort last on desc.
- CPR computed in Python (CVSS*EPSS*10), SQL uses coalesce for NULL safety.
- Priority re-sorted Python-side after SQL proxy fetch since real score
  needs policy+age context.
- Actions column sticky right with shadow — stays visible when table
  scrolls horizontally on narrow viewports.
2026-05-12 15:16:34 +02:00
vulncheck 356bf7b97f feat(risk-score): add ENISA EUVD enrichment + CPR score
EUVD (EU Vulnerability Database, ENISA) integration as second
authoritative catalog alongside CISA KEV. EU-Compliance use cases
benefit from a non-US source; CVEs confirmed by both catalogs get
the highest priority via score stacking.

Two ENISA endpoints are merged into one cached map (24h TTL):
- /exploitedvulnerabilities (analogous to CISA KEV)
- /criticalvulnerabilities (ENISA Critical flag)

Priority-Score formula:
- Exploit-Signal now triggered by KEV OR EUVD listing
- Catalog-Bonus stacking: KEV +10, EUVD +10, KEV-ransomware +5,
  EU-Critical +3. A CVE in both catalogs adds +20 base.

CPR Score (Cybersecurity Priority Risk = CVSS x EPSS x 10) added
as separate metric next to Priority, per JacquesKruger/EPSS-Server
convention. Calculated on-the-fly, no DB column needed.

New API filters: euvd_only, eu_critical, in_any_catalog,
in_both_catalogs. Setting toggle enrichment_euvd_enabled (default true).

Frontend: new EUVD column (blue badge, EU-CRIT sub-badge), CPR column
with mini-bar, four catalog filter checkboxes. Detail page splits
threat intel into CISA KEV / ENISA EUVD / EPSS sections; breakdown
shows EUVD bonus row and CPR score with both-catalogs hint.
2026-05-11 19:47:37 +02:00
vulncheck 14356c9a07 feat(notifications): configurable severity threshold for new-vuln emails
Add a settings-page dropdown (Critical / High+ / Medium+ / Low+) that
controls which severities trigger a 'new vulnerability' email during
Wazuh sync. Stored in the existing settings KV table under
'notification_min_severity', default 'critical' (no behaviour change
on upgrade).

Backend:
- email_service.should_notify_for_severity(db, severity) reads the
  threshold from settings and compares using a severity rank.
- Both sync paths in vulnerabilities.py now delegate to the helper
  instead of the hard-coded 'critical only' check (the comment
  already lied about 'CRITICAL or HIGH').

Frontend:
- New control in the Notification Settings section, persists via the
  generic /api/v1/settings/{key} PUT endpoint.
2026-05-11 15:41:40 +02:00
vulncheckandClaude Opus 4.7 42f867a58b feat(risk-score): enrich priority with EPSS + CISA KEV
Risk score now pulls from multiple threat intel sources instead of
only AI/CVSS data:

- EPSS (FIRST.org) — probability of exploitation in next 30 days
- CISA KEV — known actively exploited vulnerabilities (with ransomware flag)
- Existing Wazuh exploit flags as fallback

Adds DB columns (epss_score, epss_percentile, kev_listed, kev_*,
enrichment_sources, enrichment_updated_at), an enrichment_service
with cached KEV catalog (24h TTL in settings table) and batched
EPSS lookups, manual + bulk + KEV-refresh endpoints, automatic
enrichment after Wazuh sync, and a daily scheduler job to refresh
scores.

Frontend gets KEV badges, EPSS column with percentile, KEV-only +
EPSS-min filters, a "Refresh Threat Intel" button, and a priority
score breakdown card on the detail page.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 15:37:53 +02:00
vulncheck faf8b89168 Fix scan sync: severity fallback, solved-filter, timestamps
- Add Wazuh severity string as fallback when CVSS score is missing,
  preventing critical vulns from being classified as "none"
- Fix solved-filter to check only the LATEST alert status per CVE
  instead of filtering all CVEs that were ever solved (fixes re-emerged
  vulns being incorrectly hidden)
- Try both score.base and score.base_score for indexer compatibility
- Add detailed sync logging (severity breakdown, new/updated counts)
- Show time alongside date for detected_at in frontend and PDF reports
2026-02-15 15:54:07 +01:00
vulncheck 6969d0c62e Initial release v1.0.0
VulnCheck - Open Source Vulnerability Management for Wazuh

Features:
- Vulnerability management with Wazuh integration
- AI-powered CVE analysis (OpenAI, Anthropic, Google, DeepSeek, Ollama, Infomaniak)
- SLA policy enforcement with automated email alerts
- Automated patch verification via Wazuh Syscollector
- Role-based access control (Admin, Editor, Readonly)
- PDF/CSV reporting for compliance workflows
- Full audit trail

https://gitea.isuit.ch/vulncheck/vulncheck
2026-02-08 10:15:20 +01:00