Commit Graph
151 Commits
Author SHA1 Message Date
vulncheck 1609d3edbc fix(wazuh): scroll the states index (fixes 400 + lost findings on big agents)
Paging used from/size, which OpenSearch hard-caps at index.max_result_window
(default 10000). Agents with more findings 400'd at from=10000, the whole agent
then read as '0 vulns' and its backfill was skipped — so hosts with 24k/27k
findings silently contributed nothing. Switch full syncs to the scroll API (no
window ceiling, no unique-sort requirement), with a fallback to windowed paging
if the indexer refuses scroll; explicit-offset callers keep from/size but stop
at the window instead of erroring.

Also hide the dashboard AI Audit controls + recommendations panel from
read-only users (the audit endpoint is editor-gated and threw 'Access denied').
2026-07-14 11:30:06 +02:00
vulncheck 904a3ec1ca fix(notifications): provide {{affected_assets_count}} in the digest template
The digest only exposed the per-CVE count (inside {{rows}}), so a top-level
{{affected_assets_count}} in the digest body stayed literal in sent mail (the
preview substituted it from sample data, masking the gap). Add it to the digest
variables = distinct assets across the whole digest ('affected systems' tile).
Docs + template hint updated.
2026-07-13 14:28:00 +02:00
vulncheck 93e2b1912c fix(ui): readonly AI card, clipboard fallback, template preview vars
- Hide the AI Remediation section for read-only users (Generate hits an
  editor-only endpoint; output isn't persisted, nothing to show them).
- Copy buttons: navigator.clipboard is undefined over plain http://<ip>
  (non-secure context) so copies silently failed — add a textarea+execCommand
  fallback.
- Email template Preview: add the new sample variables (cpr_score,
  affected_assets_count, cve/asset deep links) and a sample {{rows}} HTML table
  so previews render real values instead of literal {{...}} placeholders.
2026-07-13 13:15:48 +02:00
vulncheck 65d6c62561 fix(rbac): show assignee as text for read-only (not a dead 'Unassigned' select)
Read-only users can't fetch /auth/users (admin-only), so the disabled assign
<select> resolved to 'Unassigned' even when the finding/asset WAS assigned — the
head icon showed assigned but the name was hidden. Render the assignee name
straight from the payload (assigned_user_name / group name) as read-only text
instead. Applied on both the vulnerabilities and assets lists. Also fix a
pre-existing invalid title= prop on the Nessus <svg> (use a <title> child).
2026-07-13 13:09:00 +02:00
vulncheck 867498c3d3 fix(vulns): hide the Actions column entirely for read-only users
Read-only saw an empty Actions column with just a '—' placeholder. Gate the
header, the per-row cell, and the empty-state colSpan on role so the column
disappears for read-only instead of hanging around blank.
2026-07-13 10:39:12 +02:00
vulncheck f9d9919615 feat(audit): forward audit-log events to syslog/SIEM (UDP/TCP, RFC-5424)
Every audit_logs insert is mirrored as an RFC-5424 syslog message with a
per-event severity (login failures/lockouts/access-denied → warning, security
alerts → alert, config/deletes → notice, else info). A SQLAlchemy after_insert
hook enqueues onto a bounded queue drained by one daemon worker (never blocks
the request/flush; bursty syncs drain sequentially). Config cached 30s; TCP
keeps a persistent socket with reconnect. Disabled by default → no-op until
enabled. Admin-only config card (host/port/UDP-TCP/facility) with a Test button;
POST /api/v1/settings/syslog/test sends a probe. No TLS yet (UDP+TCP).
2026-07-13 10:33:29 +02:00
vulncheck a289c07ccb feat(auth): permanent account lockout + admin unlock
Opt-in via the auth_lockout_permanent setting: after 5 failed logins an account
is locked until an admin clears it (post-incident review), instead of the
temporary 15-min auto-unlock. Safeguard: the last active admin is NEVER
permanently locked (falls back to the temporary window) so a brute-force on the
admin login can't lock everyone out for good.

- migration 037: users.locked + locked_at
- local.py: permanent-lock check + threshold escalation with last-admin guard
- POST /auth/users/{id}/unlock (admin, audit-logged); list_users exposes locked
- Settings UI: 'Permanent account lockout' toggle + per-user Unlock button/badge
2026-07-13 10:30:05 +02:00
vulncheck 5e85a5cf25 fix(rbac): hide vuln write-actions from read-only + scope notification log
- Vulnerabilities list: the per-row Change-Status / Suppress / Mark-False-
  Positive buttons and the assign dropdown were shown to read-only users and
  threw 'Access denied. Required role: editor'. Gate them (editor+); assign
  select disabled for read-only.
- CVE detail page: Dismiss/Reactivate now shown only to editor+ (fetches
  /auth/me for the role).
- Notification log (/notifications): GET /log returned EVERY sent mail —
  recipient emails + which CVE went to whom — to any authenticated user (privacy
  leak). Now admins see all; everyone else sees only notifications addressed to
  them (user_id == own).
2026-07-13 09:41:21 +02:00
vulncheck 4a25a5d1c0 feat(notifications): add CPR, affected-systems count & deep-link template vars
Both single and digest new-CVE emails now expose risk/priority context and
ready-made links so operators don't hand-build URLs:
- cpr_score: the contextual priority rating (risk-led triage)
- affected_assets_count: distinct assets carrying this CVE (blast radius),
  computed once per batch via a grouped query
- cve_link / asset_link / cve_on_asset_link: prebuilt deep links into the
  vulnerabilities view (?cve_id=, ?asset_id=, both)
Digest rows gain CPR + #Systems columns and a clickable CVE; the single-mode
default template shows CPR, affected count and two action buttons. Settings
variable hints updated.
2026-07-13 09:37:25 +02:00
vulncheck 702dfcbfb3 feat(notifications): editable digest template + nightly roundup + rate limit
#2 The digest template (email_template_new_vuln_digest, actually sent in the
default digest mode) was not editable in the UI — only the single-mode template
was — so a customized template appeared unused. Add a digest-template editor in
Settings (mode-aware Active/Inactive badges clarify which applies).

