블루 워치, 4 ~ 6일차: 알림에서 이야기로

작성자

카테고리:

← 피드로
DEV Community · Luna Meadows · 2026-07-20 개발(SW)
Cover image for Blue Watch, Day 4-6: From Alerts to a Story

Luna Meadows

If you’ve been following along, Blue Watch is my mini-SIEM: parse logs, detect suspicious patterns, generate alerts, all config-first. Day 1-3 got me a parser, a rules file, and a detector that fires four correct alerts on my test attack scenario with zero false positives.

Four alerts is nice. But four alerts sitting in a list still isn’t an incident. That’s what the last three days were about: turning raw alerts into a score, and a score into a story.

*The problem: one attacker, four disguises
*

My test scenario has a single attacker brute-forcing admin, spinning up a backdoor user shelly, and reading /etc/shadow. But the alerts that come out of the detector don’t know they’re all the same person. One alert is tagged by IP. Another is tagged by username. Without something to unify them, my “incident” looks like four unrelated blips instead of one attacker’s full campaign.

That’s the job of scorer.py.

build_ip_to_user_map() and get_entity()

First step: figure out which IPs and users actually belong together. build_ip_to_user_map() walks the event stream and links every IP to the users who logged in from it.

Then get_entity() decides who an alert really “belongs to,” in priority order:

first_user if the alert carries one (this covers privilege-escalation alerts, where the original attacker identity matters more than whoever they became)
user, if present
otherwise, the IP mapped back to a user via the map from step one

That fallback chain is the whole trick. It’s the difference between four disconnected alerts and one entity with four alerts against its name.

Scoring severity into a single number

Once every alert has an owner, score_alerts() sums up severity points per entity:

low: 20
medium: 50
high: 70
critical: 100

Then score_to_level() converts the total back into a human label: LOW, MEDIUM, HIGH, or CRITICAL. Simple, but it’s the layer that turns “a pile of alerts” into “a number a human can triage against.”

The output layer

Two ways to see the result:

save_alerts_json() writes a structured output/alerts.json — machine-readable, ready to feed into whatever comes next.
print_report_table() renders the same data as a clean terminal table, for when I just want to glance at it.

Run against the test scenario, everything unifies the way it should: admin → 290 points → CRITICAL, with all four alerts correctly attributed to one entity. Exactly what four separate, disguised alerts should have added up to.

Day 6: the timeline

Scoring answers “how bad is this?” The last piece answers “what actually happened, in order?”

timeline.py is deliberately small:

build_timeline() sorts every event chronologically. The nice trick here: ISO 8601 timestamps sort correctly as plain strings, so there’s no datetime parsing needed — just a string sort.
print_timeline() renders the play-by-play with generic field display, meaning it doesn’t need special-case logic for each event type. Any event shape that comes out of the parser just… prints.

The payoff is the full 12-event story, in order, with luna’s ordinary login sitting exactly where it should — mid-sequence, clearly harmless noise next to the actual attack. That’s the detail I was hoping for: a good detector doesn’t just catch the bad stuff, it also stays quiet about the normal stuff sitting right next to it.

원문에서 계속 ↗

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다