# Wazuh Alerts That Run a SOP

Wazuh writes every alert it raises to its indexer (OpenSearch), in `wazuh-alerts-4.x-*`. An
alerting monitor in the indexer can search those alerts on a schedule and, through a webhook
notification channel, call the gateway with what it found: each run that finds Wazuh alerts
runs a SOP, with their rule, agent and log as its input.

```mermaid
flowchart LR
    MGR["<b>Wazuh Manager</b>"] -->|alerts| IDX["<b>Indexer</b><br/>wazuh-alerts-4.x-*"]
    MON["<b>Monitor 'GoClaw: &lt;title&gt;'</b>"] -->|"every N min"| IDX
    MON -->|matches| CH["<b>Channel goclaw-&lt;key&gt;</b>"]
    CH -->|"POST (X-Token)"| GW["<b>/goclaw/hook/wazuh/&lt;key&gt;</b>"]
    GW --> SOP["<b>SOP Run on the Agent</b>"]
```

Nothing changes on the Wazuh manager: no `ossec.conf` integration, no restart.

There are two ways to set it up:

- **Ask the agent.** An admin describes it in a conversation. The agent's `wazuh_alert` tool
  makes the monitor and its channel in the indexer and the route in GoClaw, and removes them
  together. See [Asking for an alert hook](#asking-for-an-alert-hook).
- **Connect it yourself.** Make a `wazuh` route on the **Webhooks** page, and in the Wazuh
  dashboard a webhook channel and a monitor that notifies it. See
  [Connecting Wazuh without the agent](#connecting-wazuh-without-the-agent).

---

## Before you start

| What | Where |
| :--- | :--- |
| Wazuh 4.4 to 4.14 (indexer on OpenSearch 2.4 to 2.19) | 4.3's indexer has no notifications plugin; 5.0 keeps alerts in other indices, which the agent does not search yet |
| The indexer's URL, an indexer user and its password, and `allow_write: true` (only when the agent makes hooks) | `tools.observability.wazuh.<name>`: the **Integrations** page or `config.yaml` |
| An address the indexer can reach the gateway at | `gateway.ingress.url`, and a listener there |
| A SOP for the runs to run | the SOPs page, or the agent's `sop_save` tool ([Standard Operating Procedures](sop.md)) |

```yaml
gateway:
  bind: lan
  ingress:
    url: "http://192.0.2.10:8080"         # as the Wazuh indexer sees this gateway

tools:
  observability:
    wazuh:
      siem:
        url: "https://wazuh.example.com:55000"          # the manager API (wazuh_query)
        user: "wazuh-api"
        password: "${secret:WAZUH_API_PASSWORD}"
        indexer_url: "https://wazuh.example.com:9200"   # the indexer, for wazuh_alert
        indexer_user: "admin"
        indexer_password: "${secret:WAZUH_INDEXER_PASSWORD}"
        insecure: true                                  # Wazuh's own certificates; applies to both
        allow_write: true                               # off by default: the agent cannot make or remove hooks
```

The indexer user needs the `alerting_full_access` and `notifications_full_access` roles and read
access to `wazuh-alerts-*`; Wazuh's `admin` indexer user has all of them. A monitor searches with
the roles of the user that made it.

---

## Asking for an alert hook

```
Run intrusion-triage on Wazuh alerts of level 10 and above from web-01 and db-01 in the authentication_failed group.
When rule 5712 fires anywhere, run ssh-brute-force-response. Check every 2 minutes.
```

The agent answers with a plan; nothing is created until you confirm it in your next message:

```
Plan 41d0c8e2 — nothing is created yet.

Wazuh "siem" indexer (OpenSearch 2.19.3) gets an alerting monitor "GoClaw: SSH brute force":
- every 5 minute(s) it searches wazuh-alerts-4.x-* for Wazuh alerts at rule level 10 or above, in group authentication_failed, from agent web-01 or db-01
- a run that finds some sends the first 20 to its own notification channel goclaw-ssh-brute-force, which calls http://192.0.2.10:8080/goclaw/hook/wazuh/ssh-brute-force with a new secret only this route knows
- the manager's configuration, other monitors and channels are not changed
GoClaw: each run with matches runs SOP "intrusion-triage" (Intrusion triage) as agent main.
```

What the request can set: the lowest rule level (`min_level`, default 10), rule IDs, rule
groups, agent names, and how often the monitor runs (`every_minutes`, default 5).

Each run searches the interval before it, ending a minute early so that an alert the indexer
takes a moment to write is found by the next run; runs do not overlap. A run that finds nothing
calls nothing. **One run with matches is one SOP run**, however many Wazuh alerts it found: a
burst of fifty failed logins is one run, not fifty.

The plan is refused, with the reason, when the indexer lacks the alerting or notifications
plugin (Wazuh 4.3), is a Wazuh 5 indexer, has no `wazuh-alerts-4.x-*` index, already has a
channel or monitor of that name, the route key is taken, the instance has no indexer settings,
or it does not allow writes.

After creating it, the agent has the indexer send the channel's test message and says whether
it reached the route.

---

## What the SOP is given

Each run is the trigger data, in an untrusted section of every step. The agent is told:

```
Monitor: GoClaw: SSH brute force — 34 Wazuh alert(s) between 2026-10-09T02:00:00Z and 2026-10-09T02:05:00Z
- level 10, rule 5712: sshd: brute force trying to get access to the system.
  Agent: web-01 (192.0.2.10)
  Time: 2026-10-09T02:01:00.000+0000
  Groups: syslog, sshd, authentication_failures
  Source IP: 203.0.113.7
  Location: /var/log/auth.log
  Log: Oct  9 02:01:00 web-01 sshd[811]: Failed password for root from 203.0.113.7 port 52211 ssh2
  Alert ID: 1760000000.123
…
```

Up to ten of the run's alerts are told in full, and the count of the rest. One monitor's runs
keep one session.

The indexer does not deliver a run again when the gateway does not answer: the next run sends
the alerts it finds then.

The route's secret is a header of the indexer's notification channel, which anyone who may read
the channel's settings in the indexer can see.

---

## Listing, checking, removing

- **list**: every hook this gateway registered, what it searches for, whether its monitor is
  still in the indexer and enabled, and how many runs the route received.
- **test**: the indexer sends the channel's test message (plain text). The route answers it,
  counts it and runs nothing.
- **delete**: planned and confirmed like a create; removes the monitor, then its channel, and
  the route. Deleting the route on the **Webhooks** page removes them too, when the instance
  allows writes; otherwise it says what to delete in the indexer.

Only a human admin can do this, in a conversation of their own: never a SOP, cron, webhook or
subagent run.

---

## Connecting Wazuh without the agent

1. On the **Webhooks** page, create a route of type **Wazuh** with a key, the agent and the SOP.
   Saving shows the URL, the secret, and the monitor action's message to paste, once.
2. In the Wazuh dashboard, **Explore › Notifications › Channels › Create channel**: type
   **Webhook**, the route's URL, method **POST**, and the headers `Content-Type:
   application/json` and `X-Token` with the route's secret. The indexer sends only the headers it
   is given. **Send test message**: the route answers it and its **Hits** go up.
3. **Explore › Alerting › Monitors › Create monitor**: a per-query monitor on
   `wazuh-alerts-4.x-*`, a trigger such as `ctx.results[0].hits.total.value > 0`, and an action
   that notifies the channel with the message from step 1.

## Related

- [Graylog Alerts That Run a SOP](graylog-alerts.md) — the same pattern for Graylog
- [Zabbix Problems That Run a SOP](zabbix-alerts.md) — the same for Zabbix
- [SRE Tools](../observability/sre-tools.md#wazuh) — the `wazuh_query` tool and the instance's configuration
- [Standard Operating Procedures](sop.md)