#3 Add a nightly aggregated roundup: notification_schedule='nightly' suppresses
per-sync mails and a self-gated hourly scheduler job sends ONE digest per
recipient of all CVEs since the last run (windowed via
notification_nightly_last_run), at notification_nightly_hour. Plus GUI-config
send rate limiting (email_rate_delay_seconds, email_max_per_run) applied in the
dispatch loop against provider anti-spam.
2026-07-13 07:53:25 +02:00
vulncheck 01e766dace fix(vulns): hide Refresh Threat Intel from read-only users
Every other admin toolbar button on the vulnerabilities page is hidden for
read-only (role !== 'readonly'), but Refresh Threat Intel was only disabled —
so it showed greyed-out and dead. Hide it like the rest for consistency.
2026-07-11 10:46:05 +02:00
vulncheck 96e6072a41 fix(assets): hide write-actions from read-only users (no more 403 alerts)
A read-only user saw every row action (Wazuh/Nessus/app rescan, edit, delete,
assign) and Add Asset, but the backend gates those (RequireEditor/RequireAdmin)
so clicking threw 'Access denied. Required role: editor'. Fetch the current
role and show mutating actions only to editor+ (delete to admin); read actions
(installed software, coverage gap, exposure) stay for everyone.
2026-07-10 14:07:31 +02:00
vulncheck 4aac1bfd06 fix(settings): hide OpenRouter/Intune config from non-admin users
Every other card in the System-config column is already gated behind
userRole==='admin', but the OpenRouter and Intune cards rendered
unconditionally, so a read-only user saw empty admin config with dead
Save/Test buttons (backend already 403s them via RequireAdmin, so no data
leak — but confusing). Gate both like the rest.
2026-07-10 14:02:59 +02:00
vulncheck ae52cfe293 feat(assets): on-demand installed-software view per asset
New GET /assets/{id}/software pulls the live software inventory (Wazuh
syscollector packages, else Intune detectedApps) — the same inventory the
app-CVE scanner consumes — without persisting it. A list-icon button on each
Wazuh/Intune-backed asset row opens a modal with name/version/vendor.
Read-only, deduped, sorted. No DB schema (ponytail: on-demand until a
searchable/reporting inventory is actually needed).
2026-07-10 13:37:47 +02:00
vulncheck c3b9bb6cbf fix(app-scan): exclude OpenSSL FIPS builds (FP) + per-asset app re-scan button
- OpenSSL FIPS provider builds (e.g. Veeam's 'OpenSSL v3.0.0 FIPS') were
  matched against OpenSSL CVEs, but the advisories explicitly carve the FIPS
  modules OUT (vulnerable code is outside the FIPS boundary) and they carry a
  separate 4-part build version — pure false positive (CVE-2025-15467). Exclude
  via negative lookahead 'openssl(?!.*fips)'. Existing FP self-heals: next scan
  no longer detects it -> auto-resolve marks it patched (now audit-logged).
- Add an 'App CVE re-scan' action button on asset rows (Wazuh- or Intune-backed
  assets) that hits the existing POST /vulnerabilities/app-cve-scan?asset_id=,
  analogous to the Wazuh rescan button.
2026-07-10 13:33:25 +02:00
vulncheck 572b6fe217 fix(settings): handle skipped/empty sync result + clearer status colors
The sync-status poll showed 'Sync done — undefined devices…' in green when a
run was skipped by the advisory lock (result = {skipped}). Branch on skipped
(amber 'already running'), guard the stat fields with ?? 0, and give states
distinct colors: loading=blue pulse, warning=amber, success=green, error=red.
2026-07-09 15:16:54 +02:00
vulncheck 381c21034c fix(dashboard): show 10 distinct CVEs in Newly-Published/EOL widgets
The widgets fetched a per-asset-duplicated list and deduped by cve_id
client-side. When a CVE sits on many assets, a handful of CVEs x N assets fill
the whole fetch window, so dedup starved and the widget showed ~4 of 10. With
131 assets no limit can guarantee 10 distinct (10x131 > 1000 cap).

Add a distinct_cve query param to the vulnerability list: a row_number() window
partitioned by cve_id keeps one representative row per CVE (newest published,
then most recent) server-side, composing with every sort_by. Newly-Published
and EOL widgets pass distinct_cve=true (Mobile stays per-asset by design).
2026-07-09 14:02:49 +02:00
vulncheck e78b4fef80 feat(intune): sync status endpoint + GUI polls for completion
The manual sync is fire-and-forget (202), so the GUI got stuck on 'Sync started
in background' forever. Track last/current sync state in the router and expose
GET /sync/status; the settings page now polls every 3s and shows the real
result (devices/matched/created/app-findings + Defender new CVEs) or the error.
2026-07-09 13:59:00 +02:00
vulncheck d6d2c3209b fix(ui): rebrand left ~115 truevuln-blue classes with no matching CSS var
The rebrand renamed many Tailwind color classes vulncheck-blue → truevuln-blue
but left the theme token as --color-vulncheck-blue, so those 115 classes
referenced an undefined color → transparent backgrounds / unstyled text
(tester: the "Save Template" button was invisible until hover; also affected
buttons, links, focus rings, sort arrows app-wide).

Unified everything on truevuln-blue: renamed the @theme token to
--color-truevuln-blue and the remaining 156 vulncheck-blue class usages to
truevuln-blue. Now all 271 usages resolve to one defined token; 0 vulncheck-blue
left.
2026-07-09 08:21:07 +02:00
vulncheck 405b4bc541 fix(notifications): fall back to default recipients / admins for unassigned findings
New-vulnerability emails only went to a finding's assigned user/group (or its
asset's). The thousands of scanner findings are assigned to nobody, so the
nightly digest and the bulk "Notify All Critical/High" resolved zero
recipients — "Sent: 0, Failed: 0" — even with SMTP configured and an admin
email set.

_resolve_recipients_for_vuln now falls back, when the assignment cascade is
empty, to get_default_recipients(): the configured
`notification_default_recipients` setting (comma/semicolon/space-separated
emails), or — if unset — every active admin user, so "the admin gets
everything" works out of the box. The bulk-notify endpoint was rewritten to
use the same cascade (it previously only checked asset assignment, with no
fallback). Added a "Default Recipient(s)" field to the notification settings UI.
2026-07-09 08:21:07 +02:00
vulncheck c90a7bc0e5 docs: document this session's features, simplify frontend README
README.md: the "Features" section was silent on everything built this
session — added a subsection for the built-in App→CVE scanner (OSV/NVD-CPE/
cvelistV5), Wazuh false-positive suppression, and mobile device security
(EOL/EOS, Android patch-level, per-CVE Android via Samsung SMR/Google ASB);
extended the KEV bullet to mention the new advisory dashboard widget; added
the new /api/v1/advisories endpoint group to the API table.

README.DEV.md: added a 6th feature-group entry to "What this branch adds"
and a full new section (matching the doc's existing per-feature style —
what/why, module map, design decisions, out-of-scope) covering the
App→CVE engine, FP suppression, mobile device security, and the KEV
advisory feed, so this session's work has the same documentation trail as
every earlier feature drop in this doc.

frontend/README.md: was still the untouched create-next-app boilerplate,
including "Deploy on Vercel" instructions that don't apply (this app ships
via Docker Compose). Replaced with a short pointer to the root README/
README.DEV for setup and deployment.

