Wazuh is an open source security platform that collects logs from agents, decodes them into fields, matches them against rules and raises alerts. The built-in ruleset covers SSH, web servers, sudo and many other common sources, but your own applications need custom decoders and rules before Wazuh understands them. In this tutorial you will teach Wazuh to parse a custom application log, detect repeated failed logins, block the attacking IP automatically with active response, monitor a web root with File Integrity Monitoring and send high-severity alerts to Slack.

Prerequisites

To follow this guide you need:

  • A Wazuh 4.x manager, installed with the official installation assistant or packages, for example on a CubePath VPS with at least 4 GB of RAM.
  • At least one Linux agent running Ubuntu 24.04 and connected to the manager.
  • A user with sudo privileges on both machines.
  • Access to the Wazuh dashboard.

Check that the manager is running and the agent is active. On the manager:

sudo systemctl status wazuh-manager --no-pager
sudo /var/ossec/bin/agent_control -l
Wazuh agent_control. List of available agents:
   ID: 000, Name: wazuh-server (server), IP: 127.0.0.1, Active/Local
   ID: 001, Name: web01, IP: any, Active

Note the ID of your agent (001 here); you will need it later.

How Wazuh processes an event

Every log line goes through the same pipeline on the manager:

  1. Pre-decoding extracts the syslog header: timestamp, hostname and program name.
  2. Decoding runs the decoders in /var/ossec/ruleset/decoders/ and your own in /var/ossec/etc/decoders/ to extract fields such as srcip, srcuser or action.
  3. Rule matching evaluates the rules in /var/ossec/ruleset/rules/ and /var/ossec/etc/rules/. The matching rule with the highest level wins and becomes the alert.
  4. Outputs write the alert to /var/ossec/logs/alerts/alerts.json, index it for the dashboard and trigger active responses and integrations.

Never edit files under /var/ossec/ruleset/: upgrades overwrite them. Custom content goes into /var/ossec/etc/decoders/local_decoder.xml and /var/ossec/etc/rules/local_rules.xml, and custom rule IDs must be between 100000 and 120000.

The example application in this guide writes syslog-style lines like these to /var/log/myapp.log on the agent:

Sep 25 10:30:45 web01 myapp[1234]: level=warn user=alice action=login_failed ip=203.0.113.50
Sep 25 10:31:02 web01 myapp[1234]: level=info user=alice action=login_ok ip=198.51.100.7

Step 1 - Collecting the application log on the agent

The agent only reads files you tell it about. On the agent, open its configuration:

sudo nano /var/ossec/etc/ossec.conf

Add a localfile block inside <ossec_config>, next to the existing ones:

<localfile>
  <log_format>syslog</log_format>
  <location>/var/log/myapp.log</location>
</localfile>

Restart the agent and confirm it opened the file:

sudo systemctl restart wazuh-agent
sudo grep myapp.log /var/ossec/logs/ossec.log
2026/09/25 10:35:12 wazuh-logcollector: INFO: (1950): Analyzing file: '/var/log/myapp.log'.

Step 2 - Writing a custom decoder

A decoder turns a raw line into named fields. Wazuh decoders use a parent and child structure: the parent selects the log source, the children extract fields. On the manager, open the local decoder file:

sudo nano /var/ossec/etc/decoders/local_decoder.xml

Add these two decoders:

<decoder name="myapp">
  <program_name>^myapp$</program_name>
</decoder>

<decoder name="myapp-fields">
  <parent>myapp</parent>
  <regex>user=(\S+) action=(\S+) ip=(\S+)</regex>
  <order>srcuser, action, srcip</order>
</decoder>

The parent matches any line whose syslog program name is exactly myapp. The child uses Wazuh's own regex syntax (OS_Regex, which supports \S, \w, \d, + and * but not {n} quantifiers) to capture three values into the standard fields srcuser, action and srcip. Using standard field names means built-in features such as active response and GeoIP work with them automatically.

Test the decoder before restarting anything. wazuh-logtest runs the full pipeline on a line you paste:

sudo /var/ossec/bin/wazuh-logtest

Paste the first sample line and press ENTER:

**Phase 1: Completed pre-decoding.
	full event: 'Sep 25 10:30:45 web01 myapp[1234]: level=warn user=alice action=login_failed ip=203.0.113.50'
	timestamp: 'Sep 25 10:30:45'
	hostname: 'web01'
	program_name: 'myapp'

