Zero to SOC · Free open lesson series · Lesson 6 of 7
The free sixth lesson in Zero to SOC. It aims to be the clearest explanation of why
**an on/off rule can't catch an attack that's only a little suspicious in five ways at
once** — and the one shift that can — that exists anywhere. If it isn't, it isn't done.
Start here: this lesson continues directly from Lesson 1
through Lesson 5. You'll reuse the parser and the
detectors you already wrote. If you haven't done those, do them first — each takes one
sitting.
You have a five-rule panel, and in Lesson 5 you did something new with it: you wrote a rule that read your other rules and fired when two of them lined up. That was a big step — from reading the log to reading your own alarms.
But look closely at what the Lesson 5 chain actually required. It fired on one exact shape: a low-and-slow grind, then a success. No grind, no chain. Every rule you own, all the way back to Lesson 1, has that same property — it's a hard yes/no gate that needs one specific thing to be loud enough on its own: five failures in a minute, five distinct IPs, five distinct hours, a grind-then-success. Each rule waits for a single signal to cross a single line.
So here's the attack that is never loud enough for any line — and sails past all six rules.
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 | |
| 5. The drip that landed | chain detectors — rules that read alerts | |
| 6. The login that looked fine | weigh weak signals; sum the risk | ← you are here |
| 7. Follow the thread | investigate: pivot, timeline, blast radius |
The attacker already has the password. Maybe they phished it, maybe last week's low-and-slow grind finally guessed it, maybe they bought it. It doesn't matter. They don't need to guess anymore, so they never fail. They just log in. Once. Cleanly.
That single login has no failures next to it, so every failure-based rule you built — the burst counter, the distributed counter, the low-and-slow counter, the chain — has nothing to count. It's one Accepted line in a log full of Accepted lines.
But it isn't quite a normal login, and here's the thing: it's suspicious in several small ways at once.
in from a hotel. Alert on "new IP" alone and you drown, exactly as Lesson 2 warned.)
deploy, root, a service account a human should rarelytouch. (Suspicious — but ops folks do log in as deploy sometimes.)
Each one of those, on its own, is a shrug. You cannot build an on/off rule for any of them without burying yourself in false alarms — a first-seen IP, a late login, a root session are all things that happen for boring reasons every single day. That's precisely why your binary rules don't have a gate for them: the gate would never stop ringing.
But a new IP and 3 a.m. and a sensitive account, all on the same login? That's not a shrug. That's a look. The suspicion doesn't live in any one signal — it lives in the pile.
Here's the trap in one line: a yes/no rule fires on one loud signal, and misses the attack that is quietly wrong in five places at once. Lesson 6 is about counting the pile.
Same format as always. A Jun 15 baseline of known-good logins, a week of normal life, the low-and-slow grind on root from Lesson 5 that finally lands — and, threaded in, a second break-in that has no failures at all: deploy logs in once, at 3 a.m., from an IP it has never used. There's also a red herring: alice logs in from a new IP in broad daylight (she's traveling). One of these is an attack and one is a Tuesday. Your job is to tell them apart without crying wolf on alice.
Make an auth.log with this content:
Twenty-one lines. The root story you already know from Lesson 5. The new one is a single line — deploy, Accepted, Jun 23 03:14:09, 203.0.113.91 — and it looks exactly as harmless as alice's travel login three lines below it. That's the whole problem. (All IPs are from ranges reserved for documentation, so they're safe as examples.)
Paste these back in (or keep your file open). Nothing new — the parser and time helpers, plus Lesson 3's and Lesson 4's detectors, which we'll reuse as signals in a minute.
Run it: parsed 21 events.
deployRun your best failure-based rules over the log. Lesson 4's grind detector, Lesson 5's chain, Lesson 3's distributed detector — the whole panel:
Output:
L4 low_and_slow: ['root'] L3 distributed: []
Read that. Your panel flags root — correctly; that's the Lesson 5 breach. And it says nothing whatsoever about deploy. Not because the rules are broken, but because they all count failures, and deploy never failed. There is no grind to detect, no burst, no spread, no chain to trigger. From every failure-based rule's point of view, deploy logging in at 3 a.m. from a new IP is just... a login.
Now — could you catch it by lowering a bar? You have exactly one rule that looks at successes: Lesson 2's first-seen-IP baseline. Run that idea over the log and it would flag deploy's new IP — but it would flag alice's new IP too, because she's traveling. Lesson 2 told you this in plain words: "the day alice travels, rule B cries breach... a rule that cries wolf gets muted, and a muted rule catches nothing." So the one rule that can see deploy can't tell it apart from alice, and firing on both is worse than firing on neither.
That's the box you're in. The failure-based rules are silent. The one success-based rule is too noisy to turn on. deploy is an attack that no single on/off rule can catch without a false-positive flood. You need a different kind of answer.
Every rule so far returns a verdict — fire or don't. The shift in this lesson is to have your signals return evidence instead, and add it up.
Give each account a running risk score. Every weak signal adds points — a few for a first-seen IP, a few for an off-hours login, a few for touching a sensitive account, a lot for a full-blown grind. No single signal decides anything. Then draw one line across the total: score above the line, raise an alert; below it, stay quiet.
This is the exact generalization of Lesson 5. There, you chained two specific signals with a hard AND — grind AND success. Here you chain any number of signals with a soft sum, and turn the AND into a dial. A grind alone might not clear the bar. A grind plus an off-hours login plus a sensitive account clears it easily. Three shrugs that each meant nothing become one alert that means something — and a single shrug (alice's new IP) stays a shrug, because one signal's worth of points doesn't reach the line.
The beautiful part: you tune the entire system with one number. Want fewer false positives? Raise the line. Missing quiet attacks? Lower it. You're no longer editing rule logic for every edge case — you're turning a knob on a pile of evidence.
The engine consults everything you own. Your heavy detectors (Lessons 3 and 4) become high-value signals; three cheap new checks — first-seen IP, off-hours, sensitive account — become low-value ones. Each contributes points and a human-readable reason, so the alert explains itself.
Emit the alerts — and notice each one ships with the why, so an analyst reads the story off the alert instead of reconstructing it:
Run it:
{"user": "root", "risk_score": 95, "band": "critical", "signals": ["low-and-slow grind (+40)", "first-seen IP 203.0.113.90 (+25)", "off-hours login Jun 23 02:07:33 (+15)", "sensitive account (+15)"], "rule": "risk_based_alert"}
{"user": "deploy", "risk_score": 55, "band": "high", "signals": ["first-seen IP 203.0.113.91 (+25)", "off-hours login Jun 23 03:14:09 (+15)", "sensitive account (+15)"], "rule": "risk_based_alert"}Two alerts, and look at what changed.
root scores 95 — critical. Everything your panel already knew is now in one place with a number on it: the grind (Lesson 4's +40), the login from a never-seen IP (+25), at 2 a.m. (+15), on a sensitive account (+15). The chain in Lesson 5 caught this too — but now it's not a separate hard-coded rule, it's just the highest pile on the board, and the alert spells out every reason.
deploy scores 55 — high — and this is the one nothing else could catch. No grind, no burst, no failures, nothing for any Lesson 1–5 rule to bite on. Just three small wrongnesses on one clean login: a new IP (+25), 3 a.m. (+15), a sensitive account (+15). Each one alone is below the line and always will be. Stacked, they clear 50, and the attack that was invisible to your entire panel is now sitting at the top of the alert queue with its reasons attached.
And the dog that didn't bark: alice is nowhere in the output. She logged in from a first-seen IP too — but in daylight, to a normal account. That's +25 and nothing else: 25, under the line, no alert. The exact false positive that made Lesson 2 warn you off firing on "new IP" is silently, correctly swallowed. The score kept the signal without paying for the noise.
Same loop as every lesson — be the attacker, watch your detector fire. This time you're the insider with a stolen key. Do the quietest thing there is: one clean login, no failures, to a sensitive account, off-hours, from a fresh IP. The move that beats every failure-based rule you own.
Now the engine catches your stealth login too:
{"user": "root", "risk_score": 95, "band": "critical", "signals": ["low-and-slow grind (+40)", "first-seen IP 203.0.113.90 (+25)", "off-hours login Jun 23 02:07:33 (+15)", "sensitive account (+15)"], "rule": "risk_based_alert"}
{"user": "deploy", "risk_score": 55, "band": "high", "signals": ["first-seen IP 203.0.113.91 (+25)", "off-hours login Jun 23 03:14:09 (+15)", "sensitive account (+15)"], "rule": "risk_based_alert"}
{"user": "admin", "risk_score": 55, "band": "high", "signals": ["first-seen IP 203.0.113.99 (+25)", "off-hours login Jun 24 04:03:00 (+15)", "sensitive account (+15)"], "rule": "risk_based_alert"}Your admin login never failed once — the exact shape that makes Lesson 1 through Lesson 5 each look away. Run Lesson 5's chain over this same log if you want the contrast: it still returns only ['root']. The chain needs a grind to stand on; your stealth login gave it nothing. The risk engine caught admin anyway, because it isn't waiting for one loud thing — it's adding up the quiet ones.
A sixth working detection, and the caveats here are the deepest yet — because a scoring engine trades a crisp weakness for a soft one, and the soft one is easy to get wrong.
The weights and the threshold are guesses — and now there are six of them. Lesson 1 had one magic number (the threshold) and you saw how much judgment it took. This engine has a weight per signal plus the line. Set them badly and you get confident nonsense: make sensitive_account worth 50 and every routine root login is an "alert." The numbers here (40/25/15, line at 50) are a starting point, not a truth. In the real world you tune them against real traffic, watching what fires — exactly the false-positive-versus-false-negative craft from Lesson 1, now with more dials.
A pile of weak signals can gang up on the innocent. Your night-shift admin who genuinely works at 3 a.m. and just got a new laptop (new IP) on a sensitive account scores 55 too — same as the attacker. The score can't tell them apart, because you built it not to. That's the tax on catching quiet attacks: some quiet-but-legitimate behavior looks identical. Real systems soften this with per-account baselines (Lesson 2, again) — off-hours is only worth points if this account is normally asleep off-hours; a new IP costs more for a human than for a CI runner that lives in the cloud.
Scoring is only as honest as its signals. Every weakness of every underlying rule flows straight into the total. If your low-and-slow detector has a blind spot, the score inherits it. A risk engine doesn't fix bad detections — it combines them, and combining bad inputs just gives you a confident wrong number. The engine is leverage on your panel, not a substitute for building the panel well.
This is the real thing, and it has a name. What you just built — many weak signals, weighted, summed per entity, one threshold — is how modern SOCs actually cut through alert overload. The industry calls it risk-based alerting, and teams pay a lot for platforms that do exactly this. Yours is a page of Python and you understand every weight in it. No magic.
That move — stop asking each rule for a yes or no, ask it for how much evidence, and alert on the pile — is detection engineering at the altitude real SOCs operate at. You've now built a panel, taught it to read itself, and taught it to weigh what it reads.
Before moving on — answer from your head, then verify against your code:
bob logs in at 02:00 from an IP he has never used. What does he score, and does he alert?Accepted, and everything stamped Jun 15. Why both?bob isn't in SENSITIVE, so there's no third signal, and 40 < 50 — no alert. Verified: append that login and the output still lists only root, deploy, and admin. Two weak signals aren't enough by design; that's the same math that keeps alice (one signal, 25) silent.Jun 15 defines known: score the baseline against itself and every baseline login is "first-seen" against an empty history — alice and bob light up on the known-good week. Drop either guard and the engine fires on exactly the people it exists to ignore.dbadmin at 40 — is where Lesson 7 starts.You learned the idea that separates a working detection lab from a working detection team: an on/off rule fires on one loud signal and misses the attacker who is quietly wrong in five places at once — so stop deciding per rule and start scoring per account. You watched your entire failure-based panel walk past a clean 3 a.m. login, saw why the one rule that could see it was too noisy to switch on, and then turned every rule you own into evidence in a single score that caught the stealth break-in and stayed quiet about the traveler. Then you ran the quietest attack there is and watched it light up anyway.
This is the Zero to SOC flagship on the same path it started: ingest, parse, detect, correlate, baseline, pivot the key, stretch the horizon, chain the alerts — now weigh them. From here the course keeps going the same way:
false positives scoring creates.
and what do you do in the next ten minutes?*
scenarios — fast, distributed, patient, chained, stealthy — at your lab so no two students get the same exercise.
generated — a portfolio artifact, not a multiple-choice score.
If these six 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.