In Wazuh 5.0 the SOAR seam moved from the manager to the indexer. There is no integratord, no <integration> block and no script directory to drop a Python file into. The supported path is an OpenSearch alerting monitor watching the wazuh-findings-v5-* data streams, a trigger, and an action that posts a body you control to a notification channel. Your automation platform, whether that is n8n, Shuffle, TheHive or something you wrote yourself, receives an HTTP request. Everything interesting about the design is in what that request contains and how often it arrives.

The engineering work is not wiring the webhook. It is deciding the payload contract, the deduplication key, and what the playbook is allowed to write back.

What you need

  • A working 5.0 deployment producing findings. If findings are noisy, fix that first with cutting Wazuh false positives; automating a noisy feed multiplies the noise.
  • Admin access to the Wazuh dashboard, or the indexer API on port 9200.
  • An HTTPS endpoint on your automation platform that the indexer can reach.
  • Familiarity with the finding document shape from Wazuh rules, decoders and severity levels.

The field-by-field mapping is in the Wazuh migration guide, Migrating Integratord to Dashboard Notifications, and it is the single most useful page for this lesson.

Design the intake before you build it

Three constraints shape the whole design, and all three come from the documented defaults rather than from taste.

Monitor count is scarce. The Wazuh alerting module documentation states that it “supports up to 10 custom monitors per user” and that the limit applies to every monitor type. The same page lists plugins.alerting.monitor.max_monitors with a default of 1000. Wazuh’s own content occupies part of that budget already: a stock 5.0.0-beta5 install runs 22 alerting monitors before you create anything, so ten is clearly not a cluster-wide ceiling, but the per-user figure for monitors you create is still unresolved. Until that is reconciled at general availability, build as if ten is the number. Ten monitors is plenty for a severity-banded design and nowhere near enough for one monitor per customer, per rule or per playbook. Route on severity in the monitor; route on everything else inside the automation platform, which has no such limit.

Each monitor carries at most ten triggers. plugins.alerting.monitor.max_triggers defaults to 10. A monitor with several queries and one trigger per routing decision is the pattern the migration guide recommends, and it fits inside the budget.

Bursts are capped. plugins.alerting.max_actionable_alert_count defaults to 50: the maximum number of actionable alerts processed in a single monitor execution. That is a safety valve, not a promise of completeness. If a detector matches two hundred documents in one interval, your webhook will not see two hundred notifications. Treat the webhook as the trigger for work and the indexer as the source of truth, which is the right architecture regardless.

The shape that falls out of those three:

Layer Responsibility
Detector Sigma rule matching, finding creation
Monitor query Coarse filter: severity, integration
Trigger Routing decision (page, ticket, log)
Action message The payload contract
Channel Transport, credentials, headers
Playbook Enrichment, decision, ticket, write-back

Everything above the playbook row runs on the indexer.

Create the webhook channel

The Notifications module ships four pre-created channels with fixed identifiers: default_slack_channel, default_jira_channel, default_pagerduty_channel and default_shuffle_channel. They are created once on the cluster manager node at first start, in a disabled state with placeholder values, and they are never overwritten on later starts. If you run Shuffle, the channel already exists and needs only its URL and credentials.

Listing them on a stock 5.0.0-beta5 install confirms the set and their types:

curl -sk -u <INDEXER_USERNAME>:<INDEXER_PASSWORD> \
  "https://127.0.0.1:9200/_plugins/_notifications/configs"

Reduced to names and transports, the beta5 lab stack returns:

notification configs: 4 ['Slack Channel/slack', 'Jira Channel/webhook', 'PagerDuty Channel/webhook', 'Shuffle Channel/webhook']

Only Slack has a channel type of its own. Jira, PagerDuty and Shuffle are all plain webhooks with a different name on the front, which is the useful part: whatever your automation platform is, you are configuring the same object those three use.

For n8n, TheHive or anything else, create a channel of type Custom webhook under Explore > Notifications > Channels. It supports PUT, POST or PATCH, a plain webhook URL or one with query parameters, and custom headers such as Authorization and Content-Type. Header support is what lets you carry a bearer token without putting it in every monitor message.

