Only the indexer password was changed. The manager API (55000) still
answered, so the agent list came back and no phase raised; the indexer
(9200) refused every query with 401. query_vulnerabilities_from_indexer()
logged that and returned [] — so each of the 31 agents looked like a host
with no CVEs: "deferring backfill", agents_synced += 1, and at the end
"every agent returned 0 CVEs — treating as an outage" as a WARNING in the
container log. Ledger: "sync run #33 wazuh (manual): completed". Green Sync
Health, green "Synchronization Complete", no mail — nothing had been synced.
The earlier fix (51a69a6) only caught the manager-API half: it fails a run
when a phase cannot log in, and it was wired into /scans/autoscan and the
scheduler. run_wazuh_vulnerability_sync — the "Sync Data (Wazuh)" button —
never called it at all, and an indexer error never reached a caller.
Three parts, all needed:
- The client raises. An agent whose CVE list could not be read is not an
agent with no CVEs, so the indexer query raises WazuhAPIError instead of
returning []. Every caller already catches per agent — including
verify_patch_with_rescan, which used to mark a CVE PATCHED because a 401
gave it an empty list.
- A refused agent is neither synced nor empty: it becomes a FAILED per-asset
scan row and an entry in errors, and never reaches empty_agents. Otherwise
one agent answering would let reconcile_empty_agents() close every Wazuh
finding on the refused host — a mass patch on the strength of a 401.
- The manual sync fails the run (fail_if_source_broken) and answers 502, so
the dialog goes red instead of "Synchronization Complete", the ledger row
is failed, Sync Health red, and the alert mail goes out under the usual
24h cooldown.
reconcile_empty_agents() now also writes run_stats["outage"] when nobody
returned anything; button, autoscan and scheduler all turn that into a
failed run. That is the backstop for an indexer that answers 200 with
nothing — wrong index, cluster still coming up — where nothing raises.
Regression: three tests at the real seam — every agent refused (failed,
agents_synced=0, 502, one mail), one agent refused while another answers
(the refused host keeps its findings), and an indexer answering nothing for
everybody (failed via outage).