# Wazuh Custom Rules and Decoders Custom Wazuh detection content for the wittenberger.us homelab. This folder contains the working `local_rules.xml` and `local_decoder.xml` that run on the production Wazuh manager, along with documentation for each. ## Files in this folder | File | Purpose | |------|---------| | `local_rules.xml` | All custom correlation and suppression rules | | `local_decoder.xml` | Custom decoders for services shipped decoders don't handle | | `README.md` | This file. Overview and deployment procedure | | `DECODERS.md` | Detailed reference for every custom decoder | | `RULES.md` | Detailed reference for every custom rule, grouped by service | ## Rule ID range allocation Rules are numbered in service-specific ranges to prevent collisions when adding new content: | Range | Service | Notes | |-------|---------|-------| | 100000-100099 | SSH | Currently only one example rule | | 100100-100199 | Attack / reputation lookups | AlienVault IP blacklist correlation | | 100200-100299 | Dovecot | Mailcow noise suppression and recon detection | | 100300-100399 | Postfix / mail correlation | Brute force, compromise, credential stuffing | | 100400-100499 | Gitea | Auth, account lifecycle, SSH keys, tokens | | 100500-100599 | pfSense | Currently unused (shipped rules cover baseline) | Rules 87699, 87761, and 87762 are also custom despite being below 100000. These override shipped IDs to enable pfSense wrapped-log correlation. ## Deployment procedure Deploy any change from this folder to the running manager: ```bash sudo cp local_decoder.xml /var/ossec/etc/decoders/ sudo cp local_rules.xml /var/ossec/etc/rules/ sudo /var/ossec/bin/wazuh-analysisd -t # Verify no errors before restarting sudo systemctl restart wazuh-manager sudo tail -20 /var/ossec/logs/ossec.log # Verify clean startup ``` If validation fails, do not restart. Fix the XML in this folder, re-copy, re-validate. ## Testing procedure Before deploying, test any new rule against real logs using `wazuh-logtest`: ```bash sudo /var/ossec/bin/wazuh-logtest ``` Paste a real log line at the prompt. Verify: - **Phase 1** extracts `program_name` correctly - **Phase 2** shows the expected decoder and all captured fields - **Phase 3** shows the intended rule firing at the expected level Test with both malicious and benign log samples. A rule that fires on the malicious case but ALSO fires on legitimate traffic is a false positive generator and must be tuned before deploy. ## Custom decoders overview Two decoders exist because mailcow's Docker log wrapping breaks shipped decoders: - **`mailcow-postfix-sasl-fail`** — extracts srcip, src_subnet, and dstuser from wrapped Postfix SASL failures - **`mailcow-dovecot-success`** — extracts srcip from any Dovecot event containing `rip=IP,` Additional decoders handle other homelab services: - **`gitea`** and **`gitea-auth-fail`** — parse Gitea log format for auth events - **`pfsense-wrapped`** and children — parse pfSense filter logs See `DECODERS.md` for exact patterns and field extraction details. ## Sync workflow Working state on the manager should always match this repo. Recommended workflow: **Edit in repo first, deploy from repo:** ```bash # Edit the rules/decoders in this folder nano local_rules.xml # Deploy to manager sudo cp local_rules.xml /var/ossec/etc/rules/ sudo /var/ossec/bin/wazuh-analysisd -t sudo systemctl restart wazuh-manager # Commit git add local_rules.xml git commit -m "Add rule 100XXX: description" git push ``` **If you edited on the manager first, copy back:** ```bash sudo cp /var/ossec/etc/rules/local_rules.xml ./ sudo cp /var/ossec/etc/decoders/local_decoder.xml ./ git add local_rules.xml local_decoder.xml git commit -m "Sync working manager state" git push ``` Never edit both places simultaneously; you'll lose changes. ## Known suppression rules Several rules exist purely to reduce alert noise from expected internal traffic: | Rule ID | Suppresses | |---------|-----------| | 100200 | mailcow watchdog auth attempts against `watchdog@invalid` | | 100201 | Dovecot disconnects from Docker-internal range 172.22.1.x | | 100202 | Chris's own mailbox routine disconnects | | 100203 | mailcow watchdog managesieve health checks | | 100204 | Successful Dovecot logins (needed for correlation, level 0 hides from alerts) | | 100205 | Single Dovecot connections with no auth attempt (port scan primitives) | Rule 100204 is special. It intentionally suppresses ALL successful Dovecot logins from alerting because rule 100311 (compromise correlation) needs 100204 to fire in order to correlate. Without the suppression, every login would alert; with it, only the correlated compromise fires. ## Adversary intel Real credential-stuffing campaign observed against wittenberger.us mailcow: - **Pattern:** Distributed brute force against SMTP submission (587) - **Timing:** Bursts every ~3.5 hours, each lasting 6-10 seconds - **Source IPs:** Rotate every burst, no persistent single-source pattern - **Target usernames:** Common admin defaults (admin, info, contact, postmaster, demo) - **Detection rules that fire:** 100315 (credential stuffing), 100316 (volume) - **Rules that don't fire:** 100310, 100313 (single-source rules) - because IPs rotate Rules 100312-100316 were built specifically to catch this pattern after 100310 alone proved insufficient. ## Lessons applicable to CyberForce 2026 Building and tuning this ruleset over July 2026 produced several transferable lessons for the JJC Cyberwolves training curriculum. See `Cyberforce_2026/Cyberforce/Wazuh/` for training materials that reference this configuration as a case study. Key takeaways: 1. **Shipped decoders often fail on containerized services.** mailcow's double-wrapped log format broke both shipped Postfix and Dovecot decoders. Custom decoders were necessary. 2. **`same_srcip` alone is not enough.** Modern brute force uses rotating IPs. Correlate on `same_user`, subnet, and volume too. 3. **Always test with real logs.** Injected logs miss subtle formatting differences that break decoders. 4. **XML entities matter.** `<` must be `<`, `>` must be `>`, both with trailing semicolons. 5. **PCRE2 vs OSSEC regex is not interchangeable.** OSSEC lacks `.*`, `?`, `{n,m}`, and negation classes. Use PCRE2 explicitly for complex patterns. Last updated: 2026-07-31