Wazuh Active Response - Automated Incident Response

The Active Response (AR) module in Wazuh provides automated incident response capabilities. When a detection rule fires, the server sends a command to the agent to execute a countermeasure - blocking an IP address, disabling a user account, or running a custom script. This mechanism reduces response time from minutes to seconds and neutralizes threats before an analyst intervenes.

How Active Response Works

Active Response operates in conjunction with the Wazuh rule engine. The incident processing sequence is as follows:

  1. The agent collects log data from the endpoint
  2. Data is forwarded to the Wazuh server for analysis
  3. The rule engine matches the event against the ruleset
  4. When a rule with a configured Active Response triggers, the server creates a command
  5. The command is sent to the agent (or executed locally on the server)
  6. The agent runs the corresponding response script
  7. Execution results are logged for auditing

Responses can be bound to specific rules by their identifier (rules_id), rule group (rules_group), or minimum severity level (level).

Built-in Response Scripts

Wazuh ships with a set of ready-made scripts located in /var/ossec/active-response/bin/ on agents.

firewall-drop

Blocks the attacker’s IP address at the network level. On Linux, it uses iptables; on Windows, it leverages Windows Firewall. This script is commonly used for automated blocking of brute-force attack sources, port scanners, and other network-based threats.

<command>
  <name>firewall-drop</name>
  <executable>firewall-drop</executable>
  <timeout_allowed>yes</timeout_allowed>
</command>

<active-response>
  <command>firewall-drop</command>
  <location>local</location>
  <rules_id>5710,5711</rules_id>
  <timeout>600</timeout>
</active-response>

In this example, when rules 5710 or 5711 trigger (multiple failed SSH authentication attempts), the attacker’s IP address is blocked for 600 seconds.

host-deny

Adds the IP address to /etc/hosts.deny (TCP Wrappers). This operates on systems that support the TCP Wrappers mechanism - primarily Linux and BSD.

<command>
  <name>host-deny</name>
  <executable>host-deny</executable>
  <timeout_allowed>yes</timeout_allowed>
</command>

<active-response>
  <command>host-deny</command>
  <location>local</location>
  <level>8</level>
  <timeout>3600</timeout>
</active-response>

disable-account

Deactivates a user account. On Linux, it executes passwd -l; on Windows, it disables the account via net user. This script is applied when compromised user credentials are detected.

<command>
  <name>disable-account</name>
  <executable>disable-account</executable>
  <timeout_allowed>yes</timeout_allowed>
</command>

<active-response>
  <command>disable-account</command>
  <location>local</location>
  <rules_group>authentication_failures</rules_group>
  <timeout>900</timeout>
</active-response>

restart-wazuh

Restarts the Wazuh agent service. Used for applying updated configurations or recovering agent functionality after a failure.

<command>
  <name>restart-wazuh</name>
  <executable>restart-wazuh</executable>
  <timeout_allowed>no</timeout_allowed>
</command>

Stateful and Stateless Responses

Wazuh supports two Active Response operating modes.

Stateful (with rollback)

Stateful responses automatically revert the executed action after a specified timeout period. For example, firewall-drop with a 600-second timeout blocks an IP address and automatically unblocks it after 10 minutes. This mode is suitable for temporary threat containment and prevents permanent blocking of legitimate users due to false positives.

To enable stateful mode, specify <timeout_allowed>yes</timeout_allowed> in the <command> block and set the <timeout> value in the <active-response> block.

Stateless (no rollback)

Stateless responses execute an action once without automatic reversal. These are used for irreversible operations: sending notifications, creating system snapshots, or writing entries to an audit log.

For stateless mode, set <timeout_allowed> to no and omit <timeout> from the <active-response> block.

Active Response Configuration

Configuration consists of two XML blocks in ossec.conf on the Wazuh server.

The command Block

Defines the executable script and its parameters:

<command>
  <name>my-custom-response</name>
  <executable>my-script.sh</executable>
  <timeout_allowed>yes</timeout_allowed>
</command>
ParameterDescription
nameUnique command name referenced from the active-response block
executableScript name in the /var/ossec/active-response/bin/ directory
timeout_allowedEnables stateful mode with automatic rollback (yes/no)

The active-response Block

Defines trigger conditions and execution parameters:

<active-response>
  <command>my-custom-response</command>
  <location>local</location>
  <rules_id>100100,100101</rules_id>
  <timeout>300</timeout>
</active-response>
ParameterDescription
commandCommand name from the <command> block
locationExecution target: local (on the agent), server, defined-agent, all
rules_idComma-separated list of rule IDs
rules_groupRule group that triggers the response
levelMinimum rule severity level
timeoutSeconds before automatic rollback (stateful)
repeated_offendersEscalating timeouts for repeat offenders

The location Parameter

  • local - script runs on the agent that generated the alert
  • server - script runs on the Wazuh server
  • defined-agent - script runs on a specified agent (requires <agent_id>)
  • all - script runs on all agents

Repeated Offenders

The repeated_offenders parameter enables escalating block durations for recurring incidents from the same source:

<active-response>
  <command>firewall-drop</command>
  <location>local</location>
  <rules_id>5710</rules_id>
  <timeout>600</timeout>
  <repeated_offenders>1800,3600,86400</repeated_offenders>
</active-response>

In this example: first block is 10 minutes, second is 30 minutes, third is 1 hour, fourth and subsequent blocks last 24 hours.

Custom Response Scripts

Wazuh allows creating custom Active Response scripts in any programming language. The script receives alert data via standard input (STDIN) in JSON format.

Input Data Format

The script receives a JSON object with the following fields:

