Zero to SOC · Free open lesson series · Lesson 4 of 7
The free fourth lesson in Zero to SOC. It aims to be the clearest explanation of why
every time window is a blind spot — and the one idea that closes it — that exists
anywhere. If it isn't, it isn't done.
Start here: this lesson continues directly from Lesson 1,
You'll reuse the same parser. If you haven't done those, do them first — each takes one
sitting.
You've built three working detections. Lesson 1 counted failures per IP. Lesson 2 correlated failures-then-success and baselined first-seen logins. Lesson 3 counted distinct IPs per account to catch an attack spread across many machines. They watch the same events through different keys, and together they're a real little detection panel.
Every one of them shares a quieter assumption you fixed into the code back in Lesson 1 and never revisited: a time window. Remember why we added it. The naive "5 failures per IP, ever" rule was no good — "5 failures over 5 months isn't an attack; 5 failures in 5 seconds is." So we wrapped every rule in a window: count what happens inside 60 seconds, or 10 minutes, and ignore the rest. That window is what makes the rules precise instead of alerting on every account that ever accumulates a few typos.
The window is also a door we left open. An attacker who reads your rule once knows exactly how long your window is — and can simply move slower than it.
Lessons 1–6 are the detection arc; Lesson 7 opens the investigation arc.
| Lesson | The skill it adds | |
|---|---|---|
| 1. Build a SIEM from scratch | ingest → parse → detect → alert | |
| 2. Catch the breach | correlate events; baseline what's normal | |
| 3. Defeat the counter | pivot the group-by key | |
| 4. The patient attacker | stretch the horizon; count occasions | ← you are here |
| 5. The drip that landed | chain detectors — rules that read alerts | |
| 6. The login that looked fine | weigh weak signals; sum the risk | |
| 7. Follow the thread | investigate: pivot, timeline, blast radius |
Last lesson the attacker beat you by spreading out in space — many IPs, so no single source looked busy. This one beats you by spreading out in time.
Put yourself on their side again. You know the defender counts failures inside a window — say five in a minute, or many sources in ten minutes. So you don't hammer. You send one guess, then wait hours. Another guess, wait again. Two or three a day, from a single quiet IP, for a week. Inside any of the defender's windows there is never more than one failure. The burst rule sees nothing. The distinct-IP rule sees one source. Both stay dark — not because they're broken, but because the attacker tuned their pace to fit through the gap between your windows.
This is low-and-slow, the patient cousin of the brute-force. It trades speed for silence: at two guesses a day a real password list takes forever, but the attacker doesn't care, because forever and undetected beats fast and blocked. It's how credential attacks survive on monitored networks, and it's the thing junior analysts almost never catch, because every rule they own is looking at a window too short to see it.
Here's the trap in one line: any rule with a window can be beaten by an attacker slower than the window. Lesson 4 is about the one thing the patient attacker still can't hide.
Same format as before, but now spanning five days instead of one morning. Mostly normal life — alice and bob log in across the week, alice fumbles a password once like a human — and threaded through all of it, a low-and-slow attack on root from a single IP: ten guesses over five days, never two in the same hour.
Make an auth.log with this content:
Seventeen lines. Scroll them and the attack barely registers — the ten root failures are scattered so thin across five days they read like background noise. That thinness is the whole point. (All IPs are from ranges reserved for documentation, so they're safe as examples.)
Unchanged. Paste it again (or keep your earlier file open) so we have events to work with.
Run it: parsed 17 events.
This is the rule you ended Lesson 1 with — the good one, with a sliding 60-second window, the version you'd actually deploy. Run it over the new log:
Output:
per-IP burst alerts: [] worst 60s burst from the attacker IP: 1
Zero alerts, and look at why: the attacker's worst 60-second window holds exactly one failure. The same goes for Lesson 3's rule — every one of those ten failures comes from a single IP, so "distinct sources against one account" never climbs above 1. Both of your deployed rules are stone blind, and again it isn't a tuning problem. The attacker built the attack to fit between the bars of your window.
Tempting. The attacker's IP racked up ten failures total — if you went back to Lesson 1's very first rule, the windowless "5 failures per IP, ever," it would catch this drip instantly. So why not drop the window?
Because you'd be walking straight back into the bug the window was added to fix. Count failures with no sense of time and every long-lived account eventually crosses any threshold — the user who fat-fingers their password once a month, the backup job with a stale credential, the phone with an old wifi password. Over a year they each rack up a dozen failures and your "ever" rule lights them all up. You'd trade a patient attacker you miss for a hundred false alarms you can't. The window isn't the mistake. Counting raw volume is.
So don't ask the volume question at all. Ask a different one.
Here's the move, and it's the exact mirror of Lesson 3.
Last time, the attacker varied where the failures came from, so you stopped counting raw failures and started counting distinct sources — {the set of IPs}. The spread that hid them became the signal.
This time the attacker varies when. So stop counting raw failures and count distinct time-periods — {the set of hours they showed up in}. A real user who fumbles a password fails in one hour, maybe two, and then they're in and they stop. An attacker grinding a password list shows up again and again, in separate hour after separate hour, and never stops — because they never succeed. That recurrence is something they cannot fake away: the whole point of low-and-slow is to keep coming back, and coming back is precisely what we'll count.
A burst is one occasion. A drip is many occasions. Counting occasions instead of events is what makes a slow attack loud — and leaves the fast one (already caught by Lesson 1) alone.
We group by the target account (Lesson 3's key — the attacker doesn't control which account they're breaking into), and for each one we count how many distinct hours it was failed against, across the whole log. Same set-of-distinct trick you wrote last lesson; we just collect hours instead of IPs.
Emit the alerts:
Run it:
{"user": "root", "distinct_hours": 10, "failures": 10, "source_ips": 1, "first_seen": "Jun 18 23:41:55", "last_seen": "Jun 22 20:14:27", "rule": "low_and_slow_bruteforce", "severity": "high"}There's the attack your two deployed rules walked right past. The alert even tells the story on its face: one source IP, ten failures, across ten separate hours, first seen Jun 18, still going Jun 22. A five-day grind against root that never lands and never quits — no human does that. And it stayed quiet about alice, who failed once in a single hour and then logged in fine. Breach vs. butterfingers, one more time: the rule fires on the thing that keeps coming back and ignores the thing that doesn't.
Same loop as every lesson — be the attacker, watch your detector fire. This time you're being patient. Drip a slow attack against a new account (deploy) from one fresh IP: six guesses, each in a different hour, none of them close enough to trip any burst rule.
Re-run the parser block so events is fresh, then the detection again. Now it catches both patient attacks:
{"user": "root", "distinct_hours": 10, "failures": 10, "source_ips": 1, "first_seen": "Jun 18 23:41:55", "last_seen": "Jun 22 20:14:27", "rule": "low_and_slow_bruteforce", "severity": "high"}
{"user": "deploy", "distinct_hours": 6, "failures": 6, "source_ips": 1, "first_seen": "Jun 23 01:00:05", "last_seen": "Jun 23 22:05:05", "rule": "low_and_slow_bruteforce", "severity": "high"}Your deploy attack never put two failures in the same minute, hour, or even close — exactly the shape that slips under every burst rule on earth — and your detector caught it anyway, because six separate visits to one account is six too many. Run Lesson 1's 60-second burst rule over this same log if you want the contrast: still zero alerts.
A fourth working detection, and the same honest caveat as always — it catches the clean case; the real world has edges. Here's what an analyst pokes at next.
Popular accounts need a baseline. Counting distinct hours works beautifully for root, which a human should rarely touch. But a shared service account — deploy, jenkins, a CI runner — might fail across many hours for boring reasons (a misconfigured job retrying all week). The fix is the same one Lesson 2 taught: learn what's normal per account. Ten active hours means nothing for a busy automation account and means everything for a human's login.
Your bucket size is a new dial. We chose one-hour periods. Make them too coarse (per day) and a fast burst and a slow drip look the same; too fine (per minute) and you're nearly back to counting raw events. The right bucket depends on how slow an attacker you're willing to miss — and just like the threshold in Lesson 1, it's a real judgment call, not a default.
You still only see failures. This rule, like Lesson 3's, watches failed logins. The nightmare is the patient attacker who finally guesses right — the last line flips to Accepted and the failures stop. That's Lesson 2's correlation problem meeting this one: a long drip of failures followed by a success is the single highest-severity pattern in this whole series. Wiring those two rules together is exactly the kind of thing the full course builds.
Three keys now, not one. You don't retire the old rules — you add this one. The fast attacker trips Lesson 1's burst counter. The distributed one trips Lesson 3's distinct-source counter. The patient one trips this distinct-hour counter. Source, target-spread, time-spread: three views of the same events, because every single window and key has a blind spot, and the attacker only needs you to be watching just one.
That move — name the blind spot in your current rule, then add the rule that doesn't share it — is detection engineering. You've now done it four times.
Before moving on — answer from your head, then verify against your code:
root's alert reads distinct_hours: 10, failures: 10. If two of the ten guesses had landed inside the same clock hour, what changes in the alert — and does it still fire?Failed lines. What exact event sequence makes the attacker go quiet to this rule at the precise moment they become most dangerous?distinct_hours drops to 9; failures stays 10. Still fires — 9 is well over DISTINCT_PERIODS = 5. The rule counts the set of hours, so a same-hour collision only shaves the count by one; it can't hide the recurrence.Accepted — and the failures stop. This rule never reads successes, so the account goes silent exactly when it's breached. A long drip of failures followed by a success is the highest-severity pattern in this series, and catching it means wiring this rule to Lesson 2's correlation — which is where the next lesson goes.You learned the idea hiding under every "advanced" detection talk about low-and-slow attacks: a window makes a rule precise and blind at the same time, and the cure isn't a bigger window — it's counting occasions instead of events. You watched both of your deployed rules go dark against a five-day grind, resisted the trap of just removing the window, and caught the attacker by counting the one thing they can't stop doing: coming back. Then you ran the attack yourself and watched it fire.
This is the Zero to SOC flagship on the same path it started: ingest, parse, detect, correlate, baseline, pivot the key — now stretch the horizon. From here the course keeps going the same way:
not just within one.
scenarios — fast, distributed, patient — at your lab so no two students get the same exercise.
generated — a portfolio artifact, not a multiple-choice score.
If these four lessons were clear, that's the whole course: hard things explained plainly, built by your own hands, no magic.
Free to use and share. This series is open-source on purpose — it's the clearest way we know to show what the flagship is. If it helped, that's the pitch.