Zero to SOC · Free open lesson series · Lesson 2 of 7
The free follow-on to Zero to SOC Lesson 1. It aims to be the clearest explanation of
correlation and baselining — how a SIEM goes from "something looks odd" to "you've
been breached" — that exists anywhere. If it isn't, it isn't done.
Start here: this lesson continues directly from
Lesson 1. You'll reuse the parser you wrote there. If you
haven't done Lesson 1, do it first — it takes one sitting.
In Lesson 1 you built a detector that counted failed logins per IP and raised an alert when one crossed a threshold. It caught the brute-force:
{"ip": "203.0.113.77", "failed_attempts": 7, "rule": "ssh_bruteforce", "severity": "high"}
Useful. But look hard at that alert and you'll notice it never answers the question your boss actually asks: did they get in?
A brute-force that fails is internet weather — every public server gets thousands a day. A brute-force that succeeds is a breach, and someone has to stop reading dashboards and start an incident. Lesson 1 can't tell the two apart. It counts knocks on the door; it never checks whether the door opened.
That difference is the whole job, and closing it needs one new idea.
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 | ← you are here |
| 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 | |
| 7. Follow the thread | investigate: pivot, timeline, blast radius |
Lesson 1's rule was a stateless count — it looked at failures, one number per IP, and nothing else. Real detection relates events to each other over time: a failure followed by a success from the same IP means something neither event means alone.
That's correlation, and it's most of detection engineering. We're going to write two correlation rules:
account has never logged in from. (A stolen password leaves no failed attempts — Lesson 1 and rule A are both blind to it.)
Same format as Lesson 1, but now with a notion of history. The first three lines are last week's normal activity — our baseline of what "known good" looks like. The rest is today.
Make an auth.log with this content:
Three things are hiding in today's lines. See if you can spot them before your code does: alice fumbles her password once then logs in (normal — humans do this); 203.0.113.88 hammers the box and then gets in; and alice logs in a second time from a brand-new IP she's never used, with no failures at all. (All IPs are from ranges reserved for documentation, so they're safe as examples.)
Nothing new here — this is the parser you already wrote. Paste it again (or keep your Lesson 1 file open) so we have events to work with.
Run it: parsed 15 events.
The rule in one sentence: for each IP, if it failed at least THRESHOLD times and then had an Accepted after that burst, the attacker is in. The "then" is the correlation — we need the success to come after the failures, so we sort by the timestamp we already parse.
We'll write it as a function, because in a minute we're going to run it twice.
Now emit the alerts — critical this time, because this isn't noise, it's an incident:
Run it:
{"ip": "203.0.113.88", "user": "root", "failed_attempts": 8, "rule": "successful_bruteforce", "severity": "critical"}Read what your code just did. It found the one IP that failed its way in, told you which account it landed on (root), and stayed silent about alice — who failed once and then logged in, exactly like a human who fat-fingered a password. That discrimination — breach vs. butterfingers — is what separates a useful alert from noise. Lesson 1's failure-counter couldn't make it; one correlation rule can.
Now the uncomfortable part. What does an attacker do once they have a stolen password — phished, bought, reused from another breach? They log in. Once. Correctly. Zero failures.
Rule A needs THRESHOLD failures to trigger, so it sees nothing. Lesson 1 saw nothing. The most dangerous login in the file — alice from 203.0.113.200 at 11:50 — sails straight past both. To catch it we need a different kind of signal: not "too many failures," but "this doesn't match how this account normally behaves." We need a notion of normal.
This is baselining: learn what's normal from history, then flag what deviates. The simplest useful version: remember every (user, IP) pair we've seen succeed in the baseline period, and flag any successful login from a pair we've never seen.
Run it:
{"ip": "203.0.113.200", "user": "alice", "ts": "Jun 22 11:50:42", "rule": "anomalous_login_new_ip", "severity": "high"}There it is — the breach with no failed attempts, the one rule A and all of Lesson 1 were structurally blind to. You caught it not by counting anything, but by knowing what normal looked like and noticing this wasn't it.
Notice the already_flagged line, too. Without it, rule B would also fire on 203.0.113.88 (a new IP as well) and you'd get two alerts for one break-in. Real SOCs drown in duplicate alerts; having your detections aware of each other is the first taste of alert correlation, a problem you'll meet again at scale.
Same loop as Lesson 1: the fastest way to trust a detector is to attack your own box and watch it fire. This time, make the attacker succeed — and throw in a clean stolen- credential login for good measure.
Re-load the log and run both detections again (re-run the parser block so events is fresh, then the two alert blocks). Now they catch both new intrusions:
{"ip": "203.0.113.88", "user": "root", "failed_attempts": 8, "rule": "successful_bruteforce", "severity": "critical"}
{"ip": "203.0.113.150", "user": "admin", "failed_attempts": 12, "rule": "successful_bruteforce", "severity": "critical"}
{"ip": "203.0.113.200", "user": "alice", "ts": "Jun 22 11:50:42", "rule": "anomalous_login_new_ip", "severity": "high"}
{"ip": "203.0.113.201", "user": "bob", "ts": "Jun 22 15:02:00", "rule": "anomalous_login_new_ip", "severity": "high"}You generated a brute-force that broke in and a credential theft that walked in, and your own code caught both — one by correlation, one by baseline. Lesson 1's detector would have reported the first as "just more failures" and missed the second entirely.
Two working rules. Still not a product. Here's what a real analyst pokes at next.
First-seen is noisy. The day alice travels, or her ISP hands her a new address, or she connects through a VPN, she logs in from a new IP — and rule B cries breach. That's a false positive, and a rule that cries wolf gets muted, and a muted rule catches nothing. Real systems soften "new IP" with context: same country? same network/ASN? a plausible travel time since her last login, or impossible travel (Tel Aviv at 09:00, São Paulo at 09:10 — nobody flies that fast)? Each piece of context turns a dumb signal into a trustworthy one.
The threshold is still a guess, same as Lesson 1 — and a patient attacker who spreads guesses over days, or across many IPs (distributed), or low-and-slow at one try an hour, stays under it. Those are real detection problems, each with a real answer, and each is another rep.
The pattern never changes: a rule catches the obvious case, the real world supplies an edge case, you add the context that handles it. That incremental tightening — chasing the false positives and false negatives down one at a time — is detection engineering. It's the work.
Before moving on — answer from your head, then verify against your code:
already_flagged check from Detection B and run it on the original 15-line log. What prints now?nth_fail) — instead of just "at least 5 failures and any success from that IP"?root from 203.0.113.88 at Jun 22 09:20:15 joins alice's, because (root, 203.0.113.88) isn't in the baseline either. That's two alerts for one break-in Detection A already reported — the duplicate the check suppresses.(user, IP) pair that's already in the baseline — say, the attacker operating from alice's own compromised home machine. Zero failures, so A never triggers; a known pair, so B calls it normal. A baseline only flags what deviates from history — an attacker who fits inside it is invisible to both rules.You added correlation and baselining to your SIEM — the two ideas that take it from "counts events" to "understands them." You caught a brute-force that succeeded and a stolen credential that left no trace, then generated both attacks yourself and watched your code catch them. That is the daily work of a detection engineer, done from scratch, with no vendor box in sight.
This is the build at the heart of the Zero to SOC flagship deepening on the same path it started: ingest, parse, detect — now correlate and baseline. From here the course keeps going the same way:
scenarios at your lab so no two students get the same exercise.
generated — a portfolio artifact, not a multiple-choice score.
If these two 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.