Two other channel settings matter under load: opensearch.notifications.core.http.connection_timeout defaults to 5000 milliseconds and opensearch.notifications.core.http.socket_timeout to 50000 milliseconds, with max_connections at 60 and max_connection_per_route at 20. A receiver that queues work synchronously will hit the socket timeout long before it hits a connection limit.

Build a severity-banded monitor

Create the monitor under Explore > Alerting > Create Monitor. For a SOAR intake use a per document monitor: document-level monitors evaluate individual documents as they are indexed and record each matching document, which is what you want when each event becomes a unit of work. Point it at the findings data streams, not the events streams. The migration guide’s worked example uses a per document monitor on wazuh-findings-v5-security at a one-minute interval; wazuh-findings-v5-* catches every category.

Queries filter on the fields the finding carries. In 5.0 the severity field is wazuh.rule.level and it holds a string: informational, low, medium, high or critical. There is no numeric level any more, and the migration guide is explicit that there is no mechanical conversion from the 4.x 0-to-16 scale. You have to decide what your old threshold of 12 meant and which band it belongs in now.

A workable three-band split:

Trigger Query condition Action
Page wazuh.rule.level is critical Webhook to the on-call playbook
Ticket wazuh.rule.level is high Webhook to the ticketing playbook
Log wazuh.rule.level is medium Webhook to the enrichment-only playbook

Two other fields belong in the query vocabulary. wazuh.integration.name identifies the content that produced the finding and replaces the 4.x rule group for routing (the migration guide gives wazuh-fim as the file integrity example), and wazuh.rule.id takes the place of <rule_id>.

Write the payload contract

This decides whether your automation survives its first content update. The action message is a free-form Mustache template, so the JSON body is exactly the shape you specify. Define a contract, version it, and never let a playbook reach into a field the contract does not name.

Action messages address the matched document through the alerting context. The Wazuh migration guide’s own examples use {{ctx.alerts.0.sample_documents.0._source.<field>}} to reach fields on the triggering finding, plus {{ctx.monitor.name}} for the monitor and, from the OpenSearch alerting actions reference, {{ctx.trigger.name}}, {{ctx.trigger.severity}}, {{ctx.periodStart}} and {{ctx.periodEnd}}.

The full contract is in the companion files for this lesson as webhook-payload-contract.json, with a JSON Schema beside it. The fields it carries, all confirmed on a real 5.0 finding document in the official detection-workflow walkthrough:

Contract field Source field on the finding Why it is in the contract
finding.event_id wazuh.event.id Idempotency key
finding.doc_id event.doc_id Points back at the originating event
finding.doc_index event.index Needed to fetch the event
rule.id wazuh.rule.id Grouping and suppression
rule.title wazuh.rule.title Human summary
rule.level wazuh.rule.level Routing
agent.id, agent.name wazuh.agent.id, wazuh.agent.name Asset identity
integration.name wazuh.integration.name Which content fired
finding.space wazuh.space.name Standard content or your own
network.source_ip source.ip Enrichment input
finding.observed_at @timestamp Event time, not delivery time

Two categories are deliberately absent. Array fields such as wazuh.agent.groups need a Mustache section block, which does not produce clean JSON inside a fixed template. Free text such as event.original is a quoting hazard inside a JSON string. Both live on the finding, and the playbook fetches the document with doc_id and doc_index when it needs them, which is the right pattern for anything beyond a routing decision anyway.

Handle duplicates before they reach a ticket queue

Findings re-fire. A detector is a scheduled job, and the shipped ones on beta5 run at one, two or five minutes, so an event that keeps occurring keeps matching. The alerting layer runs its own lifecycle alongside that: an alert stays Active while its trigger condition holds, moves to Acknowledged when someone marks it, and to Completed automatically when the condition stops being true. Neither clock is your ticket queue.

