Commit Graph
142 Commits
Author SHA1 Message Date
vulncheck 64204f41b7 feat(advisories): Security Advisory Feeds page (ZDI/CERT-EU/BSI/Cisco + custom)
New early-warning surface the tester asked for: the newest 7-Zip advisory was
on ZDI before NVD or cvelistV5 had it. A dedicated Advisories page now shows
the CISA-KEV latest additions plus configurable RSS/Atom sources.

- advisory_feed_service: fetch + parse (RSS and Atom), cached in a setting so
  the page serves instantly; per-feed errors recorded, never fail the run.
  Default feeds verified LIVE before shipping (ZDI published/upcoming, CERT-EU,
  BSI/CERT-Bund WID, Cisco PSIRT); Cisco emits junk after the XML root, so a
  lenient per-item fallback parses it anyway. Security: any DOCTYPE/ENTITY
  declaration is refused outright (XXE / billion-laughs) — feeds never need
  DTDs, no defusedxml dependency required.
- GET /api/v1/advisories/feeds (cache; first call fetches),
  POST /feeds/refresh (editor). Config = advisory_feeds_config setting,
  editable from the page (admin): enable/disable, rename, custom URLs.
- Scheduler refresh every 6h; 'Advisories' nav item for all roles (read-only
  surface; Refresh for editor+, Configure admin-only).

Parser self-checked against rss/atom/cisco-junk/DTD-refusal fixtures.
2026-07-21 15:03:44 +02:00
vulncheck 54edb38b3a feat(app-scan): modern .NET (8/9/10) CVE detection via cvelistV5
The July .NET CVEs (CVE-2026-47304/50649/50525/50659) were invisible to the
app scan because cvelistV5's registry had no .NET entry. Added per-release keys
(dotnet-8/9/10 -> '.NET 8.0'/'9.0'/'10.0'), same reasoning as SharePoint: the
record floors are generic release floors (8.0.0 .. 8.0.29), so a shared key
would cross-match releases. The real semantic version lives in the ARP NAME
('Microsoft .NET Runtime - 8.0.16 (x64)') and bumps with every monthly patch;
the version field is an MSI build -> the scan now supports name_ver entries via
the existing _effective_version mechanic. Matches Runtime/Host/Desktop-Runtime;
SDKs excluded (8.0.1xx numbering never falls in the runtime range, runtime is
installed alongside anyway).

.NET FRAMEWORK stays out deliberately, with data this time: its ARP version
(4.8.04084) is STATIC across monthly patches — only file versions change — so
comparing it to fix builds like 4.8.4803.0 would flag every install forever.
Framework patch state is file/KB-based; Defender TVM covers it and those
findings already flow in via the Defender sync. The record's ampersand ranges
('2.0.50727.9069 & 3.0...') parse to no-match, verified harmless.

Index bumped to v7. Verified against the live records: 8.0.16 -> affected by
8.0.0..8.0.29; 8.0.29 -> clean; SDK/Framework/Targeting-Pack names don't
resolve.
2026-07-21 14:41:36 +02:00
vulncheck 948589d07f fix(scan): cross-source auto-resolve, stale installed version, Citrix stubs
Three tester-reported issues:

1. Cross-confirmed findings never auto-resolved. Each reconcile (app-scan /
   defender / msrc) skipped findings any OTHER scanner also reported, so a
   Chrome CVE seen by app-scan AND Defender was closed by neither — a patched
   host (150.0.7871.125 installed, fix .115) kept an open finding forever.
   All three reconciles now use the Nessus-backfill contract instead: drop
   YOUR OWN source when no longer detected, mark patched (audit-logged) only
   once no source is left. A finding another scanner still reports stays open
   under that scanner. Verified both directions in a self-check.

2. Installed version went stale. The app-scan upsert only filled
   package_version when empty, so the row kept its first-ever version
   (tester: Firefox showed 'Installed: 150.0.3' while 152.0.5 was on the
   box). Re-detection now refreshes the version.

