Deploy a Local Wazuh AI-Agent Lab

Deploy Wazuh locally to detect denied AI-agent tool calls from JSON telemetry.

Introduction

30 Second Summary

Automated agents can call tools faster than a person can review every request. Without a monitoring trail, a denied call can disappear among ordinary activity.

In this project, you will deploy Wazuh as a local SIEM that receives single-line JSON telemetry from a simulated AI agent. You'll expose a decoded event that stays undetected before adding a custom rule that surfaces denied tool calls in Threat Hunting.

What You'll Build

You'll watch a denied AI-agent tool call emerge in Threat Hunting as a searchable alert with its agent, tool, target, and reason visible.

By the end of this project, you'll have:

  • A browser-accessible local detection lab where you can sign in to Wazuh and investigate security activity from your Windows PC.
  • A host-side JSON telemetry feed where an allowed tool call stays quiet while a denied call becomes a security alert.
  • A custom policy-violation detector plus a repeatable Threat Hunting query that reveals the affected agent, requested tool, unauthorized target, and denial reason.
  • Secret Mission: An optional challenge to push your detection engineering skills further.

Do I have the right workstation?

You'll need a Windows PC with administrator access, internet access, at least 4 CPU cores, 8 GB of RAM, and 50 GB of storage.

Your PC also needs Docker Desktop with Docker Compose, WSL 2, and Visual Studio Code. The project checks Git plus the Microsoft WSL extension before deployment.

Before We Start

This is your moment to commit to the purpose of the local Wazuh SIEM lab before any hands-on work begins. The lab will monitor single-line JSON AI-agent tool-call telemetry for denied calls to unauthorized targets.

Deploy the Single-Node Stack

Your future AI-agent rule needs a working SIEM before telemetry has anywhere to go. The local stack turns raw events into searchable alerts.

In this step, you deploy Wazuh 4.14.8 through Docker on Windows Subsystem for Linux 2. You finish with all three central services running locally.

Why the single-node Docker stack?

The single-node deployment keeps all three central Wazuh components on one computer. That shape fits a local detection lab.

The manager analyzes events. The indexer stores alerts for the dashboard to search.

In this step, get ready to:
  • Validate the WSL resources required by the Wazuh stack.
  • Prepare the pinned Wazuh deployment inside the Linux filesystem.
  • Start the three services before signing in to the local dashboard.
Validate the WSL workstation

The Wazuh single-node stack needs at least 4 CPU cores. It also needs 8 GB of memory plus 50 GB of storage.

  • Press the Windows key to open Windows search.
  • Type WSL into the search field.
  • Press Enter to open your default Linux distribution.
  • Check the Linux resources plus the required command-line tools by running these commands:
nproc
free -h
df -h ~
docker version
docker compose version
git --version

What do these checks prove?

  • The nproc result shows how many processing units WSL can use.
  • The free -h result shows memory totals in readable units.
  • The df -h ~ result shows the storage available to your Linux home filesystem.
  • The remaining commands confirm that Docker Engine, Docker Compose, plus Git are available inside WSL.
  • Confirm that the processor count is at least 4.
  • Confirm that the total memory is at least 8 GB.
  • Confirm that the available storage is at least 50 GB.
  • Confirm that Docker prints information for both its client and server components.
  • Confirm that Docker Compose prints its version information.

Docker unavailable inside WSL?

Make sure Docker Desktop is running on Windows. Confirm that your Linux distribution is enabled under Docker Desktop's WSL integration settings.

If Docker still cannot connect, use help me connect Docker Desktop to my WSL distribution.

Choose the Git result that matches your terminal. The missing-Git paths install it with your Linux distribution's package manager.

✔️ Git prints a version

Git is ready inside WSL. You can use it to clone the pinned Wazuh repository.

ⓧ Git is missing on Debian or Ubuntu

Expect a confirmation prompt

The package manager may ask you to approve the installation. Confirm the prompt so it can finish.

  • Enter a root shell before installing Git by running these commands:
sudo -i
apt-get install git
exit

What does this installation do?

The first command opens an administrative shell. The final command returns you to your normal WSL user after Git is installed.

  • Verify the Git installation by running:
git --version

What should I see?

Git prints its installed version. That output confirms the command is available inside WSL.

ⓧ Git is missing on Fedora

Expect a confirmation prompt

