wazuh-logtest is the Wazuh server tool that shows how one log line would be processed before any real alert depends on it. Run /var/ossec/bin/wazuh-logtest on the server, paste a sample line, and it prints which decoder matched, which fields it pulled out, and which rule fired and at what level. It is the fastest way to prove a detection works, or to find out why it does not.
Think of it as a flight simulator for your detections. Instead of waiting for a real attack to discover that a rule was wrong, you fly one exact log line through the same engine the server uses and watch what happens at each step. When a detection you expected never shows up, this is the tool that tells you whether the problem is the decoder, the rule, or your own test.
This guide assumes Wazuh is already running. If it is not, start with our walkthrough on how to use Wazuh, then come back to make your detections trustworthy. We run Wazuh as a managed service, so the workflow below is the one our detection engineers reach for every time a rule behaves in a way they did not expect.
What the three phases are telling you
Every line you paste goes through three phases, and reading them in order is the whole skill. Phase 1, pre-decoding, pulls the outer envelope that standard log formats share: when the event happened, which host it came from, and which program wrote the line. Phase 2, decoding, runs the matching decoder to extract the fields inside the message, such as the username and the source IP. Phase 3, rule matching, takes those fields and checks them against the ruleset to decide whether anything is raised, and how serious it is.
The reason this matters: a rule can only test fields that a decoder actually produced. If Phase 2 comes back empty, no amount of rule tuning in Phase 3 will help, because the field the rule is looking for does not exist yet. Most "my rule will not fire" problems are really Phase 2 problems wearing a Phase 3 costume, and wazuh-logtest is what lets you see the difference in seconds.
Running wazuh-logtest, and the one flag that matters
Start a session on the server and paste a line at the prompt. The tool keeps the session open so you can paste more lines, which matters for rules that count repeats. Press Ctrl+C to exit.
$ sudo /var/ossec/bin/wazuh-logtest Starting wazuh-logtest v4.x Type one log per line >> Oct 15 10:22:14 web01 myapp: Failed login for user 'admin' from 203.0.113.50
The session holds state. A counter called firedtimes tracks how often each rule has matched since you started, which is how you exercise a rule that only escalates after several hits. Exit the session and that count resets to zero.
There are a handful of flags, but one earns its keep above the rest. The Wazuh documentation calls -v, verbose mode, the key option for troubleshooting a rule or decoder problem, and that is exactly right: it prints the decoders and rules that were considered for the line, not just the final match, so you can see where the chain broke.
| Flag | What it does | When you reach for it |
|---|---|---|
| -v | Prints the verbose result, including the decoders and rules that were evaluated for the line. | Almost always. This is the one flag that answers why a rule did or did not fire. |
| -d | Debug output from the analysis engine itself. | Deep decoder problems, when even verbose does not explain the parse. |
| -q | Quiet: suppresses the banner and prompts. | Scripting, or feeding lines in from a pipe. |
| -U rule:level:decoder | Returns a zero exit status only if the line produces that exact rule id, level, and decoder. | Turning a known-good line into a pass or fail check you can automate. |
Worked example: a log that will not decode
Here is the situation that sends most people to this tool. You have a custom application, call it myapp, that logs failed logins, and you want an alert when one source hammers it. You wrote a rule, but nothing fires. Paste a real line and watch what the tool says.
**Phase 1: Completed pre-decoding. full event: 'Oct 15 10:22:14 web01 myapp: Failed login for user 'admin' from 203.0.113.50' timestamp: 'Oct 15 10:22:14' hostname: 'web01' program_name: 'myapp' **Phase 2: Completed decoding. No decoder matched. **Phase 3: Completed filtering (rules). No rule matched.
Phase 1 worked: it read the timestamp, the host web01, and the program myapp. But Phase 2 says no decoder matched, so the username and the IP were never extracted. That is the root cause. Your rule keys on a srcip field that does not exist, so it can never match. The tool found the problem in one paste.
The fix is a decoder. A parent decoder claims the lines from this program, and a child decoder uses a regular expression to lift the fields you care about into named values. Custom decoders live in local_decoder.xml.
<decoder name="myapp"> <program_name>^myapp</program_name> </decoder> <decoder name="myapp-login"> <parent>myapp</parent> <regex type="pcre2">Failed login for user '(\w+)' from ([\d.]+)</regex> <order>user, srcip</order> </decoder>
The parent matches on the program name. The child runs its pattern and maps the two capture groups, in order, to the field names user and srcip. Those names are the contract between the decoder and the rule, so they have to match on both sides. Now add the rules that use them. Custom rules live in local_rules.xml and use IDs of 100000 or higher so they never collide with the built-in set.
<group name="local,myapp,"> <rule id="100200" level="5"> <decoded_as>myapp</decoded_as> <description>myapp: failed login for $(user) from $(srcip)</description> </rule> <rule id="100201" level="10" frequency="6" timeframe="120"> <if_matched_sid>100200</if_matched_sid> <same_source_ip /> <description>myapp: repeated failed logins from one source IP</description> </rule> </group>
The first rule simply records a decoded failed login at a quiet level 5. The second watches for the pattern: it escalates to level 10 when the first rule matches six times inside two minutes from the same source IP. The same_source_ip tag scopes the count to one attacker, so six unrelated failures from six different hosts do not trip it, but one machine hammering the box does.
Edits to these files are not live until the server reloads them, so restart the manager with sudo systemctl restart wazuh-manager, then start a fresh wazuh-logtest session and paste the same line again.
**Phase 2: Completed decoding. name: 'myapp' user: 'admin' srcip: '203.0.113.50' **Phase 3: Completed filtering (rules). Rule id: '100200' Level: '5' Description: 'myapp: failed login for admin from 203.0.113.50'
Phase 2 now shows the myapp decoder with user and srcip filled in, and Phase 3 reports rule 100200 at level 5. Now paste the same line again, several times, in that session and inside the two-minute window. Watch the firedtimes counter climb with each paste; once it crosses the rule's threshold, the escalation fires instead.
**Phase 3: Completed filtering (rules). Rule id: '100201' Level: '10' Description: 'myapp: repeated failed logins from one source IP' firedtimes: 6
That is the whole loop: decode, match, tune, re-test. Run the session again with -v and you will also see the rule chain the engine walked to get there, which is how you confirm your rule, and not some broader built-in, is the one doing the work.
Why your rule did not fire: a quick checklist
Almost every "it will not fire" case is one of a short list, and each has a tell in the output. Read Phase 2 first to confirm the fields exist, then read Phase 3 for the rule decision. The table maps the symptom you see to the cause and the fix.
| What you see in the tool | Likely cause | The fix |
|---|---|---|
| Phase 2 shows no decoder, or your fields are missing | No decoder matched the line, so the field your rule keys on was never created. | Write or correct a decoder; confirm the field names in your decoder order match what the rule checks. |
| Decoder matches but the rule still does not fire | The rule names a field the decoder did not extract, or a value that does not match. | Compare the rule field against the exact field shown in Phase 2; names and values must line up. |
| A different rule fires than the one you wrote | A parent or more general rule matched first, or your rule is missing its if_sid link. | Read the verbose output for the rule chain; add the right if_sid or if_matched_sid. |
| The rule fires but never raises an alert | The rule is level 0, which Wazuh treats as ignored by design. | Give it a level of 1 or higher, or keep it at 0 on purpose to silence known noise. |
| A frequency rule never escalates | You did not paste enough lines inside the session, or the timeframe elapsed. | Paste the line enough times in one session and inside the timeframe; watch the firedtimes counter climb. |
| You edited a file and nothing changed | The running ruleset is still the old one. | Restart wazuh-manager, then start a fresh wazuh-logtest session so it loads your change. |
The habit worth building: before you blame the rule, confirm the decoder. If the field is not in Phase 2, the rule was never going to work, and you have just saved yourself an hour of editing the wrong file.
Where wazuh-logtest stops
The tool tests logic, not plumbing, and it is honest to know the difference. It proves that a given line, if it reaches the server, decodes and matches the way you expect. It does not prove that your agent is actually shipping that log, that the real-world format matches your sample byte for byte, or that an active-response script or integration downstream will behave. Treat it as a bench test for detections, then confirm the end to end path with a real event in the dashboard.
Two more edges catch people. First, the frequency counter lives in the session, so a correlation rule that depends on timing behaves differently at the prompt than under real traffic; the tool shows you the logic is sound, not that the timing will line up in production. Second, it reads the ruleset the manager has loaded, so an edit you have not restarted into the manager simply will not appear. The dashboard offers the same test under its developer tools, but the command line with -v stays the reliable path when you are actually debugging.
Used this way, wazuh-logtest turns detection engineering from guesswork into something you can check. Write the decoder, test it, write the rule, test it, and only then trust it in the live pipeline. If you would rather have that tuning done for you, our team runs it as part of managed Wazuh, and the same instinct carries into the Wazuh API and into why we built on Wazuh in the first place.