3. Citrix published-app registry stubs ('Firefox 1.0', vendor 'Delivered by
   Citrix') describe software NOT installed on the box and produced
   ancient-CVE false positives (CVE-2008-2798 against Firefox 1.0). Packages
   whose vendor names Citrix are skipped in both scanners; the stub vendor
   field is the discriminator, real installs keep their real vendor.
2026-07-21 14:22:50 +02:00
vulncheck 56454dff2a feat(notifications): aggregate the digest to one row per CVE
A CVE on 200 hosts was 200 rows and the counters counted each occurrence
(tester saw 'Neue Schwachstellen 11848' with 'Kritisch 467, Hoch 11381' —
per-(CVE,asset), not distinct). Collapse the digest to one row per CVE:

- {{rows}} is now one line per CVE, sorted by CPR desc, columns CVE | Severity |
  CVSS | CPR | #Systems | Package. The CVE links to ?cve_id= (no asset_id), i.e.
  the vulnerability view listing every affected asset; #Systems says how many.
- Severity counts and {{total}} are per distinct CVE, so 'Neue Schwachstellen'
  and the tiles no longer multiply by host count. {{affected_assets_count}}
  stays distinct assets across the digest.
- default digest template + preview sample + settings hint updated (Host column
  dropped — meaningless once aggregated). Notification-history subject/body show
  distinct-CVE count.

Verified on the tester's shape: 47× CVE-2026-50387 + 2× another -> 2 rows,
counters {critical:1, high:1}, #Systems 47.
2026-07-16 09:58:35 +02:00
vulncheck b4d0caf8ad fix(app-scan): find newest Firefox CVEs + SharePoint 2013 (both were my bad calls)
The tester proved two of my 'structurally impossible' claims wrong by patching
around them locally. Both were wrong, and for the same reason as before —
checking too narrow a slice of the data.

1. Newest Firefox CVEs were never indexed. Mozilla states the INVERSE: no
   affected range at all, only 'version 152.0.6, lessThanOrEqual *,
   status: unaffected'. _ranges_from_affected skipped every non-affected entry,
   so the CVE vanished. 'X and up are fixed' == 'below X is affected', so a
   lone unaffected floor now yields a fix bound. Guarded: only when the entry
   declared no affected range of its own AND there is exactly one floor —
   several floors mean several branches (release vs ESR) and picking one would
   over- or under-report.

2. SharePoint 2013 does have real fix ranges. I concluded otherwise from six
   RECENT MSRC docs — of course a product EOL since 2023-04-11 is absent there.
   The CVE records from its supported years carry real builds
   (CVE-2023-23395: 15.0.0 .. 15.0.5537.1000) and an unpatched farm is behind
   all of them. That is exactly what the tester's Nessus reports.

Verified against the live records, both directions:
  Firefox 147.0.4.0 / 152.0.5 -> affected;  152.0.6 / 153.0 -> clean
  SharePoint Foundation 2013 @ 15.0.4569.1506 -> flagged by CVE-2023-23395
                                                 and CVE-2022-35823
  regression: WS2016 @ 14393.5000 -> flagged, @ 14393.9234 -> clean

Index bumped to v6 so it rebuilds once.
2026-07-16 09:38:25 +02:00
vulncheck d304463996 feat(app-scan): Windows OS CVEs, family-matched (Server AND Client)
Re-enables the OS scan that c4abbaa turned off, with the guard that was missing.
Two independent checks, each one learned from a real false positive:

1. FAMILY — the entry's product name must belong to the asset's Windows family.
   Windows 11 24H2 and Windows Server 2025 share build line 10.0.26100 but keep
   separate revision sequences, so CVE-2026-41089 (Server 2025 only, fix .32860)
   matched a fully-patched 24H2 client at .8655. The index now keeps the product
   name to make this possible.
2. RELEASE-BOUNDED — the range's floor and fix must sit on one build line. Older
   records use a generic 10.0.0 floor that swallows every lower build
   (CVE-2021-26432 put a 17763 fix on a 14393 host).

Together the range only answers 'patched or not' within the host's own release.

Verified end-to-end against the real records, both directions:
  Win11 24H2 @ 26100.8655   -> EMPTY  (the reported FP)
  WS2016    @ 14393.9234    -> EMPTY  (patched; also rejects the 2021 record)
  WS2025    @ 26100.32690   -> CVE-2026-41089 (.32860), CVE-2026-47291 (.32995)
  Win11 23H2@ 22631.5000    -> CVE-2026-47291 (.22631.7219) only
  WS2016    @ 14393.5000    -> CVE-2026-47291 (.14393.9234) only

Index bumped to v5 (entries carry the product name) so it rebuilds once.
2026-07-16 09:32:42 +02:00
vulncheck 2519427b41 feat(app-scan): SharePoint 2016/2019/Subscription CVE detection via cvelistV5
SharePoint was simply absent from the curated registry — the app scan never
looked for it, which is why Nessus found those CVEs and we did not. cvelistV5
carries usable numeric ranges for it, verified across 82 recent CVEs rather
than one sample:

  Microsoft SharePoint Enterprise Server 2016       16.0.0 .. 16.0.5556.1005
  Microsoft SharePoint Server 2019                  16.0.0 .. 16.0.10417.20153
  Microsoft SharePoint Server Subscription Edition  16.0.0 .. 16.0.19725.20384

One key PER RELEASE, not a shared 'sharepoint' key: every range uses a generic
16.0.0 floor and all three releases report 16.0.x, so a shared key would let a
2016 install (16.0.5456) fall inside the 2019 range — the same cross-release
false positive the Windows OS scan just produced. The release is decided by the
product NAME; the range then only answers 'patched or not' within it.

2013 stays out: Microsoft publishes no fixes for it, so no range exists. Its
EOL finding is the signal there — this changes nothing for a 2013-only host.
2026-07-16 09:29:24 +02:00
vulncheck c4abbaa1c1 revert(app-scan): turn the Windows OS scan back off — cross-release FPs
Identifying a Windows release by its build LINE is not sound, so the whole
premise of 5703dfa fails. Windows 11 24H2 and Windows Server 2025 both sit on
10.0.26100 but keep separate revision sequences:

  CVE-2026-41089 affects Windows Server 2025 ONLY, fix 10.0.26100.32860
  a fully-patched 24H2 client reports 10.0.26100.8655
  -> 8655 < 32860, so the client matched a Server-only CVE

The same-line floor/fix guard from e19c8d9 was necessary but not sufficient: it
only rejects the old generic-floor records, not two releases sharing a line.

Disabling rather than attempting a third in-place fix: the FPs are in the
tester's data now, and I have already been wrong three times here by
generalising from too few records. Windows products are no longer indexed at
all, so the findings self-heal via the app-scan auto-resolve on the next run.
Package scanning is untouched.

Re-enabling needs the entry's product NAME in the index, matched against the
asset's OS family, verified against CVE-2026-41089 (client must not match) and
CVE-2026-47291 (each must match its own line). Written up in scan_asset_os.
2026-07-16 09:26:29 +02:00
vulncheck e19c8d9da7 fix(app-scan): stop cross-release Windows false positives
Regression from 5703dfa. A Server 2016 host (10.0.14393.9234) was flagged with
CVE-2021-26432 'fixed in 10.0.17763.2114' — a Server 2019 build. My claim that
cvelistV5 ranges are release-bounded came from checking ONE modern record, and
older ones are not:

  CVE-2021-26432  version=10.0.0        lessThan=10.0.17763.2114   (generic floor)
  CVE-2026-47291  version=10.0.14393.0  lessThan=10.0.14393.9234   (release-bounded)

A '10.0.0' floor makes the range swallow every lower build, so a 14393 host fell
inside a 17763 fix. The OS scan now requires the floor and the fix to sit on the
same build line and skips the rest — such an entry carries no release info at
all, and the OS string alone can't supply it. Costs nothing in practice: Windows
servicing is cumulative, so a host behind on an old CVE is already flagged by
that line's newer ones. Existing false positives self-heal on the next scan via
the app-scan auto-resolve.

Also:
- scheduler job renamed to '(Windows OS)' — it covers client too, not just Server
- dashboard: AI Audit History hidden from read-only (AI endpoints are editor+)
- dashboard: 'Export Report' had no onClick at all (dead for every role) — now
  links to the reports page
- compliance: the Impact-CSV upload card is admin-only (the import endpoint is
  RequireAdmin), so it no longer 403s silently for other roles
2026-07-14 15:36:10 +02:00
vulncheck 5703dfa806 feat(app-scan): Windows OS CVEs via cvelistV5 bounded ranges (Server AND Client)
Corrects bf529ed, which claimed Microsoft products cannot use the CPE/cvelistV5
path. That was wrong, and the reasoning behind it was a sampling error: the
'no ranges' check looked at CVE-2013-3900 and the 'lessThan: publication' check
at CVE-2021-24072 — both ancient. Modern MS records carry real numeric ranges.
Re-verified on the CVE the tester cited:

  CVE-2026-47291 cvelistV5: 20/20 affected entries have a numeric lessThan,
                            0 'publication'
                 NVD:       22/24 microsoft cpeMatch have versionEndExcluding
  CVE-2026-45467 (SharePoint, current): lessThan 16.0.5556.1005 etc.

So there is no Server/Client split and no reason to special-case Microsoft.

Implementation reads cvelistV5, not NVD, for one concrete reason: NVD flattens
these to an END bound only, so a 1607 host (14393.x) sits inside the 22H2 range
(endExcluding 19045.7417) and false-positives. cvelistV5 keeps the range bounded
(10.0.22631.0 .. 10.0.22631.7219), and those bounds select the host's release by
themselves — no build->release table, Server and Client fall out for free.

- pattern products in the index (one key for the whole 'Windows 10|11|Server'
  family) so a new release doesn't need a registry row; anchored so SharePoint,
  Teams and '.NET Framework on Windows Server 2016' can't be swallowed
- scan_asset_os() flags straight from asset.os_version; runs for every Windows
  asset, no software inventory required
- index bumped to v4 (entries changed) so it rebuilds once
2026-07-14 13:01:25 +02:00
vulncheck d4ac987c03 feat(msrc): SharePoint CVE detection via FixedBuild (2016/2019/SubEd)
Extends the MSRC scan from OS to installed MS software, hooked into the app-CVE
scan which already holds the inventory. Verified against live data first:

- Syscollector DOES report the serviced build, not RTM ('Microsoft SharePoint
  Foundation 2013' -> 15.0.4569.1506), so the FixedBuild compare is sound — the
  false-positive worry that blocked this doesn't apply.
- Branch semantics are per-product, not universal: Windows Server 2016 (14393)
  and SharePoint 2019 (10417) keep a stable 3rd segment, but SharePoint 2016
  moved 5535->5539->5543->5552->5556 in six months and SubEd 19127->19725.
  Prefix-matching those would silently match NOTHING, so products now carry a
   flag; SharePoint identifies by name (2016/2019/SubEd all report
  16.0.x) and compares plain installed < fixed, which is right because MS
  servicing is cumulative.
- SharePoint 2013 deliberately unsupported here: 0 products across six MSRC
  docs — Microsoft ships no fixes for it, so no FixedBuild can exist. Its EOL
  finding is the signal instead.

Reconcile is scoped to the product labels each pass actually evaluated —
otherwise the OS pass would auto-resolve every SharePoint finding it never
looked at, and vice versa.
2026-07-14 12:21:27 +02:00
vulncheck f5c2c58ab9 fix(eol): detect SharePoint end-of-life (Foundation/Server 2013-2019)
Neither the MS-lifecycle export nor the slug lookup knew SharePoint, so
Foundation/Server installs never produced an EOL finding — tester hit this on a
SharePoint Foundation 2013 box (EOL since 2023-04-11) with 2016/2019 going EOL
2026-07-14. endoflife.date does track it as 'sharepoint'; two gaps had to close:

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

Subscription Edition is still supported and is skipped by the existing
wrapper-token guard, so it can't produce a false EOL. Dates verified against
the live endoflife.date API.
2026-07-14 12:04:40 +02:00
vulncheck e0b3e531b2 feat(msrc): patch-level Windows Server CVE detection from MSRC FixedBuild
Closes the Wazuh-CTI delay for Microsoft OS CVEs, which NVD/cvelistV5 provably
cannot: they carry no fixed build for MS products (rangeless CPEs /
lessThan:'publication'), so they can't distinguish a patched host from an
unpatched one. MSRC's CVRF does — each Type-2 remediation pairs a FixedBuild
with the ProductIDs it covers, e.g. CVE-2026-33834 -> 10.0.14393.9140 for
'Windows Server 2016' (+ Server Core), KB5087537.

- msrc_scan_service: builds a curated product->(cve, fixed_build, kb) index from
  the monthly CVRF docs (cached in a Setting, nightly rebuild), then flags a host
  when its build is behind the newest fix ON ITS OWN servicing branch (a 2016
  fix says nothing about a 2019 host). Auto-resolves + audit-logs once the host
  catches up; never touches findings another scanner reports.
- Product mapping is anchored so 'Microsoft .NET Framework 4.8 on Windows Server
  2016' can't be mistaken for the OS family. Windows client (10/11) is out of
  scope until there's a build->release table.
- Nightly job at 05:10 + POST /api/v1/vulnerabilities/msrc-scan (editor).

Verified against live MSRC data: a host at exactly 10.0.14393.9140 is correctly
NOT flagged; an older build is; a different branch is ignored.
2026-07-14 11:59:46 +02:00
vulncheck bf529ed421 docs(app-scan): record why Microsoft products can't use the CPE/cvelistV5 path
Investigated adding Windows Server (and SharePoint) to the app-CVE scanner to
beat the Wazuh CTI delay. Verified against live data that the premise doesn't
hold: NVD gives MS OS entries as rangeless CPEs
(cpe:2.3:o:microsoft:windows_server_2016:-:* with versionEndExcluding=null) and
cvelistV5 MS records use lessThan:'publication' — neither carries the fixed
build, so neither distinguishes a patched host from an unpatched one. _in_range
already rejects those wildcard/'-' versions, so registry entries would match
nothing while spending NVD quota. Leave a note where the next person will look
instead of shipping dead entries; MSRC (KB + FixedBuild) is the viable source.
2026-07-14 11:40:53 +02:00
vulncheck 7e15d6552a feat(notifications): keep lifecycle/EOL findings out of CVE mails
New notification_lifecycle_mode setting (default 'exclude'): pseudo-findings
that aren't real CVEs — EOL-* (endoflife.date), ANDROID-PATCH-* (patch-level
staleness), NESSUS-PLUGIN-* — no longer mix into the vulnerability mails. The
rule is 'anything not CVE-*' so future pseudo prefixes are covered without a
code change. Applies to both delivery modes (single/digest) and both schedules
(per-sync/nightly); 'include' restores the legacy mixed behaviour. Excluded
findings stay fully visible in the dashboard — only the mail routing changes.
2026-07-14 11:34:02 +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 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 d698726621 feat(notifications): sort digest CVE table by CPR descending
The aggregated digest rows now render highest-CPR (most urgent) first, rows
without a CPR last — risk-led ordering the tester asked for. Per-CVE
affected-systems count was already surfaced (the #Systems column).
2026-07-13 10:10:05 +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 440e96f965 fix(notifications): send new-CVE emails for app-scan & Defender sources
dispatch_new_vuln_notifications was only called by the Wazuh and Nessus syncs,
so brand-new findings from the app-CVE scanner and Defender TVM never triggered
a notification (tester: 'only Wazuh mails arrive'). Call the same digest
dispatch after each of those syncs enriches its new findings.
2026-07-13 07:40:53 +02:00
vulncheck 7bb396a606 fix(eol/app-scan): supersede stale EOL releases + detect Android Chrome
Two Chrome-on-mobile issues:

1. Duplicate EOL findings (EOL-CHROME-149 AND -150 on one iPad running only
   150). A product runs one release per asset, so upsert_eol_vulnerability now
   supersedes any other open EOL-<slug>-* on the same asset (marks it patched +
   audit-logs it). Generic — also cleans up OS major upgrades (Server 2016->2019
   etc.). Fixes the tester's 'why two EOL' question.

2. Android Chrome went undetected: Intune reports it by package id
   'com.android.chrome', which didn't match the 'google chrome' registry regex.
   Add it to both scanners (curated NVD-CPE + cvelistV5). Android shares Chrome's
   version numbers and security fixes with Desktop (per Google), so the same
   google:chrome ranges apply. Bare 'Chrome' (iOS, WebKit-backed) stays
   unmatched by design.
2026-07-10 15:32:52 +02:00
vulncheck 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 ebea13f740 fix(audit): log auto-resolve status changes for app-scan/defender/verify
Automated patch transitions were silently flipping status to 'patched' with no
audit trail — no global audit-log row and no per-CVE Change History entry, so
the remediation was invisible (Revisionssicherheit gap the tester flagged on
CVE-2026-15131). log_vulnerability_change already writes one AuditLog row that
feeds BOTH surfaces; Wazuh/Nessus sync already called it, three paths did not:

- app-scan auto-resolve (_resolve_stale_app_findings): now logs source=app_scan
- Defender TVM had NO auto-resolve at all — add one (defender-only, guarded to
  non-empty machine responses) that logs source=defender_sync
- verify_patch_with_rescan flipped patched/patch_failed without logging — now
  records the outcome (source=verify_patch_rescan)

All automated (user_id=None) with a WHO/WHEN/WHY reason string.
2026-07-10 08:03:33 +02:00
vulncheck 18080a31e4 fix(app-scan): auto-resolve patched findings + import CVSS from cvelistV5
Two gaps surfaced by a Chrome finding (CVE-2026-15131):

1. App-scan was additive-only: after Chrome was updated past the vulnerable
   version, the CVE simply stopped being re-detected but the open finding
   lingered forever. Add a per-asset reconcile — an app-scan-ONLY open finding
   not re-detected this run (and not claimed by Wazuh/Nessus/Defender) is marked
   patched. Guarded to runs that actually had a package inventory so a transient
   fetch failure can't false-close everything.

2. The cvelistV5 scanner hardcoded cvss=None, so findings it created showed CVSS
   N/A even though the record carries a 3.1 vector. Extract baseScore/severity
   (_cvss_from_record, CNA>ADP, 3.1>3.0>4.0), store per index entry (index bumped
   to v3), and backfill cvss_score/fixed_version/severity onto existing findings
   when better data arrives. Self-check for the parser under __main__.
2026-07-09 15:16:54 +02:00
vulncheck ee4e1d9c8b fix(intune): serialise sync with a Postgres advisory lock (deadlock fix)
Two overlapping Intune/Defender syncs updated the same asset rows in different
orders -> 'deadlock detected' during flush. The router's _INTUNE_SYNC_RUNNING
flag is per-worker (Gunicorn has several) and the nightly scheduler runs in its
own context, so neither guards cross-process. Wrap the whole sync in
pg_try_advisory_lock: a second concurrent caller is now cleanly skipped instead
of deadlocking. Lock is released (rollback-safe) in finally; the nested Defender
sync shares the same locked session.
2026-07-09 13:40:50 +02:00
vulncheck 937251fc24 feat(intune/defender): AAD-id cross-merge + readable device names, rename-safe
Tester hit an asset named "<enrollment-GUID>_IPad_6/16/2026_9:58 AM" — Intune's
management-name blob that Graph puts in `deviceName` for supervised/userless/
ABM iOS devices. Two improvements:

1. Readable, stable hostname (_clean_device_name in intune_service): when
   deviceName is a management-name GUID blob, compose "<model>-<serial>"
   (e.g. iPad13,4-DMPXK7MP9Y) from fields we already fetch; real names pass
   through unchanged. Existing ugly assets self-heal on the next sync — the
   asset is matched by its stable id and its hostname refreshed (rename
   tracking, which also covers the autodeploy renames the tester asked about).

2. Cross-source merge on the Entra/AAD device id. The same physical device has
   different per-service ids (intune_device_id ≠ defender_machine_id) and only
   merged by hostname before — which fails when Intune's deviceName and
   Defender's computerDnsName differ (both often the management name). Now both
   syncs store + match on the AAD device id (Intune `azureADDeviceId` ==
   Defender `aadDeviceId`): match cascade id → aad_device_id → hostname, and
   each sync backfills both ids so one physical device becomes one asset.

Migration 036 adds assets.aad_device_id (indexed, nullable, idempotent).
Verified the name-cleaning against the exact blob + real-name + Windows cases.
2026-07-09 13:21:03 +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 b35c2c95f7 fix(app-cve-scan): .NET version-from-name, Teams add-in FPs, cvelistV5 platform filter
Three tester-reported issues:

1. .NET Runtime/Desktop Runtime/SDK/Host not detected. Wazuh syscollector
   reports the MSI build number (48.x/94.x) in the version FIELD while the
   real semantic version (6.0.16) is only in the display NAME — so the scan
   queried a bogus version that never matched NVD's 6.0.x/8.0.x ranges.
   Registry entries can now set name_ver=True; _effective_version pulls the
   version from the name for those. The finding's package_version now shows
   the semantic version too. Developer reference packs (Framework X.Y
   Targeting Pack / Multi-Targeting Pack / SDK / Client Profile) are excluded
   — they're build-time assemblies, not the installed runtime, and would
   false-positive against runtime CVE ranges. The standalone .NET SDK still
   matches (it's a real product with its own CVEs).

2. Teams false positives: "Microsoft Teams Meeting Add-in for Microsoft
   Office" and "Microsoft Teams VDI Citrix plugin" were matched as Teams and
   picked up Teams-app CVEs (CVE-2023-29330, CVE-2025-49737). They're
   separate products with their own versioning. The Teams name-regex (both
   the NVD-CPE and cvelistV5 registries) now excludes add-in/plugin/vdi/
   citrix/machine-wide.

3. cvelistV5 scanner had no OS-platform check, so a macOS-only Teams CVE
   (CVE-2025-49737) could land on a Windows host. The index now stores each
   affected[] entry's platforms; scan filters by the asset's OS family — but
   only when the record lists a real OS (Windows/macOS/Linux/Android/iOS).
   Microsoft also uses platforms for CPU arch ("x64-based Systems"), so
   arch-only records are kept (can't judge the OS → never drop on arch).
   Also dropped the "Teams for Mac" product pair (belt-and-suspenders, since
   not every CNA fills platforms). Index cache bumped to v2 to force a
   rebuild with the new platforms field.

All resolution/version/platform logic verified against the real installed-
name strings and the actual CVE CPE data.
2026-07-09 08:21:07 +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 a75d2d87be feat(app-cve-scan): detect .NET Runtime/SDK and Microsoft Teams CVEs
Tester-reported false negatives (Wazuh's own detector misses these; our
app-scan is independent — it works off Wazuh's raw package inventory, not
Wazuh's CVE engine, so it can catch what Wazuh's detector can't).

- Microsoft .NET Runtime/Desktop Runtime/SDK/Host, and .NET Framework: NVD
  files ALL of them (6/7/8/9 and Framework 2.0–4.8.1) under the SAME CPE
  (cpe:2.3:a:microsoft:.net), each major version scoped by its own cpeMatch
  range — one registry row covers both families via the existing NVD-CPE
  scanner. .NET Framework itself typically isn't a Windows "installed
  program" (it's a Windows feature, not an MSI package), so it's usually
  invisible to Wazuh syscollector's package list regardless of this
  registry row — documented as a Wazuh-agent data-source limit, not
  something curation can fix. "Teams Machine-Wide Installer" excluded
  (placeholder entry, not a real Teams binary).
- Microsoft Teams (new + classic, both use cpe:2.3:a:microsoft:teams):
  added to both the NVD-CPE registry and the cvelistV5 registry (defense in
  depth — a fresh Teams CVE from Microsoft's own CNA data can predate NVD's
  CPE processing, as seen with Edge CVE-2026-58523 today).

Verified resolution against realistic installed-program name strings
(Desktop Runtime/SDK/Framework variants, Teams, Teams Machine-Wide).

Deferred (separate, larger pieces — not in this change):
- SAP Business Client/GUI/Analysis for MS Office: NVD's CPE for these covers
  a whole minor version (e.g. all of 2.8) with no patch-level (PL/SP)
  granularity, so a naive match would false-positive on already-patched
  installs (tester's own example: 2.8 SP29 flagged when only earlier SPs are
  vulnerable). Needs a dedicated PL/SP-aware matcher — parse the SP/PL
  number out of the installed version/display string and compare against a
  curated per-CVE fixed-SP table sourced from SAP's own advisories, similar
  in shape to the Samsung SMR-vs-ASB precision work. Real scope, own turn.
- Microsoft Teams classic EOL flagging: no endoflife.date product exists for
  Teams; would need a hardcoded EOL/retirement-date entry (same shape as the
  existing ms_lifecycle_service exotics for Silverlight etc). Small add-on,
  can do on request.
2026-07-07 13:40:22 +02:00
vulncheck 41947920dc fix(app-cve-scan): NVD cache TTL, zero-width ranges, Edge, Graph 429 pacing
1. NVD/OSV per-(product,version) cache TTL 7d → 24h. A version queried
   BEFORE a new CVE for that exact version is published stays cached empty
   for the whole window, hiding the CVE from every host on that version
   until it expires — exactly the tester's CVE-2026-14152 (undetected) vs
   CVE-2026-13778 (detected, same day) discrepancy. 24h still collapses
   most redundant traffic (many hosts share a version).

2. cvelistV5 range parser: some CNA records emit "version" == "lessThan"
   (e.g. a few Chrome entries) — read literally that's a zero-width,
   impossible range, so the CVE was silently unmatchable. NVD treats these
   as an open floor (no lower bound); we now do the same instead of
   dropping the CVE. Verified against the exact malformed record.

3. Added Microsoft Edge (Chromium-based) to the cvelistV5 registry — it was
   only in the NVD-CPE registry, so a fresh Edge CVE with no NVD-CPE data
   yet (verified: CVE-2026-58523 has none) was invisible to cvelistV5
   detection too, despite the CNA (Microsoft) publishing its own
   affected[]/cpeApplicability directly in the record.

4. Graph get_detected_apps: paced with a 300ms minimum gap between calls on
   one client. A device-by-device sync loop was sustaining 429s from Graph
   (single-call retry wasn't enough against back-to-back device calls).

(No change: the "400 then 200" on the first device of the beta fallback is
expected — _v1_detected_apps_ok is per GraphClient instance, and a fresh
client is built each sync run, so the first device in a run always probes
v1.0 once before flipping to beta for the rest of that run.)
2026-07-07 13:37:07 +02:00
vulncheck 7425d79453 feat(android): Samsung SMR as primary source, ASB as fallback; fix stale cache
1. Samsung SMR precision (tester's point 1): security.samsungmobile.com's
   yearly page (?year=YYYY) serves ALL ~12 months' content server-side in the
   raw HTML — the accordion is pure CSS/JS, it doesn't gate what's delivered
   — so it IS reliably parseable per month, contrary to what I assumed
   earlier. New samsung_smr_service parses each SMR-MMM-YYYY block: Google
   Critical/High CVEs MINUS the "Not applicable to Samsung devices" list,
   plus Samsung Semiconductor Critical/High fixes. Cached per year (24h).

   android_cve_service.check_android_cves now takes `manufacturer`: Samsung
   devices try the SMR month first (source 'samsung-smr'), falling back to
   raw ASB (source 'android-asb') only when SMR doesn't cover that month or
   the fetch fails. Non-Samsung Android stays on ASB. This directly fixes
   the false positive the tester hit: CVE-2025-59604 (Qualcomm-only) is
   explicitly "Not applicable to Samsung devices" on Samsung's own page and
   is now excluded — verified against the live page (absent from the parsed
   June-2026 set).

2. Cache bug (tester's point 2): a device stuck showing only the newest
   month's CVEs despite older months (verified live: 2025-09 had 59, 2025-12
   had 57 AOSP critical/high CVEs) turned out to be stale cache entries from
   before today's URL-format and SoC-section-filter fixes — the "non-empty is
   immutable" cache rule kept serving pre-fix (empty or wrong) data forever.
   Bumped _CACHE_PREFIX (asb_month_v2_) so every month is refetched under the
   current, correct logic.

Both parsers verified against live data (JS + Python) before shipping:
19 months of Samsung SMR (2025-01–2026-07) parsed without error with
plausible per-month counts; ASB severity counts confirmed non-zero and
variable across all 12 lookback months.
2026-07-07 13:30:55 +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 f645cadc7b fix(android-asb): only import AOSP CVEs, skip chipset/SoC vendor sections
Raw ASB import created false positives on Samsung (and any) devices: ASB
lists chipset-vendor CVEs (Qualcomm/MediaTek/Unisoc/Imagination/Arm) that
only affect devices with that SoC — Samsung's own SMR marks many "Not
applicable to Samsung devices". The parser now walks section headers + rows
in order and keeps only the AOSP sections (Framework/System/Kernel/Runtime/
Media/Google Play/Widevine) that apply to any Android device at that patch
level; SoC/vendor sections are dropped.

Verified on 2025-09: 115 total → 59 AOSP kept, ~49 SoC dropped; the Framework
KEV CVE-2025-48543 is retained.

(Samsung's own page can't scope per-device either without JS — it loads
month bodies via AJAX — so AOSP-section filtering is the robust fix. Samsung-
proprietary SVE CVEs remain out of scope.)
2026-07-05 17:00:08 +02:00
vulncheck 4cd39b163a fix(enrichment): pass db to VulnOverrideService in cvelistV5 date pass
VulnOverrideService.__init__ requires db, but the bulk cvelistV5 ZIP date
pass called it with no args → "missing 1 required positional argument: 'db'"
and fell back to slow per-CVE HTTP lookups. db is in scope; pass it.
2026-07-05 17:00:08 +02:00
vulncheck 101445109f fix: ASB 2026 URL format, cvelistV5 fixed_version, macOS OS-CVE scan
- android-asb: from 2026 Google nests the bulletin under a year segment
  (/bulletin/2026/2026-01-01); older months stay flat (/bulletin/2025-10-01).
  fetch_asb_month now tries the year-nested URL first, then flat. Verified:
  2026-01 → nested 200/flat 404; 2025-xx → nested 404/flat 200.
- cvelistV5 scan: stop using lessThanOrEqual as fixed_version — that bound
  means the version is still affected (no published fix), so leave
  fixed_version empty (correct "no patch available", e.g. 7-Zip CVE-2026-58052
  ≤26.02). Only lessThan is a real fix target.
- app-cve OS scan: add macOS (cpe:2.3:o:apple:macos) alongside iOS/iPadOS so
  Mac assets get OS CVEs (e.g. Apple CVE-2026-43700, previously only Defender
  saw it).
2026-07-05 16:54:50 +02:00
vulncheck 59e4afbeb3 fix(android-asb): negative-cache empty/404 months to stop the refetch storm
Future ASB months Google hasn't published yet return 404. fetch_asb_month
only cached successful months, so every Android device re-fetched the same
404 months on every sync (the tester saw 2026-01…06 hammered dozens of times).

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

NVD_API_KEY recommended for scale (rate limit). Run alembic upgrade head.
2026-06-21 12:16:44 +02:00