The package manager may ask you to approve the installation. Confirm the prompt so it can finish.

  • Enter a root shell before installing Git by running these commands:
sudo -i
dnf install git
exit

What does this installation do?

The first command opens an administrative shell. The final command returns you to your normal WSL user after Git is installed.

  • Verify the Git installation by running:
git --version

What should I see?

Git prints its installed version. That output confirms the command is available inside WSL.

The Microsoft WSL extension lets Visual Studio Code edit Linux files with Linux-side tools. A connection indicator in the editor confirms that the workspace uses WSL.

  • Test the VS Code WSL connection by running:
code .

What does this command do?

The code . command opens the current Linux directory in VS Code. The WSL extension connects the Windows editor to that directory.

Choose the result that matches the VS Code window.

✔️ VS Code is connected to WSL

The lower-left Status Bar shows your WSL distribution. You may see a value such as WSL: Ubuntu.

ⓧ VS Code is not connected to WSL

Finding the connection status can be fiddly because it sits at the far left of the Status Bar. Install the Microsoft WSL extension before trying the connection again.

  • Press Ctrl+Shift+X in VS Code to open the Extensions view.
  • Enter WSL in the extension search field.
  • Select the Microsoft WSL extension.
  • Click Install.
  • Return to your WSL terminal after the installation finishes.
  • Retry the connection by running:
code .

What should change?

VS Code opens the current Linux directory again. Its lower-left Status Bar now identifies your WSL distribution.

Still missing the WSL connection?

Press F1 in VS Code. Select WSL: Connect to WSL to connect to your default distribution.

Use help me connect VS Code to WSL if the connection indicator remains missing.

Prepare the pinned Wazuh deployment

The Wazuh indexer needs a larger virtual memory map count than some WSL environments provide by default. Setting the required value prevents the indexer from failing during startup.

  • Return to the WSL terminal from the workstation checks.
  • Set the WSL kernel value from a root shell by running these commands:
sudo -i
sysctl -w vm.max_map_count=262144
exit

What does this setting control?

The sudo -i command opens a root shell. The sysctl command raises the map count available to the indexer.

The exit command returns you to your normal user. The terminal prints the setting with the value 262144 when the change succeeds.

Kernel setting rejected?

Confirm that the password belongs to a WSL user with administrator access. Enter that user's Linux password when the root shell requests it.

Use help me set vm.max_map_count in WSL if the setting still fails.

The repository stays inside the WSL Linux filesystem for faster bind mounts. The release tag keeps every service on the tested v4.14.8 deployment.

  • Move to your Linux Desktop before cloning the repository by running these commands:
cd ~/Desktop
git clone https://github.com/wazuh/wazuh-docker.git -b v4.14.8
cd wazuh-docker/single-node

What does this clone create?

Git downloads the official Wazuh Docker repository into wazuh-docker. The branch flag selects the pinned v4.14.8 release.

The final command places your terminal inside wazuh-docker/single-node. The remaining deployment commands run from this directory.

  • Open the single-node directory in the WSL-connected VS Code window by running:
code .

What should open?

VS Code switches its workspace to wazuh-docker/single-node. The lower-left Status Bar continues to show the WSL connection.

Certificate generation prepares encrypted communication between the Wazuh components. The first run may pause while Docker downloads the generator image.

  • Generate the indexer certificates by running:
docker compose -f generate-indexer-certs.yml run --rm generator

What does this command generate?

Docker Compose runs the certificate generator defined in generate-indexer-certs.yml. The --rm flag removes the temporary generator container after it finishes.

The command returns you to the shell after the certificates are generated. Your single-node directory is now ready to start the stack.

Certificate generation failed?

Confirm that Docker Desktop is still running. Check that your terminal remains inside wazuh-docker/single-node.

A download failure can also come from an interrupted internet connection. Use help me troubleshoot Wazuh certificate generation to inspect the terminal output.

Start the stack and sign in

The certificates now let the manager, indexer, plus dashboard communicate securely. Starting the Compose project turns those definitions into running local services.

The first startup can take a few minutes because Docker downloads the pinned Wazuh images. Quiet periods during the download are expected.

  • Start the Wazuh stack in the background by running:
docker compose up -d

What does this command start?

Docker Compose starts wazuh.manager, wazuh.indexer, plus wazuh.dashboard. The -d flag leaves them running in the background.

