AI-assisted threat hunting with Wazuh and Claude Code

Contents

TL;DR: Stand up the Wazuh server/indexer/dashboard, enroll agents on every host you care about, then let an AI agent (Claude Code) do the hunting: it queries the Wazuh indexer through the dashboard’s search proxy with curl, runs aggregations mapped to MITRE ATT&CK tactics, and triages what’s noise vs. what’s real. The agent is fast at the mechanical part — pivoting across source IPs, users, FIM changes, process events — so you spend your time on judgement, not query syntax.

Prerequisites

  • A Wazuh deployment (server + indexer + dashboard). The all-in-one quickstart is enough to start.
  • Agents installed on the hosts you want visibility into (Linux and/or Windows).
  • An AI coding agent with shell access (this guide uses Claude Code) that can run curl and read JSON.

Steps

  1. Install Wazuh. The fastest path is the all-in-one installer, which stands up the indexer, server, and dashboard on one box:

    curl -sO https://packages.wazuh.com/4.x/wazuh-install.sh
    sudo bash ./wazuh-install.sh -a
    

    It prints the generated admin password at the end. Log into the dashboard at https://wazuh.example.com and confirm it loads.

  2. Enroll your agents. On each host, install the agent and point it at the manager. On Linux:

    WAZUH_MANAGER="wazuh.example.com" apt-get install wazuh-agent   # after adding the repo
    sudo systemctl enable --now wazuh-agent
    

    On Windows, install the MSI with the same WAZUH_MANAGER variable, then start the service. In the dashboard, Agents management should show each host as Active.

  3. Get complete telemetry (this is the real work). Default Wazuh only watches a few logs. Before hunting, widen visibility or you’ll hunt blind:

    • Linux: enable auditd + the Wazuh audit integration (process execution), confirm FIM (syscheck) watches /etc, /root/.ssh, cron and web roots, and turn on rootcheck + SCA (CIS benchmarks).
    • Windows: deploy Sysmon with a good config, and collect the Security, System, PowerShell/Operational, and Defender channels. Enable command-line and PowerShell script-block logging via policy.
  4. Hand the agent short-lived credentials via cookies. The Wazuh dashboard proxies searches to the indexer, so you don’t need to open the indexer port directly — you just need a logged-in session. Rather than share a long-lived password or API key, export the short-lived session cookies from your browser after logging in and give those to the agent. Two ways to grab them:

    • Chrome DevTools: F12Application → Storage → Cookies → select the dashboard origin, and copy the security_authentication value.
    • A cookie-editor extension (e.g. Cookie-Editor): open it on the dashboard tab and export the cookies as JSON. You’ll see the Wazuh set — security_authentication, wz-token, wz-user, currentApi, currentPattern, and friends.
    Cookie-Editor extension listing the Wazuh dashboard session cookies

    The Cookie-Editor extension showing the dashboard’s session cookies — security_authentication is the one the search proxy checks.

    The one that authenticates the search proxy is security_authentication. Because it’s tied to your login session it expires on its own — that’s the point. Hand it over, hunt, and it dies shortly after; nothing long-lived is exposed. Confirm the agent can read alerts:

    COOKIE='security_authentication=<your-session-cookie>'
    curl -sk -H "Cookie: $COOKIE" -H "osd-xsrf: true" \
      -H "Content-Type: application/json" \
      -X POST "https://wazuh.example.com/api/console/proxy?path=wazuh-alerts-*/_search&method=POST" \
      -d '{"size":0,"track_total_hits":true}'
    

    A JSON response with a hits.total count means the agent can now read your alerts. Still treat the cookie like a credential while it’s live — it grants dashboard access — but its short lifetime is exactly why cookies beat a static key here.

  5. Have the agent profile the data first. Before hunting specifics, ask it to map the terrain: time range, which agents report, alert-level distribution, top rules, rule groups, and the MITRE techniques already present. This tells you where the signal is. A single aggregation does it:

    {
      "size": 0,
      "aggs": {
        "agents": { "terms": { "field": "agent.name", "size": 20 } },
        "levels": { "terms": { "field": "rule.level", "size": 20 } },
        "mitre":  { "terms": { "field": "rule.mitre.id", "size": 30 } }
      }
    }
    
  6. Hunt tactic by tactic (MITRE ATT&CK). Point the agent at each tactic and let it pivot. The high-value queries on a fresh internet-facing deployment:

    • Credential Access (T1110): aggregate failed auths by data.srcip and data.srcuser — brute-force sources and the usernames they spray.
    • Initial Access / Valid Accounts (T1078): aggregate successful auths by source IP. The key question: does any brute-force source IP ever appear in the success list? If yes → likely compromise.
    • Persistence (T1136 / T1543): new user/account creation, new services, new scheduled tasks or cron jobs.
    • Defense Evasion (T1562): agent-stopped events, log clearing, security-tool tampering.
    • FIM (syscheck): files added/modified in sensitive paths.
  7. Triage: separate deployment noise from real threats. On a new install, most anomalies trace to the install window — service accounts the installer created, agent restarts, firewall config writes. The agent is good at correlating timestamps to prove “this clustered during setup.” What’s left after that is the real hunt surface. Turn confirmed detections into permanent rules so the next hunt is automatic.

