Configure the Wazuh integration
Trigger groups, the API key file, the enrichment envelope field by field, the verdict gates, and what to check when nothing appears.
On this page (8)
Configure the Wazuh integration Documentation
What the connector reads, what it emits, and how the verdict on an enrichment alert is decided.
Read
coveragebeforeband. Only known-clean — coverage: known-clean. In coverage, no malicious evidence. licenses the word "clean"; no-data — coverage: no-data. Not in coverage. This is not a verdict — nothing was looked at. means unknown, which is a different thing again; malicious-evidenced — coverage: malicious-evidenced. In coverage, with positive evidence of malice. and ambiguous — coverage: ambiguous. In coverage, and the evidence points both ways. mean there is evidence, whatever the band says.whisper.explaindoes not returncoverageat all. Full contract: Coverage — what we looked at.
The API key file, and the container case
--api-key-file points at a file containing the key and nothing else. It is needed only for the keyed modes — the on-demand CLI and the agent-activity log source. Per-alert enrichment runs without it.
On a containerised manager, skip the flag and inject WHISPER_API_KEY as a container-env secret.
If the keyed modes stop working, check the key first: an unrecognised key is not rejected, it is treated as no key at all. The key is in the console.
Choosing trigger groups and rule IDs
--group sshd is a starting point, not a recommendation. Pick the groups whose alerts actually carry external indicators — perimeter, DNS, proxy, IDS — and leave the rest alone. Every alert in a trigger group is scanned, and a group full of local-only events costs a scan and produces nothing.
Indicators are pulled from a maintained table of dotted alert-field paths. The live ones include srcip, dstip, audit.srcip, src_ip, dst_ip (Cisco FTD/ASA and Sophos decoders) and dest_ip (Suricata eve). Suricata uses dest_ip, not dst_ip — both are carried, and three plausible-looking ECS-style spellings were dropped from the table because they do not exist in Wazuh.
The loop guard
The connector injects alerts. Those alerts must never trigger the connector. The guard is why an enrichment alert cannot enrich itself into a loop — if you add a trigger group broad enough to include the whisper_enrichment group, the guard is what stops it, but the right fix is to narrow the group.
The enrichment envelope, field by field
Everything lands under data.whisper.* on a new alert. Nullable fields are stripped before send, so an absent key means "not applicable", never null.
| Field | Type | What it is |
|---|---|---|
schema_version | keyword | Envelope version, currently 1.0 |
ioc | keyword | The indicator that was enriched |
type | keyword | ipv4 · ipv6 · domain |
known | boolean | Whisper has a real graph node for this indicator. This is not a safety signal — it means the graph knows it exists |
verdict | keyword | known_good · known_bad · suspicious · unknown — derived from evidence, never a copy of the score |
risk_score | float | The explain() score. Evidence, never the verdict |
level | keyword | The explain() level enum, verbatim |
available | boolean | The scoring backend answered. false forces verdict = unknown |
advisory | keyword | Present only when there is one — e.g. allowlist-vouched |
permalink | keyword | Deep link to the graph view for this indicator |
graph_node_id | keyword | Stable node id, e.g. ipv4/185.220.101.1 |
coverage | object | granularity is always present. See the limitation below |
source_ref | object | Back-link to the triggering alert: rule_id, alert_id, agent_id, field_path, truncated original_full_log |
The verdict gates
The score is evidence, never the answer. verdict is derived from the level enum plus feed polarity plus coverage — never from the score alone, and never by parsing the human explanation string, because the enum and the explanation can disagree.
Evaluated in order, first match wins:
| Condition | verdict |
|---|---|
| The backend did not answer, or the indicator was not found | unknown |
| A positive trust signal, and no confirmed-bad or threat evidence, and a non-severe level | known_good |
| A confirmed-malicious node flag, or a severe level with at least one confirmed-bad feed category | known_bad |
| Any threat category or weak bad flag short of confirmed-bad | suspicious |
| Found, but only trust or neutral evidence and no score band | unknown |
Trust never overrides threat. The known_good gate requires no confirmed-bad or threat evidence, so an allowlisted-but-compromised host derives known_bad, not known_good.
No data is not benign. A clean result means "not listed at this granularity", not "safe" — which is why the found-but-no-evidence row resolves to unknown and never to known_good.
Listed in N feeds is not itself bad. Polarity comes from the feed's category and reliability from its weight. Gate on category and weight, not on a raw feed count.
Danger — silent-wrong. Gate a Wazuh rule on
coverage, never onbandalone.140.82.121.3returnsband: LOWwithcoverage: malicious-evidenced— a host with positive evidence of malice that any band-only gate treats as unremarkable. The connector's own verdict derivation is built to avoid exactly this, and a rule you write on top of it can reintroduce it in one line.
What coverage carries here. coverage.granularity is always present and is what a rule can read. Nullable fields are stripped before send, so gate on the keys you see rather than on the absence of one.
Turning on the agent-activity log source
install.sh --logs opts in to the agent-activity log source, which brings your tenant's own agent DNS, egress and identity activity into Wazuh. It needs a key, because it reads data that belongs to your account.
Troubleshooting
Nothing appears at all. Check grep whisper: /var/ossec/logs/integrations.log for the invoke → api → emit sequence. No invoke means the trigger group never fired; no api means egress to graph.whisper.security is blocked; no emit means the indicator was skipped.
invoke but no indicator. The alert carried no global IP or domain. Non-global addresses are skipped by design, and an alert full of RFC1918 addresses produces nothing.
Enrichment stopped after a rule change. The field the indicator used to come from may no longer be decoded. Check which field_path the last successful enrichment recorded in source_ref.
It worked and now the verdicts changed. The verdict is a live read of the graph. Feeds are added and removed, and a host's evidence changes underneath a query you did not change. That is the intended behaviour, not a fault.