**Phase 2: Completed decoding.
	name: 'myapp'
	action: 'login_failed'
	srcip: '203.0.113.50'
	srcuser: 'alice'

Phase 3 does not appear yet because no rule matches. Press Ctrl+C to exit.

Step 3 - Writing custom rules

Rules decide what is worth an alert and at which level (0 to 15). A good pattern is a level 0 base rule that groups all events from the source, with child rules for the interesting cases. Open the local rules file on the manager:

sudo nano /var/ossec/etc/rules/local_rules.xml

Add a new group at the end of the file:

<group name="myapp,">

  <rule id="100100" level="0">
    <decoded_as>myapp</decoded_as>
    <description>MyApp: messages grouped.</description>
  </rule>

  <rule id="100101" level="5">
    <if_sid>100100</if_sid>
    <action>login_failed</action>
    <description>MyApp: failed login for user $(srcuser) from $(srcip).</description>
    <group>authentication_failed,pci_dss_10.2.4,gdpr_IV_35.7.d,</group>
  </rule>

  <rule id="100102" level="10" frequency="5" timeframe="120">
    <if_matched_sid>100101</if_matched_sid>
    <same_srcip />
    <description>MyApp: repeated failed logins from $(srcip), possible brute force.</description>
    <mitre>
      <id>T1110</id>
    </mitre>
    <group>authentication_failures,pci_dss_10.2.4,pci_dss_10.2.5,gdpr_IV_35.7.d,</group>
  </rule>

</group>

What each part does:

  • 100100 catches everything the myapp decoder produced, at level 0 so it never generates an alert on its own.
  • 100101 fires when the decoded action is login_failed.
  • 100102 is a correlation rule: it fires when 100101 has matched repeatedly for the same source IP within 120 seconds.
  • The pci_dss_* and gdpr_* group names map the alerts to compliance requirements, which is how they appear in the dashboard's PCI DSS and GDPR modules. The <mitre> block links the alert to the MITRE ATT&CK technique for brute force.

Validate the configuration and restart the manager to load the rules:

sudo /var/ossec/bin/wazuh-analysisd -t
sudo systemctl restart wazuh-manager

wazuh-analysisd -t prints nothing and exits with status 0 when the rules and decoders are valid; otherwise it reports the file and the error.

Test again with wazuh-logtest. Paste the failed login line and you will now see Phase 3:

**Phase 3: Completed filtering (rules).
	id: '100101'
	level: '5'
	description: 'MyApp: failed login for user alice from 203.0.113.50.'
	groups: '['myapp', 'authentication_failed', 'pci_dss_10.2.4', 'gdpr_IV_35.7.d']'
	firedtimes: '1'
**Alert to be generated.

wazuh-logtest keeps state within a session, so paste the same line a few more times. After enough repetitions, rule 100102 fires at level 10 instead.

Step 4 - Blocking attackers with active response

Active response runs a command on the agent when a rule fires. Wazuh ships a firewall-drop script that adds an iptables rule dropping traffic from srcip and removes it after a timeout. The manager's default ossec.conf already defines the firewall-drop command; you only need to bind it to your rule.

On the manager, open the configuration:

sudo nano /var/ossec/etc/ossec.conf

Add this block inside <ossec_config>:

<active-response>
  <command>firewall-drop</command>
  <location>local</location>
  <rules_id>100102</rules_id>
  <timeout>600</timeout>
</active-response>

location set to local runs the command on the agent that reported the event, so the web server blocks the attacker itself. timeout removes the block after 600 seconds. You can add the built-in SSH brute force rule (5763) to rules_id as a comma-separated list if you also want SSH covered.

Restart the manager:

sudo systemctl restart wazuh-manager

To test it, append six failed logins from a test address to the log on the agent:

for i in 1 2 3 4 5 6; do
  echo "$(date '+%b %d %H:%M:%S') $(hostname) myapp[1234]: level=warn user=test action=login_failed ip=203.0.113.50" | sudo tee -a /var/log/myapp.log > /dev/null
done

Check the active response log and the firewall on the agent:

sudo tail -n 3 /var/ossec/logs/active-responses.log
sudo iptables -S | grep 203.0.113.50
2026/09/25 10:42:10 active-response/bin/firewall-drop: Starting
2026/09/25 10:42:10 active-response/bin/firewall-drop: {"version":1,"origin":{"name":"node01","module":"wazuh-execd"},"command":"add",...}
-A INPUT -s 203.0.113.50/32 -j DROP
-A FORWARD -s 203.0.113.50/32 -j DROP

