
Three detections from my home SOC lab, each written by hand for Wazuh, tested against a real attack, and checked for false positives. For every one I followed the same loop: trigger the attack → find the raw event → check what Wazuh already does → write the rule → trigger again → try something harmless that looks similar. The lab itself (Proxmox, OPNsense, Wazuh, Sysmon) is described in My InfoSec HomeLab.
All rules, as Wazuh XML and as portable Sigma, are in detection-lab on GitHub.
| # | Detects | ATT&CK | Data source | Wazuh rule |
|---|---|---|---|---|
| 1 | PowerShell with an encoded command | T1059.001 | Sysmon Event 1 | 100101, level 12 |
| 2 | Scheduled task created with schtasks |
T1053.005 | Sysmon Event 1 | 100103, level 10 |
| 3 | SSH brute force | T1110.001 | Linux auth log | 100105, level 12 |
1. PowerShell started with an encoded command (T1059.001)
What it is: PowerShell accepts a whole command as Base64 after -EncodedCommand. Attackers use it so the real command doesn't appear in plain text. PowerShell also accepts any unambiguous shortening of the flag, and -enc is the one you see most in the wild.
Test: a harmless command, encoded by hand:
$t = [Text.Encoding]::Unicode.GetBytes('Write-Host sprint30')
$b = [Convert]::ToBase64String($t)
powershell.exe -enc $b
What Wazuh does by default: surprisingly little. The built-in rule 92057 (base64 encoded command, level 12) stayed silent. Its regex lists -e, -ec, -en, -enco and others, but not -enc, and it also requires the parent to be PowerShell. The event only hit 92027 at level 4, which nobody looks at.
Rule:
<rule id="100101" level="12">
<!-- 61603 = every Sysmon Event 1; 92027/92057 = built-ins that can win the match first -->
<if_sid>61603, 92027, 92057</if_sid>
<field name="win.eventdata.image" type="pcre2">(?i)\\powershell\.exe$</field>
<field name="win.eventdata.commandLine" type="pcre2">(?i)\s-e(c|nc|ncodedcommand)?\s</field>
<description>Sprint30 D1: PowerShell encoded command</description>
<mitre><id>T1059.001</id></mitre>
</rule>
The lesson behind the three parents: in Wazuh a child rule is only checked under a parent that matched. If a built-in rule on the same event wins first, a rule hanging only under the base rule 61603 never gets a chance.
Validation: -enc from PowerShell ✅ fires · -enc from cmd.exe ✅ fires (92057 stays silent) · -Command "Get-Date" ✅ no alert.
What it misses: PowerShell accepts every shortening (-en, -encod, -encodedcomman …), a slash instead of a dash, and Unicode dash look-alikes. A wider regex for that is in the repo notes. Also out of scope: pwsh.exe and a renamed powershell.exe.
2. Scheduled task created via schtasks (T1053.005)
What it is: a scheduled task lets an attacker run something again at every logon or boot. With /ru system it runs as SYSTEM. schtasks.exe is built into Windows, so nothing has to be downloaded.
Test: Atomic Red Team, Scheduled Task Startup Script. It creates two tasks that start calc.exe, one at logon and one at boot as SYSTEM.
Rule:
<rule id="100103" level="10">
<if_sid>61603</if_sid>
<field name="win.eventdata.image" type="pcre2">(?i)\\schtasks\.exe$</field>
<field name="win.eventdata.commandLine" type="pcre2">(?i)\s/create\s</field>
<description>Sprint30 D3: scheduled task created via schtasks</description>
<mitre><id>T1053.005</id></mitre>
</rule>
Validation: the Atomic test ✅ 2 alerts (one per task) · schtasks /query ✅ no alert.
The interesting part, the gap: PowerShell's Register-ScheduledTask talks to the Task Scheduler API directly, so schtasks.exe never starts and my rule sees nothing. I tested it: 100103 stayed silent. What did fire was Windows Security event 4698 (a scheduled task was created, Wazuh rule 60228), which contains the full task XML, but only at level 4.
So 100103 detects the tool, while 4698 detects the result, however the task was made. The better design is both: a raised-level rule on 4698 for suspicious task content (cmd/powershell actions, SYSTEM, paths in %TEMP%), plus 100103 for the command-line context. That's my next iteration.
3. SSH brute force (T1110.001)
What it is: password guessing against SSH. One wrong password is a typo; eight in a minute from the same IP is a tool.
Test: Hydra from Kali against a throwaway lab account, with the top 200 passwords of rockyou:
hydra -l labtest -P top200.txt -t 4 ssh://10.10.10.30
What Wazuh does by default: a lot. Rule 5760 fires for every failed password, and 5763 brute force, 2502, 5551 and 40111 (all level 10) fired within seconds. The most valuable one: 40112, multiple authentication failures followed by a success (level 12).
Rule: counts Wazuh's own failure alerts per source IP.
<rule id="100105" level="12" frequency="8" timeframe="60">
<!-- 5760 = sshd: authentication failed -->
<if_matched_sid>5760</if_matched_sid>
<same_srcip />
<description>Sprint30 D5: SSH brute force, 8+ fails in 60s from one IP</description>
<mitre><id>T1110.001</id></mitre>
</rule>
Validation: Hydra ✅ fires within seconds (46 alerts in one run) · one typo followed by the correct password ✅ no alert.
Honest notes:
- This rule doesn't fill a hole: the built-ins already caught it. What it adds is a clear name, an ATT&CK ID and a threshold I control.
- Sigma can't express it. A Sigma rule describes one event, and one failed password isn't an attack. The Sigma file in the repo matches the single
Failed passwordline; the counting lives in Wazuh. - The real gap is after the attack. When I logged in with the cracked password four minutes later, Wazuh only produced level-3 login success events. Nothing linked them to the brute force. Next rule: successful login from an IP that just triggered 100105.
- In my lab every attack comes from 10.10.10.2, because Proxmox NATs Kali into the lab network. In a real network you'd see the attacker, or their VPN.
The full investigation of this attack (timeline, IOCs, NIST 800-61 recommendations) is in its own post.
What surprised me
- Most raw events are invisible. Sysmon process creations land on a level-0 base rule. Unless you write a child rule, they never show up in the dashboard.
- Built-in rules can hide your rule. If a built-in matches first, your rule under the base rule never runs. You have to list the built-in as a second parent.
- The spelling of an attack matters.
-encvs-ec,schtasksvsRegister-ScheduledTask: same technique, different telemetry, different result. Testing the variants found more than testing the textbook case.
Two more detections, LSASS credential dumping and Run key persistence, are next.
Rules: detection-lab/wazuh · detection-lab/sigma
About the author
- 30 years in creative leadership (in the ad industry as a Creative Director) taught me one core skill: dissecting complex systems and finding the flaw before someone else does. My tech roots run back to 8-bit machine code on a C64 (yeah – 80s, baby!) Today, I’m combining that legacy hacker curiosity with modern threat detection and offensive security. BATTLE-TESTED LOGIC: Decades of high-pressure problem solving and systems analysis. HACKER MINDSET: I don't just follow playbooks; I reverse-engineer how the attack path actually works. CONTINOUS SKILL-UP: CompTIA Security+ certified (among other certs), building home labs, and constantly training for L1 ops to future Pentesting.
Comments
Join the discussionNo comments yet. Be the first to comment.
Sprint-30
30 working days of hands-on security: a home SOC lab, detection engineering, pentesting and Active Directory attack + defence. Everything from my own lab.





