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).
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.
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).
- 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.
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.
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.
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.
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.
Tester: a Nessus sync correctly soft-inactivated asset #36 (verified in
DB + audit ASSET_DEACTIVATED), but it then vanished from the Assets view
entirely — not even visible with an INACTIVE badge.
Cause: the asset list defaulted to ACTIVE-only and hid INACTIVE behind the
"Show inactive" toggle. But the whole purpose of the Status column is to
surface soft-inactive hosts inline (amber INACTIVE badge), not bury them.
Fix: default list now shows ACTIVE + INACTIVE (hiding only
DECOMMISSIONED, which is operator-final). The toggle is repurposed to
"Show decommissioned" (include_inactive=true reveals those). Status-filter
and audit/history behaviour unchanged.
Tester screenshots — four issues:
1) Dashboard/assets wasted huge left/right gutters. Dropped the
max-w-[1800px] cap on both pages -> content uses full available width
(minus AppShell padding).
2) After a Nessus re-sync, a finding still showed BOTH the legacy
EOL-NESSUS-{plugin_id} row AND the new slug-named row (e.g. Adobe
Reader -> EOL-ADOBE-ACROBAT-...). The slug alias already resolves, but
the old plugin-id row was never removed. _upsert_nessus_eol now deletes
the legacy EOL-NESSUS-{plugin_id} row for the asset whenever the plugin
resolves to a real product slug. Re-sync homogenises existing data.
3) "Newly Published" still looked unsorted: until the NVD published_date
backfill drains, most rows have published_date NULL and were ordered by
id (meaningless). Added a secondary sort on the CVE's own year+sequence
so "newest CVE number first" holds even before backfill. Bumped the
no-key NVD backfill cap 60 -> 150/run so dates fill faster.
4) Assets "Assigned To" select truncated to "Unas..." — widened
maxWidth 140 -> 200px (minWidth 120).
From tester screenshot feedback (batch 2026-06-03):
#3 Asset deletion audit was thin ("Asset deleted: <truncated>") with
no clear WHO/impact. Now logs analog to the CVE status-change
entries: event_description names the host + IP + deleting user +
cascade-removed vuln count, and old_value carries a JSON snapshot
(hostname, ip, os, source, status, wazuh_agent_id, vuln count,
deleted_by). Audit committed BEFORE the delete so it survives even
if the cascade fails.
#2 + #4 Dashboard + Assets wasted large left/right gutters on wide
monitors and the assets table got squished/clipped (max-w-7xl =
1280px). Bumped dashboard, assets and scans pages to max-w-[1800px]
so wide screens use the space and the assets columns (incl. the new
Exposure col) stop clipping.
Still open (needs the tester's TXT, unreadable from Downloads
sandbox): EOL/EOS MS-product handling + remaining widget-fill polish.
Tester feedback round 2026-06-01 — 8 issues grouped into 4 fixes.
**A. Sync-driven INACTIVE reconciliation** (highest priority)
- AssetSource.NESSUS enum value added; pre-existing MANUAL assets with
a nessus_host_uuid re-tagged in 027 migration.
- New `reconcile_missing_from_sync()` helper in asset_lifecycle flips
ACTIVE assets of a given source (WAZUH/NESSUS) to INACTIVE when their
id (wazuh_agent_id or nessus_host_uuid) is missing from the latest
sync. Vice-versa reactivates INACTIVE assets that re-appear.
- Hooked from nessus_sync.run_nessus_sync (after per-host loop, before
commit) and from POST /api/v1/assets/sync_wazuh (after the agents
loop). Both return assets_inactivated/assets_reactivated counts in
their response.
- INACTIVE-asset CVEs now hidden from list_vulnerabilities and
get_dashboard_statistics by default (new
`include_inactive_assets` query param opts back in).
- AuditEventType gains ASSET_DEACTIVATED + ASSET_REACTIVATED so the
transitions are filterable in the audit UI.
**B. EOL pseudo-CVE naming for Nessus + endoflife.date**
- nessus_sync: new `_office_pseudo_cve()` returns
EOL-MS-OFFICE-{YEAR} for Office variants, else EOL-NESSUS-{pid}.
`_normalise_office_pkg()` collapses MUI/Proofing strings to "MS
Office". Title suffix "OSX MUI..." trimmed for Office.
- nessus_sync: end-of-pass sweep collapses pre-existing
EOL-NESSUS-{pid} Office rows into the unified EOL-MS-OFFICE-{year}
anchor (status=patched + audit).
- eol_service: _PRODUCT_SLUGS now maps microsoftoffice[proofing/osxmui
/osxmuigerman/formac] -> ms-office. _YEAR_KEYED_SLUGS includes
ms-office. _pseudo_cve_id produces EOL-MS-OFFICE-{year} for free.
- eol_service: existing-row branch bumps detected_at so the
sort_by=detected_at widget ranks freshest finding first.
**C. Asset view icon asymmetry + dashboard panel fix**
- assets/page.tsx: coverage-gap icon now shows for any asset with
wazuh_agent_id OR nessus_host_uuid.
- nessus_sync._find_or_create_asset: backfills ip_address on existing
assets (UUID / hostname match) so the "Launch targeted Nessus scan"
button (already gated only on asset.ip_address) lights up.
- frontend types + backend AssetResponse expose nessus_host_uuid +
source.
- page.tsx CVE-id cell: `break-all` -> `whitespace-nowrap` so the
free L/R dashboard space isn't wasted on character-wrapped ids.
**D. Assets table sortable columns**
- list_assets: new sort_by / sort_order with whitelist-driven ORDER BY.
Last-scan sort uses nulls_last(). Joins Policy/User only for those
columns. Unknown sort_by falls back to hostname asc.
- assets/page.tsx: sortBy/sortOrder state, handleSort toggler, SortArrow
indicator, clickable headers with cursor-pointer + hover style, and
a useEffect re-fetch on change. Mirrors the pattern in
vulnerabilities/page.tsx.
**Out of scope (flagged)**
- `delete_asset` remains a hard delete with cascade. The user's
"previously-deleted asset not re-created by Scan+Sync" expectation is
intentional design — the auto-create gate (auto_create_assets) and
the new INACTIVE flip cover the rest of the use cases.
- DECOMMISSIONED AssetStatus is still unreachable through automation.
- Vulnerability-count column in assets table stays non-sortable (it
is a Python-side count, not a SQL column).
**Migration**
- 027_add_nessus_source_and_audit_events.py: ALTER TYPE for both
enums + UPDATE backfill. Run `alembic upgrade head` once on prod.
eol_service: Office (and other year-keyed products) report a build
number from Wazuh syscollector (16.0.4266.1001) while endoflife.date
keys releases by year (name/label '2016'). The numeric prefix match in
_pick_release never matched, so Office 2016 EOL went undetected. Add a
year-from-product-name resolver (_YEAR_KEYED_SLUGS, _extract_year,
_pick_release_by_year) used before the numeric fallback. SQL Server and
other numeric-versioned products are unaffected.
dashboard: long EOL pseudo-CVE ids (EOL-WINDOWS-SERVER-2016) plus
whitespace-nowrap forced the 'Newly EOL/EOS' widget wider than its grid
column, scrolling CPR/Flags out of view. CVE cell now break-all.
assets: 9 columns at px-6 padding (~432px) overflowed max-w-7xl and
clipped the Actions column. Reduced table padding to px-3.
Tester: Wazuh's vuln-detector misses some products (MS365, mRemoteNG,
…). Chosen approach (safe option): surface the GAP, make NO automatic
CVE assignment → zero false positives.
Backend
- GET /api/v1/assets/{id}/coverage-gap: fetches syscollector packages,
cross-references against the asset's open-vuln package_name blob
(token match, len>=3 to avoid substring noise). Returns packages
WITHOUT any finding, each annotated with its endoflife.date status
when known (EOL / EOL SOON / out-of-active-support) — a hint only,
never a CVE claim. Pure local data, no NVD, no rate-limit risk.
Frontend
- Amber check-circle action per Wazuh-linked asset opens a modal
listing the gap packages (name / version / EOL hint), with a clear
"investigate manually — no automatic CVE claim" disclaimer.
Operator workflow: spot a high-value uncovered package (e.g. an EOL
mRemoteNG), investigate in NVD/vendor advisories, mark accordingly.
Deliberately conservative — keeps data quality intact.
Tester: use Wazuh IT-hygiene (open listeners) as an extra risk
indicator — a host exposing VNC/RDP/Telnet is network-vulnerabler
regardless of CVE count.
Schema (migration 026)
- assets.network_exposure_score (FLOAT 0-100, partial-indexed >0)
- assets.exposed_services (JSON [{port, proto, service, risk, ip}])
- assets.exposure_updated_at
Service (exposure_service.py)
- get_ports() added to WazuhClient (/syscollector/{agent}/ports).
- analyze_ports(): classifies LISTENING sockets against a risky-port
table (Telnet 40, RDP/VNC/SMB 30-35, FTP/rsh 28-30, DB ports 22-26,
SSH 8, …). Loopback-bound listeners excluded. Score = strongest
listener at full weight + 40% of each additional, capped 100.
- refresh_all_exposure() walks Wazuh-linked assets, persists.
API + scheduler
- POST /api/v1/assets/refresh-exposure (on-demand).
- Nightly job 02:30 UTC.
- AssetResponse exposes network_exposure_score + exposed_services
(JSON parsed to list) + exposure_updated_at.
Frontend
- Assets list: new Exposure column — coloured score badge (red ≥60,
orange ≥30, yellow else) with the top services inline + full list
in the tooltip.
- "⚡ Exposure" toolbar button triggers a refresh.
Informational only — does NOT auto-change asset criticality (operator
owns that). Surfaces the data so the operator can raise criticality
on heavily-exposed hosts.
Tester: when a system is removed from Wazuh + Nessus (org's "system
no longer exists" process), VulnCheck kept a data corpse needing
manual deletion. Hard-delete would lose the vuln history + break the
revisionssicher audit trail. Compromise: SOFT-inactive.
Service (asset_lifecycle.py)
- reconcile_asset_lifecycle(db): asset whose last_scan (any source
sync) is older than `asset_inactive_after_days` (setting, default
30, 0=disabled) → status INACTIVE. Recently-seen INACTIVE assets
auto-revive to ACTIVE. DECOMMISSIONED is operator-final, never
auto-revived. Both transitions audit-logged (user_id=None).
Vuln rows + audit survive — only the default views hide it.
Scheduler + endpoint
- Nightly job at 04:15 (after URS).
- POST /api/v1/assets/reconcile-lifecycle for on-demand run.
Asset list
- Default now shows ACTIVE only (was active+inactive). New
include_inactive=true param surfaces inactive + decommissioned.
Frontend
- "Show inactive" checkbox on the assets page (re-fetches on toggle).
- Status badge: active=green, inactive=amber, decommissioned=grey,
each with an explanatory tooltip.
Configurable retention: set asset_inactive_after_days in settings
(0 disables auto-inactivation entirely for orgs that prefer manual).
Dashboard widget:
- new 'Unified Risk Score (URS)' card above the existing compliance
widget. Hidden when no asset has a URS yet so empty installs stay
clean. Three columns:
* Avg URS (big colour-coded number per band)
* Severity spread badges (CRITICAL/HIGH/MEDIUM/LOW/NONE counts)
* Top-5 highest-risk assets with URS + severity pill
- 'Open >' link jumps to /compliance page
Asset edit modal:
- new 'Asset Criticality' dropdown (Low/Normal/High/Critical, with
the multiplier ×0.7..×1.5 shown next to each option for transparency)
- value preloaded from asset.criticality on edit, persisted via the
PUT /api/v1/assets/{id} endpoint extended in the previous commit
- ScanSchedule: scanner_type field now exposed in API schema + UI
modal dropdown (Wazuh / Nessus); existing schedules default to wazuh
- Schedule list shows WAZUH/NESSUS badge per row
- NessusClient: _post() helper + launch_scan(scan_id, alt_targets)
uses Nessus POST /scans/{id}/launch with alt_targets override
- POST /api/v1/vulnerabilities/nessus/scan-host: resolves asset IP,
picks scan template from config/request, launches targeted Nessus
scan on that single host — useful for post-patch rescans
- Assets page: purple globe button per asset with IP address triggers
targeted Nessus scan via scan-host endpoint
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>