Before you inspect the stack, which three services do you expect Docker Compose to report?

  • Inspect the service state by running:
docker compose ps

What should I see?

The status table lists wazuh.manager, wazuh.indexer, plus wazuh.dashboard as running. This confirms that the full central stack started.

That is the deployment running: Wazuh now has an analyzer, an alert store, plus a browser interface on your PC.

A service is still starting?

The dashboard can briefly report that the indexer connection is unavailable while startup finishes. Wait a short while before running the status command again.

If a service stops, check that your workstation met the resource minimums. Use help me diagnose a stopped Wazuh container to work through the service state.

The dashboard uses a self-signed certificate because every connection stays inside this local lab. Your browser warns you before allowing that local certificate.

The default credentials belong only in this isolated environment. Keep them out of other accounts plus production systems.

  • Enter https://localhost in your Windows browser.
  • Expand the certificate warning's advanced options.
  • Continue to the local dashboard.
  • Enter admin as the username.
  • Enter SecretPassword as the password.
  • Submit the sign-in form.

You have the complete local Wazuh stack online. The signed-in dashboard is ready to receive the detection work that follows.

Your manager, indexer, plus dashboard are running from the pinned deployment. Next up, you will send Wazuh a denied AI-agent event to expose the gap between decoding data and detecting a threat.

Inspect the Detection Gap

Your local Wazuh stack is running. The monitoring pipeline is ready for its first AI-agent event.

A SIEM can parse structured data while still missing the behavior you care about. This step separates data decoding from security detection by testing one denied tool call.

In this step, get ready to:
  • Send a denied JSON tool-call event through wazuh-logtest.
  • Inspect the event as it moves through the processing phases.
  • Confirm whether rule ID 100101 appears in the result.
Run the denied event

The /var/ossec/bin/wazuh-logtest utility tests one log entry against Wazuh's decoders and rules. Its phased output shows where the event gains structure or security meaning.

  • Switch back to your WSL 2 terminal from earlier.
  • Launch the test utility through the running manager service by running this command:
docker compose exec wazuh.manager /var/ossec/bin/wazuh-logtest

What does this command do?

  • The docker compose exec command runs a program inside a service that is already running.
  • The wazuh.manager service contains the decoders and rules used by the lab.
  • The /var/ossec/bin/wazuh-logtest utility displays the processing phases for each submitted event.

The terminal enters the interactive test utility. It now waits for a log event.

Utility Did Not Start?

  • Confirm that wazuh.manager still shows as running in the Compose status output from the previous step.
  • Return to wazuh-docker/single-node if your terminal path changed.

Ask for help with the service connection if the utility still does not start: help me troubleshoot wazuh-logtest inside the manager service

Before you submit the event, pause to predict how much meaning Wazuh will recognize from the fields alone.

  • Paste the denied tool-call event into the waiting test utility as one complete line:
{"source":"mcp_agent","agent_id":"lab-agent-01","event_type":"tool_call","decision":"deny","tool_name":"http_get","target":"unauthorized-host","approved_target":"juice-shop","reason":"target_not_allowlisted"}

What Does This Event Represent?

  • The source field identifies the event as AI-agent telemetry.
  • The decision field records that the tool call was denied.
  • The target field names the unauthorized destination.
  • The reason field records why the policy rejected the call.
  • Submit the event by pressing Enter.
Compare decoding with detection

Each processing phase answers a different question about the event. Reading them in order reveals the exact point where interpretation stops.

  • Read Phase 1 to confirm that the raw event entered the processing pipeline.
  • Locate Phase 2 in the terminal output.
  • Confirm that Phase 2 exposes source, agent_id, event_type, decision, tool_name, target, approved_target, and reason.
  • Check the complete result for rule ID 100101.

You will see the eight telemetry fields decoded in Phase 2.

Rule ID 100101 is absent from the result. The denied call has structure without a project-specific detection match.

Don't See the Decoded Fields?

  • Compare the pasted event with the single-line JSON shown above.
  • Remove any line breaks introduced while copying the event.
  • Submit the corrected event again inside the same test session.

Ask for help if Phase 2 still does not expose the fields: help me debug this JSON event in wazuh-logtest

You found the gap this test was designed to expose. Wazuh reads the event successfully without assigning rule ID 100101.

What the Gap Proves

A decoder turns structured event data into fields that rules can inspect. The decoded fields only become security evidence after a rule combines them into a meaningful condition.

