I built this while teaching myself detection engineering. It has five detections so far, and each one comes with a PowerShell detector, a Sigma rule, a sample log, and a Pester test:
- SSH brute force, including whether the attacker got in (T1110)
- A new account created with root privileges, a common backdoor (T1136)
- Web attacks like SQL injection and path traversal (T1190)
- Impossible travel, a stolen login used from another country (T1078)
- Reverse shell and crypto miner after a break-in (T1059, T1496)
The first section below walks through the brute-force detection in detail.
When someone tries to break into a server over SSH, they usually guess passwords again and again, and every failed guess gets written to the auth log. One or two failures is normal because people mistype their passwords. The hard part is telling the difference between a person fumbling their login and a machine hammering the door dozens of times.
The detector reads the auth log and pulls out the failed logins. It groups them by the IP address they came from and counts how many times each IP failed. Any IP with 5 or more failures gets flagged. If that same IP also has a successful login, it gets marked CRITICAL, because that usually means the attack worked and someone is now inside.
I chose 5 as the threshold on purpose. It sits above what a normal user does when they mistype (2 or 3 tries) but below the volume you see in a real attack, so it catches attackers without raising an alarm over every typo.
This lines up with MITRE ATT&CK technique T1110, Brute Force.
- logs/sample_auth.log: a sample SSH auth log containing a brute-force attack.
- detect_bruteforce.ps1: the detector, written in PowerShell.
- detections/ssh_bruteforce.yml: the same detection as a Sigma rule, so it works in any SIEM.
- detect_bruteforce.Tests.ps1: a Pester test that proves the detector catches the known attacker.
.\detect_bruteforce.ps1
Invoke-Pester .\detect_bruteforce.Tests.ps1A real user who mistypes their password several times in a row could get flagged. The threshold is set to make that unlikely, but it is worth knowing about.
After an attacker breaks in, they often create a new user account so they can log back in later. If that account has UID 0, it has full root powers. A normal new account never gets UID 0, so one that does is a strong sign someone planted a backdoor.
This detector reads the account-creation logs and flags any new user with UID 0. It does not use a threshold like the brute-force detector, because a single root backdoor is already a full compromise, so one is too many. It maps to MITRE ATT&CK T1136, Create Account.
Files for this detection:
- logs/newuser_sample.log: sample log containing a backdoor account (name "support", UID 0).
- detect_newuser_root.ps1: the detector.
- detections/new_root_account.yml: the same rule as a Sigma rule.
- detect_newuser_root.Tests.ps1: a Pester test that proves it catches the backdoor.
Run it:
.\detect_newuser_root.ps1
Invoke-Pester .\detect_newuser_root.Tests.ps1Web servers log every request. Normal visitors ask for pages like /about or /products. Attackers send strange requests to try to break the app, for example slipping ' OR '1'='1 into a URL to trick the database, or using ../../ to climb out of the site folder and read files like /etc/passwd.
This detector looks for those patterns in the access log and flags the IP that sent them. There's no threshold here. Nobody types a SQL injection by accident, so one request is enough for an alert.
I picked this one because it's the same kind of attack behind several flaws CISA added to its exploited list in September 2026. Sangoma Switchvox, for one, was hit through SQL injection. Maps to MITRE ATT&CK T1190, Exploit Public-Facing Application.
Files:
- logs/web_access_sample.log
- detect_webattack.ps1
- detections/web_attack.yml
- detect_webattack.Tests.ps1
This one is different from the others. The attacker's login works. There are no failed attempts and no strange requests, because they're using a real password or a stolen session cookie. In 2026 this is one of the most common ways in, through infostealer malware and fake login pages that get around MFA.
The only clue is where and when. If the same account signs in from Kenya and then from Russia 19 minutes later, one of those sign-ins isn't the real person. The detector groups sign-ins by user, puts them in time order, and compares each one with the one before it. If the country changes in under 6 hours, it raises an alert. Maps to MITRE ATT&CK T1078, Valid Accounts.
Carol in the sample log is there on purpose. She signs in from Kenya and then from the UK 16 hours later. A direct flight is around 9 hours, so that's possible, and the detector leaves her alone. One of the tests checks exactly that.
Known weaknesses:
- It uses a flat 6 hour rule instead of real distances, so a quick hop to a neighbouring country (Kenya to Uganda, say) would still get flagged.
- VPNs make people look like they're somewhere else. In real SOCs this is one of the most common false positives for this kind of alert.
Files:
- logs/signin_sample.csv
- detect_impossible_travel.ps1
- detections/impossible_travel.yml
- detect_impossible_travel.Tests.ps1
This is what happens right after a break-in like Detection 3. Once an attacker can run code on a server, they usually open a reverse shell to control it from far away, and often drop a crypto miner to use the server's CPU for themselves.
The trick with a reverse shell is direction. Firewalls block strangers connecting in but allow the server to connect out, so the attacker makes the victim connect out to them instead. The fingerprint is a web server like nginx suddenly launching a shell (bash) that talks to an outside IP, which it has no honest reason to do. A miner like xmrig is flagged on its own, since nothing normal runs it.
No threshold here. A web server spawning a shell that phones home is an attack the first time. Maps to MITRE ATT&CK T1059 and T1496.
The detector leaves normal activity alone, like nginx starting php-fpm, or cron running backup.sh.
Known weakness: an attacker who renames their tools or uses a shell not on the list could slip past. Real setups pair this with egress filtering, which blocks the outbound connection whatever the tool is called.
Files:
- logs/process_sample.csv
- detect_reverse_shell.ps1
- detections/reverse_shell.yml
- detect_reverse_shell.Tests.ps1
I tested Detection 3 against OWASP Juice Shop running locally in Docker. Sending real SQL injection and path traversal at it exposed two gaps my sample logs had hidden. Real logs URL-encode the attack, so ' OR arrives as %27%20OR, and they wrap the client IP as an IPv4-mapped IPv6 address like ::ffff:172.17.0.1. I hardened the detector to URL-decode each line before matching and to pull the IPv4 out of that format. The existing Pester test still passes, so the fix did not break the original behaviour.
After the PHP2Shell bugs (CVE-2026-109618 and CVE-2026-109619) hit in October 2026, I ran a vulnerable PHP 8.4.26 container on my laptop, locked to 127.0.0.1, and tested this detector against simulated post-exploitation activity from that box.
What I found:
- It caught Apache opening a shell, even though I wrote it a month before PHP2Shell was public. It looks for behaviour, not one specific bug.
- It missed Apache running curl to download a file, which is usually the attacker's next step. I added curl and wget to the list and added a Pester test so the gap can't come back.
Known limits:
- It only sees what happens after the break-in, not the malicious request itself.
- Fixed in November 2026: alerts are now labelled by what actually happened. Downloads with curl or wget are Tool download (T1105), /dev/tcp and netcat are Reverse shell (T1059), and other commands are Command execution (T1059).
- It reads a CSV file. It is a rule, not a live alarm.
Based on the November 2026 ARTEX campaign, where attackers found leaked app secrets on GitHub and used them to sign in to Azure within minutes. One IP signed in as many different apps, because it held many stolen keys.
Two rules:
- Rule A: an app signs in from an IP that is not in its known-good baseline.
- Rule B: one IP signs in as 3 or more different apps within 10 minutes.
What I learned building it: my first version of Rule A learned each app's normal IP from the same log it was checking. When an app's very first sign-in came from the attacker, the attacker's IP became "normal". Rule A missed two attacker sign-ins and raised a false alarm on the company's real server. That is a poisoned baseline. I fixed it by loading the baseline from a separate file representing an earlier clean period, and added a Pester test so the bug cannot come back. Rule B caught the attacker the whole time, which is why the detection uses two different rules.
There is also a KQL version for Microsoft Sentinel in kql/stolen_cloud_key.kql, using the AADServicePrincipalSignInLogs table. I validated both rules in Azure Data Explorer against the same sample sign-ins (kql/stolen_cloud_key_test.kql). Rule A returned the same three attacker sign-ins as the PowerShell version, and Rule B returned 199.19.77.84 signing in as three apps (docs/kql-rule-a.png, docs/kql-rule-b.png). It is not yet deployed as a live Sentinel analytics rule.
Known limits:
- The baseline must be kept up to date. If an app moves to a new server and the list is not updated, Rule A raises false alarms (a stale baseline).
- An attacker who uses only one stolen key, from an IP already in the baseline, would not be caught by either rule.
- Rule B's threshold of 3 apps in 10 minutes is a starting point, not tuned on real data.
- The KQL version of Rule B groups time into fixed 10 minute bins, so sign-ins at 09:09 and 09:11 can fall into different bins. The PowerShell version uses a sliding window and does not have this gap.