Not touched: .env.example DEFAULT_ADMIN_EMAIL (admin@vulnmanager.local) —
verified consistent with README's own reference to it, no drift.
2026-07-07 16:46:43 +02:00
vulncheck a4b8c85d06 fix(rebrand): catch the split-span brand name the string rename missed
Nav/Drawer render the name as two separate JSX text nodes ("Vuln" + a
<span> for the color-accented "Check") for the two-tone styling — the
literal string "VulnCheck" never appears in the source, so the earlier
exact-string rename pass didn't touch it. Logo/Nav still showed "VulnCheck"
after that commit (tester screenshot). Now "True" + accented "Vuln",
matching the rest of the rebrand. logo.svg itself is just the shield+check
glyph with no embedded text — nothing to change there.
2026-07-07 16:39:18 +02:00
vulncheck dbad9a365e chore(rebrand): VulnCheck → TrueVuln
Renames the product name in every user-visible surface and internal
self-reference: page title, nav/shell, login/MFA pages, email templates and
subject prefixes ([VULNCHECK] → [TRUEVULN]), TOTP issuer label, report/PDF
headers, notification previews, outbound User-Agent/HTTP-Referer headers we
set ourselves, docs (README, ARCHITECTURE, PROJECT_OVERVIEW, DATABASE_SCHEMA,
README.DEV, TROUBLESHOOTING is untouched — see below), and .env.example
placeholder config (LDAP/OIDC/SAML example domains and paths).

