Best Practices

    Writing Custom Wazuh Rules: A Hands-On Guide with Examples

    Learn the Wazuh rule XML schema, decoders, if_sid, regex, frequency, timeframe, and write production-grade custom detection rules with worked examples.

    WazuGuardix Team June 16, 2026 11 min read

    Wazuh ships excellent built-in detections, but your environment is unique, your applications, your business logic, your insider-threat model. That is where custom rules earn their keep. This guide walks the full path: log line --> decoder --> rule --> test.

    Anatomy of a Wazuh rule

    <rule id="100100" level="10">
      <if_sid>5710</if_sid>
      <match>Failed password</match>
      <description>SSH brute force candidate</description>
      <group>authentication_failed,</group>
    </rule>
    • id, must be 100000+ for custom rules.
    • level, 0 (silent) to 15 (max severity).
    • if_sid, chain off a parent rule (massively reduces CPU).
    • match / regex, text the rule looks for inside the decoded log.
    • group, tags used for filtering + correlation.

    Decoders come first

    A rule only fires if a decoder has extracted fields from the raw log. For custom apps, write a custom decoder in /var/ossec/etc/decoders/local_decoder.xml first:

    <decoder name="myapp">
      <prematch>^MYAPP </prematch>
    </decoder>
    <decoder name="myapp-fields">
      <parent>myapp</parent>
      <regex>user=(\S+) action=(\S+) status=(\S+)</regex>
      <order>user,action,status</order>
    </decoder>

    Frequency + timeframe = correlation

    For brute-force-style detections combine frequency and timeframe with same_source_ip:

    <rule id="100110" level="12" frequency="6" timeframe="120">
      <if_matched_sid>100100</if_matched_sid>
      <same_source_ip/>
      <description>SSH brute force: 6 failed logins in 2 minutes</description>
    </rule>

    5 worked examples

    a. Detect new sudo binary

    <rule id="100200" level="12">
      <if_sid>550</if_sid>
      <field name="file">/usr/bin/sudo</field>
      <description>sudo binary changed on disk</description>
    </rule>

    b. Detect cleartext password in app log

    <rule id="100210" level="13">
      <decoded_as>myapp</decoded_as>
      <regex>password=\w+</regex>
      <description>Application logged a cleartext password</description>
    </rule>

    c. Off-hours admin login

    <rule id="100220" level="10">
      <if_sid>5715</if_sid>
      <time>10:00 pm - 6:00 am</time>
      <description>Admin SSH login outside business hours</description>
    </rule>

    d. New process from /tmp

    <rule id="100230" level="12">
      <if_sid>92301</if_sid>
      <field name="image">^/tmp/</field>
      <description>Process executed from /tmp -- possible dropper</description>
    </rule>

    e. Multiple distinct accounts failing on one host

    <rule id="100240" level="13" frequency="5" timeframe="180">
      <if_matched_sid>5710</if_matched_sid>
      <different_user/>
      <description>Password spraying: 5 different users failing on same host</description>
    </rule>

    The right workflow

    1. Capture a raw log sample.
    2. Run wazuh-logtest, confirm a decoder matches.
    3. Write the rule in local_rules.xml.
    4. Run wazuh-logtest again, confirm rule fires at correct level.
    5. configtest, then restart wazuh-manager.
    6. Watch /var/ossec/logs/alerts/alerts.json to confirm production traffic triggers as expected.

    Official reference

    Commands and configuration in this guide are based on the official Wazuh documentation. For the canonical source, see:

    Custom RulesDetection EngineeringDecodersBest Practices

    Need help running Wazuh in production?

    Our team designs, deploys and operates Wazuh end to end, from indexer sizing to custom rules and 24/7 SOC monitoring.