Deduplicate in the playbook on a key you control. wazuh.event.id is a per-event identifier and makes a clean idempotency key for processing a single finding exactly once. For collapsing a burst into one ticket, a composite of rule_id plus agent_id plus a time bucket works better, because that is the unit an analyst will actually investigate.

Action throttling is a blunt second layer: it limits how often an action fires within a window, and plugins.alerting.action_throttle_max_value caps that window at 24 hours. Use it to protect a paging channel, not as a deduplication strategy, because it drops notifications rather than merging them.

Enrich with what Wazuh already knows

Before a playbook calls an external service, check whether the finding already carries the answer.

Geo and ASN context is added in the manager’s enrichment stage, so source.geo and source.as are already on the document. Threat intelligence matches from Wazuh CTI arrive under wazuh.threat, including the indicator and which field matched. There is no reason to spend a reputation API call on an IP the pipeline already flagged. Wazuh threat intelligence with CTI, MISP and KVDB lists covers that enrichment and how to add your own indicators.

Asset context lives in the state indices rather than on the finding: wazuh-states-inventory-ports, wazuh-states-inventory-packages and wazuh-states-vulnerabilities hold the current picture of the endpoint. A playbook that reads those before deciding severity is doing what an analyst would do manually.

Evidence-based downgrade, and closing the loop

The most valuable thing a playbook does is remove work, and the next most valuable is to show its reasoning where the analyst is looking.

Case management in the Security Analytics module gives you the write-back. A PUT /_plugins/_security_analytics/findings/_update sets a case object on findings, with status, comment and tags. All fields are optional, updates are partial, and one request can update up to 50 findings. Documented statuses include ACTIVE, ACKNOWLEDGED, COMPLETED, ERROR, DELETED and AUDIT.

curl -k -u <USER>:<PASSWORD> -X PUT "https://<INDEXER_IP>:9200/_plugins/_security_analytics/findings/_update" -H 'Content-Type: application/json' -d @downgrade.json

The body names the finding by document id and index, and sets the case fields:

{
  "findings": [
    {
      "_id": "<finding-document-id>",
      "_index": "<finding-index-name>",
      "case": {
        "status": "ACKNOWLEDGED",
        "comment": "Automated triage: source IP is in the scanner allowlist, no CTI match",
        "tags": ["auto-triage", "downgraded"]
      }
    }
  ]
}

Findings written this way are queryable. wazuh.case.status is a keyword field, so an analyst dashboard can exclude everything a playbook has already closed, and you can measure how much of the feed the automation actually absorbed.

Rules for what a playbook may do, which I would apply anywhere:

  • It may enrich, tag, comment and downgrade medium and below.
  • It may open a ticket at any severity.
  • It may not set COMPLETED on anything it downgraded itself. A human closes.
  • It never auto-closes critical. AI-assisted triage for Wazuh findings goes further into the guardrails when a model is in the loop.

Ticketing: TheHive and the rest

Jira has a pre-created channel, and the migration guide shows the base64 Basic authorisation header pattern for it. TheHive has no shipped channel, so it is a custom webhook: POST, an Authorization header holding the API key, and a body matching TheHive’s own alert or case API. Build that body from the contract fields rather than from raw finding paths, so a schema change in Wazuh touches one template instead of every ticket type. One channel per external service, one monitor combining queries and triggers, one action per service with its own message: that is the structure the migration guide recommends, and it holds up.

Active Response as an opt-in tail

Wazuh 5.0 inverts Active Response. An Active Response monitor is a document-level monitor that writes execution requests to the wazuh-active-responses data stream; the manager reads them and distributes the actions to agents. The constraints are tight and worth knowing before you plan around it: the monitor must target indices matching wazuh-findings-v5-*, the maximum execution interval is one minute, and only DocumentLevelTrigger is supported. Requests that break those rules are rejected with 400 Bad Request.

Documents in that stream carry the response name, executable, extra arguments, location (local, defined-agent or all), target agent, type (stateless or stateful) and stateful timeout, along with event.doc_id and event.index pointing at the finding that caused it. Retention is three days by default, so it is an audit trail with a short memory.