Your test has isolated the missing detection layer. Next, you'll add the rule that turns a denied tool call into a security alert.

Add the Policy-Violation Detector

Your last test proved that Wazuh can decode the denied JSON event into useful fields. The event still has no project-specific security meaning.

Now you will turn those fields into security evidence. You will build a custom rule chain that classifies AI-agent telemetry before detecting denied tool calls.

In this step, get ready to:
  • Create the custom rule file plus the supporting live-telemetry files.
  • Copy the custom rules into the running Wazuh manager.
  • Confirm that the denied event matches rule 100101 at level 10.
Create the detector files

A custom rule chain separates broad event classification from the higher-severity policy decision. The supporting files prepare the same detector for live telemetry in the next step.

  • Switch back to the WSL terminal from earlier.
  • Open the current wazuh-docker/single-node folder in a WSL-connected Visual Studio Code window by running this command:
code .

What does this command do?

The command opens your current WSL folder in Visual Studio Code. Keeping the workspace in the Linux filesystem gives the later bind mount direct access to your project files.

You should see the existing files from wazuh-docker/single-node in the file tree.

  • Use the folder creation control in the VS Code file tree to create ai-lab inside single-node.
  • Use the folder creation control beside ai-lab to create rules inside it.

You should now see ai-lab/rules in the file tree.

  • Use the folder creation control beside ai-lab to create config inside it.
  • Use the folder creation control beside ai-lab to create telemetry inside it.

Your ai-lab folder should now contain rules, config, plus telemetry.

  • Use the file creation control beside ai-lab/rules to create ai_agent_rules.xml.
  • Define the event classifier plus the deny detector by pasting this code into ai-lab/rules/ai_agent_rules.xml:
<group name="ai_agent,">
  <rule id="100100" level="0">
    <decoded_as>json</decoded_as>
    <field name="source">mcp_agent</field>
    <description>AI agent telemetry event.</description>
  </rule>

  <rule id="100101" level="10">
    <if_sid>100100</if_sid>
    <field name="event_type">tool_call</field>
    <field name="decision">deny</field>
    <description>AI agent $(agent_id) denied tool $(tool_name) for target $(target): $(reason).</description>
    <group>policy_violation,</group>
  </rule>
</group>

How does the rule chain work?

  • Rule 100100 classifies decoded JSON events whose source field contains mcp_agent.
  • Level 0 prevents the classifier from becoming a security alert by itself.
  • Rule 100101 requires the parent match through if_sid.
  • The two field conditions require tool_call plus deny in the same event.
  • The description inserts the decoded agent ID plus the tool details into the alert.
  • The policy_violation group gives the alert a searchable security category.
  • Save ai-lab/rules/ai_agent_rules.xml in Visual Studio Code.
  • Confirm that ai_agent_rules.xml remains listed inside ai-lab/rules.

Rule file showing XML problems?

Check that both rule elements have matching closing elements. Confirm that the outer group element wraps both rules.

Use this prompt to inspect the structure without changing the rule IDs: help me find the XML structure problem in my Wazuh rule file

  • Use the file creation control beside ai-lab/config to create ai-agent-localfile.xml.
  • Define the JSON collection path by pasting this code into ai-lab/config/ai-agent-localfile.xml:
<ossec_config>
  <localfile>
    <location>/var/log/ai-agent/events.jsonl</location>
    <log_format>json</log_format>
  </localfile>
</ossec_config>

What does this collection block do?

  • The location element points the manager to /var/log/ai-agent/events.jsonl.
  • The json log format sends each complete line through Wazuh's built-in JSON decoder.
  • Save ai-lab/config/ai-agent-localfile.xml in Visual Studio Code.
  • Confirm that ai-agent-localfile.xml remains listed inside ai-lab/config.

Collection path looks different?

Keep the location exactly as /var/log/ai-agent/events.jsonl. The Compose override exposes the host telemetry folder at /var/log/ai-agent.

Use this prompt to compare the paths: help me check whether my Wazuh collection path matches my Compose bind mount

The collection path needs a host folder inside the running manager. A small Docker Compose override adds that bind mount while leaving the vendor file intact.

  • Use the file creation control beside single-node to create docker-compose.ai-lab.yml.
  • Define the telemetry bind mount by pasting this code into docker-compose.ai-lab.yml:
