Two tester findings.
1. Windows Server 2016 wrongly flagged as EOL (HIGH) though it still
gets monthly CUs until 2027-01. Cause: is_eoas (active-support
ended 2022) was treated the same as is_eol → MEDIUM/critical-ish
finding even while security patches still flow.
New three-tier model in _build_eol_status():
- is_eol : security support ENDED (eolFrom past, no ESU) →
HIGH, cvss 9.0, title "EOL".
- is_eol_soon : eolFrom within EOL_SOON_DAYS (90) but future →
MEDIUM, cvss 5.5, title "EOL SOON (Nd)".
- is_eoas only: mainstream support ended, security patches still
flow → LOW, cvss 3.0, title "end-of-active-support".
Server 2016 (eoas 2022, eol 2027-01) now → LOW today, flips to
MEDIUM "EOL SOON" ~90d before Jan 2027, HIGH after.
_days_until() handles eolFrom given as a bool (endoflife quirk).
Both check_eol + check_os_eol share _build_eol_status now.
2. Nessus "Scan + Import" failed with "Server disconnected without
sending a response" on launch. That's the Nessus Essentials (free)
API-launch limitation — it drops the connection and the scan never
enters 'running'. The job now catches the launch NessusAPIError: if
the scan already has completed/imported results it imports those
(flagged via job.launch_warning + a clear stage message); otherwise
it fails with an actionable hint ("this edition may not allow
API-triggered scans — launch in the Nessus UI then Sync now").
eol-check loops (router + scheduler) updated to also surface
is_eol_soon findings.
Tester referenced PacketFence's Nessus scan-engine integration:
trigger a scan FROM the tool instead of manually launching in
Nessus then clicking Sync. VulnCheck already had scan-host (launch
only) + sync (import only) — Plan F chains them server-side.
Client
- NessusClient.get_scan_status(scan_id) → lower-cased status string.
Job (override_jobs.py — reuses the in-memory tracker)
- start_nessus_scan_import_job(user_id, asset_id?, scan_id?):
spawns a worker thread, returns job_id.
- worker: resolve scan_id (explicit → default_scan_ids[0] → first
visible) → launch_scan (alt_targets = asset IP when asset_id set)
→ poll get_scan_status every 15s up to 1h → on completed/imported
run run_nessus_sync(scan_ids=[id]). Terminal states
completed/imported/canceled/aborted/empty; canceled/aborted abort
the import. Timeout raises with the last seen status.
Endpoints
- POST /nessus/scan-and-import {asset_id?, scan_id?} → {job_id}
- GET /nessus/scan-and-import/status/{job_id} → live job state
(stage / nessus_status / polls / result / error)
Frontend
- Settings → Tenable Nessus row gains a purple "Scan + Import"
button next to "Sync now". Polls the status endpoint every 5s,
surfaces the stage string inline, terminal state shows the
created/merged/patched summary. User can navigate away — work is
server-side.
Out of scope (later):
- Per-asset rescan button on the vuln detail / asset page wired to
the same job (currently only the global Settings trigger).
- Scan-template picker UI (still uses default_scan_ids[0] / first
visible scan).
The synchronous /override/vulnrichment endpoint blocks the browser
request for the full duration of the correction — 23 minutes on the
tester's instance, well past every reasonable client timeout. Result:
'backend connection failed' modal and zero feedback about whether
the work actually ran.
New endpoints (sync one kept untouched for single-CVE backward compat):
POST /override/vulnrichment/start
Returns {job_id, status: 'queued', poll_url} immediately.
Spawns a daemon thread that does the heavy lift.
GET /override/vulnrichment/status/{job_id}
Returns the live snapshot: status, stage, total, done,
updated/checked/not_found, error if any.
GET /override/vulnrichment/jobs
Recent jobs ringbuffer for an admin overview.
State lives in an in-memory dict (override_jobs.py) — survives
requests, not backend restarts. That's fine; a restart would have
killed the worker thread anyway and the user re-clicks. Ringbuffer
caps at 50 entries, oldest finished gets evicted.
Worker owns its own SessionLocal() so it does not collide with the
trigger request's DB session. Stages narrate what's currently
happening ('downloading vulnrichment ZIP snapshot', 'fetching N
CVEs from github raw', etc.) so the frontend can show a meaningful
progress message even though we don't get per-CVE callbacks from
inside correct_vulnerability_scores.
Frontend modal that polls the status endpoint comes in the next commit.