After 600 seconds the log shows a delete command and the iptables rule disappears.

Step 5 - Monitoring files with File Integrity Monitoring

File Integrity Monitoring (FIM, the syscheck module) records checksums, owners and permissions of files and alerts when they change. It is enabled by default for system directories such as /etc and /usr/bin, with a scan every 12 hours. For a web server, the web root deserves real-time monitoring, because a new PHP file there is a classic sign of a web shell.

On the agent, open /var/ossec/etc/ossec.conf and find the <syscheck> section. Add these lines inside it:

<directories realtime="yes" report_changes="yes">/var/www/html</directories>
<ignore>/var/www/html/cache</ignore>

realtime="yes" uses inotify to report changes within seconds instead of waiting for the next scan. report_changes="yes" stores a copy of text files so alerts include a diff of what changed; do not enable it on directories with secrets or very large files.

Restart the agent:

sudo systemctl restart wazuh-agent

Create a test file on the agent:

echo '<?php echo "test"; ?>' | sudo tee /var/www/html/fim-test.php

Within a few seconds the manager records an alert for rule 554 (file added). Check it on the manager:

sudo grep -A4 'fim-test.php' /var/ossec/logs/alerts/alerts.log | head -n 8
Rule: 554 (level 5) -> 'File added to the system.'
File '/var/www/html/fim-test.php' added
Mode: realtime

Remove the test file afterwards; the deletion appears as rule 553. In the dashboard, the File Integrity Monitoring module shows the same events per agent.

Step 6 - Checking vulnerability detection

Since Wazuh 4.8, vulnerability detection runs on the manager, uses the Wazuh Cyber Threat Intelligence feed and is enabled by default. The agents' syscollector module sends the installed package inventory, which the manager compares against known CVEs. Confirm the module is on in the manager's ossec.conf:

sudo grep -A4 '<vulnerability-detection>' /var/ossec/etc/ossec.conf
  <vulnerability-detection>
    <enabled>yes</enabled>
    <index-status>yes</index-status>
    <feed-update-interval>60m</feed-update-interval>
  </vulnerability-detection>

Results appear in the dashboard under Vulnerability Detection, grouped by agent, package and severity. The first feed download can take a while after installation, so allow some time before expecting results.

Step 7 - Sending high-severity alerts to Slack

Instead of writing a custom script, use Wazuh's built-in Slack integration. Create an incoming webhook in Slack, then add this block to the manager's ossec.conf, inside <ossec_config>:

<integration>
  <name>slack</name>
  <hook_url>https://hooks.slack.com/services/your_webhook_path</hook_url>
  <level>10</level>
  <alert_format>json</alert_format>
</integration>

Replace your_webhook_path with the path of your webhook. level sends only alerts of level 10 and above, which here includes the brute force rule 100102. You can use <rule_id> or <group> instead to filter more precisely.

Restart the manager and repeat the test from Step 4:

sudo systemctl restart wazuh-manager

The alert appears in the Slack channel. Delivery problems are logged in /var/ossec/logs/integrations.log.

Troubleshooting

wazuh-logtest shows the event only decoded by a generic decoder. The program name in the line does not match ^myapp$. Check the Phase 1 program_name value; if your application logs without a syslog header, match with <prematch> on a fixed string instead of <program_name>.

The manager does not start after editing rules. Run sudo /var/ossec/bin/wazuh-analysisd -t and fix the reported line. Typical causes are a duplicate rule ID, an if_sid pointing to a rule that does not exist, or unescaped < and & characters inside XML.

No alerts arrive from the agent. Confirm the agent is Active with agent_control -l, that the log file really grows, and check /var/ossec/logs/ossec.log on the agent for connection errors to port 1514.

Active response never runs. Look at /var/ossec/logs/active-responses.log on the agent. If it is empty, check that the alert contains srcip (the script needs it) and that the rule ID in rules_id is the one that actually fires.

Conclusion

You collected a custom application log, parsed it with your own decoders, raised correlated brute force alerts mapped to compliance requirements, blocked the attacker with active response, watched the web root in real time and sent critical alerts to Slack. The same decoder and rule pattern works for any application you run. As next steps, move agent settings such as FIM directories into centralized configuration in /var/ossec/etc/shared/default/agent.conf, tune noisy built-in rules by overriding them with overwrite="yes" in local_rules.xml, and review the dashboard's MITRE ATT&CK module to see which techniques your rules cover.