services:
  wazuh.manager:
    volumes:
      - type: bind
        source: ./ai-lab/telemetry
        target: /var/log/ai-agent

What does this override do?

  • The override adds a bind mount to the existing wazuh.manager service.
  • The source points to ./ai-lab/telemetry on the WSL host.
  • The target exposes that folder as /var/log/ai-agent inside the manager.
  • The vendor's docker-compose.yml file remains unchanged.
  • Save docker-compose.ai-lab.yml in Visual Studio Code.
  • Confirm that docker-compose.ai-lab.yml appears beside the vendor's docker-compose.yml file.

Override file in the wrong folder?

Place docker-compose.ai-lab.yml directly inside wazuh-docker/single-node. Its relative source path starts from that folder.

Use this prompt to check the layout: help me verify the file locations for my Wazuh Compose override

  • Use the file creation control beside ai-lab/telemetry to create events.jsonl.
  • Save ai-lab/telemetry/events.jsonl without entering any content.

The editor should show an empty events.jsonl file. This preserves a clean starting point for the live feed.

Keep the feed empty

The next step starts live collection before adding events. An empty file lets you observe the allowed event plus the denied event in a controlled order.

✔️ Awesome, I've got everything!

Great. Double check that all four files are saved before copying the rule into the manager.

ⓧ I'd like to double check the full code

Your ai-lab/rules/ai_agent_rules.xml file should match this content exactly.

<group name="ai_agent,">
  <rule id="100100" level="0">
    <decoded_as>json</decoded_as>
    <field name="source">mcp_agent</field>
    <description>AI agent telemetry event.</description>
  </rule>

  <rule id="100101" level="10">
    <if_sid>100100</if_sid>
    <field name="event_type">tool_call</field>
    <field name="decision">deny</field>
    <description>AI agent $(agent_id) denied tool $(tool_name) for target $(target): $(reason).</description>
    <group>policy_violation,</group>
  </rule>
</group>

Your ai-lab/config/ai-agent-localfile.xml file should match this content exactly.

<ossec_config>
  <localfile>
    <location>/var/log/ai-agent/events.jsonl</location>
    <log_format>json</log_format>
  </localfile>
</ossec_config>

Your docker-compose.ai-lab.yml file should match this content exactly.

services:
  wazuh.manager:
    volumes:
      - type: bind
        source: ./ai-lab/telemetry
        target: /var/log/ai-agent

Your ai-lab/telemetry/events.jsonl file should contain zero characters. Leave the editor completely blank.

Copy and test the custom rule

The rule file must live under the manager's persistent rules directory before the test utility can evaluate it. Copying the file there preserves your host-side source while giving Wazuh access to the detector.

  • Switch back to the WSL terminal in wazuh-docker/single-node.
  • Copy the saved rules into the running manager by running this command:
docker compose cp ai-lab/rules/ai_agent_rules.xml wazuh.manager:/var/ossec/etc/rules/ai_agent_rules.xml

What does this command do?

The command copies your host-side rule file to /var/ossec/etc/rules/ai_agent_rules.xml inside wazuh.manager.

That destination sits inside the manager's persistent configuration volume. The copied rule remains available after the container is recreated.

The command should return to the terminal prompt without reporting a failure. This confirms that the manager received the rule file.

Rule file did not copy?

Confirm that your terminal is still inside wazuh-docker/single-node.

Check that ai-lab/rules/ai_agent_rules.xml appears in the Visual Studio Code file tree. Save the file before repeating the copy command.

Use this prompt to inspect both paths: help me troubleshoot copying my Wazuh custom rule into the manager service

  • Launch the interactive Wazuh rule test by running this command:
docker compose exec wazuh.manager /var/ossec/bin/wazuh-logtest

What happens during this test?

The command starts wazuh-logtest inside the running manager. Saved custom rules become available to this utility without a manager restart.

The utility decodes each pasted event before evaluating the rule chain. Phase 3 reports the final filtering match.

Before you paste the event, which rule do you expect Phase 3 to match?

  • Paste this denied event as one complete line at the logtest prompt:
{"source":"mcp_agent","agent_id":"lab-agent-01","event_type":"tool_call","decision":"deny","tool_name":"http_get","target":"unauthorized-host","approved_target":"juice-shop","reason":"target_not_allowlisted"}

Why does this event match?