Also renamed the on-disk cache file paths (/tmp/vulncheck-*.zip|csv|json →
/tmp/truevuln-*), kept consistent across the two files that share the
cvelistV5 ZIP cache path — first run after deploy re-downloads that ~557 MB
cache once (harmless, disposable).

Deliberately LEFT UNCHANGED (not branding — real external references or
infra identifiers; renaming the text without renaming the underlying thing
would just break/mislead):
- The actual Gitea repo URL/path (gitea.isuit.ch/vulncheck/vulncheck) and the
  README lines derived from it (git clone target dir, tree listing) — a real
  repo rename is a manual Gitea-side step (Settings → repository name) the
  user would need to do themselves, and existing clones would need
  `git remote set-url` after.
- The real support mailbox (support-vulncheck.sq9vd@passmail.net, in both
  README and TROUBLESHOOTING) and the Buy Me A Coffee link — both point to
  accounts that still exist under the old name; renaming the text alone
  wouldn't create new ones.
- GitNexus MCP resource URIs in CLAUDE.md/AGENTS.md (gitnexus://repo/
  vulncheck/...) — tied to GitNexus's own index name for this repo, not our
  branding; those files are untracked in this repo anyway.
- docker-compose.yml container/network/Postgres user+db names
  (vulnmanager-*) — explicit user decision: infra naming carries real
  deploy/data risk on an already-running instance and isn't part of the
  product-branding ask.
- The Tailwind color token class `vulncheck-blue` (frontend/app/globals.css)
  — invisible internal CSS variable name, renaming it would touch ~270
  className occurrences for zero user-visible benefit.

