<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Luna Meadows</title>
    <description>The latest articles on DEV Community by Luna Meadows (@luna_meadows).</description>
    <link>https://dev.to/luna_meadows</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4037025%2F0c052ada-e949-4059-ae1b-2d2b7b11a984.jpg</url>
      <title>DEV Community: Luna Meadows</title>
      <link>https://dev.to/luna_meadows</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/luna_meadows"/>
    <language>en</language>
    <item>
      <title>Blue Watch, Day 4-6: From Alerts to a Story</title>
      <dc:creator>Luna Meadows</dc:creator>
      <pubDate>Mon, 20 Jul 2026 13:05:28 +0000</pubDate>
      <link>https://dev.to/luna_meadows/blue-watch-day-4-6-from-alerts-to-a-story-2hb6</link>
      <guid>https://dev.to/luna_meadows/blue-watch-day-4-6-from-alerts-to-a-story-2hb6</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;*&lt;em&gt;The problem: one attacker, four disguises&lt;br&gt;
*&lt;/em&gt;&lt;br&gt;
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.&lt;/p&gt;

&lt;p&gt;That's the job of scorer.py.&lt;/p&gt;

&lt;p&gt;build_ip_to_user_map() and get_entity()&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First step:&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;Then get_entity() decides who an alert really "belongs to," in priority order:&lt;/p&gt;

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

&lt;p&gt;That fallback chain is the whole trick. It's the difference between four disconnected alerts and one entity with four alerts against its name.&lt;/p&gt;

&lt;p&gt;Scoring severity into a single number&lt;/p&gt;

&lt;p&gt;Once every alert has an owner, score_alerts() sums up severity points per entity:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;low: 20&lt;br&gt;
medium: 50&lt;br&gt;
high: 70&lt;br&gt;
critical: 100&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;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."&lt;/p&gt;

&lt;p&gt;The output layer&lt;/p&gt;

&lt;p&gt;Two ways to see the result:&lt;/p&gt;

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

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Day 6: the timeline&lt;/p&gt;

&lt;p&gt;Scoring answers "how bad is this?" The last piece answers "what actually happened, in order?"&lt;/p&gt;

&lt;p&gt;timeline.py is deliberately small:&lt;/p&gt;

&lt;p&gt;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.&lt;br&gt;
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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>security</category>
      <category>100daysofcode</category>
    </item>
    <item>
      <title>Building Blue Watch: a tiny SIEM with a big attitude</title>
      <dc:creator>Luna Meadows</dc:creator>
      <pubDate>Sun, 19 Jul 2026 21:50:35 +0000</pubDate>
      <link>https://dev.to/luna_meadows/building-blue-watch-a-tiny-siem-with-a-big-attitude-3g5k</link>
      <guid>https://dev.to/luna_meadows/building-blue-watch-a-tiny-siem-with-a-big-attitude-3g5k</guid>
      <description>&lt;p&gt;Okay so I'm building a mini SIEM. If you just went "a mini what?"  same energy I had a week ago. &lt;br&gt;
A &lt;strong&gt;SIEM (Security Information and Event Management system)&lt;/strong&gt; is basically the thing that watches your logs, notices when something shady is happening, and yells about it. Big companies pay big money for big versions of this. I am making a smol one, called Blue Watch, mostly out of stubbornness and curiosity.&lt;br&gt;
Rule number one I set for myself: config over code. Detection logic should live in a config file, not be hardcoded spaghetti buried in if statements. Future me will thank present me. Probably.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step zero: fake my own crime scene&lt;/strong&gt;&lt;br&gt;
Before writing a single line of detection logic, I needed logs to detect things in. Real-world logs are messy and you never actually know the "right answer" — so instead I hand-crafted a single deliberate attack story and wrote it out as a realistic auth.log file, syslog-style timestamps and all:&lt;/p&gt;

&lt;p&gt;🕵️ An attacker brute-forces their way into an admin account (lots of failed logins, then... success)&lt;br&gt;
🐚 Admin (now compromised) creates a sneaky new user — a classic "leave myself a backdoor" move&lt;br&gt;
🔓 That new backdoor account immediately goes digging somewhere it really shouldn't&lt;br&gt;
👩‍💻 Meanwhile, a totally innocent employee logs in from a normal IP at roughly the same time, just doing her job&lt;/p&gt;

&lt;p&gt;That last one was the important bit — I needed to know my detector could tell the difference between an attacker and someone just... checking their email.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 1: teaching myself regex, the hard way&lt;/strong&gt;&lt;br&gt;
I will be honest: I did not know regex before this. Now I have opinions about it.&lt;br&gt;
I built a parser that reads raw log lines and pulls structure out of chaos using named capture groups, character classes, and way too much trial and error. Three different line "shapes" (logins, privilege escalations, account creations) each needed their own pattern.&lt;br&gt;
The real win wasn't the regex though — it was writing a normalize() step that takes whatever any of the three patterns spat out and mushes it into one consistent event shape. Doesn't matter which log line it came from, downstream code doesn't need to care.&lt;br&gt;
Bugs squashed along the way: unterminated strings, mysterious NoneType crashes (regex didn't match, oops), off-by-one group numbering, and — my personal favorite — an IndentationError caused by tabs sneaking in next to spaces like a tiny gremlin.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 2: rules as data, not dogma&lt;/strong&gt;&lt;br&gt;
This was the fun part. Instead of writing "if failed login count &amp;gt; 5" buried somewhere in Python, I moved every detection rule into a YAML file. Each rule even gets tagged with a real MITRE ATT&amp;amp;CK technique ID, because if I'm going to pretend to be a security engineer I might as well pretend properly.&lt;br&gt;
Ended up with four rules covering: repeated failed logins, repeated suspicious IP activity, privilege escalation right after login, and — the spiciest one — persistence via account creation (a.k.a. "the attacker just planted a way back in").&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 3: the detector finally detects something&lt;/strong&gt;&lt;br&gt;
Two core functions carried the whole day:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;One that groups events by any field a rule asks for (a user, an IP, whatever) and checks it against a threshold — so one function now powers multiple rules instead of copy-pasted near-duplicates.&lt;/li&gt;
&lt;li&gt;One that checks whether event A is followed by event B within some time window, using real datetime math instead of vibes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And then — the best kind of bug — a false positive. My privilege-escalation rule was matching people to each other's activity just because the timing lined up. My innocent employee's login was accidentally getting blamed for the backdoor account's actions, purely by coincidence of timestamps. Fixed it by adding a same_user toggle to the rule config, since some sequences genuinely need "same person did both things" and others (like sudo → new account) very much don't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;End result:&lt;/strong&gt; all four rules fired exactly once each, correctly reconstructing the entire attack from first failed login to backdoor to the shadow file — and zero false alarms on the one honest person in the logs. 🎉&lt;br&gt;
What's next&lt;br&gt;
&lt;strong&gt;Day 4 is scorer.py&lt;/strong&gt; — turning a pile of individual alerts into one aggregate threat score per user/IP, so instead of "here are 4 alerts," Blue Watch can say "hey, this person is very clearly the problem."&lt;br&gt;
More soon 👁️🔵&lt;br&gt;
xoxo &lt;/p&gt;

&lt;p&gt;Following along? This is a build-in-public series — feedback, questions, and "actually you should use X instead" comments always welcome.&lt;/p&gt;

</description>
      <category>security</category>
      <category>python</category>
      <category>beginners</category>
      <category>100daysofcode</category>
    </item>
  </channel>
</rss>