The event first satisfies rule 100100 because the decoded source field contains mcp_agent.

Rule 100101 then checks the decoded event_type plus decision fields. Their values identify a denied tool call.

You should see Phase 3 report rule ID 100101 at level 10. The description should include the agent ID plus the tool details from the event.

That closes the detection gap. Your decoded AI-agent telemetry now produces a project-specific policy-violation match.

Phase 3 does not show rule 100101?

Confirm that you saved ai_agent_rules.xml before copying it. Repeat the copy command after correcting the host-side file.

Keep the pasted event on one line. Check that its event_type plus decision values match the rule exactly.

Use this prompt to compare the decoded fields with the detector: help me find why Wazuh rule 100101 is not matching my denied JSON event

Your custom detector now recognizes a denied AI-agent tool call in the rule sandbox. Next up, you will connect the host-side telemetry file to the manager and surface the live alert in Threat Hunting.

Stream Telemetry into Threat Hunting

Your custom rule already turns a denied tool call into a level 10 match during Wazuh log testing. The detector now needs to prove itself against a live file.

A sandbox match does not help an analyst until the manager consumes the JSON telemetry. The alert must also reach the SIEM through the live Docker Compose deployment.

In this step, get ready to:
  • Merge the telemetry collector into the live Docker Compose deployment.
  • Write one allowed event plus one denied event to the host-side feed.
  • Find the denied event in Threat Hunting with its investigation fields.
Connect the live telemetry feed

The existing bind mount exposes your host telemetry folder inside the manager container. The manager configuration now needs a collector that watches the mounted file as live JSON.

  • Stop the stock stack from the existing wazuh-docker/single-node terminal by running this command:
docker compose down

What does this command do?

This stops the three stock containers while preserving the named volumes. Your copied custom rule plus the existing Wazuh data remain available for the merged deployment.

You will see the containers stop before the terminal returns to its prompt.

  • In the Explorer sidebar of VS Code, select config/wazuh_cluster/wazuh_manager.conf.
  • Place your cursor inside the existing <ossec_config> element near the bottom of the file.
  • Add the live collector by pasting this block:
  <localfile>
    <location>/var/log/ai-agent/events.jsonl</location>
    <log_format>json</log_format>
  </localfile>

How does the collector work?

  • The location points to the mounted telemetry file inside the manager container.
  • The log_format value sends every complete line through Wazuh's built-in JSON decoder.
  • Save config/wazuh_cluster/wazuh_manager.conf.
  • Validate the merged Compose deployment by running this command:
docker compose -f docker-compose.yml -f docker-compose.ai-lab.yml config -q

What does this validation check?

Docker Compose merges docker-compose.yml first. It applies docker-compose.ai-lab.yml as the override.

The -q option validates the merged Compose configuration without printing it. A successful check returns you to the terminal prompt.

  • Start the merged deployment by running this command:
docker compose -f docker-compose.yml -f docker-compose.ai-lab.yml up -d

What changes in this deployment?

The override adds the host telemetry folder to wazuh.manager. The manager can now read every line saved to ai-lab/telemetry/events.jsonl.

The dashboard can take a few minutes to reconnect while the indexer starts. That pause is expected during a local stack restart.

  • Return to the signed-in dashboard tab from earlier.
  • Refresh https://localhost to confirm the dashboard loads again.

You will see the Wazuh dashboard after the manager plus the indexer become available. The terminal output from the start command shows all three services entering their running state.

Dashboard not returning?

  • Check the start-command output for a service that stopped during startup.
  • Confirm the collector block sits inside the existing top-level <ossec_config> element.
  • Run the merged validation command again after correcting the file.

Get help diagnosing a Wazuh container that stops after adding a localfile collector. Visit the Wazuh community for environment-specific support.

Write the two telemetry events

The feed uses one complete JSON object per line. The first event represents an approved request that should stay below the policy-violation rule.

  • In the VS Code Explorer sidebar, select the empty ai-lab/telemetry/events.jsonl file.
  • Add the allowed tool call by pasting this single line:
{"source":"mcp_agent","agent_id":"lab-agent-01","event_type":"tool_call","decision":"allow","tool_name":"http_get","target":"juice-shop","approved_target":"juice-shop","reason":"policy_pass"}

Why does this event stay quiet?