Verified: backend py_compile clean on every touched .py file; frontend tsc
clean (two pre-existing, unrelated errors remain: assets/page.tsx SVG title
prop, mfa-setup missing qrcode.react types). All diffs are exact-string
renames — no other changes riding along.
2026-07-07 16:34:40 +02:00
vulncheck 5d2669df21 feat(advisories): CISA KEV "actively exploited" awareness feed
New security-advisory section, independent of asset findings: a rolling view
of what's being actively exploited in the wild so operators see 0-days/
exploited CVEs even before a scanner flags an affected asset.

- Reuses the KEV catalog enrichment already fetches + caches (24h); added
  vendor/product/name to the cached entry (enrichment ignores the extras).
- advisory_service.get_recent_kev: newest KEV entries, annotated with whether
  the CVE is already in inventory (+ asset count) via one grouped query.
- GET /api/v1/advisories/kev-recent (new advisories router).
- Dashboard widget "Actively Exploited · CISA KEV": CVE, vendor/product,
  date added, 🔒 ransomware flag, and an "In inventory · N" badge (vs
  "not seen"). Rows link to our CVE detail; View All → the CISA catalog.

Backend py_compile + frontend tsc clean.
2026-07-05 17:17:55 +02:00
vulncheck ade5f435d9 fix(assets): Nessus filter via vuln-source; fix(ui): AI remediation render
assets: the previous filter keyed Nessus on nessus_host_uuid, but imports
whose host had no uuid (matched by hostname) carry it nowhere — they fell
into MANUAL. Now each scanner filter also matches assets with ≥1 finding
whose `sources` list contains that scanner ("nessus"/"wazuh"/...), which is
the maintained multi-source truth. MANUAL = no linkage AND no scanner vuln.

ui: the no-dependency AI-remediation renderer showed GFM tables as raw pipes,
literal "\n" inside code blocks, and "<br>" as text. Now: unescape literal
\n/\t, render <br> as a line break inline (without splitting the line, which
would corrupt table rows), and render GFM tables (header/sep/body).
2026-06-29 14:13:31 +02:00
vulncheck 35940de51e feat(dashboard): Mobile Security widget (EOL/EOS + Android patch level)
Separate dashboard widget for phones & tablets, kept apart from the
desktop-software EOL widget (which now excludes mobile findings).

Reuses existing data — the findings are already vuln rows. New
finding_type=mobile filter on the vulns endpoint ORs the mobile cve_ids
(ANDROID-PATCH-%, EOL-IPHONE/IPAD/SAMSUNG-MOBILE/SAMSUNG-GALAXY-TAB-%) and
bypasses the pseudo-CVE exclusion. The widget feeds from it (top 10, cvss
desc → vendor-EOL 9 > patch ≥1y 8 > EOL-soon 5.5 > end-of-active-support 3),
deduped by cve_id+asset so the same EOL stream on N devices stays N rows.
"View All" deep-links to /vulnerabilities?finding_type=mobile, which the
list page now reads from the URL.

ponytail: one combined widget, not two — severity sort already surfaces the
worst (vendor-EOL reached) above the milder patch-staleness rows, and each
row's title says which it is. Split into two later if the volume warrants.
2026-06-25 09:59:33 +02:00
vulncheck 02cfb39486 feat(assets): filter inventory by sync source
Dropdown on the Assets page (All / Wazuh / Nessus / Intune+Defender /
Manual). Backend already accepts ?source=; this just wires the UI.
2026-06-23 10:01:37 +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 cc9954595c fix(ui): Exposure/Risk sort NULLs-last + Affected-Asset links to its findings
Two tester bugs:
- Assets list: sorting Exposure or Risk descending surfaced the empty
  ("—") rows first instead of the real high scores. The generic numeric
  sort branch didn't use nulls_last; now NULLs always sort last in both
  directions.
- CVE detail "Affected Asset" linked to /assets?id= (just the asset list).
  Now links to /vulnerabilities?asset_id=<id> → all findings on that asset.
2026-06-21 11:36:16 +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 d0d27b2c99 fix(assets): paginate the Assets list (was silently capped at 100)
Tester: the Assets page showed at most 100 entries with no paging and no
page-size control.

- Backend list_assets now returns the pre-pagination total in an
  X-Total-Count header (body stays a plain array — the asset dropdowns on
  the scans/vulnerabilities pages still consume a list). limit/offset were
  already supported.