My stance has not changed with the new model: leave it off. Wire the playbook to propose the action, have a human approve it, and only after a playbook has run in propose-only mode for a full change cycle should you consider letting it write to that stream, and then only for a named scope. Wazuh Active Response has the full gating checklist.

Common errors

400 Bad Request when saving an Active Response monitor

Cause: the monitor targets an index outside wazuh-findings-v5-*, uses an interval longer than one minute, or uses a trigger type other than DocumentLevelTrigger. All three constraints are enforced on creation and on update.

Fix: point the monitor at wazuh-findings-v5-*, set the schedule to one minute or less, and use a document-level trigger.

The monitor runs, the alert appears, and the external system shows nothing

Cause: almost always the message body. The migration guide states that if the payload format is incorrect, no event appears on the external side. The second most common cause is the notifications host deny list blocking the receiver’s address range.

Fix: send a test message from the channel and read the far side’s own logs. Then check opensearch.notifications.core.http.host_deny_list in /etc/wazuh-indexer/opensearch.yml.

Notifications stop when you add more monitors

Cause: the documented per-user custom monitor limit of 10.

Fix: consolidate. One monitor can hold multiple queries and up to ten triggers, so a severity-banded monitor with per-service triggers replaces many single-purpose monitors. Move fine-grained routing into the automation platform.

A burst of matches produces far fewer webhooks than findings

Cause: plugins.alerting.max_actionable_alert_count, default 50 actionable alerts per monitor execution.

Fix: treat the webhook as a work trigger and have the playbook query the findings index for the full set. Raising the setting moves the ceiling but does not remove it.

Credentials in a monitor message

Cause: a token pasted into the action template rather than into the channel.

Fix: put credentials in the channel’s webhook headers or URL, which is what the custom webhook type is for. Monitor messages are visible to anyone with alerting read access. Hardening Wazuh itself covers the surrounding access model.

Next: Wazuh Compliance: PCI DSS, ISO 27001, SOC 2, CMMC and HIPAA.

Files for this lesson

Every file this lesson refers to, exactly as used. Open a file to read it in the browser, or download the set as a zip. Download wazuh-soar.zip. The full set for all lessons is at wazuh-tutorial-files.zip.

Frequently asked questions

How do you send Wazuh alerts to a SOAR platform in 5.0?

You create a notification channel of type webhook in the Notifications module, then an alerting monitor on the wazuh-findings-v5-* data streams whose trigger action posts a Mustache-templated JSON body to that channel. The manager is not involved. In 4.x this was the integratord daemon reading ossec.conf; that daemon and the integration block no longer exist.

How many alerting monitors can you create in Wazuh 5.0?

The Wazuh alerting documentation states a limit of 10 custom monitors per user across all monitor types, while the cluster setting plugins.alerting.monitor.max_monitors carries a default of 1000. A stock 5.0.0-beta5 install already runs 22 shipped monitors, so ten is not a cluster-wide cap, but the per-user figure is unsettled. Design for the smaller figure: monitors per severity band rather than per tenant or per rule.

Can a SOAR platform write back into Wazuh?

Yes, through the Security Analytics findings update API. A PUT to /_plugins/_security_analytics/findings/_update sets the case status, an analyst comment and tags on up to 50 findings in one request. Statuses include ACTIVE, ACKNOWLEDGED and COMPLETED. That is how a playbook records its verdict where analysts will actually see it.

Why does my Wazuh webhook never reach my automation platform?

Check three things. The notification channel deny list, opensearch.notifications.core.http.host_deny_list, silently blocks whole CIDR ranges and the documented example blocks RFC 1918 space. The message body must match what the receiver expects, because a malformed payload produces nothing on the far side. And the socket timeout defaults to 50000 milliseconds, so a slow receiver looks like a lost message.

Should Active Response be part of the SOAR design?

Only as an explicit opt-in, per environment, after the detection feeding it has been tuned. The 5.0 model is an Active Response monitor writing to the wazuh-active-responses data stream, which the manager reads and dispatches. Nothing fires by default, and that default is worth keeping until a playbook has been running in report-only mode long enough to trust.