The event follows the telemetry contract expected by rule 100100. Its decision value is allow, so it does not satisfy rule 100101.

  • Save ai-lab/telemetry/events.jsonl.
  • Confirm the file shows one complete JSON object on its first line.

The allowed event is now available to the live collector. Your file provides a normal baseline before the policy violation arrives.

The second event uses the same tool-call shape. Its denied decision plus unauthorized target satisfy the custom detector.

  • Place your cursor at the end of the first event.
  • Press Enter to create the second line.
  • Append the denied tool call by pasting this single line:
{"source":"mcp_agent","agent_id":"lab-agent-01","event_type":"tool_call","decision":"deny","tool_name":"http_get","target":"unauthorized-host","approved_target":"juice-shop","reason":"target_not_allowlisted"}

Why does this event trigger the rule?

Rule 100101 requires event_type to equal tool_call. It also requires decision to equal deny.

The alert description carries the agent ID plus the tool name. It also carries the target plus the reason for investigation.

  • Save ai-lab/telemetry/events.jsonl again.
  • Confirm the file contains exactly two complete JSON lines.

Wazuh now receives the denied event through the host-side feed. The custom rule can turn that live event into an indexed alert.

Telemetry not being collected?

  • Confirm each JSON object occupies one actual line in events.jsonl.
  • Check that the second line starts with an opening brace.
  • Check that the second line ends with a closing brace.

Help me check why Wazuh is not collecting my two JSONL telemetry events.

Find the policy violation in Threat Hunting

Before you search, which of the two events do you think rule 100101 returns?

  • Return to the signed-in Wazuh Dashboard tab.
  • Select Threat Hunting.
  • Enter rule.id:100101 in the search bar.
  • Press Enter to apply the query.
  • Select the returned denied-event alert.

You will see the denied http_get call to unauthorized-host. The allowed call to juice-shop does not create rule 100101.

The alert details show data.agent_id, data.tool_name, data.target, plus data.reason. Those fields identify the actor plus the denied action for investigation.

You made the full detection path work. Your local SIEM now turns a host-side denied tool call into a searchable security alert.

No alert in Threat Hunting?

  • Refresh Threat Hunting after the manager has had time to process the saved event.
  • Confirm the query is exactly rule.id:100101.
  • Confirm the denied event still has decision set to deny.

Help me trace a missing Wazuh rule 100101 alert from events.jsonl to Threat Hunting.

✔️ Awesome, I've got everything!

Great work. Your live telemetry path now distinguishes the allowed call from the denied policy violation.

ⓧ I'd like to double check the full code

Your ai-lab/config/ai-agent-localfile.xml file should match this collector definition.

<ossec_config>
  <localfile>
    <location>/var/log/ai-agent/events.jsonl</location>
    <log_format>json</log_format>
  </localfile>
</ossec_config>

This file preserves the standalone collector configuration used when you merged the block into the manager configuration.

Your ai-lab/rules/ai_agent_rules.xml file should still contain the base JSON rule plus the policy-violation rule.

<group name="ai_agent,">
  <rule id="100100" level="0">
    <decoded_as>json</decoded_as>
    <field name="source">mcp_agent</field>
    <description>AI agent telemetry event.</description>
  </rule>

  <rule id="100101" level="10">
    <if_sid>100100</if_sid>
    <field name="event_type">tool_call</field>
    <field name="decision">deny</field>
    <description>AI agent $(agent_id) denied tool $(tool_name) for target $(target): $(reason).</description>
    <group>policy_violation,</group>
  </rule>
</group>

These rules decode project telemetry before classifying denied tool calls as level 10 policy violations.

Your ai-lab/telemetry/events.jsonl file should contain these two single-line events in this order.

{"source":"mcp_agent","agent_id":"lab-agent-01","event_type":"tool_call","decision":"allow","tool_name":"http_get","target":"juice-shop","approved_target":"juice-shop","reason":"policy_pass"}
{"source":"mcp_agent","agent_id":"lab-agent-01","event_type":"tool_call","decision":"deny","tool_name":"http_get","target":"unauthorized-host","approved_target":"juice-shop","reason":"target_not_allowlisted"}

The first line supplies the allowed baseline. The second line supplies the denied event returned by the Threat Hunting query.

Your docker-compose.ai-lab.yml override should match this bind mount.

services:
  wazuh.manager:
    volumes:
      - type: bind
        source: ./ai-lab/telemetry
        target: /var/log/ai-agent