Commands

A reusable query helper so the agent (or you) can fire aggregations quickly:

#!/usr/bin/env bash
# wq.sh — query wazuh-alerts-* via the dashboard console proxy; body on stdin
COOKIE='security_authentication=<your-session-cookie>'
curl -sk --max-time 40 \
  -H "Cookie: $COOKIE" -H "osd-xsrf: true" -H "Content-Type: application/json" \
  -X POST "https://wazuh.example.com/api/console/proxy?path=wazuh-alerts-*/_search&method=POST" \
  -d @-
# Brute-force sources: who is hammering SSH, and which hosts
./wq.sh <<'EOF'
{"size":0,
 "query":{"match":{"rule.groups":"authentication_failed"}},
 "aggs":{"src":{"terms":{"field":"data.srcip","size":15},
                "aggs":{"agents":{"terms":{"field":"agent.name","size":5}}}},
         "users":{"terms":{"field":"data.srcuser","size":20}}}}
EOF

The prompts I used

The whole hunt was two prompts. The first sets up access and asks for a plan; the second turns the agent loose on the data.

Here's a Wazuh dashboard (admin) at https://wazuh.example.com and the
short-lived session cookies exported from my cookie editor: <cookies>.
List the steps a threat hunter would take to make this platform useful.
I have 3 Linux and 1 Windows agent (1 macOS upcoming).

That produced a phased roadmap (verify the plumbing → expand telemetry → enrichment → tune noise → active hunting → operationalize):

AI agent output: a phased Wazuh threat-hunting roadmap from foundation checks through operationalizing

The agent’s phased roadmap — from verifying agents report, through expanding telemetry, to the active-hunting loop and operationalizing.

Then, to actually hunt:

Do the active-hunting phase for me.

The agent found the dashboard’s search proxy, wrote a small curl query helper, and ran aggregations across MITRE ATT&CK tactics against the live alert index — no manual query writing on my part.

What the agent found

The agent’s actual hunt report — real hostnames and IPs redacted, but the structure is the point:

AI agent threat-hunt results: a MITRE ATT&CK tactic breakdown with a no-compromise verdict and an SSH brute-force action item (hostnames and IPs redacted)

The agent’s Phase-4 results — a per-tactic table, a no-compromise verdict, and the one real action item (SSH brute-force). Hostnames and source IPs are blacked out.

And the same report as sanitized text (hostnames and IPs replaced with placeholders) — note the shape of the answer, not the specifics:

Data hunted: ~2,900 alerts, ~1.5 h window (deployment + first hour of exposure).
Verdict: No evidence of compromise. One active external threat: SSH brute-force.

Hunt results by ATT&CK tactic
- Credential Access — Brute Force (T1110.001): ~400 failed auths from ~90 source
  IPs against all 3 internet-facing hosts (e.g. 203.0.113.11, 203.0.113.24).
  Sprayed usernames: admin, test, remote, ubuntu, deploy...  → REAL, active,
  but unsuccessful (internet bot noise).
- Initial Access — Valid Accounts (T1078): only successful SSH source =
  198.51.100.10 (6 successes, 0 failures). No attacker IP ever succeeded. → benign
  (admin host — confirm).
- Persistence — Create Account (T1136): 1 event = service account created by the
  installer at deploy time. → benign.
- Defense Evasion — Agent stopped (T1562.001): all inside the install window. → benign.
- FIM (syscheck): all changes = firewall + monitoring-agent config at setup. → benign.
- Windows: logon + new service + agent install events. → benign.
- Malware (ClamAV): 1 "virus" hit = actually a DB-update log line. → false positive.

The one thing that needs action: disable SSH password auth (keys only), restrict SSH
by source IP, add fail2ban / Active Response. Confirm 198.51.100.10 is your admin host.

Coverage gaps: no auditd/execve on Linux and no Sysmon on Windows → cannot hunt
process execution yet. Only ~1.5 h of data → no baseline. Fix telemetry, then re-hunt.

The agent’s key move was cross-referencing brute-force source IPs against the successful-login source IPs. Zero overlap → no compromise, despite hundreds of scary-looking failed-login alerts. That correlation is exactly the tedious pivot an AI agent is good at.

Gotchas

  • Hunting is only as deep as your telemetry. Without auditd on Linux and Sysmon on Windows, you cannot hunt process execution — no LOLBins, no reverse shells, no command lines. Fix telemetry before blaming the hunt.
  • You need a baseline. A brand-new deployment has an hour of data and no sense of “normal.” Re-run the hunt after 1–2 weeks of steady-state before trusting deviation-based conclusions.
  • The session cookie is a credential. Anyone with it can read your dashboard. Don’t paste it into shared logs, and rotate it (re-login) when done. For anything long-lived, use a proper Wazuh API user instead of a browser session.
  • The AI does pivots, not judgement. It’s excellent at “aggregate failures by source IP and cross-reference against successes.” Whether an IP is your admin jump host or an attacker is still your call — feed it the context it can’t know.
  • Lock down SSH regardless. Internet-facing hosts get continuous brute-force. Disable password auth (keys only) and restrict SSH by source; that removes the single loudest source of alerts and the underlying risk at once.