- Assets page: page + page-size (50/100/250/500/1000) controls, prev/next/
  first/last, "X–Y of N", debounced search, page resets to 1 on any
  filter/search/page-size change.
- The asset dropdowns on the scans + vulnerabilities pages now request
  limit=1000 so large estates aren't silently truncated there either.
2026-06-16 10:49:48 +02:00
vulncheck 4d6ccb71d2 fix: heavy manual endpoints run off the event loop (no more GUI freeze)
Tester: clicking "M365 CVEs" froze the whole web GUI until the cvelistV5
comparison finished — same class of bug as the earlier date-backfill case.

Cause: these endpoints were `async def` but call fully-synchronous,
blocking services inline (httpx + DB), starving the single asyncio event
loop. Fix: declare them as plain `def` — FastAPI/Starlette then runs them
in a worker threadpool, off the event loop, so the GUI stays responsive
(responses/counts unchanged). Converted: m365-check, eol-check,
exploit-intel/refresh, bulk enrich (Refresh Threat Intel),
override/vulnrichment (Correct CVSS), and the Nessus /sync trigger.

Also:
- Intune detectedApps: v1.0 `$expand=detectedApps` returns HTTP 400 on the
  managedDevice; now falls back to the beta /detectedApps navigation
  collection (paginated). Still best-effort.
- Defender 403 ("Missing application roles"): /api/machines needs
  Machine.Read.All in addition to Vulnerability.Read.All — documented in
  the Intune settings tooltip + .env.example. (Sync stays non-fatal.)
2026-06-15 15:04:00 +02:00
vulncheck 8e8cfee19a feat(msrc): host-matched KB + MSRC update-guide link in remediation block
Tester, two MSRC-detail changes:

1) The MSRC remediation listed a KB for every Windows version (noise).
   Now the block shows only the KB(s) matching the affected host's Windows
   build branch (e.g. installed 10.0.26100.32690 → only the 26100-line
   KBs, newest first); falls back to the full list when nothing matches or
   the host build is unknown. Filtering is server-side in
   GET /vulnerabilities/{id}/remediations using the asset os_version.