This override maps the host telemetry directory to the path watched by the manager collector.

Secret mission

Escalate Repeated Violations

A single denied call can be accidental. Add stateful correlation that escalates three policy violations from the same agent within 60 seconds into a level-12 circuit-breaker alert.

Clean Up Your Resources

Clean Up Your Resources

This local Wazuh lab has no ongoing service charges. Its running containers consume local resources.

Decide whether to keep the lab running, pause it for later, or delete it entirely.

Resources you used:

  • Three Wazuh containers named wazuh.manager, wazuh.indexer, and wazuh.dashboard.
  • Persistent Docker volumes containing Wazuh configuration, indexed alerts, and dashboard state.
  • Self-signed certificates generated for communication between the local Wazuh components.
  • The wazuh-docker repository folder in your WSL Linux filesystem.
  • The telemetry, rules, and configuration stored under wazuh-docker/single-node/ai-lab.
  • The Compose override stored at wazuh-docker/single-node/docker-compose.ai-lab.yml.

Keep everything running

No action is needed. Choose this if you plan to keep testing detections or connect the lab to a future AI agent.

  • Leave the three Wazuh containers running.
  • Keep the wazuh-docker folder in your WSL Linux filesystem.
  • Use https://localhost when you return to the dashboard.
  • Allow enough local CPU capacity, memory, and disk space for the stack.

Pause - I'll come back to this later

Shut down the containers to free local computing resources. Your named volumes and host-side files remain available.

  • Switch back to your WSL terminal in wazuh-docker/single-node.
  • Stop the merged stack by running this command:
docker compose -f docker-compose.yml -f docker-compose.ai-lab.yml down

What does this command do?

The command stops the three containers. Your named volumes remain available for the next start.

Having Trouble Stopping the Stack?

  • Confirm your terminal is inside wazuh-docker/single-node.
  • Confirm both Compose files remain in that directory.

Help me stop the merged Wazuh stack safely.

  • Return to wazuh-docker/single-node when you are ready to resume.
  • Reuse the merged stack start command from Step 4.

Delete - I don't want to use this again

Remove the containers, volumes, certificates, and host-side project files. This clears the local detection lab from your WSL environment.

This Deletion Is Permanent

Your indexed alerts disappear when the volumes are removed. Choose this option only when you have finished using the lab.

  • Switch back to your WSL terminal in wazuh-docker/single-node.
  • Remove the Wazuh containers and their named volumes by running this command:
docker compose -f docker-compose.yml -f docker-compose.ai-lab.yml down -v

What Gets Removed?

The command removes the three containers plus their named volumes. Your indexed alerts and volume-backed configuration are permanently deleted.

Volumes Still Present?

  • Confirm your terminal is inside wazuh-docker/single-node.
  • Confirm the command includes both Compose files.

Help me remove the Wazuh lab volumes safely.

The Compose teardown leaves the cloned repository on disk. Removing that folder also deletes the generated certificates and every file under ai-lab.

  • Move above the cloned repository and delete wazuh-docker by running these commands:
cd ../..
rm -rf wazuh-docker

What Do These Commands Do?

The first line moves your terminal above the cloned repository. The second line deletes the repository plus its certificates, telemetry, rules, and Compose files.

Folder Still Present?

  • Close the VS Code workspace connected to the deleted repository.
  • Retry the folder deletion from the directory containing wazuh-docker.

Help me delete the Wazuh repository from WSL.

  • Confirm the repository is gone by running this command:
ls

What Should You See?

You should no longer see wazuh-docker in the directory listing. Your local lab has now been removed.

Nice Work!

Nice Work!

Fantastic work! Your local Wazuh SIEM now turns denied AI-agent tool calls into searchable security alerts.

You've learned how to:

  • Run a browser-accessible single-node detection lab on Wazuh 4.14.8. Preserve its state through persistent Docker volumes.
  • Stream single-line JSON telemetry from ai-lab/telemetry/events.jsonl into Wazuh. Prove that decoding structured fields is separate from identifying a policy violation.
  • Create a custom policy-violation detector with rule 100101. Inspect data.agent_id, data.tool_name, data.target, and data.reason in Threat Hunting.
  • Secret Mission: Added circuit-breaker correlation with rule 100102. Three denied calls from the same agent within 60 seconds now trigger a level 12 alert.

Ready to quiz yourself?