{
  "version": 1,
  "origin": {
    "name": "my-custom-response",
    "module": "active-response"
  },
  "command": "add",
  "parameters": {
    "alert": {
      "rule": {
        "id": "5710",
        "level": 10,
        "description": "Multiple SSH authentication failures"
      },
      "data": {
        "srcip": "192.168.1.100"
      },
      "agent": {
        "id": "001",
        "name": "web-server"
      }
    }
  }
}

The command field accepts either add (execute the action) or delete (revert the action for stateful scripts).

Bash Script Example

#!/bin/bash

LOCAL=$(dirname "$0")
LOG_FILE="/var/ossec/logs/active-responses.log"

read -r INPUT_JSON

COMMAND=$(echo "$INPUT_JSON" | jq -r '.command')
SRCIP=$(echo "$INPUT_JSON" | jq -r '.parameters.alert.data.srcip // empty')

if [ -z "$SRCIP" ]; then
    echo "$(date) - No source IP found" >> "$LOG_FILE"
    exit 1
fi

if [ "$COMMAND" = "add" ]; then
    echo "$(date) - Blocking IP: $SRCIP" >> "$LOG_FILE"
    iptables -A INPUT -s "$SRCIP" -j DROP
elif [ "$COMMAND" = "delete" ]; then
    echo "$(date) - Unblocking IP: $SRCIP" >> "$LOG_FILE"
    iptables -D INPUT -s "$SRCIP" -j DROP
fi

exit 0

Python Script Example

#!/usr/bin/env python3

import sys
import json
import datetime

LOG_FILE = "/var/ossec/logs/active-responses.log"

def log_message(message):
    timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    with open(LOG_FILE, "a") as f:
        f.write(f"{timestamp} - {message}\n")

def main():
    input_data = sys.stdin.readline().strip()
    if not input_data:
        log_message("No input received")
        sys.exit(1)

    try:
        alert = json.loads(input_data)
    except json.JSONDecodeError:
        log_message("Invalid JSON input")
        sys.exit(1)

    command = alert.get("command", "")
    srcip = alert.get("parameters", {}).get("alert", {}).get("data", {}).get("srcip", "")

    if not srcip:
        log_message("No source IP in alert")
        sys.exit(1)

    if command == "add":
        log_message(f"Custom action triggered for IP: {srcip}")
        # Custom remediation logic here
    elif command == "delete":
        log_message(f"Custom action reverted for IP: {srcip}")
        # Revert logic here

    sys.exit(0)

if __name__ == "__main__":
    main()

Deploying a Custom Script

  1. Place the script in /var/ossec/active-response/bin/ on the agent
  2. Set permissions: chmod 750 /var/ossec/active-response/bin/my-script.sh
  3. Set ownership to root:wazuh
  4. Add <command> and <active-response> blocks to ossec.conf on the server
  5. Restart the Wazuh manager to apply changes

Rule-triggered Responses

Binding to Specific Rules

<active-response>
  <command>firewall-drop</command>
  <location>local</location>
  <rules_id>31104,31105,31106</rules_id>
  <timeout>600</timeout>
</active-response>

Binding to a Rule Group

<active-response>
  <command>host-deny</command>
  <location>local</location>
  <rules_group>web_attacks</rules_group>
  <timeout>1800</timeout>
</active-response>

Binding to Severity Level

<active-response>
  <command>disable-account</command>
  <location>local</location>
  <level>12</level>
  <timeout>0</timeout>
</active-response>

With <timeout>0</timeout>, the action executes without automatic rollback - the account remains locked until manually re-enabled.

Testing Active Response

Validating Configuration

Before deployment, verify the configuration syntax:

docker exec wazuh-manager /var/ossec/bin/wazuh-control reload

Manual Script Execution

For testing, run the response script manually on the agent:

echo '{"version":1,"origin":{"name":"test","module":"active-response"},"command":"add","parameters":{"alert":{"data":{"srcip":"10.0.0.99"}}}}' | /var/ossec/active-response/bin/firewall-drop

Checking the Active Response Log

The response script execution log is located on the agent:

tail -f /var/ossec/logs/active-responses.log

Using wazuh-logtest

Use wazuh-logtest to verify that rules with associated responses trigger correctly:

docker exec -it wazuh-manager /var/ossec/bin/wazuh-logtest

Comparison with Other Platforms

CapabilityWazuh Active ResponseSplunk SOARELK Watcher
Automated responseBuilt-in, agent-levelVia playbooks and integrationsVia actions (email, webhook)
Endpoint executionYes, agent runs the scriptThrough connectors and appsNo, server-side actions only
Stateful with rollbackYes, built-in timeoutThrough playbook logicNo
Custom scriptsBash, Python, any languagePython playbooksNo, only preset actions
CostFree (open source)Commercial licenseBasic free, advanced paid
Configuration complexityLow (XML configuration)High (visual editor)Medium (JSON/YAML)

More about detection rules: Wazuh Architecture

Troubleshooting

Script Does Not Execute

  • Check script permissions: ls -la /var/ossec/active-response/bin/
  • Confirm the script has 750 permissions and root:wazuh ownership
  • Review /var/ossec/logs/ossec.log for errors
  • Verify the name in <executable> matches the script filename

Block Is Not Automatically Removed

  • Confirm <timeout_allowed>yes</timeout_allowed> is set in the <command> block
  • Verify a <timeout> value is defined in the <active-response> block
  • Check that the script properly handles the delete command

False Positives

  • Refine trigger conditions: use <rules_id> instead of <level>
  • Add exceptions through whitelists in Wazuh rules
  • Raise the trigger threshold in the detection rule
  • Start with a longer timeout and reduce it as you fine-tune

Response Executes but Is Not Logged

  • Check the existence and permissions of /var/ossec/logs/active-responses.log
  • Verify the script writes execution results to the log

More about agent installation: Installing the Wazuh Agent

More about Wazuh components: Platform Components

Last updated on