2) Prepend the deterministic MSRC update-guide link for the CVE
   (https://msrc.microsoft.com/update-guide/vulnerability/<CVE>) to the
   MSRC block. Advisory items now render as clickable links in the UI
   (also surfaces OSV/Ubuntu advisory URLs that were previously hidden).

No migration / no new fetch — pure response shaping over cached MSRC rows.
2026-06-15 08:30:57 +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 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 e2625e036e feat(ui): render AI remediation as markdown with copy-able code blocks
Tester: the AI remediation output showed raw markdown (**bold**, ``` fences)
verbatim.

Added a dependency-free mini-markdown renderer for the AI Remediation
section: fenced code blocks become dark, horizontally-scrolling <pre>
blocks with a language label and a Copy button; **bold**, `inline code`,
headings, and bullet/numbered lists are formatted. Other text passes
through.

Also added an "EOL check done: …" summary log line (incl. the
ms_lifecycle/exotics finding count) so it's verifiable whether the
Visual C++ / Silverlight MS-lifecycle fallback fired.
2026-06-08 14:20:30 +02:00
vulncheck 95750458d2 feat(ai): configure OpenRouter key via the Settings GUI
Tester: let the OpenRouter API key be set in the UI like the other
integrations, not only via env.

- openrouter_api_key added to PROTECTED_SETTING_KEYS → encrypted at rest
  (same Fernet key as Wazuh/SMTP/Nessus configs). ai_service already reads
  env first, then this setting, so no service change needed.
- Settings page: new "AI Remediation (OpenRouter)" card — API key
  (password, shows configured/not-set), model (default openrouter/free),
  optional comma-separated fallbacks, Save + Remove Key. Admin-only (PUT
  /settings is RequireAdmin).
- settings GET redaction now treats an empty decrypted protected value as
  unset, so "Remove Key" reflects correctly after reload.

Env OPENROUTER_API_KEY still overrides the stored value when present.
2026-06-06 11:09:08 +02:00
vulncheck efee1d6e61 feat(ui): EOL asset column + hide dead External-References on pseudo-CVEs
Tester, two EOL-detail/widget polish items:

1) CVE detail: the External References block (NVD / CVE.org / Exploit-DB
   links) is now hidden for EOL- / NESSUS-PLUGIN- pseudo-CVEs — those have
   no real CVE record, so the links led nowhere. Shown only when cve_id
   matches /^CVE-/.

2) Dashboard "Newly EOL / EOS" widget: replaced the (usually empty) CPR
   column with the affected asset's hostname, linked to
   /vulnerabilities?asset_id=<id>&eol=1 so a click shows ALL of that host's
   EOL findings. renderVulnWidget gains an assetColumn option; the two CVE
   widgets keep CPR.
2026-06-06 11:05:10 +02:00
vulncheck 7bc7f996a0 feat(ui): Dismiss / Reactivate control on the CVE detail page
Tester: EOL findings (and any vuln) could only be dismissed from the list,
not the detail page — which only showed a status badge.

Added a Dismiss button next to the status badge: marks the finding
false-positive with an audit-logged reason (hides it from the default
active list). When already false-positive it flips to Reactivate. Reuses
the existing /false-positive and /unmark-false-positive endpoints. Works
for EOL pseudo-CVE rows too — they're ordinary vulnerability records.
2026-06-06 10:17:01 +02:00
vulncheck ba7f2069e2 feat(ui): EOL dashboard widget shows product name, not pseudo-CVE id
Tester: the "Newly EOL / EOS" widget's first column showed the synthetic
EOL pseudo-CVE id (EOL-CHROME-148) which is noise. renderVulnWidget now
takes optional labelField/firstColHeader; the EOL widget uses
package_name with a "Product" header (tooltip still shows the id). The two
CVE widgets are unchanged.
2026-06-06 09:50:58 +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 67c201497e feat(ui): default the vulnerabilities list to CPR sort (desc)
Tester: CPR is the best single risk-based metric, so the main list should
lead with it. Changed the default sort from priority to cpr (descending).
Users can still click any column header to re-sort.
2026-06-06 09:32:57 +02:00
vulncheck 3b541780f8 fix(enrich): date backfill is fire-and-forget — stop 503 proxy timeout
The /dates/backfill endpoint awaited the whole job before responding, so a
ZIP walk over thousands of CVEs outran the reverse-proxy request timeout →
503.

Now it launches the date fill in a detached daemon thread (own DB session)
and returns 202 immediately. A module-level guard prevents a double-click
from stacking concurrent 600 MB ZIP walks. Progress is in the backend logs
("CVE dates:" / "Date backfill done"). Button shows "started" and refreshes
the list a few seconds later.
2026-06-03 16:12:14 +02:00
vulncheck 3adf02c6cb feat(enrich): on-demand "Backfill Dates" button (non-blocking, keyless)
Tester confusion: "Refresh Threat Intel" never set published dates
(nvd_dates_set=0) — by design it skips the date pass to stay fast/non-
blocking — so dates only filled on the nightly job, leaving the operator
waiting and unsure.

New POST /vulnerabilities/dates/backfill: date-only enrichment (no
EPSS/KEV) over every CVE row missing a published_date, via the cvelistV5
ZIP/raw cascade with NVD fallback. Runs in a worker thread
(asyncio.to_thread) with its own DB session, so the GUI stays responsive.
No NVD API key needed — cvelistV5 is the primary, keyless source.

Adds a "Backfill Dates" button on /vulnerabilities so the operator can
trigger it explicitly and see the result, instead of waiting for the
nightly scheduler.
2026-06-03 16:06:08 +02:00
vulncheck 8a614ae5e5 fix(ui): render kernel "Fixed in" git-commit hash readably
Tester: the "Fixed in" field showed a raw 40-char git commit hash
(7713bd320ed4fc3d08a22...) that overflowed into the FIX badge and looked
broken.

Linux-kernel CVEs report their fix as an upstream commit hash, not a
Debian package version. Added formatFixedVersion(): a hex string (12-64
chars, no version separators) is shown as "upstream commit <short>" with
the full hash on hover; real versions render unchanged. Added break-words
so nothing overflows the cell.
2026-06-03 15:42:37 +02:00