<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
    <title>Daniel&#x27;s Detection Notes</title>
    <subtitle>Hands-on detection engineering notes by Daniel Jeczen: building EDR and SIEM labs, exploring Windows telemetry, and turning security events into useful detections.</subtitle>
    <link rel="self" type="application/atom+xml" href="https://adasumizox.github.io/atom.xml"/>
    <link rel="alternate" type="text/html" href="https://adasumizox.github.io"/>
    <generator uri="https://www.getzola.org/">Zola</generator>
    <updated>2026-09-17T00:00:00+00:00</updated>
    <id>https://adasumizox.github.io/atom.xml</id>
    <entry xml:lang="en">
        <title>Detection Is a Data-Modeling Problem</title>
        <published>2026-09-17T00:00:00+00:00</published>
        <updated>2026-09-17T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://adasumizox.github.io/posts/07-detection-as-data-modeling/"/>
        <id>https://adasumizox.github.io/posts/07-detection-as-data-modeling/</id>
        
        <content type="html" xml:base="https://adasumizox.github.io/posts/07-detection-as-data-modeling/">&lt;p&gt;&lt;em&gt;Building an EDR from scratch, essay 7 of 13.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Detection engineering has a glamorous reputation. It’s the part with the
attacker in it — behavioral analytics, threat hunting, the rule that catches
the thing nobody else caught.&lt;&#x2F;p&gt;
&lt;p&gt;Having now built a small one, my honest assessment is that the clever rules
are the easy part, and they’re not where the failures live. The failures
live in the data model underneath: whether a rule &lt;em&gt;can&lt;&#x2F;em&gt; express what you
mean, whether it means the same thing tomorrow, and — the one that nearly
got me — whether the system tells you when it has quietly stopped working.&lt;&#x2F;p&gt;
&lt;p&gt;This essay is about that substrate.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;one-vocabulary-two-purposes&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#one-vocabulary-two-purposes&quot; aria-label=&quot;Anchor link for: one-vocabulary-two-purposes&quot;&gt;One vocabulary, two purposes&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;The first decision sounds like an implementation detail and turns out to
shape everything. From &lt;code&gt;backend&#x2F;src&#x2F;detect.rs&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! Rules match the *normalized* EventRow (post-flatten), not raw proto:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! field names in a rule are the ClickHouse column names, so a rule that&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! works in SQL works here and vice versa.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Rules could have matched against the protobuf message — it’s what arrives on
the wire, and it’s the natural object in the code. Instead they match the
flattened database row, using database column names.&lt;&#x2F;p&gt;
&lt;p&gt;The reason is a workflow, not a technical constraint. Real detection
engineering goes like this: an analyst is hunting, writing SQL against
historical events, and finds something. A query returns rows that shouldn’t
exist. Now they want that query to run continuously against new data.&lt;&#x2F;p&gt;
&lt;p&gt;If rules speak a different language than queries, that transition is a
translation step — and translation is where meaning quietly drifts. The
analyst’s &lt;code&gt;WHERE reg_key LIKE &#x27;%\CurrentVersion\Run%&#x27;&lt;&#x2F;code&gt; becomes some
&lt;code&gt;registry.key_path&lt;&#x2F;code&gt; field in a rule DSL, and the two are &lt;em&gt;almost&lt;&#x2F;em&gt; the same,
and the difference surfaces six months later as an alert that never fired.&lt;&#x2F;p&gt;
&lt;p&gt;When both speak the same names, promoting a hunt to a rule is copying a
condition. And it works in reverse, which is just as valuable: given an
alert, you can paste its condition into SQL and ask “how often did this
happen last month?” The investigation and the detection are the same
sentence in the same language.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Make the exploratory tool and the production tool speak the same
vocabulary.&lt;&#x2F;strong&gt; The friction you remove is conceptual, not typing.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;correctness-and-volume-are-different-problems&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#correctness-and-volume-are-different-problems&quot; aria-label=&quot;Anchor link for: correctness-and-volume-are-different-problems&quot;&gt;Correctness and volume are different problems&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;The second idea is a separation that I think a lot of systems get wrong by
conflating.&lt;&#x2F;p&gt;
&lt;p&gt;Two things sound similar: &lt;em&gt;don’t record the same alert twice&lt;&#x2F;em&gt; and &lt;em&gt;don’t
spam the analyst&lt;&#x2F;em&gt;. They are unrelated problems, and this engine solves them
with unrelated mechanisms — deliberately.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! works in SQL works here and vice versa. Evaluation is pure — the same&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! rows always yield the same alerts — which is what makes replay safe:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! an alert&amp;#39;s identity is (agent_id, sequence, event_index, rule_id), so&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! a replayed WAL batch re-derives byte-identical alert keys&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Alert identity is derived entirely from its inputs: which agent, which
event, which rule. No timestamps of evaluation, no counters, no randomness.
So when a WAL batch replays — which, per &lt;a href=&quot;https:&#x2F;&#x2F;adasumizox.github.io&#x2F;posts&#x2F;02-the-shape-of-a-pipeline&#x2F;&quot;&gt;essay 2&lt;&#x2F;a&gt;,
is routine — the same alerts are re-derived with the same keys and collapse
in the database exactly like the events do.&lt;&#x2F;p&gt;
&lt;p&gt;This is the dedupe token idea from the pipeline, applied one plane up, and it
works for the same reason: identity computed from content is stable under
repetition. Purity isn’t an aesthetic preference here. It’s the property that
makes replay safe, and any impurity — an evaluation timestamp in the key, say
— would silently break it.&lt;&#x2F;p&gt;
&lt;p&gt;Noise suppression is the other problem, and note how differently it’s
treated:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! Noise suppression is separate from replay dedupe and deliberately&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! best-effort: an in-memory window per (rule_id, agent_id) that survives&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! neither restarts nor multi-instance deployments — the deterministic&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! alert key is what guarantees correctness, suppression only reduces&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! volume.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Suppression is explicitly allowed to be wrong. It lives in memory, dies on
restart, and doesn’t coordinate across instances. If a backend restarts, you
might see a duplicate alert you’d have preferred to suppress. Nobody’s
correctness is violated; someone is mildly annoyed.&lt;&#x2F;p&gt;
&lt;p&gt;That’s a completely legitimate place to be sloppy — &lt;em&gt;provided&lt;&#x2F;em&gt; you’re clear
about which mechanism carries the guarantee. The failure I’ve seen in real
systems is using the suppression cache as the deduplication mechanism, at
which point a restart doesn’t produce a duplicate alert, it produces a
&lt;strong&gt;missing&lt;&#x2F;strong&gt; one, and nobody notices because the alert that didn’t fire is
invisible.&lt;&#x2F;p&gt;
&lt;p&gt;Which leads directly to the subtlest line in the module:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! volume. The check&#x2F;commit split is load-bearing: evaluate() only READS&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! the window and commit() is called strictly after the alert insert&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! succeeded, so an alert whose insert failed is never remembered as&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! &amp;quot;already fired&amp;quot; and lost on the retry.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Evaluation reads the suppression window. It does not update it. The update
happens only after the alert has actually been written.&lt;&#x2F;p&gt;
&lt;p&gt;Get that ordering backwards — mark it suppressed at evaluation time, then
insert — and here’s your incident: the database write fails, the retry
arrives, the suppression window says “already fired,” and the alert is
dropped. It was never stored anywhere. The only trace is a metric counter
that doesn’t match.&lt;&#x2F;p&gt;
&lt;p&gt;The general rule is worth stating carefully because it’s violated everywhere:
&lt;strong&gt;a record that says “I did X” must be written after X, never before.&lt;&#x2F;strong&gt; The
window between attempt and confirmation is exactly where crashes and errors
live, and a system that has already recorded success on entering that window
cannot tell the difference between done and never-happened.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;knowing-what-your-data-means&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#knowing-what-your-data-means&quot; aria-label=&quot;Anchor link for: knowing-what-your-data-means&quot;&gt;Knowing what your data means&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;Now my favorite rule-writing detail, because it isn’t about rules at all.
From &lt;code&gt;backend&#x2F;rules&#x2F;README.md&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Every string condition here uses the ASCII-case-insensitive form&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;(`contains_i`, `eq_i`). The registry is a case-insensitive namespace, so&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;the case recorded in the event is the *writer&amp;#39;s* choice, not a fact about&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;the key: a value that does not already exist is stored with whatever case&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;created it. A case-sensitive `reg_value_name eq &amp;quot;Debugger&amp;quot;` would miss&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;`debugger` for free, which is not a detection worth shipping.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Sit with “the case recorded in the event is the writer’s choice, not a fact
about the key.”&lt;&#x2F;p&gt;
&lt;p&gt;The Windows registry is case-insensitive but case-&lt;em&gt;preserving&lt;&#x2F;em&gt;. Create a
value named &lt;code&gt;debugger&lt;&#x2F;code&gt; and the registry stores it as &lt;code&gt;debugger&lt;&#x2F;code&gt; while
treating it as identical to &lt;code&gt;Debugger&lt;&#x2F;code&gt; for every lookup. Windows will happily
use it. Your telemetry faithfully records the lowercase form. And a rule
matching &lt;code&gt;Debugger&lt;&#x2F;code&gt; exactly sees nothing.&lt;&#x2F;p&gt;
&lt;p&gt;That’s not a bug in the sensor — the sensor reported precisely what
happened. It’s a rule that mistook an &lt;em&gt;incidental encoding&lt;&#x2F;em&gt; for &lt;em&gt;semantic
content&lt;&#x2F;em&gt;. And the bypass costs an attacker exactly one keystroke, with no
tooling and no cleverness.&lt;&#x2F;p&gt;
&lt;p&gt;You cannot find this class of bug by testing your rule, because your test
will use the canonical casing, and it will pass. You find it by asking a
question about the data itself: &lt;strong&gt;for this field, which parts of the value
are meaningful and which are the writer’s arbitrary choice?&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;The same question, asked elsewhere, produces: paths on case-insensitive
filesystems, domain names, HTTP headers, Unicode normalization forms,
trailing dots on FQDNs, &lt;code&gt;&#x2F;tmp&lt;&#x2F;code&gt; versus &lt;code&gt;&#x2F;private&#x2F;tmp&lt;&#x2F;code&gt; on macOS. Every one is a
place where two representations mean the same thing, and where a comparison
that doesn’t know it becomes a free bypass.&lt;&#x2F;p&gt;
&lt;p&gt;The packaged rule shows what this looks like in practice — note that &lt;em&gt;both&lt;&#x2F;em&gt;
conditions use the &lt;code&gt;_i&lt;&#x2F;code&gt; form:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;json&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF8B39;&quot;&gt;  &amp;quot;where&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: [&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    {&lt;&#x2F;span&gt;&lt;span style=&quot;color: #72F1B8;&quot;&gt;&amp;quot;field&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF8B39;&quot;&gt; &amp;quot;reg_key&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #72F1B8;&quot;&gt; &amp;quot;op&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF8B39;&quot;&gt; &amp;quot;contains_i&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #72F1B8;&quot;&gt; &amp;quot;value&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF8B39;&quot;&gt; &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #36F9F6;&quot;&gt;\\&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF8B39;&quot;&gt;CurrentVersion&lt;&#x2F;span&gt;&lt;span style=&quot;color: #36F9F6;&quot;&gt;\\&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF8B39;&quot;&gt;Run&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;},&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    {&lt;&#x2F;span&gt;&lt;span style=&quot;color: #72F1B8;&quot;&gt;&amp;quot;field&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF8B39;&quot;&gt; &amp;quot;reg_data&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #72F1B8;&quot;&gt; &amp;quot;op&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF8B39;&quot;&gt; &amp;quot;contains_i&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #72F1B8;&quot;&gt; &amp;quot;value&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF8B39;&quot;&gt; &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #36F9F6;&quot;&gt;\\&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF8B39;&quot;&gt;AppData&lt;&#x2F;span&gt;&lt;span style=&quot;color: #36F9F6;&quot;&gt;\\&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF8B39;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ],&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The key path is case-insensitive because the registry is. But &lt;code&gt;reg_data&lt;&#x2F;code&gt; is
a &lt;em&gt;filesystem path the attacker chose&lt;&#x2F;em&gt;, and &lt;code&gt;C:\Users\bob\appdata\roaming\x.exe&lt;&#x2F;code&gt;
works perfectly well on Windows. Any field an adversary controls the encoding
of needs the same treatment, for the same reason.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;when-healthy-is-a-lie&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#when-healthy-is-a-lie&quot; aria-label=&quot;Anchor link for: when-healthy-is-a-lie&quot;&gt;When healthy is a lie&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;Now the story that reorganized how I think about validation, and it’s the
reason this essay exists.&lt;&#x2F;p&gt;
&lt;p&gt;The alerts table has an &lt;code&gt;event_type&lt;&#x2F;code&gt; column, an enum listing every event
type an alert can be about. It was created with 11 types. Meanwhile the
events table grew — module loads, process access, TCC, exec-maps, registry —
to 23. The alerts enum never followed.&lt;&#x2F;p&gt;
&lt;p&gt;So a rule matching on, say, &lt;code&gt;registry_value_set&lt;&#x2F;code&gt; produced an alert whose
&lt;code&gt;event_type&lt;&#x2F;code&gt; byte had no corresponding enum member. Here’s what happened
next, from &lt;code&gt;backend&#x2F;schema&#x2F;015_alerts_registry.sql&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;--      an alert whose stored byte had no enum member. That does NOT fail&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;--      the INSERT — RowBinary writes the byte unvalidated — it fails on&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;--      READ, when the column is formatted (UNKNOWN_ELEMENT_OF_ENUM),&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;--      breaking SELECT * on the table with nothing logged at ingest.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Trace the observable state of the system. Batches are acknowledged. The
alert counter climbs. No error is logged, anywhere, by anyone. The metrics
dashboard is green. Every health check passes.&lt;&#x2F;p&gt;
&lt;p&gt;And &lt;code&gt;SELECT * FROM edr.alerts&lt;&#x2F;code&gt; throws.&lt;&#x2F;p&gt;
&lt;p&gt;The detection pipeline was “working” by every signal it emitted, and the
alerts were unreadable. The gap between the operation failing and anyone
finding out was bounded only by how long until someone looked at the table.
In this case, seven schema versions.&lt;&#x2F;p&gt;
&lt;p&gt;Two things are worth extracting.&lt;&#x2F;p&gt;
&lt;p&gt;The first is about &lt;strong&gt;where validation lives&lt;&#x2F;strong&gt;. RowBinary is a schema-less
binary format: values are written positionally with no names and no types on
the wire. That’s what makes it fast, and the speed is real. But “no schema
on the wire” means the receiver cannot check anything — it writes the bytes
you gave it. The validation you &lt;em&gt;thought&lt;&#x2F;em&gt; the database was doing was never
happening; you had simply never sent it anything invalid before.&lt;&#x2F;p&gt;
&lt;p&gt;Fast serialization formats buy their speed by trusting the sender. The trust
doesn’t remove the need for validation, it &lt;em&gt;relocates&lt;&#x2F;em&gt; it — and if you don’t
consciously relocate it, it lands nowhere. Which is why the fix isn’t only
the migration:&lt;&#x2F;p&gt;
&lt;p&gt;Two unit tests now pin the alerts enum’s membership and the struct’s column
order against the DDL file itself. The check moved into the build, because
that’s the last place both sides of the contract are visible at once.&lt;&#x2F;p&gt;
&lt;p&gt;The second extraction is subtler and rather beautiful:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;--      Extending the enum also HEALS rows already written that way: the&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;--      stored byte becomes a legal member again and reads back correctly,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;--      so no repair pass is needed after this runs.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The old rows repair themselves. Because the byte was stored &lt;em&gt;faithfully&lt;&#x2F;em&gt; —
ClickHouse wrote exactly what it was given — the data was never corrupt. It
was &lt;strong&gt;uninterpretable&lt;&#x2F;strong&gt;, which is a different condition entirely. The value
was right; the dictionary needed to read it was incomplete. Extend the
dictionary and every historical row becomes readable.&lt;&#x2F;p&gt;
&lt;p&gt;That distinction — &lt;em&gt;lossless but unreadable&lt;&#x2F;em&gt; versus &lt;em&gt;lossy&lt;&#x2F;em&gt; — is worth
carrying around. Systems that store faithfully and interpret separately can
recover from interpretation bugs. Systems that normalize on the way in
cannot: they’ve thrown away the thing they’d need to fix it later. It’s the
same instinct as &lt;a href=&quot;https:&#x2F;&#x2F;adasumizox.github.io&#x2F;posts&#x2F;03-naming-things-that-move&#x2F;&quot;&gt;essay 3&lt;&#x2F;a&gt;’s insistence that
absence be representable. Preserve what you were given; decide what it means
as late as you can.&lt;&#x2F;p&gt;
&lt;p&gt;There’s a companion hazard right next to it, mentioned in the same file:
RowBinary is &lt;em&gt;positional&lt;&#x2F;em&gt;, so the Rust struct’s field order and the DDL’s
column order are a single contract split across two files in two languages.
Get them out of step and every value shifts one column over — &lt;code&gt;severity&lt;&#x2F;code&gt;
lands in &lt;code&gt;state&lt;&#x2F;code&gt;, &lt;code&gt;agent_id&lt;&#x2F;code&gt; lands in &lt;code&gt;sequence&lt;&#x2F;code&gt; — and again, nothing errors.
Hence the comment on the struct:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;&#x2F; One edr.alerts row; field order matches the DDL exactly (RowBinary).&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;A comment is not enforcement, which is why there’s a test. But the pattern to
notice is that &lt;strong&gt;implicit contracts need explicit tests&lt;&#x2F;strong&gt;, and the most
dangerous contracts are the ones expressed as an agreement about &lt;em&gt;order&lt;&#x2F;em&gt;,
because they fail silently and totally.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;retention-makes-you-denormalize&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#retention-makes-you-denormalize&quot; aria-label=&quot;Anchor link for: retention-makes-you-denormalize&quot;&gt;Retention makes you denormalize&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;One more modeling decision, driven by an unglamorous force:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;    -- Context copied from the matched event at detection time, so triage&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;    -- answers &amp;quot;what ran &#x2F; what was touched &#x2F; where did it connect&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;    -- without a join and unaffected by the events TTL.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;An alert copies its context from the event that fired it. Normalization says
don’t — store the reference, join when needed. The reason to break that rule
is stated plainly in the migration: events live 30 days, alerts live 90, and
an investigation routinely outlives its evidence.&lt;&#x2F;p&gt;
&lt;p&gt;An alert that says “rule X fired on event &lt;code&gt;(agent, 4821, 3)&lt;&#x2F;code&gt;” is, on day 45,
a pointer into a void. The join returns nothing. The alert is technically
still there and practically worthless.&lt;&#x2F;p&gt;
&lt;p&gt;Normalization implicitly assumes both sides of a relationship live the same
length of time. When they don’t, a foreign key is a promise that expires.
&lt;strong&gt;Different retention periods force denormalization&lt;&#x2F;strong&gt;, and the copy isn’t
redundancy — it’s the only way the alert stays meaningful for its own
lifetime.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;where-the-difficulty-actually-is&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#where-the-difficulty-actually-is&quot; aria-label=&quot;Anchor link for: where-the-difficulty-actually-is&quot;&gt;Where the difficulty actually is&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;None of the hard parts in this essay were about detection logic. The rules
themselves are almost trivial — a few string comparisons ANDed together,
matching persistence keys that have been documented for twenty years.&lt;&#x2F;p&gt;
&lt;p&gt;The hard parts were: whether a rule can be expressed in the same language as
a query; whether alert identity survives replay; which mechanism carries the
guarantee and which is allowed to be sloppy; whether a comparison respects
the semantics of the namespace it’s comparing in; whether a serialization
format validates or merely stores; whether a foreign key outlives what it
points to.&lt;&#x2F;p&gt;
&lt;p&gt;Every one of those is data modeling. And every one of them can break a
detection &lt;em&gt;without breaking anything visible&lt;&#x2F;em&gt; — which is the recurring theme
of this whole series and the specific reason detection is unforgiving. A
crashed system announces itself. A detection that stopped firing looks
exactly like a quiet week.&lt;&#x2F;p&gt;
&lt;p&gt;Which raises the next question. If silent failure is the enemy, and the code
runs inside kernels on machines you’ll never touch, how do you ever know it
works? The next essay tackles that problem.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>What You Cannot See</title>
        <published>2026-09-09T00:00:00+00:00</published>
        <updated>2026-09-09T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://adasumizox.github.io/posts/06-what-you-cannot-see/"/>
        <id>https://adasumizox.github.io/posts/06-what-you-cannot-see/</id>
        
        <content type="html" xml:base="https://adasumizox.github.io/posts/06-what-you-cannot-see/">&lt;p&gt;&lt;em&gt;Building an EDR from scratch, essay 6 of 13.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Every sensor in this series watches an operating system abstraction.
Process creation. File open. Socket connect. Module load. The events are
delivered because the kernel maintains those abstractions and offers hooks
where they’re maintained.&lt;&#x2F;p&gt;
&lt;p&gt;Which implies something uncomfortable, and it’s the honest starting point
for this essay: &lt;strong&gt;anything that accomplishes its goal without going through
the abstraction you’re watching is invisible to you.&lt;&#x2F;strong&gt; Not hard to see. Not
low-fidelity. Invisible, structurally, in a way no amount of tuning fixes.&lt;&#x2F;p&gt;
&lt;p&gt;An attacker who understands your sensor’s hook points is not trying to
evade your detection logic. They’re trying to accomplish their goal via a
path that never touches your hook. That’s a completely different game, and
it’s why the interesting question for a telemetry system isn’t “what does
it catch” but “what is it &lt;em&gt;incapable&lt;&#x2F;em&gt; of catching, and do we know?”&lt;&#x2F;p&gt;
&lt;p&gt;This essay is about the boundary. It ends up being a lesson in
epistemology more than in kernels.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;when-the-obvious-field-lies-to-you&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#when-the-obvious-field-lies-to-you&quot; aria-label=&quot;Anchor link for: when-the-obvious-field-lies-to-you&quot;&gt;When the obvious field lies to you&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;Start with something that looks like solid ground. A process starts, and
you record the path of the executable. Every EDR does this; most detection
logic is built on it.&lt;&#x2F;p&gt;
&lt;p&gt;Then consider &lt;code&gt;memfd_create&lt;&#x2F;code&gt;. It gives you a file descriptor backed by
anonymous memory — a “file” that exists only in RAM, with no directory
entry anywhere on disk. You can write an ELF binary into it and &lt;code&gt;execve&lt;&#x2F;code&gt; it.
The process runs. Nothing was ever written to the filesystem.&lt;&#x2F;p&gt;
&lt;p&gt;What does your sensor report for the image path? Something like
&lt;code&gt;&#x2F;dev&#x2F;fd&#x2F;3&lt;&#x2F;code&gt;. That’s not a lie exactly, but it’s useless: it’s a synthetic
path naming a file descriptor in a process that is about to be replaced. It
tells you nothing about what ran. Your hash of the image can’t be computed
from it. Your allowlist can’t match it. Your analyst sees a path that
resolves to nothing.&lt;&#x2F;p&gt;
&lt;p&gt;The field is present, well-formed, and empty of meaning. Those are the worst
kind of blind spot, because a null would at least announce itself.&lt;&#x2F;p&gt;
&lt;p&gt;The fix, from &lt;code&gt;linux&#x2F;bpf&#x2F;edr_probes.bpf.c&lt;&#x2F;code&gt;,
is to stop asking the path and start asking the inode:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;c&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt;		if&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #36F9F6;&quot;&gt;BPF_CORE_READ&lt;&#x2F;span&gt;&lt;span&gt;(file, f_inode, i_nlink)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt; ==&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97E72;&quot;&gt; 0&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;			ev&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt;-&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt;exec_flags &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFFFFFEE;&quot;&gt;|=&lt;&#x2F;span&gt;&lt;span&gt; EDR_LX_EXEC_UNLINKED;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;i_nlink&lt;&#x2F;code&gt; is the number of directory entries pointing at this inode. Zero
means &lt;em&gt;nothing on the filesystem names this file&lt;&#x2F;em&gt;. It might be a memfd. It
might be a normal binary that was unlinked after being opened — the classic
“drop, execute, delete” that leaves a running process with no file behind
it. Either way, one integer answers a question the path could not: is this
executable reachable by name?&lt;&#x2F;p&gt;
&lt;p&gt;A second read distinguishes the specific case:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;c&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt;		if&lt;&#x2F;span&gt;&lt;span&gt; (dn &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt;&amp;amp;&amp;amp;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #36F9F6;&quot;&gt;		    bpf_probe_read_kernel&lt;&#x2F;span&gt;&lt;span&gt;(dname,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt; sizeof&lt;&#x2F;span&gt;&lt;span&gt;(dname), dn)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt; ==&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97E72;&quot;&gt; 0&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt; &amp;amp;&amp;amp;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF7EDB;&quot;&gt;		    dname&lt;&#x2F;span&gt;&lt;span&gt;[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97E72;&quot;&gt;0&lt;&#x2F;span&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt; ==&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF8B39;&quot;&gt; &amp;#39;m&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt; &amp;amp;&amp;amp;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF7EDB;&quot;&gt; dname&lt;&#x2F;span&gt;&lt;span&gt;[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97E72;&quot;&gt;1&lt;&#x2F;span&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt; ==&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF8B39;&quot;&gt; &amp;#39;e&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt; &amp;amp;&amp;amp;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF7EDB;&quot;&gt; dname&lt;&#x2F;span&gt;&lt;span&gt;[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97E72;&quot;&gt;2&lt;&#x2F;span&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt; ==&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF8B39;&quot;&gt; &amp;#39;m&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt; &amp;amp;&amp;amp;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF7EDB;&quot;&gt;		    dname&lt;&#x2F;span&gt;&lt;span&gt;[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97E72;&quot;&gt;3&lt;&#x2F;span&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt; ==&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF8B39;&quot;&gt; &amp;#39;f&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt; &amp;amp;&amp;amp;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF7EDB;&quot;&gt; dname&lt;&#x2F;span&gt;&lt;span&gt;[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97E72;&quot;&gt;4&lt;&#x2F;span&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt; ==&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF8B39;&quot;&gt; &amp;#39;d&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt; &amp;amp;&amp;amp;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF7EDB;&quot;&gt; dname&lt;&#x2F;span&gt;&lt;span&gt;[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97E72;&quot;&gt;5&lt;&#x2F;span&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt; ==&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF8B39;&quot;&gt; &amp;#39;:&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;			ev&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt;-&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt;exec_flags &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFFFFFEE;&quot;&gt;|=&lt;&#x2F;span&gt;&lt;span&gt; EDR_LX_EXEC_MEMFD &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt;|&lt;&#x2F;span&gt;&lt;span&gt; EDR_LX_EXEC_UNLINKED;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;That’s a manually unrolled six-character string comparison, which looks
absurd next to &lt;code&gt;strncmp&lt;&#x2F;code&gt; until you remember it’s running under the eBPF
verifier, where a fixed-size read at a constant offset is provable and a
variable-length loop is an argument. The comment says exactly that: “A short
fixed read keeps the verifier out of variable-offset territory.”&lt;&#x2F;p&gt;
&lt;p&gt;But the technique is what I want to underline. The path was unreliable, so
the probe moved &lt;em&gt;closer to the mechanism&lt;&#x2F;em&gt; — from a rendered string to the
kernel structure the string was rendered from. Sensor design is full of
this. When the convenient field is derived, ask what it’s derived from, and
whether the source is harder to lie about.&lt;&#x2F;p&gt;
&lt;p&gt;Two boolean flags, and a class of execution that used to be indistinguishable
from a normal process start becomes a query you can write. The attacker’s
goal was to run code without leaving a file. They achieved it. They just
couldn’t do it without producing an inode with zero links, which is the one
thing the technique fundamentally requires.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Look for what the technique cannot avoid.&lt;&#x2F;strong&gt; Anything else is a signature,
and signatures get bypassed.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;absence-as-a-signal&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#absence-as-a-signal&quot; aria-label=&quot;Anchor link for: absence-as-a-signal&quot;&gt;Absence as a signal&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;There’s a stranger version of this, and it’s my favorite piece of design in
the whole codebase.&lt;&#x2F;p&gt;
&lt;p&gt;Loading a kernel module is the rootkit’s move — it’s the transition from
“code running on the machine” to “code &lt;em&gt;inside&lt;&#x2F;em&gt; the machine.” Linux offers
two syscalls for it. &lt;code&gt;finit_module&lt;&#x2F;code&gt; takes a file descriptor. &lt;code&gt;init_module&lt;&#x2F;code&gt;
takes a memory buffer.&lt;&#x2F;p&gt;
&lt;p&gt;The file-based path is straightforward to instrument, and the probe grabs
the name:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;c&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	dn &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFFFFFEE;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #36F9F6;&quot;&gt; BPF_CORE_READ&lt;&#x2F;span&gt;&lt;span&gt;(file, f_path.dentry, d_name.name);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	n &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFFFFFEE;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #36F9F6;&quot;&gt; bpf_probe_read_kernel_str&lt;&#x2F;span&gt;&lt;span&gt;(ev&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt;-&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF7EDB;font-style: italic;&quot;&gt;name&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt; sizeof&lt;&#x2F;span&gt;&lt;span&gt;(ev&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt;-&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt;name), dn);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The memory-based path has no file at all, and therefore nothing to name:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;c&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;* init_module: the module image is a user buffer — no file, so no name.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt; * The absence IS the signal (a fileless kernel-module load). *&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;That comment is doing real philosophical work. There is no name to report,
and rather than treating that as a data-quality problem to paper over, the
design treats &lt;em&gt;the shape of the missing data&lt;&#x2F;em&gt; as the finding. An event that
says “a kernel module was loaded and I cannot tell you from where” is not
a degraded event. It is a high-signal one, because the legitimate path —
&lt;code&gt;modprobe&lt;&#x2F;code&gt;, &lt;code&gt;insmod&lt;&#x2F;code&gt;, the distro’s own tooling — uses the file-based
syscall. The nameless variant is the interesting one almost by definition.&lt;&#x2F;p&gt;
&lt;p&gt;There’s an even neater trick layered on top, which relies on &lt;em&gt;ordering&lt;&#x2F;em&gt;
rather than content. The attempt hooks fire when a load is requested; a
separate tracepoint fires when a module is actually accepted. So an attempt
with no matching acceptance means the kernel refused the load — a signature
check failing, lockdown mode engaging, a malformed module.&lt;&#x2F;p&gt;
&lt;p&gt;Nobody emits a “module load refused” event. That fact is constructed by
noticing that a thing which should have followed didn’t.&lt;&#x2F;p&gt;
&lt;p&gt;The general move: &lt;strong&gt;you can detect an event by observing the hole where its
consequence should be.&lt;&#x2F;strong&gt; It requires knowing the normal causal chain well
enough to notice a missing link, which is a much deeper form of
instrumentation than pattern-matching on fields. Missed connections. Absent
heartbeats. Requests without responses. Once you start looking for holes,
they’re everywhere, and they’re often where the interesting things hide.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;choosing-your-blindness&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#choosing-your-blindness&quot; aria-label=&quot;Anchor link for: choosing-your-blindness&quot;&gt;Choosing your blindness&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;Now the uncomfortable kind of blind spot: the one you build yourself,
knowingly, because the alternative is worse.&lt;&#x2F;p&gt;
&lt;p&gt;Watching executable memory mappings would be a superb detection surface. A
DLL being mapped into a process is how sideloading works, how injection
works, how a malicious plugin gets in. So: report every file mapped
executable.&lt;&#x2F;p&gt;
&lt;p&gt;Try it and the sensor dies. Every process launch maps dozens of system
libraries — 30 to 100 DLLs on Windows, dyld’s whole flood on macOS. The
event rate is orders of magnitude above anything interesting, the ring
buffer overflows, and you lose events &lt;em&gt;including the ones you cared about&lt;&#x2F;em&gt;.
A firehose of noise doesn’t just bury the signal; it evicts it.&lt;&#x2F;p&gt;
&lt;p&gt;So you filter. And here’s the thing worth sitting with: &lt;strong&gt;filtering is
deliberately choosing what to be blind to.&lt;&#x2F;strong&gt; The filter isn’t a performance
detail, it’s a coverage decision with a security consequence, and it should
be argued rather than tuned into place.&lt;&#x2F;p&gt;
&lt;p&gt;The story of choosing this particular filter is recorded in the
&lt;code&gt;README&lt;&#x2F;code&gt;, and it’s a good one because the elegant idea lost:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;a pure W^X filter was tried first and rejected — `max_protection` is `RWX`&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;for nearly every mapping on recent macOS, and a *current* RWX mapping is&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;hardware-blocked on Apple Silicon&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The elegant idea was W^X: report mappings that are both writable and
executable, since legitimate code is generally one or the other. Principled,
mechanism-based, exactly the kind of filter this essay has been praising.&lt;&#x2F;p&gt;
&lt;p&gt;It doesn’t work, for two independent reasons that only reality could have
supplied. &lt;code&gt;max_protection&lt;&#x2F;code&gt; — the &lt;em&gt;maximum&lt;&#x2F;em&gt; protection a mapping could ever
be granted — is RWX for almost everything on modern macOS, so the field
doesn’t discriminate. And &lt;em&gt;current&lt;&#x2F;em&gt; RWX is hardware-blocked on Apple
Silicon, so the thing you’d actually want to catch can’t exist in that form
anyway. The theory was clean and the platform simply doesn’t behave the way
the theory assumed.&lt;&#x2F;p&gt;
&lt;p&gt;What shipped instead is a crude path exclusion: skip &lt;code&gt;&#x2F;System&lt;&#x2F;code&gt; and &lt;code&gt;&#x2F;usr&lt;&#x2F;code&gt; on
macOS, skip the Windows directory on Windows. It’s an inelegant heuristic
and everyone involved knows it. The README says so plainly — Apple’s own
developer tooling reports as a non-system code load, and on Windows every
application’s own DLLs under &lt;code&gt;Program Files&lt;&#x2F;code&gt; report too.&lt;&#x2F;p&gt;
&lt;p&gt;But it’s honest about its own nature, it’s cheap enough to run in kernel
context, and it reduces the volume enough that the interesting events
survive. Sometimes the crude filter that works beats the beautiful one that
doesn’t, and the professional skill is noticing which situation you’re in.&lt;&#x2F;p&gt;
&lt;p&gt;One structural point, easy to miss and load-bearing: this filtering happens
&lt;strong&gt;in the kernel&lt;&#x2F;strong&gt;, before the event is written to the ring. Filtering in
user space would move the drop counter, not the load. The kernel would still
produce the events, the ring would still overflow, and you’d still lose data
— you’d just discard it after paying for it. A high-volume callback needs its
filter at the source.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-floor&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-floor&quot; aria-label=&quot;Anchor link for: the-floor&quot;&gt;The floor&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;And finally, the blind spot with no clever workaround. From the README’s
limitations:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;**Manually mapped (&amp;quot;reflective&amp;quot;) images are invisible** on Windows: a DLL&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;written into memory and relocated by hand creates no image section, so no&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;load-image notification is ever delivered.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Reflective DLL loading works by doing the loader’s job yourself. Allocate
memory, copy in the PE, walk the relocation table, resolve imports, jump to
the entry point. The result is functioning code in a process — and the
operating system’s loader was never involved, so there is no image section,
so &lt;code&gt;PsSetLoadImageNotifyRoutine&lt;&#x2F;code&gt; has nothing to notify about. The hook isn’t
failing. The event genuinely does not exist.&lt;&#x2F;p&gt;
&lt;p&gt;macOS has the identical hole from the other direction: &lt;code&gt;MAP_ANON&lt;&#x2F;code&gt; memory
with shellcode in it is anonymous, file-backed by nothing, and there is no
“file mapped executable” event to deliver.&lt;&#x2F;p&gt;
&lt;p&gt;The README’s framing is the right one:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;in both cases the sensor sees code loads that pass through the OS loader,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;not code that bypasses it.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;That single sentence defines the sensor’s epistemic boundary better than any
feature list could. You are not watching “code execution.” You are watching
&lt;em&gt;the operating system’s code-loading machinery&lt;&#x2F;em&gt;, and the difference is
exactly the set of techniques that skip it.&lt;&#x2F;p&gt;
&lt;p&gt;Knowing the boundary changes what you do about it. You stop trying to make
the file-mapping sensor catch reflective loading — it cannot, ever — and you
start looking for the &lt;em&gt;consequences&lt;&#x2F;em&gt; an in-memory payload can’t avoid: the
network connection it eventually makes, the process it injects into, the
persistence it establishes, the child it spawns. The technique evades one
sensor. It rarely evades the whole system, because it still has to
accomplish something, and accomplishing things means using abstractions.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why-writing-them-down-is-the-actual-skill&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#why-writing-them-down-is-the-actual-skill&quot; aria-label=&quot;Anchor link for: why-writing-them-down-is-the-actual-skill&quot;&gt;Why writing them down is the actual skill&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;The &lt;code&gt;README&lt;&#x2F;code&gt; has a “Known limitations” section that runs for
over a hundred lines. Memory-mapped writes attributed at mapping time rather
than at write time. Linux MODIFY firing on open-intent rather than first
write. DNS invisible on macOS loopback because content filters never see it.
TCC events that need an entitlement the build doesn’t have. Windows UDPv6
decoded from documentation but never ground-truthed against a real capture.&lt;&#x2F;p&gt;
&lt;p&gt;That list is, I’d argue, the most valuable artifact in the repository, and
it’s worth being explicit about why.&lt;&#x2F;p&gt;
&lt;p&gt;An analyst investigating an incident is constantly making inferences from
absence. “There’s no network connection from that process, so it never
called home.” “There’s no file write, so nothing was dropped.” Those
inferences are only valid if the sensor would have seen it. Absence of
evidence is evidence of absence &lt;em&gt;only&lt;&#x2F;em&gt; when you have coverage — and the
limitations list is precisely the document that tells you when you don’t.&lt;&#x2F;p&gt;
&lt;p&gt;This is the same principle as the counted ring drops from
&lt;a href=&quot;https:&#x2F;&#x2F;adasumizox.github.io&#x2F;posts&#x2F;02-the-shape-of-a-pipeline&#x2F;&quot;&gt;essay 2&lt;&#x2F;a&gt;, one level up. There, the pipeline
made data loss &lt;em&gt;known&lt;&#x2F;em&gt; rather than silent. Here, the documentation makes
coverage gaps known rather than implied. In both cases the underlying value
is the same: a system that tells you what it doesn’t know is trustworthy in
a way that a system implying total coverage never is.&lt;&#x2F;p&gt;
&lt;p&gt;And there’s a professional dimension. Vendors are structurally incentivized
to describe coverage and stay quiet about gaps, because gaps read as
weakness in a bake-off. The result is a market full of tools whose users
systematically overestimate what they can conclude from silence. Writing the
gaps down, next to the code that causes them, is the opposite bet: it makes
the tool less impressive and considerably more useful.&lt;&#x2F;p&gt;
&lt;p&gt;Which sets up the next question. Given a stream of events with known
coverage and known holes, how do you actually turn it into a claim that
something bad is happening? That turns out to be much less about clever
rules than about the shape of the data underneath them. That data model is
the subject of the next essay.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Cutting the Network from Inside</title>
        <published>2026-09-02T00:00:00+00:00</published>
        <updated>2026-09-02T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://adasumizox.github.io/posts/05-cutting-the-network-from-inside/"/>
        <id>https://adasumizox.github.io/posts/05-cutting-the-network-from-inside/</id>
        
        <content type="html" xml:base="https://adasumizox.github.io/posts/05-cutting-the-network-from-inside/">&lt;p&gt;&lt;em&gt;Building an EDR from scratch, essay 5 of 13.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Network isolation is the response action that looks easiest and is hardest.&lt;&#x2F;p&gt;
&lt;p&gt;The pitch is simple enough that a manager can specify it in one sentence: a
machine is compromised, cut it off the network, contain the damage. Every
commercial EDR has the button. It usually has a reassuring icon.&lt;&#x2F;p&gt;
&lt;p&gt;And the implementation is a paradox with a very short fuse. You are going to
tell a machine to stop talking to the network — using the network. The
instruction to sever connectivity arrives over the connectivity it severs.
If you do the obvious thing, the machine goes silent mid-sentence and stays
silent forever, because the only channel you had for saying “actually, let
it back on” was the one you just cut.&lt;&#x2F;p&gt;
&lt;p&gt;That’s not a race condition you’ll catch in code review. It’s a machine that
needs someone to physically walk to it. Multiply by the number of hosts an
analyst can click through in a panic, and a containment feature becomes an
outage.&lt;&#x2F;p&gt;
&lt;p&gt;Everything interesting about this action follows from that one problem.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;isolation-isn-t-block-everything&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#isolation-isn-t-block-everything&quot; aria-label=&quot;Anchor link for: isolation-isn-t-block-everything&quot;&gt;Isolation isn’t “block everything”&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;The naive version — drop all traffic — is unusable, and the exemption list
tells you a lot about how networks actually work. From
&lt;code&gt;shipper&#x2F;src&#x2F;isolate.rs&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! Self-harm is the whole problem (see proto IsolateHostAction): a&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! ruleset that fails to exempt the agent&amp;#39;s own backend endpoint bricks&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! the host — isolated, unreachable, and un-un-isolatable. So the&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! executor refuses to apply ANYTHING unless it can parse and resolve&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! the backend endpoint, and validates every operator CIDR before the&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! kernel is touched (all-or-nothing).&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Start with the refusal, because it’s the most important line. If the agent
cannot determine its own backend endpoint, it applies &lt;em&gt;nothing at all&lt;&#x2F;em&gt;. Not
a best-effort ruleset, not a partial block. The action fails and reports
failure.&lt;&#x2F;p&gt;
&lt;p&gt;This runs against a reflex most of us have — partial progress feels
responsible, and “we contained most of it” sounds better than “we did
nothing.” Here it’s precisely backwards. A partially-applied isolation is a
bricked host, which is strictly worse than an uncontained one: you’ve lost
the machine &lt;em&gt;and&lt;&#x2F;em&gt; your visibility into it, and now the incident includes a
site visit. When a failure mode is unrecoverable, refusing to start is the
safe branch.&lt;&#x2F;p&gt;
&lt;p&gt;The rest of the exemption list is a tour of the protocols people forget:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! kernel is touched (all-or-nothing). The exemption set is deliberately&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! minimal: backend endpoint, loopback, DHCP (v4 + v6, or lease expiry&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! takes the host off the network underneath the containment), and IPv6&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! neighbor discovery (ARP lives below IP, but NDP rides ICMPv6 — block&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! it and v6 connectivity dies, including to the backend).&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;Loopback&lt;&#x2F;strong&gt; — block it and half the software on the machine breaks, because
local services talk to each other over &lt;code&gt;127.0.0.1&lt;&#x2F;code&gt; constantly. You’d contain
the host by breaking it.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;DHCP&lt;&#x2F;strong&gt; is the one that gets people, and it’s a lovely example of a
time-delayed failure. Block DHCP and everything works fine. For hours. Then
the lease expires, the renewal is blocked, the interface loses its address,
and the host drops off the network entirely — including the exempted backend
path, which was exempted by IP on an interface that no longer has one. Your
isolation silently upgrades itself to a brick, long after anyone is watching.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;IPv6 neighbor discovery&lt;&#x2F;strong&gt; is the same trap with a protocol-layering twist.
On IPv4, address resolution is ARP, which sits &lt;em&gt;below&lt;&#x2F;em&gt; IP and isn’t touched
by IP-layer filtering — you can block everything and ARP still works. IPv6
folded that function into NDP, which rides on ICMPv6, which &lt;em&gt;is&lt;&#x2F;em&gt; IP traffic.
Apply the IPv4 mental model to an IPv6 host and you block neighbor discovery,
and v6 connectivity dies — including to your backend, if it’s reached over
v6. Two protocols doing the same job at different layers, and the difference
is only visible if you know the history.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-you-refuse-to-exempt&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#what-you-refuse-to-exempt&quot; aria-label=&quot;Anchor link for: what-you-refuse-to-exempt&quot;&gt;What you refuse to exempt&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;The omissions are more interesting than the inclusions, because each one is
a deliberate acceptance of pain:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! it and v6 connectivity dies, including to the backend). Deliberately&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! NOT exempted: established flows (an active C2 session must die) and&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! DNS (re-resolution would open a tunnel; backend IPs are pinned at&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! apply time — a backend failover during isolation needs an operator&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! allow_cidrs entry planned ahead).&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;Established flows must die.&lt;&#x2F;strong&gt; Almost every firewall ruleset on earth
begins with “allow established and related” — it’s the first line of every
tutorial, because without it stateful protocols break. But an active C2
session &lt;em&gt;is&lt;&#x2F;em&gt; an established connection. Exempt established flows and you have
built a containment that specifically permits the thing you’re containing.
The attacker’s live shell survives; only their ability to open &lt;em&gt;new&lt;&#x2F;em&gt;
connections is blocked. This is, I’d bet, a real bug in real products.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;DNS must die&lt;&#x2F;strong&gt;, which hurts more than it sounds. Blocking name resolution
breaks a lot of software in confusing ways. But DNS is a general-purpose
bidirectional channel that every network permits by default — DNS tunneling
is a mature, tooled technique, not a curiosity. Leaving a hole for it
because “it’s just name resolution” defeats the exercise.&lt;&#x2F;p&gt;
&lt;p&gt;The cost is stated plainly rather than hidden: backend IPs are pinned as
addresses at the moment of application, so if the backend fails over to a
different address while a host is isolated, that host can’t follow, and
recovering it needs a CIDR that an operator planned in advance. That’s a
genuine operational constraint. Writing it down in the module header, next to
the decision that causes it, is what separates an accepted trade-off from a
future incident report.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;half-applied-is-worse-than-not-applied&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#half-applied-is-worse-than-not-applied&quot; aria-label=&quot;Anchor link for: half-applied-is-worse-than-not-applied&quot;&gt;Half-applied is worse than not applied&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;Since a partially-applied ruleset is the disaster case, atomicity isn’t a
nice-to-have. There must never be an instant where the block-everything rule
exists and the exemption doesn’t.&lt;&#x2F;p&gt;
&lt;p&gt;What I find genuinely satisfying is that all three platforms provide a way to
do this, and all three arrived at it independently.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Linux&lt;&#x2F;strong&gt; uses an &lt;code&gt;nft -f&lt;&#x2F;code&gt; file load, which applies as a single transaction.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;macOS&lt;&#x2F;strong&gt; uses PF anchors, and the sequencing is spelled out step by step:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;&#x2F; sequenced so the policy only ever becomes visible complete:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;&#x2F; (1) `pfctl -E` enables PF with a reference token — rules without&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;&#x2F; enforcement would be silent theater, PF ships disabled on macOS;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;&#x2F; (2) the whole ruleset lands in the anchor via one&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;&#x2F; `pfctl -a edr_isolate -f -` load (an anchor load is a single commit,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;&#x2F; the `nft -f` analog); (3) the main ruleset gets the anchor&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;&#x2F; attachment only if missing — attaching AFTER the load means the&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;&#x2F; first evaluation already sees the full policy;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Step 3 is the one to admire. The anchor is &lt;em&gt;populated first&lt;&#x2F;em&gt;, then attached
to the main ruleset. Attach first and there’s a window — however brief —
where an empty or half-loaded anchor is live. Ordering the operations so the
dangerous window never opens is cheaper and more reliable than trying to make
the window small.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Windows&lt;&#x2F;strong&gt; uses the Windows Filtering Platform:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;&#x2F; mutations run inside one FwpmTransaction — commit or nothing, the&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;&#x2F; `nft -f` atomicity.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Three operating systems, three unrelated APIs, one requirement. When a
constraint shows up identically across independent designs, it’s usually
telling you something true about the problem rather than about the tools.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;rules-don-t-apply-to-conversations-already-happening&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#rules-don-t-apply-to-conversations-already-happening&quot; aria-label=&quot;Anchor link for: rules-don-t-apply-to-conversations-already-happening&quot;&gt;Rules don’t apply to conversations already happening&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;Here’s the concept I’d most want a reader to take away, because it
generalizes far beyond firewalls and it’s counterintuitive until it isn’t.&lt;&#x2F;p&gt;
&lt;p&gt;A stateful firewall doesn’t evaluate rules for every packet. That would be
expensive. Instead, the first packet of a flow is evaluated against the rules,
and the verdict is cached in a &lt;strong&gt;state table&lt;&#x2F;strong&gt;. Subsequent packets of that
flow match the state and skip rule evaluation entirely.&lt;&#x2F;p&gt;
&lt;p&gt;Which means: &lt;strong&gt;changing the rules does not affect conversations already in
progress.&lt;&#x2F;strong&gt; You install a perfect block-everything ruleset, and the attacker’s
existing session sails straight through it, because that session’s packets
never reach the rules at all.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;&#x2F; first evaluation already sees the full policy; (4) `-F states`&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;&#x2F; flushes the state table, because PF consults states BEFORE rules —&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;&#x2F; a pre-isolation flow&amp;#39;s state would ferry C2 packets straight past&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;&#x2F; the block.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;You have to flush the state table. The ruleset is only half the enforcement;
the cached verdicts are the other half, and they’re older than your policy.&lt;&#x2F;p&gt;
&lt;p&gt;Windows has the same problem and solves it as a side effect, which is a
pleasant piece of luck:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;&#x2F; table and macOS anchor exactly. Adding&#x2F;deleting ALE filters makes&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;&#x2F; BFE reauthorize established connections, so live C2 dies at apply&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;&#x2F; with no extra work.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;WFP’s ALE (Application Layer Enforcement) layers reauthorize existing
connections when filters change. The state invalidation is automatic.&lt;&#x2F;p&gt;
&lt;p&gt;The general shape — &lt;em&gt;a policy change must invalidate the caches that were
built under the old policy&lt;&#x2F;em&gt; — is everywhere once you look. Session tokens
issued before a permission was revoked. Connection pools holding
credentials rotated an hour ago. CDN caches serving content you’ve since
made private. In every case the new rule is correct and completely bypassed,
because something old is short-circuiting the check. Ask, always: what
already decided this, before I changed the rules?&lt;&#x2F;p&gt;
&lt;h2 id=&quot;living-in-someone-else-s-house&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#living-in-someone-else-s-house&quot; aria-label=&quot;Anchor link for: living-in-someone-else-s-house&quot;&gt;Living in someone else’s house&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;An enterprise machine’s firewall is not yours. It has corporate policy,
maybe a VPN client, maybe &lt;code&gt;firewalld&lt;&#x2F;code&gt;, maybe Docker rewriting rules whenever
a container starts. You are a guest.&lt;&#x2F;p&gt;
&lt;p&gt;The pattern that makes this survivable is an owned namespace:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;&#x2F; The agent-owned nftables table (Linux). Created, flushed, and deleted&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;&#x2F; by name only — other tables (firewalld, docker) are never touched.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt;pub const&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97E72;&quot;&gt; TABLE_NAME&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt;: &amp;amp;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FE4450;&quot;&gt;str&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFFFFFEE;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF8B39;&quot;&gt; &amp;quot;edr_isolate&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;&#x2F; The agent-owned PF anchor (macOS) — the same owned-namespace idea as&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;&#x2F; the Linux table: loaded, listed, and flushed by name only. The main&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;&#x2F; ruleset is touched exactly once, to attach the anchor reference&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;&#x2F; (see render_pf_main_ruleset), never to edit other rules.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Windows gets the same treatment with a fixed provider and sublayer GUID. In
all three cases the rule is: everything the agent creates lives under one
name it owns, it only ever manipulates things under that name, and cleanup is
“delete my container” rather than “remove the rules I think I added.”&lt;&#x2F;p&gt;
&lt;p&gt;“Touched exactly once” is the discipline that makes it work. There’s one
unavoidable interaction with shared state — the anchor has to be &lt;em&gt;referenced&lt;&#x2F;em&gt;
from the main ruleset or it’s never consulted — and that single touch is
called out explicitly, so the exception stays an exception.&lt;&#x2F;p&gt;
&lt;p&gt;This is just namespacing, the same instinct that gives you Java packages and
Kubernetes namespaces. It’s worth naming as a general technique for
modifying systems you don’t own: &lt;strong&gt;claim a namespace, confine yourself to
it, and make removal a single operation.&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;deciding-what-a-reboot-means&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#deciding-what-a-reboot-means&quot; aria-label=&quot;Anchor link for: deciding-what-a-reboot-means&quot;&gt;Deciding what a reboot means&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;One more decision, easy to skip past, that is actually a policy question
disguised as an API flag:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;&#x2F; `nft -f` atomicity. The engine session is the default non-dynamic&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;&#x2F; one and no filter carries FWPM_FILTER_FLAG_PERSISTENT: containment&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;&#x2F; outlives the agent process but not a reboot, matching the Linux&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;&#x2F; table and macOS anchor exactly.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Should isolation survive a reboot?&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Yes&lt;&#x2F;strong&gt; is defensible: the machine was compromised, containment should be
durable, and an attacker who can reboot shouldn’t be able to escape it.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;No&lt;&#x2F;strong&gt; is what’s implemented, and the reasoning is about recoverability.
Isolation that persists across reboots means a mistake — a bad rule, a
resolution failure, a mis-clicked host — needs manual intervention on the
console. Isolation that dies at reboot means the worst case has a recovery
path any user can perform, and the agent can simply re-apply it on startup if
the host is genuinely still contained.&lt;&#x2F;p&gt;
&lt;p&gt;I don’t think there’s a universal right answer; it depends on whether you
fear attackers with reboot access more than you fear your own mistakes at 2am.
What matters is that the question got asked and answered &lt;em&gt;the same way on all
three platforms&lt;&#x2F;em&gt;. WFP filters can be persistent. The agent deliberately
doesn’t make them so, specifically to match the Linux and macOS behavior.&lt;&#x2F;p&gt;
&lt;p&gt;That’s the discipline that keeps cross-platform systems sane. Every platform
offers slightly different capabilities, and the temptation is to use whatever
each one provides. Do that and you get three subtly different products
wearing one name, and an operator who learned the behavior on Linux is
surprised by Windows during an incident. Choosing the &lt;em&gt;same&lt;&#x2F;em&gt; semantics
everywhere — even when it means declining a feature — is what makes fleet
behavior predictable.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;testing-the-thing-you-can-t-test&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#testing-the-thing-you-can-t-test&quot; aria-label=&quot;Anchor link for: testing-the-thing-you-can-t-test&quot;&gt;Testing the thing you can’t test&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;Last piece, and it’s the one that made this action tractable at all:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! Policy is separated from mechanism so the dangerous decision logic is&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! exhaustively unit-tested without ever touching a firewall:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;!   * `IsolateControl` — the OS primitive (atomically replace &#x2F; remove&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;!     the containment ruleset), one thin impl per platform.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;!   * `IsolateExecutor` — the pure guard&#x2F;rendering logic over any&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;!     `IsolateControl`, driven by a mock in tests.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The dangerous part of this feature isn’t the firewall call. It’s the
&lt;em&gt;decisions&lt;&#x2F;em&gt;: which exemptions to include, whether the backend resolved, what
to do about a malformed CIDR, what the ruleset text should say. All of that
is pure computation — inputs to strings — and pure computation can be tested
exhaustively, on any machine, in milliseconds, with no root and no risk.&lt;&#x2F;p&gt;
&lt;p&gt;So the rendering functions produce ruleset text that gets compared against
golden files, and the guard logic runs against a mock. The part that actually
touches the kernel shrinks to a thin layer whose only job is to hand a
prepared string to &lt;code&gt;nft&lt;&#x2F;code&gt;, &lt;code&gt;pfctl&lt;&#x2F;code&gt;, or BFE.&lt;&#x2F;p&gt;
&lt;p&gt;Which means most of this feature’s correctness was established on a Linux
laptop, including the Windows and macOS behavior. Not all of it — you still
need real drills to learn things like PF’s state-table ordering — but the
decision logic, which is where the bricking bugs live, is verified without a
firewall in sight.&lt;&#x2F;p&gt;
&lt;p&gt;That inversion, and how far you can push it when your target is a kernel you
cannot attach a debugger to, returns later in the series.&lt;&#x2F;p&gt;
&lt;p&gt;But first, the uncomfortable inverse of everything this series has covered so
far: not what the sensors see, but what they structurally cannot. That is
the subject of the next essay.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>The Authority Problem</title>
        <published>2026-08-26T00:00:00+00:00</published>
        <updated>2026-08-26T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://adasumizox.github.io/posts/04-the-authority-problem/"/>
        <id>https://adasumizox.github.io/posts/04-the-authority-problem/</id>
        
        <content type="html" xml:base="https://adasumizox.github.io/posts/04-the-authority-problem/">&lt;p&gt;&lt;em&gt;Building an EDR from scratch, essay 4 of 13.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Up to this point, everything in this series has been read-only. Sensors
observe, the pipeline delivers, the backend stores. If the whole system were
compromised tomorrow, the damage would be a serious data breach: an attacker
learns what runs on your machines.&lt;&#x2F;p&gt;
&lt;p&gt;Now we add the letter R. Response. Kill this process, quarantine that file,
cut this host off the network.&lt;&#x2F;p&gt;
&lt;p&gt;And the moment you do that, you have built something categorically
different. You have deployed, to every endpoint in the organization, a
privileged agent that accepts instructions from the network and carries them
out. That is a description of a botnet. The only thing separating your
security tool from the worst-case incident in your company’s history is the
answer to one question: &lt;strong&gt;how does an endpoint know that an instruction is
legitimate?&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Get that answer wrong and your EDR becomes the most efficient lateral
movement infrastructure an attacker could ask for — pre-installed
everywhere, running as root, already trusted, already exempt from your own
monitoring.&lt;&#x2F;p&gt;
&lt;p&gt;This essay is about the answer.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;two-questions-that-look-like-one&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#two-questions-that-look-like-one&quot; aria-label=&quot;Anchor link for: two-questions-that-look-like-one&quot;&gt;Two questions that look like one&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;The most common way to get this wrong is a confusion so natural that people
make it without noticing.&lt;&#x2F;p&gt;
&lt;p&gt;The agent connects to the backend over mutual TLS. Both sides present
certificates, both verify, the channel is encrypted and authenticated.
Excellent. Now a command arrives over that channel. Should the agent execute
it?&lt;&#x2F;p&gt;
&lt;p&gt;The tempting reasoning: the channel is authenticated, so the message came
from the backend, so the message is legitimate. Each step is true. The
conclusion does not follow — because “this message came from the backend” and
“a human being with authority decided this should happen” are &lt;em&gt;different
propositions&lt;&#x2F;em&gt;, and TLS only establishes the first.&lt;&#x2F;p&gt;
&lt;p&gt;Say it more carefully:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Transport security&lt;&#x2F;strong&gt; answers: &lt;em&gt;who am I talking to right now?&lt;&#x2F;em&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Authorization&lt;&#x2F;strong&gt; answers: &lt;em&gt;who decided this should happen, and were they
allowed to?&lt;&#x2F;em&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;A compromised backend passes the first test perfectly. It holds the
legitimate server key. Every TLS check succeeds. If the agent’s only gate is
“did this arrive over the authenticated channel,” then whoever owns the
backend owns every endpoint, instantly and totally.&lt;&#x2F;p&gt;
&lt;p&gt;And backends get compromised. They’re internet-adjacent, they run a stack of
dependencies, they’re administered by humans. Designing as though the
central server can never be breached is not a security posture, it’s a hope.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;designing-for-your-own-compromise&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#designing-for-your-own-compromise&quot; aria-label=&quot;Anchor link for: designing-for-your-own-compromise&quot;&gt;Designing for your own compromise&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;So the real design exercise is to assume the backend &lt;em&gt;is&lt;&#x2F;em&gt; compromised and
ask what an attacker can then do.&lt;&#x2F;p&gt;
&lt;p&gt;The answer this system gives is stated in the first paragraph of
&lt;code&gt;backend&#x2F;src&#x2F;tasking.rs&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! The backend is a RELAY, never an authority (see proto AgentTasking): it&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! decodes each SignedTask payload ONLY to read agent_id (routing) and&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! task_id (dedupe&#x2F;ack) — it never inspects the signature, so it holds no&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! signing key and cannot mint a task an agent will run. All verification&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! is the agent&amp;#39;s (crate::task on the shipper side).&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Tasks are signed by an operator with an Ed25519 key. The backend never has
that key. It cannot create a valid task, cannot modify one without breaking
the signature, and — this is the elegant part — &lt;em&gt;cannot even verify one&lt;&#x2F;em&gt;,
because it has no reason to.&lt;&#x2F;p&gt;
&lt;p&gt;Note the deliberate refusal in “it never inspects the signature.” A backend
that verified signatures would be no less secure, but it would blur the
architecture: someone would eventually add a “trusted internal task” path
for convenience, and the property would quietly die. Making the backend
structurally incapable of the check keeps the boundary honest.&lt;&#x2F;p&gt;
&lt;p&gt;So what &lt;em&gt;can&lt;&#x2F;em&gt; a compromised backend do? Exactly three things: withhold
tasks, delay tasks, and replay tasks it has already seen. That is a real
attack surface — a backend that silently drops every quarantine command is a
serious problem — but it is a &lt;strong&gt;denial&lt;&#x2F;strong&gt; surface, not an &lt;strong&gt;execution&lt;&#x2F;strong&gt;
surface. The attacker can prevent responses. They cannot cause them.&lt;&#x2F;p&gt;
&lt;p&gt;That distinction is the entire point of the design, and it generalizes: when
you’re deciding where to put a security check, ask what each component can do
if it’s fully owned. Components that &lt;em&gt;can’t&lt;&#x2F;em&gt; do damage need less protection
than components that can. Here, the backend was demoted from a trusted
authority to an untrusted courier, and the amount of trust the system needs
to place in its most exposed component dropped to nearly zero.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;claims-versus-proof&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#claims-versus-proof&quot; aria-label=&quot;Anchor link for: claims-versus-proof&quot;&gt;Claims versus proof&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;The second structural idea concerns identity, and it has the same flavor:
never accept an assertion when you can require a proof.&lt;&#x2F;p&gt;
&lt;p&gt;Every message on the wire contains an &lt;code&gt;agent_id&lt;&#x2F;code&gt; field. An agent connecting
to the task stream says “I am &lt;code&gt;web-07&lt;&#x2F;code&gt;, give me my tasks.” The obvious
implementation reads the field and routes accordingly, and it is trivially
exploitable — any enrolled agent can claim to be any other agent and receive
its commands.&lt;&#x2F;p&gt;
&lt;p&gt;The fix is to treat the field as a &lt;em&gt;claim&lt;&#x2F;em&gt; that must match a &lt;em&gt;proof&lt;&#x2F;em&gt;, and
the proof is the client certificate. From
&lt;code&gt;backend&#x2F;src&#x2F;authn.rs&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;!   edr-agent:&#x2F;&#x2F;&amp;lt;agent_id&amp;gt;    an agent; may publish telemetry and open a&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;!                             task stream only as &amp;lt;agent_id&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;!   edr-operator:&#x2F;&#x2F;&amp;lt;name&amp;gt;     an operator; may submit tasks (&amp;lt;name&amp;gt; is&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;!                             for audit logging)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The certificate carries a URI in its Subject Alternative Name, and the URI
scheme &lt;em&gt;is&lt;&#x2F;em&gt; the role. An &lt;code&gt;edr-agent:&#x2F;&#x2F;&lt;&#x2F;code&gt; certificate may open a task stream —
but only for the exact &lt;code&gt;agent_id&lt;&#x2F;code&gt; written into its own SAN. An
&lt;code&gt;edr-operator:&#x2F;&#x2F;&lt;&#x2F;code&gt; certificate may submit tasks and nothing else. An enrolled
agent, even a fully compromised one, cannot submit tasks to other agents,
because it doesn’t hold a certificate with the operator scheme.&lt;&#x2F;p&gt;
&lt;p&gt;Two details in that module are worth dwelling on because they’re the kind of
choice that separates a design from an implementation.&lt;&#x2F;p&gt;
&lt;p&gt;The first is &lt;em&gt;why the SAN rather than the Common Name&lt;&#x2F;em&gt;, which is where most
people’s instinct goes:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! Identity is a URI Subject Alternative Name, never the Subject CN: a DN&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! may carry several CNs in several string encodings (RFC 6125 deprecates&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! CN for name binding), while a scheme-scoped URI SAN is unambiguous and&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! cannot collide with the DNS SANs of a server or web-PKI cert.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;A Distinguished Name can contain multiple CN components, in several string
encodings, and different libraries will hand you different ones. An
identifier that different parsers read differently is not an identifier —
it’s a vulnerability with a waiting period. The web PKI learned this
expensively enough that RFC 6125 deprecated the practice. A scheme-scoped
URI has exactly one reading.&lt;&#x2F;p&gt;
&lt;p&gt;The second is what happens when a certificate is &lt;em&gt;ambiguous&lt;&#x2F;em&gt;:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! The leaf must carry EXACTLY one EDR-scheme SAN: zero means the cert&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! was not enrolled as an EDR identity, two or more is a misissued cert&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! that could act as either — both are refused rather than guessed at.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;A certificate holding both an agent SAN and an operator SAN is refused
outright. Not “prefer the more specific one,” not “take the first” — refused.
Because a cert like that can only exist through an enrollment bug or an
attack, and in either case the correct response to “I cannot tell what this
is” is to stop. Every “pick a sensible default when the input is ambiguous”
in a security path is a place where an attacker gets to choose which
interpretation you take.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;burning-a-key-not-banning-a-name&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#burning-a-key-not-banning-a-name&quot; aria-label=&quot;Anchor link for: burning-a-key-not-banning-a-name&quot;&gt;Burning a key, not banning a name&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;Certificates get stolen. So there has to be a way to say “that credential is
no longer valid,” and the choice of &lt;em&gt;what&lt;&#x2F;em&gt; you revoke turns out to encode a
philosophy.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! The list is keyed on the certificate SERIAL (the standard CRL key),&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! not the identity: &amp;quot;this key is burned&amp;quot;, so recovery is revoke the old&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! serial and mint a fresh cert for the same agent_id.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Revoking the &lt;em&gt;serial&lt;&#x2F;em&gt; says “this particular key is compromised.” Revoking
the &lt;em&gt;identity&lt;&#x2F;em&gt; would say “this host is banned.” They diverge exactly when it
matters: a laptop is stolen, you revoke, IT reimages the machine and
reissues it. Under serial-keyed revocation, the new certificate for the same
&lt;code&gt;agent_id&lt;&#x2F;code&gt; just works, because it’s a different key. Under identity-keyed
revocation, you’ve permanently poisoned a hostname and now you’re renaming
machines to work around your own security tool.&lt;&#x2F;p&gt;
&lt;p&gt;The general form: &lt;strong&gt;revoke the credential, not the principal.&lt;&#x2F;strong&gt; Compromise
is a property of a secret, not of the person or machine that held it.&lt;&#x2F;p&gt;
&lt;p&gt;There’s also a nicely pragmatic touch — serials are compared in canonical
form, lowercase hex with leading zeros stripped, “so &lt;code&gt;openssl x509 -serial -noout&lt;&#x2F;code&gt; output pastes straight into the file.” Revocation is an emergency
procedure performed by a stressed human at an unpleasant hour. A format
mismatch that silently fails to revoke is a security control defeated by
usability. Meeting the operator where they already are is a security
property.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;a-valid-signature-is-not-a-fresh-authorization&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#a-valid-signature-is-not-a-fresh-authorization&quot; aria-label=&quot;Anchor link for: a-valid-signature-is-not-a-fresh-authorization&quot;&gt;A valid signature is not a fresh authorization&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;Now the deepest problem in the essay, and the one I found genuinely
surprising.&lt;&#x2F;p&gt;
&lt;p&gt;Signatures don’t expire. A message signed today verifies identically in five
years — that’s what makes them useful. But it means an attacker who captures
a signed message holds something permanently valid. Replay is therefore not
an edge case; it’s the natural consequence of how signatures work.&lt;&#x2F;p&gt;
&lt;p&gt;Worse, in this system, replay is also &lt;em&gt;completely normal traffic&lt;&#x2F;em&gt;. Delivery
is at-least-once, so honest duplicates arrive routinely. The agent cannot
treat repetition as an attack, because repetition is the protocol working
correctly.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;shipper&#x2F;src&#x2F;task.rs&lt;&#x2F;code&gt; answers with a sequence of
checks, each catching what the previous one can’t:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! Every SignedTask must pass, in order: Ed25519 signature over the raw&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! payload bytes against the pinned operator key (the backend only&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! relays — it can drop or delay tasks, never mint them), payload decode,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! agent binding (a task signed for another host is a cross-host replay),&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! freshness of issued_at (temporal replay; the window is symmetric&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! because agent clocks skew both ways), and task_id dedupe (at-least-once&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! delivery makes benign re-sends normal; the same set stops malicious&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! same-window replays).&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Signature: was this authored by the key holder. Agent binding: was it
authored &lt;em&gt;for this host&lt;&#x2F;em&gt; — otherwise a task captured from one machine
replays against another. Freshness: was it authored recently, bounding how
long a captured task stays dangerous. Dedupe: have I already done this one.&lt;&#x2F;p&gt;
&lt;p&gt;Four checks, four distinct attacks, none redundant. And the freshness window
is symmetric because agent clocks drift in both directions — a detail that
sounds pedantic until a host with a fast clock rejects every legitimate task
it receives.&lt;&#x2F;p&gt;
&lt;p&gt;Then comes the case that breaks the whole scheme, and it took me a while to
even see it. From &lt;code&gt;shipper&#x2F;src&#x2F;config.rs&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! The policy lives INSIDE the signed payload, so the backend can withhold&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! or delay policy but never mint it. The central hazard is stale-policy&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! replay: a captured OLDER policy under a fresh task_id&#x2F;issued_at is&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! still validly signed, so the task-level replay defenses (dedupe,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! freshness) do not cover it.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Read that twice. Configuration policy is signed. An attacker captures a
policy document from six months ago — one that, say, monitored fewer
registry keys, or set a heartbeat interval so long the agent looks alive
while blind. They can’t modify it; the signature would break. But they don’t
need to. They wrap that genuinely-signed old policy in a &lt;em&gt;fresh envelope&lt;&#x2F;em&gt;
with a new task ID and a current timestamp.&lt;&#x2F;p&gt;
&lt;p&gt;Every defense passes. The signature is real. The agent binding is right. The
timestamp is current. The task ID has never been seen. The agent applies a
six-month-old policy, and the attacker has rolled back its configuration
using nothing but valid, correctly-signed messages.&lt;&#x2F;p&gt;
&lt;p&gt;The envelope was fresh. The &lt;em&gt;content&lt;&#x2F;em&gt; was ancient. Freshness of transmission
tells you nothing about currency of intent.&lt;&#x2F;p&gt;
&lt;p&gt;The fix has to live in the content’s own semantics:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! freshness) do not cover it. `AgentPolicy.version` monotonicity is the&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! identity guard: only version &amp;gt; current applies; version == current with&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! identical content is success (goal state met — idempotence under&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! at-least-once, surviving even a restart that emptied the result cache);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! equal version with different content, or any lower version, refuses.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The policy carries a version, and the agent only ever moves forward. An old
policy is refused on the strength of its own contents, no matter how fresh
its wrapper.&lt;&#x2F;p&gt;
&lt;p&gt;The general lesson is one I now apply everywhere: &lt;strong&gt;a signature proves
authorship, not currency.&lt;&#x2F;strong&gt; If a document’s &lt;em&gt;age&lt;&#x2F;em&gt; matters to its safety, the
document itself must say how old it is, in terms the recipient can order.
You cannot bolt freshness onto the transport and expect it to protect the
payload.&lt;&#x2F;p&gt;
&lt;p&gt;Notice too the treatment of the equal-version case. Same version, identical
content: success. Because under at-least-once delivery the agent will
genuinely receive the same policy twice, and the honest question isn’t “did I
process this message” but “is the world in the state this message asks for.”
Idempotence expressed as goal-state rather than as message-history — which
means it survives a restart that wiped every cache.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;trusting-your-own-disk-carefully&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#trusting-your-own-disk-carefully&quot; aria-label=&quot;Anchor link for: trusting-your-own-disk-carefully&quot;&gt;Trusting your own disk, carefully&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;One last move, because it inverts a rule the rest of the system follows
strictly.&lt;&#x2F;p&gt;
&lt;p&gt;Policy has to survive restarts, so it’s written to disk. Disk is not a trust
boundary — an attacker on the host can edit files. What should the agent do
at startup if the stored policy fails verification?&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! validity. Disk is untrusted storage: tamper means the load refuses and&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! the agent runs baked defaults at version 0 — never a startup failure,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! because bricking the sensor over a corrupt config file trades&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! availability for nothing (an attacker with store write access already&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! owns the host).&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Everywhere else in this system, bad security configuration means &lt;em&gt;fail to
start&lt;&#x2F;em&gt;. Here it means fall back to defaults and keep running. The reasoning
is a threat-model argument rather than a taste argument: the attacker who
can corrupt that file already has host write access. Refusing to start
doesn’t deny them anything — it just kills your telemetry, which is a gift.
Running on safe defaults keeps the sensor reporting, from a host you now have
reason to look at closely.&lt;&#x2F;p&gt;
&lt;p&gt;The same header notes that load deliberately skips the freshness check,
“because freshness gates admission over the NETWORK; a policy this agent
itself admitted and persisted is durable state, not a new admission.” Which
is exactly the transport-versus-content distinction again, seen from the
other side: freshness was always a property of &lt;em&gt;accepting a message&lt;&#x2F;em&gt;, never
of &lt;em&gt;holding a fact&lt;&#x2F;em&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-authority-actually-requires&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#what-authority-actually-requires&quot; aria-label=&quot;Anchor link for: what-authority-actually-requires&quot;&gt;What authority actually requires&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;Strip it down and the same principle produces every decision here.&lt;&#x2F;p&gt;
&lt;p&gt;Authority must be &lt;strong&gt;provable at the point of action&lt;&#x2F;strong&gt;. Not asserted at the
point of transmission, not implied by the channel, not inherited from a
component’s position in the architecture. The agent — the thing that will
actually terminate the process — has to be able to verify, from the message
in front of it and a key it already holds, that someone with the right to
order this ordered it, for this host, recently, and that it hasn’t already
been done.&lt;&#x2F;p&gt;
&lt;p&gt;Everything else follows. The backend is a relay because a courier needs no
authority. Identity comes from certificates because claims need proofs.
Revocation targets serials because compromise is a property of secrets.
Policy carries a version because signatures don’t age.&lt;&#x2F;p&gt;
&lt;p&gt;And the payoff is that you can say something concrete about the worst case.
If the backend falls, the attacker gets to see telemetry and to withhold
responses. They do not get to run commands on ten thousand machines. For a
system with this much power distributed this widely, being able to state that
bound — and point at the code that enforces it — is the difference between a
security tool and a liability.&lt;&#x2F;p&gt;
&lt;p&gt;Next comes the action with the largest blast radius of all: cutting a
machine off the network, over the network, without cutting the connection
you would need to undo it.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Naming Things That Move</title>
        <published>2026-08-19T00:00:00+00:00</published>
        <updated>2026-08-19T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://adasumizox.github.io/posts/03-naming-things-that-move/"/>
        <id>https://adasumizox.github.io/posts/03-naming-things-that-move/</id>
        
        <content type="html" xml:base="https://adasumizox.github.io/posts/03-naming-things-that-move/">&lt;p&gt;&lt;em&gt;Building an EDR from scratch, essay 3 of 13.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;An analyst looks at an alert and clicks &lt;em&gt;kill this process&lt;&#x2F;em&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Between that click and the signal, a few hundred milliseconds pass. In
those milliseconds the process exits on its own — it was a short-lived
script, it was always going to exit — and the kernel, being a good steward
of a small resource, hands its process ID to the next thing that asks. On a
busy machine that might be a database backend, a session manager, or the
SSH daemon holding the connection your incident responder is sitting in.&lt;&#x2F;p&gt;
&lt;p&gt;The signal arrives. It is delivered, correctly and successfully, to
something that has nothing to do with the alert.&lt;&#x2F;p&gt;
&lt;p&gt;This is not an exotic race. It’s the ordinary consequence of a fact so
familiar that it hides in plain sight: &lt;strong&gt;a PID is not an identity. It’s a
name, drawn from a small pool, that gets recycled.&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Endpoint telemetry is full of this. Almost everything you want to talk
about is short-lived, and almost every name you have for those things is
either recycled, mutable, or both. Getting this wrong doesn’t produce a
crash — it produces a system that is confidently, quietly, wrong about
which thing it’s discussing. This essay is about the general shape of the
problem and the handful of moves that solve it.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-makes-a-name-an-identity&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#what-makes-a-name-an-identity&quot; aria-label=&quot;Anchor link for: what-makes-a-name-an-identity&quot;&gt;What makes a name an identity&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;Three properties separate a usable identity from a mere label:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Unique&lt;&#x2F;strong&gt; at any instant. Two live things never share it.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Stable&lt;&#x2F;strong&gt; for the lifetime of what it names. It doesn’t change underneath
you.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Non-recycled&lt;&#x2F;strong&gt; across lifetimes. Once a thing is gone, its identity is
never issued to a different thing.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;PIDs have the first two and spectacularly fail the third. Filesystem paths
fail the second &lt;em&gt;and&lt;&#x2F;em&gt; the third — a path is a lookup, resolved fresh every
time you use it, and someone else can change what it resolves to between
your two uses. Names that fail the third property are the dangerous ones,
because failure looks like success: you get an answer, it’s just about a
different object than you meant.&lt;&#x2F;p&gt;
&lt;p&gt;The recurring fix, in every case below, is the same: &lt;strong&gt;pair the unrecycled
name with something that pins down &lt;em&gt;which&lt;&#x2F;em&gt; lifetime you meant.&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-pair-that-doesn-t-recycle&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-pair-that-doesn-t-recycle&quot; aria-label=&quot;Anchor link for: the-pair-that-doesn-t-recycle&quot;&gt;The pair that doesn’t recycle&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;For processes, the second half of the pair is the start time. PIDs cycle;
the &lt;em&gt;combination&lt;&#x2F;em&gt; of a PID and the moment that PID began doesn’t, because
time doesn’t run backwards.&lt;&#x2F;p&gt;
&lt;p&gt;The kill action in &lt;code&gt;shipper&#x2F;src&#x2F;kill.rs&lt;&#x2F;code&gt; builds its
entire safety story on this:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! PID reuse is the whole problem (see proto KillProcessAction): the task&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! carries the intended victim&amp;#39;s start time, and a kill proceeds only if the&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! LIVE process at that pid has a matching start time (within tolerance).&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! A recycled pid always starts later than the one the operator saw, so it&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! never matches — the guard refuses instead of killing an innocent process.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The operator’s task doesn’t say “kill PID 4823.” It says “kill PID 4823,
which started at 14:22:07.431.” Before signalling, the agent reads the start
time of whatever is &lt;em&gt;currently&lt;&#x2F;em&gt; living at 4823 and compares. Mismatch means
the thing you meant is already gone, and the correct action is to refuse.&lt;&#x2F;p&gt;
&lt;p&gt;Note the asymmetry that makes this airtight: a recycled PID always starts
&lt;em&gt;later&lt;&#x2F;em&gt; than the one the operator observed. The comparison can never be
fooled by a reused PID, because reuse only ever moves the timestamp forward,
past any plausible measurement error.&lt;&#x2F;p&gt;
&lt;p&gt;Which is what licenses something that would otherwise look sloppy:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;&#x2F; How far a live process&amp;#39;s start time may differ from the requested one and&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;&#x2F; still count as the same process. Absorbs the observation-vs-start skew and&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;&#x2F; cross-platform rounding; it does NOT need to distinguish reuse, since&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;&#x2F; reuse moves the start time by far more than any measurement jitter.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt;const&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97E72;&quot;&gt; DEFAULT_TOLERANCE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FE4450;&quot;&gt; Duration&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFFFFFEE;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FE4450;&quot;&gt; Duration&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #36F9F6;&quot;&gt;from_secs&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97E72;&quot;&gt;2&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;A fuzzy comparison in a security guard should make you nervous. It doesn’t
here, and the comment explains exactly why: the tolerance absorbs &lt;em&gt;jitter&lt;&#x2F;em&gt;,
and the threat it defends against operates at a completely different
magnitude. Nobody’s PID gets recycled within two seconds &lt;em&gt;and&lt;&#x2F;em&gt; lands within
two seconds of the original start time. The signal and the noise live orders
of magnitude apart, so a sloppy-looking threshold is in fact a precise one.&lt;&#x2F;p&gt;
&lt;p&gt;That’s a reusable habit for any threshold you’re tempted to add: don’t ask
“is this value right,” ask “what’s the ratio between what I’m tolerating and
what I’m detecting.” If it’s small, you don’t have a threshold problem, you
have a design problem.&lt;&#x2F;p&gt;
&lt;p&gt;There’s a subtler trap underneath, and it’s the kind of thing that only
shows up in production. A start time is meaningless without knowing &lt;em&gt;which
clock&lt;&#x2F;em&gt; produced it. Linux offers several — &lt;code&gt;CLOCK_REALTIME&lt;&#x2F;code&gt; jumps when NTP
corrects it, &lt;code&gt;CLOCK_MONOTONIC&lt;&#x2F;code&gt; doesn’t count suspend, &lt;code&gt;CLOCK_BOOTTIME&lt;&#x2F;code&gt;
does. If the sensor stamps the event from one clock family and the kill
guard reads the live process from another, your comparison drifts by
whatever the difference happens to be that day, and your guard starts
refusing legitimate kills after an NTP correction. The sensor here reads
&lt;code&gt;task-&amp;gt;start_boottime&lt;&#x2F;code&gt; and applies the REALTIME−BOOTTIME offset precisely so
both sides of the comparison end up in the same family. Two numbers being
“a timestamp” is not enough for them to be comparable.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;a-path-is-a-question-not-an-answer&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#a-path-is-a-question-not-an-answer&quot; aria-label=&quot;Anchor link for: a-path-is-a-question-not-an-answer&quot;&gt;A path is a question, not an answer&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;Files have the same disease in a more virulent form.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;&#x2F;tmp&#x2F;payload.bin&lt;&#x2F;code&gt; is not a file. It’s a &lt;em&gt;query&lt;&#x2F;em&gt; — instructions for
traversing directories to find an inode — and it’s re-evaluated every single
time you use it. Between your &lt;code&gt;stat()&lt;&#x2F;code&gt; and your &lt;code&gt;open()&lt;&#x2F;code&gt;, anything with
write access to any directory along that path can change the answer. That’s
the TOCTOU (time-of-check to time-of-use) family, and for a security tool
it’s not a theoretical concern: quarantining a file is precisely the moment
an attacker most wants to redirect you.&lt;&#x2F;p&gt;
&lt;p&gt;Imagine quarantine done naively. Hash the path to confirm it’s the malicious
file. Then move the path into the quarantine store. In between, the attacker
replaces the path with a symlink to &lt;code&gt;&#x2F;etc&#x2F;shadow&lt;&#x2F;code&gt;. You have just used your
elevated privileges to hash a known-bad file and quarantine a critical
system file. Your logs will say the operation succeeded.&lt;&#x2F;p&gt;
&lt;p&gt;The fix is to stop using the name after the first lookup. From
&lt;code&gt;shipper&#x2F;src&#x2F;quarantine.rs&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;!   * `FileControl` — the OS primitives (pin a file to one inode&#x2F;object,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;and, concretely:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt;                .&lt;&#x2F;span&gt;&lt;span style=&quot;color: #36F9F6;&quot;&gt;custom_flags&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FE4450;&quot;&gt;libc&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FE4450;&quot;&gt;O_NOFOLLOW&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt; |&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FE4450;&quot;&gt; libc&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FE4450;&quot;&gt;O_CLOEXEC&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;O_NOFOLLOW&lt;&#x2F;code&gt; refuses to traverse a final symlink — so the swap-a-symlink
attack fails at the door rather than succeeding silently. And once that
&lt;code&gt;open&lt;&#x2F;code&gt; returns, the file descriptor &lt;em&gt;is&lt;&#x2F;em&gt; the identity. It refers to one
inode. Renaming the path doesn’t move it, deleting the path doesn’t
invalidate it, and no amount of directory manipulation can make it point
somewhere else. Every subsequent operation goes through the descriptor:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;            &#x2F;&#x2F; fchmod on the pinned inode: a path swap cannot redirect it.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;fchmod&lt;&#x2F;code&gt; rather than &lt;code&gt;chmod&lt;&#x2F;code&gt;. &lt;code&gt;fstat&lt;&#x2F;code&gt; rather than &lt;code&gt;stat&lt;&#x2F;code&gt;. The pattern is
consistent, and once you see it you’ll see it everywhere in well-written
systems code: &lt;strong&gt;resolve the name exactly once, then work with the handle.&lt;&#x2F;strong&gt;
The handle is the identity; the path was only ever a way to find it.&lt;&#x2F;p&gt;
&lt;p&gt;The same code then does something I want to highlight because it’s a habit
worth stealing:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;                &#x2F;&#x2F; instant; prove the blob IS our pinned inode.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Having moved the file into the store, it &lt;em&gt;verifies&lt;&#x2F;em&gt; that the object now in
the store is the same object it pinned — and rolls back if not. It doesn’t
assume the operation it just performed did what it intended. In a system
where an adversary is actively trying to make your operations mean something
other than what you meant, that distinction between “I issued the correct
call” and “the correct thing happened” is the whole game.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;identity-by-content&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#identity-by-content&quot; aria-label=&quot;Anchor link for: identity-by-content&quot;&gt;Identity by content&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;Sometimes you don’t want to identify a &lt;em&gt;file&lt;&#x2F;em&gt; at all — you want to identify
a &lt;em&gt;payload&lt;&#x2F;em&gt;, wherever it happens to live. The same malware dropped at ten
paths on ten machines is one thing worth tracking, not ten.&lt;&#x2F;p&gt;
&lt;p&gt;That’s what a cryptographic hash gives you: an identity derived from
content rather than location. Two files with the same SHA-256 are the same
payload, no matter what they’re named or which host they’re on.&lt;&#x2F;p&gt;
&lt;p&gt;The schema in &lt;code&gt;backend&#x2F;schema&#x2F;001_events.sql&lt;&#x2F;code&gt;
makes a small decision here that reveals something about designing for
consumers rather than for storage:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;    -- agent-computed sha256 of the executable image, lowercase hex;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;    -- &amp;#39;&amp;#39; = not hashed (EXIT events, vanished file, over the size cap,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;    -- or a sensor without enrichment). Hex, not raw: IOC lists join&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;    -- directly in SQL.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Raw bytes would be half the size. Hex is stored anyway, because every threat
intelligence feed on earth distributes hashes as lowercase hex, and an
analyst with a list of indicators should be able to write a join and get an
answer — not first write a conversion. Sixteen extra bytes per row buys
frictionless use of the column for its actual purpose. Storage efficiency
that makes data harder to use is a false economy.&lt;&#x2F;p&gt;
&lt;p&gt;Note also that &lt;code&gt;&#x27;&#x27;&lt;&#x2F;code&gt; means “no hash,” and the comment enumerates the reasons
why. Which brings us to the theme that turns out to matter most of all.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;absence-is-not-zero&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#absence-is-not-zero&quot; aria-label=&quot;Anchor link for: absence-is-not-zero&quot;&gt;Absence is not zero&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;If a value can be missing, its absence must be &lt;em&gt;representable&lt;&#x2F;em&gt; and must not
be confusable with any real value. This sounds obvious. It is violated
constantly, usually by accident, usually via a language’s default value.&lt;&#x2F;p&gt;
&lt;p&gt;The uid column is the sharpest example:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;    -- acting process&amp;#39;s real&#x2F;effective uid; 4294967295 = not captured&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;    -- (pre-uid WAL replay or platform without parity). uid 0 IS root,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;    -- so absence must never look like it — rules must exclude the&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;    -- sentinel explicitly.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The obvious default for a missing integer is 0. For a uid, 0 is &lt;strong&gt;root&lt;&#x2F;strong&gt;.
Default an unknown user to 0 and every event from a sensor that couldn’t
determine the user now claims to be a root action. Your detection rules —
which quite reasonably treat root activity as more interesting — light up
on a field that means “we don’t know.” You’d have manufactured a false
positive generator out of a language default.&lt;&#x2F;p&gt;
&lt;p&gt;The same reasoning appears for timestamps, and the choice of sentinel is
neatly argued:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;    -- identity (pids recycle, the pair doesn&amp;#39;t). 1970-01-01 00:00:00 =&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;    -- not captured (pre-field agent) — no real process starts at the&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;    -- epoch, so the zero default is unambiguous, unlike uid.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Epoch-0 is safe as a sentinel &lt;em&gt;because no real process starts then&lt;&#x2F;em&gt;. Zero is
unsafe for uid &lt;em&gt;because uid 0 is real and important&lt;&#x2F;em&gt;. Identical mechanism,
opposite verdicts — and the difference is entirely about the semantics of
the domain, not about the type. There is no general rule like “use 0 for
missing” or “use -1 for missing.” You have to look at what the values mean
and pick something outside the real range.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;in-kernel-or-not-at-all&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#in-kernel-or-not-at-all&quot; aria-label=&quot;Anchor link for: in-kernel-or-not-at-all&quot;&gt;In-kernel or not at all&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;Container attribution is where identity gets genuinely hard, and where the
solution is instructive.&lt;&#x2F;p&gt;
&lt;p&gt;The naive approach: an event arrives with a PID; read
&lt;code&gt;&#x2F;proc&#x2F;&amp;lt;pid&amp;gt;&#x2F;cgroup&lt;&#x2F;code&gt;; parse out the container. It works when you test it.
It fails in production for a reason you’ve already seen twice in this essay
— by the time you read &lt;code&gt;&#x2F;proc&lt;&#x2F;code&gt;, the process may be gone, and the PID may
belong to something else. Your container attribution is now not just missing
but &lt;em&gt;wrong&lt;&#x2F;em&gt;, and short-lived processes are exactly the ones an attacker
uses.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;shipper&#x2F;src&#x2F;cgroup.rs&lt;&#x2F;code&gt; refuses that whole
approach:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! module turns that number into the runtime&amp;#39;s container id.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;!&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! Why the inode: on cgroup v2 the id the kernel returns equals the cgroup&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! directory&amp;#39;s inode (its kernfs node id — a full 64-bit ino since 5.5;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! the agent&amp;#39;s BTF-enabled floor sits there). So the mapping is a scan of&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! &#x2F;sys&#x2F;fs&#x2F;cgroup for the directory whose inode matches — keyed on the&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! stable id, never on a pid that may already be gone (the whole reason&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! the id is stamped in-kernel rather than re-read from &#x2F;proc).&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The eBPF probe calls &lt;code&gt;bpf_get_current_cgroup_id()&lt;&#x2F;code&gt; &lt;em&gt;at event time, in
kernel context, while the process is definitionally alive&lt;&#x2F;em&gt;. That number
rides along in the event. Resolution to a human-meaningful container id
happens later, in user space, at leisure — because unlike the PID, the
cgroup id is stable and doesn’t recycle out from under you.&lt;&#x2F;p&gt;
&lt;p&gt;This is the general principle, and it’s the most portable lesson in the
essay: &lt;strong&gt;capture identity at the moment of the event, in the context where
it’s unambiguous. Resolve it to something readable whenever you like.&lt;&#x2F;strong&gt;
Deferring &lt;em&gt;capture&lt;&#x2F;em&gt; is a correctness bug. Deferring &lt;em&gt;resolution&lt;&#x2F;em&gt; is just
scheduling.&lt;&#x2F;p&gt;
&lt;p&gt;There’s also a small piece of Linux trivia in there that I enjoy far more
than I should. The cgroup id &lt;em&gt;equals the directory’s inode number&lt;&#x2F;em&gt;, because
cgroups are backed by kernfs and the “id” is just the kernfs node id. That’s
not a documented mapping so much as a consequence of how the thing is built
— which means the resolver can find the cgroup by scanning for a matching
inode, with no parsing and no API. Understanding the implementation gave a
cleaner solution than the interface advertised.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;making-identity-durable&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#making-identity-durable&quot; aria-label=&quot;Anchor link for: making-identity-durable&quot;&gt;Making identity durable&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;The final move is to take these carefully-constructed identities and turn
them into something you can query, which is where
&lt;code&gt;backend&#x2F;schema&#x2F;006_process_index.sql&lt;&#x2F;code&gt;
comes in:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;-- Backend-materialized process index: one row per process identity&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;-- (agent_id, pid, start_time) — pids recycle, the pair doesn&amp;#39;t.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Two events describe a process — its creation and its exit — and they may
arrive out of order, or twice, or hours apart if a WAL replayed. The index
has to fold them into one row regardless:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;-- Replay contract: an insert deduplicated by token skips the view too. A&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;-- replay that outlives the token window re-feeds the view, so every&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;-- column is an idempotent fold (min&#x2F;max) — replaying a byte-identical&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;-- row cannot change the folded state. Out-of-order arrival (exit before&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;-- create, WAL replay) folds the same way: merges are order-blind.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Every column folds with &lt;code&gt;min&lt;&#x2F;code&gt; or &lt;code&gt;max&lt;&#x2F;code&gt;. Those operations are commutative,
associative, and idempotent — which is the algebraic way of saying that
order doesn’t matter, grouping doesn’t matter, and repetition doesn’t
matter. Given those three properties, “exit arrived before create” and “this
batch replayed four times” stop being cases you handle and become cases that
cannot arise.&lt;&#x2F;p&gt;
&lt;p&gt;And the sentinel discipline from earlier shows up again, now doing real
work:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;-- uid&#x2F;euid fold with min so the 4294967295 absence sentinel loses to&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;-- any captured value — and 0 still IS root, never absence.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Because absence is &lt;code&gt;4294967295&lt;&#x2F;code&gt; — the &lt;em&gt;largest&lt;&#x2F;em&gt; possible u32 — folding with
&lt;code&gt;min&lt;&#x2F;code&gt; means any real value beats it automatically. The choice of sentinel
and the choice of fold operator were made together, and the result is that
“prefer a real value over a missing one” needs no conditional logic at all.
It’s a consequence of arithmetic.&lt;&#x2F;p&gt;
&lt;p&gt;That’s the thing I’d point at if someone asked what good data modeling looks
like. Not the absence of special cases, but special cases that dissolve into
the structure so completely that there’s nothing left to get wrong.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-tax-and-the-return&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-tax-and-the-return&quot; aria-label=&quot;Anchor link for: the-tax-and-the-return&quot;&gt;The tax and the return&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;Look at what all this cost. Every process reference carries a timestamp.
Every file operation carries a descriptor. Every event carries a cgroup id
stamped in kernel context. Every nullable column carries an explicitly
chosen sentinel and a comment defending it. It’s more fields, more plumbing,
more to explain.&lt;&#x2F;p&gt;
&lt;p&gt;What it buys is that a whole category of bug becomes impossible rather than
unlikely. Not “we tested it and didn’t see the race” — actually impossible,
because a recycled PID cannot match a start time, a descriptor cannot be
redirected by a rename, and a &lt;code&gt;min&lt;&#x2F;code&gt;-fold cannot be disturbed by a replay.&lt;&#x2F;p&gt;
&lt;p&gt;Which matters especially here, because the next thing this system does with
these identities is act on them: terminate the process, quarantine the file,
cut the machine off the network. Once you’re taking destructive action on a
remote host, “we’re pretty sure this is the right process” isn’t a standard
anyone can live with.&lt;&#x2F;p&gt;
&lt;p&gt;And that raises a question this essay has been carefully avoiding: who gets
to decide that the action happens at all? That is the authority problem in
the next essay.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>The Shape of a Pipeline</title>
        <published>2026-08-12T00:00:00+00:00</published>
        <updated>2026-08-12T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://adasumizox.github.io/posts/02-the-shape-of-a-pipeline/"/>
        <id>https://adasumizox.github.io/posts/02-the-shape-of-a-pipeline/</id>
        
        <content type="html" xml:base="https://adasumizox.github.io/posts/02-the-shape-of-a-pipeline/">&lt;p&gt;&lt;em&gt;Building an EDR from scratch, essay 2 of 13.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Once the three sensors from the last essay are producing events, a
suspiciously boring problem appears: get the events to a server.&lt;&#x2F;p&gt;
&lt;p&gt;I say suspiciously because everyone underestimates it, including me. It
sounds like plumbing. It reads like a weekend of work. And then you sit
down to write it and discover that you are re-deriving, from scratch, the
same structure that sits inside Kafka, inside PostgreSQL’s WAL, inside
&lt;code&gt;journald&lt;&#x2F;code&gt;, inside every message queue you’ve ever used, and — if you go
back far enough — inside TCP.&lt;&#x2F;p&gt;
&lt;p&gt;That convergence is the interesting thing. This essay is about why the
shape is nearly forced, which decisions inside it are actually free, and
the one place where I think the conventional answer is a lie people tell
in marketing copy.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;three-things-you-can-do-when-you-can-t-keep-up&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#three-things-you-can-do-when-you-can-t-keep-up&quot; aria-label=&quot;Anchor link for: three-things-you-can-do-when-you-can-t-keep-up&quot;&gt;Three things you can do when you can’t keep up&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;Start with the only situation that matters. Data is arriving faster than
you can move it along. You have exactly three options, and there is no
fourth:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Drop it.&lt;&#x2F;strong&gt; Throw away data. Fast, bounded, and lossy.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Buffer it.&lt;&#x2F;strong&gt; Hold it somewhere. Preserves data, and converts a
throughput problem into a memory problem, which is a worse problem
because it kills the whole process instead of one event.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Push back.&lt;&#x2F;strong&gt; Refuse to accept more until you’ve caught up, so the
pressure propagates upstream to whoever is producing.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;Every stage of every pipeline picks one. A system’s character is entirely
determined by which stage picks which, and the failure you get in
production is always the failure of whichever stage picked &lt;em&gt;drop&lt;&#x2F;em&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Here is the thing that took me longest to internalize: &lt;strong&gt;backpressure is
not a way to avoid dropping data. It’s a way to choose where the dropping
happens.&lt;&#x2F;strong&gt; If the producer is a kernel probe watching a machine that is
compiling a large C++ project, the events exist whether or not anyone is
ready. Pressure has to terminate somewhere. Pushing back moves the
overflow up the chain; it doesn’t delete it.&lt;&#x2F;p&gt;
&lt;p&gt;So the real design question isn’t “how do we never lose events.” It’s
&lt;strong&gt;where do we want the loss, and can that place be honest about it?&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;putting-the-loss-where-it-can-be-counted&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#putting-the-loss-where-it-can-be-counted&quot; aria-label=&quot;Anchor link for: putting-the-loss-where-it-can-be-counted&quot;&gt;Putting the loss where it can be counted&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;The answer this agent settles on is stated as an invariant, and it’s the
single most load-bearing sentence in the codebase: the kernel-adjacent ring
is the only place allowed to drop events — and it counts what it drops.&lt;&#x2F;p&gt;
&lt;p&gt;Both halves matter, and the second half is the one people forget.&lt;&#x2F;p&gt;
&lt;p&gt;The kernel ring is the right place to lose data for unglamorous reasons.
It is a fixed-size allocation that exists whether you like it or not. It
sits at the boundary where the producer genuinely cannot be slowed down —
you are not going to make a process-creation event wait while your
userspace agent catches up, because that would mean the security tool is
now a performance bottleneck in &lt;code&gt;fork()&lt;&#x2F;code&gt;, which is how you get uninstalled.
And critically, the kernel gives you a drop counter for free, because it
knows exactly how many times it failed to write.&lt;&#x2F;p&gt;
&lt;p&gt;That last property is what turns a data loss into a &lt;em&gt;known&lt;&#x2F;em&gt; data loss, and
the difference between those two is the whole ballgame. A pipeline that
loses 4% of events and reports “everything is fine” is worse than useless
— it’s actively misleading, because an analyst will interpret the absence
of an event as evidence that nothing happened. A pipeline that loses 4% and
says so lets you make a decision: add capacity, tune the filters, or accept
the gap knowingly.&lt;&#x2F;p&gt;
&lt;p&gt;Everything downstream of the ring, therefore, is forbidden from dropping.
It must push back instead. You can watch the invariant being enforced in
one line in
&lt;code&gt;shipper&#x2F;src&#x2F;linux_sensor.rs&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! events into the shipper&amp;#39;s bounded channel. blocking_send provides the&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! backpressure link: a stalled shipper stops this loop and overflow is&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;blocking_send&lt;&#x2F;code&gt;, not &lt;code&gt;try_send&lt;&#x2F;code&gt;. The distinction is four characters wide
and it’s the entire policy. &lt;code&gt;try_send&lt;&#x2F;code&gt; would return an error when the
channel is full, and the natural thing to write next is a &lt;code&gt;continue&lt;&#x2F;code&gt; — at
which point you have quietly created a second drop site, uncounted,
invisible, in user space. &lt;code&gt;blocking_send&lt;&#x2F;code&gt; instead parks the reader thread,
which stops draining the kernel ring, which pushes the overflow back to the
one place designed to absorb it.&lt;&#x2F;p&gt;
&lt;p&gt;A bounded channel with a blocking send &lt;em&gt;is&lt;&#x2F;em&gt; the backpressure. There’s no
framework involved. The design lives in a function name.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why-exactly-once-is-a-marketing-term&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#why-exactly-once-is-a-marketing-term&quot; aria-label=&quot;Anchor link for: why-exactly-once-is-a-marketing-term&quot;&gt;Why “exactly once” is a marketing term&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;Now the events need to cross a network to a backend, and here we meet the
most persistently misunderstood problem in distributed systems.&lt;&#x2F;p&gt;
&lt;p&gt;You send a batch. You don’t get a response. What happened?&lt;&#x2F;p&gt;
&lt;p&gt;Maybe the batch never arrived. Maybe it arrived, was written to the
database, and the acknowledgement was lost on the way back. From where you
sit those two situations are &lt;em&gt;indistinguishable&lt;&#x2F;em&gt; — the evidence available
to you is identical — and yet they demand opposite actions. Resending is
correct in the first case and creates a duplicate in the second.&lt;&#x2F;p&gt;
&lt;p&gt;There is no protocol that fixes this. You cannot add a handshake, because
the acknowledgement of the handshake can also be lost, forever, turtles all
the way down. This is the Two Generals problem and it is not an engineering
gap, it’s a proof.&lt;&#x2F;p&gt;
&lt;p&gt;So “exactly once delivery” does not exist. What exists — and what every
mature system actually does, whatever the brochure says — is &lt;strong&gt;at-least-once
delivery plus idempotent processing&lt;&#x2F;strong&gt;. Send until acknowledged, accept
duplicates as normal, and make the receiver’s handling of a duplicate a
no-op. The system’s &lt;em&gt;observable behavior&lt;&#x2F;em&gt; is exactly-once. The delivery
never was.&lt;&#x2F;p&gt;
&lt;p&gt;The receiving half is a single line in
&lt;code&gt;backend&#x2F;src&#x2F;lib.rs&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt;                .&lt;&#x2F;span&gt;&lt;span style=&quot;color: #36F9F6;&quot;&gt;with_option&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF8B39;&quot;&gt;&amp;quot;insert_deduplication_token&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt; &amp;amp;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF7EDB;&quot;&gt;token&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The token is &lt;code&gt;&amp;lt;agent_id&amp;gt;:&amp;lt;sequence&amp;gt;&lt;&#x2F;code&gt;. ClickHouse remembers recently-seen
tokens and silently discards an insert that repeats one. The agent is free
to be dumb and persistent; the backend makes repetition harmless.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-constraint-nobody-warns-you-about&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-constraint-nobody-warns-you-about&quot; aria-label=&quot;Anchor link for: the-constraint-nobody-warns-you-about&quot;&gt;The constraint nobody warns you about&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;Here’s where it stops being textbook, and it’s my favorite thing in the
whole pipeline.&lt;&#x2F;p&gt;
&lt;p&gt;Once you’ve decided that identity is &lt;code&gt;(agent_id, sequence)&lt;&#x2F;code&gt; attached to a
&lt;em&gt;batch&lt;&#x2F;em&gt;, you have — without necessarily noticing — signed a contract that
constrains you forever after. From
&lt;code&gt;shipper&#x2F;src&#x2F;queue.rs&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! Because the backend dedupes on (agent_id, sequence), recovery must resend&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! the exact batches as originally constructed — re-batching the same events&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! under an old sequence would get them silently deduped away. So the WAL&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! stores whole batches, and replay bypasses the batching logic entirely.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Read that failure mode carefully, because it’s beautifully nasty. Suppose
after a crash you replay your unsent events and, being tidy, you repack
them into fuller batches. Batch 100 originally held events A and B; now
batch 100 holds A, B, C, and D. You send it. The backend sees token
&lt;code&gt;agent:100&lt;&#x2F;code&gt;, recognizes it, and drops the insert as a duplicate.&lt;&#x2F;p&gt;
&lt;p&gt;Events C and D are gone. Not delayed — &lt;em&gt;gone&lt;&#x2F;em&gt;, permanently, and with a
successful acknowledgement returned to the agent. No error is logged
anywhere. Your metrics are green. The data is missing.&lt;&#x2F;p&gt;
&lt;p&gt;The lesson generalizes well beyond this codebase: &lt;strong&gt;when you make an
identifier the basis of deduplication, you freeze the boundaries of whatever
that identifier names.&lt;&#x2F;strong&gt; The batch stopped being a transient performance
optimization the moment it became an identity. It’s now a durable structure
you are not allowed to reorganize.&lt;&#x2F;p&gt;
&lt;p&gt;Which is exactly why the write-ahead log stores whole encoded batches, and
why replay is a separate code path that walks the disk rather than
re-entering the batcher. The pipeline has a “no re-batching” rule the way a
database has a “no rewriting history” rule, and for the same underlying
reason.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-durability-actually-costs&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#what-durability-actually-costs&quot; aria-label=&quot;Anchor link for: what-durability-actually-costs&quot;&gt;What durability actually costs&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;The write-ahead log itself is the oldest trick in the book: write the data
to disk and force it to physical media &lt;em&gt;before&lt;&#x2F;em&gt; you tell anyone you have
it, and reclaim the space only after someone else has taken responsibility.&lt;&#x2F;p&gt;
&lt;p&gt;The header spells the ordering out:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! Durable on-disk queue: a write-ahead log of EventBatch records so events&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! survive a shipper restart. A batch is appended and fsync&amp;#39;d *before* the&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! first send attempt and reclaimed only after the backend acks it.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;fsync&lt;&#x2F;code&gt; before the first attempt, reclaim after the ack. Those two
bookends define a window in which the data exists in two places at once,
and that redundancy is the entire point. Every durable system in existence
is some variation on this trick.&lt;&#x2F;p&gt;
&lt;p&gt;The part that separates a real WAL from a toy one is what happens when you
find damage on startup:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! missing cursor is safe: everything replays and the backend dedupes. A&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! torn record at the tail of the *last* segment is a normal crash artifact&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! and is truncated away; corruption anywhere else is a hard error.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This is a small masterclass in thinking about failure, so let me unpack the
three cases separately.&lt;&#x2F;p&gt;
&lt;p&gt;A &lt;strong&gt;torn record at the tail of the last segment&lt;&#x2F;strong&gt; is what a crash &lt;em&gt;looks
like&lt;&#x2F;em&gt;. You were in the middle of appending; the machine lost power; the
final record is half-written. That isn’t corruption, it’s the expected
physical signature of an interruption. Truncate it and move on — the events
in it were never acknowledged to anyone, so nothing is lost that anyone
believes exists.&lt;&#x2F;p&gt;
&lt;p&gt;A &lt;strong&gt;damaged record in the middle&lt;&#x2F;strong&gt; is a different animal entirely. Nothing
in normal operation writes there. Its presence means a bad disk, a filesystem
bug, or something modifying your files. Recovering from that by skipping the
bad record would be the worst possible choice: you’d silently drop unknown
data and continue as if healthy. So it’s a hard error, loudly.&lt;&#x2F;p&gt;
&lt;p&gt;A &lt;strong&gt;missing ack cursor&lt;&#x2F;strong&gt; is safe to lose because losing it can only cause
&lt;em&gt;over&lt;&#x2F;em&gt;-delivery — you replay batches the backend already has, and the
deduplication makes that free. The cursor is an optimization, and it’s
designed so that its failure mode lands on the harmless side.&lt;&#x2F;p&gt;
&lt;p&gt;That’s the design principle underneath all three: for every piece of state,
ask which direction its corruption pushes you, and arrange for the cheap
direction to be the one that happens. Redundant work: fine. Silent data
loss: never.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;decoupling-and-the-luxury-of-a-slow-consumer&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#decoupling-and-the-luxury-of-a-slow-consumer&quot; aria-label=&quot;Anchor link for: decoupling-and-the-luxury-of-a-slow-consumer&quot;&gt;Decoupling, and the luxury of a slow consumer&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;The last structural idea is visible in the module header of
&lt;code&gt;shipper&#x2F;src&#x2F;ship.rs&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! Batching gRPC publisher, decoupled into a batcher and a sender that&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! meet at the durable queue: the batcher drains the channel and appends&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! every batch to the WAL; the sender reads batches back off disk and&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! delivers them at-least-once (same sequence until acked). A dead&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! backend therefore parks only the sender — telemetry keeps flowing to&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! disk up to Config::max_queue_bytes, past which the batcher stops&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! consuming and backpressure walks channel -&amp;gt; reader thread -&amp;gt; kernel&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F;! ring (still the only place designed to drop).&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Two independent components that never speak to each other; they share only
a file. The batcher’s job is to get events onto disk. The sender’s job is to
get things off disk and onto the network. Neither knows the other exists.&lt;&#x2F;p&gt;
&lt;p&gt;The consequence is that a backend outage — which is the common case, not
the exotic one; deploys happen, networks partition — degrades gracefully
along a predictable path. The sender parks. The batcher keeps writing.
Telemetry accumulates on disk, which is cheap and enormous. Only when the
disk cap is reached does pressure resume its walk up the chain toward the
kernel ring.&lt;&#x2F;p&gt;
&lt;p&gt;There’s a nice systems-thinking observation buried in there. Introducing an
intermediate storage layer didn’t just add buffering — it &lt;strong&gt;changed the
failure mode from correlated to independent&lt;&#x2F;strong&gt;. Without the WAL, a network
problem immediately becomes a kernel-ring problem, because the whole chain
is rigidly coupled and pressure transmits instantly. With it, the two
failures are separated by hours of disk capacity. Same components, one
extra layer, and an outage that would have cost you telemetry now costs you
nothing but disk space.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;boring-on-purpose&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#boring-on-purpose&quot; aria-label=&quot;Anchor link for: boring-on-purpose&quot;&gt;Boring on purpose&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;Look at what the finished pipeline actually is. A bounded channel. A
blocking send. A size-and-time batcher. An append-only log with CRCs. A
retry loop. A dedupe token.&lt;&#x2F;p&gt;
&lt;p&gt;Not one of those is clever. There is no novel algorithm anywhere, nothing
you couldn’t explain to a competent engineer in a minute each. And that’s
the achievement rather than an apology for it — because this layer sits
between kernel code that can crash a machine and a detection engine whose
correctness depends utterly on its input being complete. It is the part of
the system that &lt;em&gt;must&lt;&#x2F;em&gt; be reasoned about without effort, at 3am, by someone
who didn’t write it.&lt;&#x2F;p&gt;
&lt;p&gt;The interesting decisions were all about placement and honesty. Put the
lossy stage where loss is unavoidable and countable. Put durability before
the first send, not after. Keep failures independent by inserting a layer
between them. Make every piece of state fail toward redundant work rather
than toward silence.&lt;&#x2F;p&gt;
&lt;p&gt;Do that, and what’s left is boring. Which means the thinking worked.&lt;&#x2F;p&gt;
&lt;p&gt;The next problem doesn’t yield to boring, though. Once you have a complete,
durable stream of events, you have to say &lt;em&gt;what each one happened to&lt;&#x2F;em&gt; — and
on a running machine, the things you’re naming keep disappearing and the
names keep getting reused. That identity problem is where the next essay
begins.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>How an Operating System Decides to Let You Watch It</title>
        <published>2026-08-05T00:00:00+00:00</published>
        <updated>2026-08-05T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://adasumizox.github.io/posts/01-how-an-os-lets-you-watch-it/"/>
        <id>https://adasumizox.github.io/posts/01-how-an-os-lets-you-watch-it/</id>
        
        <content type="html" xml:base="https://adasumizox.github.io/posts/01-how-an-os-lets-you-watch-it/">&lt;p&gt;&lt;em&gt;Building an EDR from scratch, essay 1 of 13.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Here is an awkward fact about endpoint security software: the program you
want to install and the program you want to catch are asking the operating
system for the same things.&lt;&#x2F;p&gt;
&lt;p&gt;Both want to see every process that starts. Both want to know when files
are written. Both want to watch the network. Both want to survive reboots,
resist termination, and run before the user logs in. If you squint at the
capability list of an EDR agent and the capability list of a competent
rootkit, they are the same list. The difference is intent, and intent is
not something a kernel can typecheck.&lt;&#x2F;p&gt;
&lt;p&gt;So every operating system has had to answer a question that has no clean
technical answer: &lt;em&gt;how do we let someone watch everything, without letting
everyone watch everything?&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Windows, Linux, and macOS each answered it differently. Not slightly
differently — differently in kind, from different first principles, with
different ideas about where trust comes from. And because the answer is a
trust model rather than an API, it propagates upward. It shapes what your
sensor can observe, what it costs to be wrong, and — this is the part I
did not expect — &lt;em&gt;when in the software lifecycle your mistakes become
visible&lt;&#x2F;em&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;That last one turned out to be the most useful lens I found. Let me get
there the long way.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;three-answers-to-one-question&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#three-answers-to-one-question&quot; aria-label=&quot;Anchor link for: three-answers-to-one-question&quot;&gt;Three answers to one question&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;I’ll name the three models before defending them, because having the words
makes the rest of this readable:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Windows trusts by ceremony.&lt;&#x2F;strong&gt; You may enter the kernel. First, perform
the rituals.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Linux trusts by proof.&lt;&#x2F;strong&gt; You may not be trusted, so submit your program
for mechanical verification and it will run only if safety is provable.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;macOS trusts by brokerage.&lt;&#x2F;strong&gt; You may not enter the kernel at all.
Apple will go inside, look around, and tell you what it saw.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Each is internally coherent. Each is, I think, defensible. And each one
extracts a completely different tax.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;windows-the-bet-on-ceremony&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#windows-the-bet-on-ceremony&quot; aria-label=&quot;Anchor link for: windows-the-bet-on-ceremony&quot;&gt;Windows: the bet on ceremony&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;The Windows model is the oldest and the most straightforward: security
software runs in the kernel, with the same privileges as the kernel,
because that is where the information is. There is no sandbox. If your
driver dereferences a bad pointer at &lt;code&gt;DISPATCH_LEVEL&lt;&#x2F;code&gt;, the machine
bugchecks, and every user of that machine finds out at once.&lt;&#x2F;p&gt;
&lt;p&gt;Given that, Windows does not try to make your code safe. It tries to make
sure that only code someone was willing to &lt;em&gt;sign their name to&lt;&#x2F;em&gt; gets to be
unsafe. Trust is established out of band — a signing certificate, an
attestation process, a company with a legal identity — and enforced at load
time.&lt;&#x2F;p&gt;
&lt;p&gt;What I find architecturally interesting is how thoroughly that ceremony is
woven into the APIs themselves. It isn’t a single check at the door. The
privileged APIs individually refuse to work for a driver that hasn’t
performed the ritual. From the header comment in
&lt;code&gt;windows&#x2F;driver&#x2F;edr_driver.c&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;c&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt; *&lt;&#x2F;span&gt;&lt;span&gt; MUST be linked &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt;&#x2F;&lt;&#x2F;span&gt;&lt;span&gt;INTEGRITYCHECK or PsSetCreateProcessNotifyRoutineEx&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt; *&lt;&#x2F;span&gt;&lt;span&gt; fails with &lt;&#x2F;span&gt;&lt;span style=&quot;color: #36F9F6;&quot;&gt;STATUS_ACCESS_DENIED&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97E72;&quot;&gt;0xC0000022&lt;&#x2F;span&gt;&lt;span&gt;).&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;&#x2F;INTEGRITYCHECK&lt;&#x2F;code&gt; is a flag that marks the image as requiring signature
verification at load. Note what’s happening: the process-notification API
won’t talk to you unless your binary has &lt;em&gt;asked to be held to a stricter
standard&lt;&#x2F;em&gt;. It’s an opt-in that gates a capability. You can’t accidentally
get process-creation visibility; you have to declare, in your link line,
that you intend to be the kind of code that is checked.&lt;&#x2F;p&gt;
&lt;p&gt;The other half of the Windows model is stranger and more human. Filesystem
minifilters — the mechanism for seeing file operations — stack in an order
determined by a number called an &lt;strong&gt;altitude&lt;&#x2F;strong&gt;:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; *   reg add HKLM\SYSTEM\CurrentControlSet\Services\&amp;lt;svc&amp;gt;\Instances\EdrAgentInstance&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; *       &#x2F;v Altitude &#x2F;t REG_SZ &#x2F;d 385100 &#x2F;f&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; * (385100 sits in the FSFilter Activity Monitor range 360000-389999;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Think about what an altitude actually is. Every filter driver on the system
sits at a point in a global, numeric, totally-ordered namespace. Antivirus
lives around 320000. Activity monitors live in 360000–389999. Encryption
drivers live lower, so they see plaintext. Replication drivers live higher.
Your position determines whether you observe a write before or after some
other vendor’s product transforms it — which means it determines whether
your product &lt;em&gt;works&lt;&#x2F;em&gt; in the presence of theirs.&lt;&#x2F;p&gt;
&lt;p&gt;And the allocation of these numbers is not enforced by the kernel. It is
administered by Microsoft, by request, as a registry of reserved ranges.
It’s a social protocol wearing a &lt;code&gt;REG_SZ&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;I love this as a design artifact, because it’s an honest admission that the
hard problem in kernel-mode security software was never memory safety — it
was &lt;em&gt;coordination between mutually distrustful vendors who all want to be
first&lt;&#x2F;em&gt;. Windows solved it with a phone book. My driver uses 385100 because
it’s in the right neighborhood and nobody else on my lab machine is home;
a shipping product would have that number assigned to it, in writing.&lt;&#x2F;p&gt;
&lt;p&gt;The payoff for all this ceremony is that Windows gives you the sharpest
semantics of the three platforms. When &lt;code&gt;PsSetCreateProcessNotifyRoutineEx&lt;&#x2F;code&gt;
fires, you are inside process creation, holding structures the kernel is
using, before the first instruction of the new image runs. Nothing is
inferred. Nothing is a heuristic. You are standing in the room where it
happens.&lt;&#x2F;p&gt;
&lt;p&gt;The tax is that the room has no railings.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;linux-the-bet-on-proof&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#linux-the-bet-on-proof&quot; aria-label=&quot;Anchor link for: linux-the-bet-on-proof&quot;&gt;Linux: the bet on proof&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;Linux, arriving at the same problem two decades later and with a very
different culture, made close to the opposite bet. Rather than deciding
&lt;em&gt;who&lt;&#x2F;em&gt; may run unsafe code in the kernel, it asked whether the code could be
made safe enough that the question doesn’t matter.&lt;&#x2F;p&gt;
&lt;p&gt;eBPF is the answer, and the verifier is the mechanism. You submit a program
in a restricted instruction set; a static analyzer in the kernel simulates
every reachable path and refuses to load anything it can’t prove terminates,
stays in bounds, and touches only what it’s allowed to touch. If the proof
succeeds, the program is JIT-compiled and runs at native speed in kernel
context. If it fails, you get a rejection and a stack trace, and nothing
happens to the machine.&lt;&#x2F;p&gt;
&lt;p&gt;This inverts the cost structure completely. On Windows, the cost of a
mistake is paid at runtime, by the customer, as a blue screen. On Linux,
it’s paid at development time, by me, as an argument with a program that
cannot be talked out of its position.&lt;&#x2F;p&gt;
&lt;p&gt;What makes this more than a sandbox story is where the verifier’s
constraints show up in the &lt;em&gt;design&lt;&#x2F;em&gt; of the sensor. Consider a snippet from
the exec probe in
&lt;code&gt;linux&#x2F;bpf&#x2F;edr_probes.bpf.c&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;c&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;	&#x2F;* Stack copies: key&#x2F;value pointers into the ringbuf reservation are&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;	 * a verifier gamble across kernels; the stack is always legal. *&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	pid &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFFFFFEE;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt; ev&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt;-&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt;pid;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	ppid &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFFFFFEE;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt; ev&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt;-&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt;ppid;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #36F9F6;&quot;&gt;	bpf_map_update_elem&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt;&amp;amp;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF7EDB;font-style: italic;&quot;&gt;exec_ppid&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt; &amp;amp;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF7EDB;font-style: italic;&quot;&gt;pid&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt; &amp;amp;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF7EDB;font-style: italic;&quot;&gt;ppid&lt;&#x2F;span&gt;&lt;span&gt;, BPF_ANY);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;There is a perfectly good copy of &lt;code&gt;pid&lt;&#x2F;code&gt; sitting in the ring buffer
reservation two lines up. Copying it to the stack first is redundant work
in any normal program. But whether the verifier accepts a pointer into a
ring-buffer reservation as a map key varies across kernel versions, and a
program that loads on my laptop and is rejected on a customer’s 5.10 kernel
is a support incident, not a bug report. So the code carries an extra
assignment forever, as a compatibility hedge.&lt;&#x2F;p&gt;
&lt;p&gt;That’s the real shape of eBPF development. You are not writing C for a
machine; you are writing C for a proof checker whose exact reasoning powers
differ between the kernels your program must run on. The verifier is a
compiler audience with opinions.&lt;&#x2F;p&gt;
&lt;p&gt;The second half of the Linux answer addresses a different problem, and it’s
one Windows never had to solve. Kernel data structures on Linux are not a
stable ABI. &lt;code&gt;struct task_struct&lt;&#x2F;code&gt; is laid out differently in every kernel
build, sometimes differently between two distributions of the same version.
A sensor that reads &lt;code&gt;task-&amp;gt;start_boottime&lt;&#x2F;code&gt; at a hardcoded offset is
correct exactly once.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;CO-RE&lt;&#x2F;strong&gt; — Compile Once, Run Everywhere — fixes this by shipping type
information with the kernel (BTF) and having the loader relocate every
field access at load time against the layout the running kernel actually
has. &lt;code&gt;BPF_CORE_READ(bprm, filename)&lt;&#x2F;code&gt; compiles to a relocation entry, not
an offset. The kernel tells you where &lt;code&gt;filename&lt;&#x2F;code&gt; lives on &lt;em&gt;this&lt;&#x2F;em&gt; machine,
today.&lt;&#x2F;p&gt;
&lt;p&gt;Step back and notice what those two mechanisms together represent. The
verifier says: &lt;em&gt;I don’t trust your code, so prove it.&lt;&#x2F;em&gt; CO-RE says: &lt;em&gt;I don’t
trust your assumptions about me, so ask at runtime.&lt;&#x2F;em&gt; Linux replaced the
vendor-trust relationship with a pair of machine-checked contracts. That’s
a genuinely different theory of how to make a kernel extensible, and it
puts the burden on the extension author rather than on a certificate
authority.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;macos-the-bet-on-brokerage&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#macos-the-bet-on-brokerage&quot; aria-label=&quot;Anchor link for: macos-the-bet-on-brokerage&quot;&gt;macOS: the bet on brokerage&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;Apple looked at kernel extensions and concluded the entire category was a
mistake.&lt;&#x2F;p&gt;
&lt;p&gt;Not “risky.” Not “in need of better tooling.” A mistake — third-party code
in the kernel was a permanent, unfixable source of instability and
compromise, and the right move was to remove the capability from the
platform. Kexts were deprecated, then walled off behind boot-policy changes
that a normal user will never make.&lt;&#x2F;p&gt;
&lt;p&gt;But security software still needs the data. So Apple built
&lt;strong&gt;Endpoint Security&lt;&#x2F;strong&gt;: Apple’s own code sits at the kernel hook points, and
your agent — running in &lt;em&gt;user space&lt;&#x2F;em&gt;, as an ordinary process — subscribes to
a stream of events over a framework API. You get the semantics of kernel
instrumentation with the failure domain of a user process. If your ES
client segfaults, your ES client segfaults. The machine keeps running.&lt;&#x2F;p&gt;
&lt;p&gt;Here’s the subscription from the shim in
&lt;code&gt;macos&#x2F;esf&#x2F;edr_esf_shim.c&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;c&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FE4450;&quot;&gt;    es_event_type_t&lt;&#x2F;span&gt;&lt;span&gt; events&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt;[]&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFFFFFEE;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        ES_EVENT_TYPE_NOTIFY_EXEC,     ES_EVENT_TYPE_NOTIFY_EXIT,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        ES_EVENT_TYPE_NOTIFY_CREATE,   ES_EVENT_TYPE_NOTIFY_CLOSE,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        ES_EVENT_TYPE_NOTIFY_TRUNCATE, ES_EVENT_TYPE_NOTIFY_UNLINK,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        ES_EVENT_TYPE_NOTIFY_RENAME,   ES_EVENT_TYPE_NOTIFY_MMAP,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;That reads like a config file. On Windows the equivalent capability is a
signed kernel driver with a registered altitude and a callback running at
elevated IRQL. On macOS it’s an array of enum values passed to a library
function from a process that could, in principle, be a shell script’s
child. The complexity didn’t vanish — Apple absorbed it, and now maintains
it on your behalf.&lt;&#x2F;p&gt;
&lt;p&gt;The tax is subtle and it comes due in two places.&lt;&#x2F;p&gt;
&lt;p&gt;The first is that &lt;strong&gt;the broker decides what exists&lt;&#x2F;strong&gt;. On Windows or Linux
you can, with enough determination, hook something nobody anticipated you’d
want. Under ES, if Apple hasn’t modeled an event type, that event is not
merely hard to observe — it is unobservable, and your options are a feature
request and a wait measured in major OS releases.&lt;&#x2F;p&gt;
&lt;p&gt;The interesting flip side is that a broker can also model things the other
platforms &lt;em&gt;don’t have words for&lt;&#x2F;em&gt;. Two entries further down that same array:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;c&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;        &#x2F;* Mach task-port acquisition (macOS 10.15 &#x2F; 12.0). CONTROL + READ&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;         * only — INSPECT&#x2F;NAME are dominated by diagnostics tooling. *&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        ES_EVENT_TYPE_NOTIFY_GET_TASK, ES_EVENT_TYPE_NOTIFY_GET_TASK_READ,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;A Mach task port is a capability handle to another process’s address space.
Acquiring one is the macOS injection primitive — the thing that stands
where &lt;code&gt;OpenProcess&lt;&#x2F;code&gt; + &lt;code&gt;WriteProcessMemory&lt;&#x2F;code&gt; stands on Windows. There is no
equivalent event on the other two platforms in this codebase, because Mach
ports are a macOS abstraction and Apple chose to model access to them
explicitly. The broker can be &lt;em&gt;more&lt;&#x2F;em&gt; expressive than raw hooking, when the
broker understands its own OS’s semantics better than an outsider would.&lt;&#x2F;p&gt;
&lt;p&gt;The second tax is the one that turned out to matter most in practice, and
it’s the reason I ended up thinking about all of this in terms of &lt;em&gt;time&lt;&#x2F;em&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;where-your-mistakes-surface&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#where-your-mistakes-surface&quot; aria-label=&quot;Anchor link for: where-your-mistakes-surface&quot;&gt;Where your mistakes surface&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;Under Endpoint Security, permission is not a runtime check that returns an
error code. Permission is an &lt;strong&gt;entitlement&lt;&#x2F;strong&gt;: a cryptographic grant, issued
by Apple, to a specific developer account, after a review. Your binary
carries it in its signature.&lt;&#x2F;p&gt;
&lt;p&gt;Development builds can fake this. With System Integrity Protection relaxed
and &lt;code&gt;amfi_get_out_of_my_way&lt;&#x2F;code&gt; set, an ad-hoc-signed client with a synthetic
entitlement will be accepted by &lt;code&gt;es_new_client&lt;&#x2F;code&gt;, and events flow. Every
category in that array above is delivered on my dev VM.&lt;&#x2F;p&gt;
&lt;p&gt;Except one. TCC-modification events — notifications that a privacy grant
(camera, microphone, full disk access) changed — are gated more strictly
than the rest. The client connects. The subscription succeeds. No error is
returned anywhere. And no &lt;code&gt;TCC_MODIFY&lt;&#x2F;code&gt; event is ever delivered, because
the faked entitlement clears the door but not that particular room.&lt;&#x2F;p&gt;
&lt;p&gt;There is no bug to find. The code is right. The only fix is a genuine
Apple-issued entitlement, which is a business process, not an engineering
one.&lt;&#x2F;p&gt;
&lt;p&gt;And that, finally, is the pattern I want to leave you with. Line the three
models up by &lt;em&gt;when a mistake becomes visible&lt;&#x2F;em&gt;:&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;&#x2F;th&gt;&lt;th&gt;Trust model&lt;&#x2F;th&gt;&lt;th&gt;Mistakes surface at&lt;&#x2F;th&gt;&lt;th&gt;Failure mode&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Windows&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;Ceremony — sign, register, enter&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;Runtime&lt;&#x2F;strong&gt;, on the customer’s machine&lt;&#x2F;td&gt;&lt;td&gt;Loud and catastrophic (bugcheck)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Linux&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;Proof — submit to the verifier&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;Load time&lt;&#x2F;strong&gt;, on your machine&lt;&#x2F;td&gt;&lt;td&gt;Loud and safe (rejection with a trace)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;macOS&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;Brokerage — Apple looks for you&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;Deployment&lt;&#x2F;strong&gt;, at organizational scale&lt;&#x2F;td&gt;&lt;td&gt;&lt;em&gt;Silent&lt;&#x2F;em&gt; — an event class that simply never arrives&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;Windows fails at the worst time in the worst way, but tells you
unambiguously. Linux fails at the best possible time, before anything is at
risk, and the price is that you argue with a proof checker for an afternoon.
macOS fails at the strangest time — long after the code is written and
tested — and the failure is an absence, which is the hardest thing in the
world to notice.&lt;&#x2F;p&gt;
&lt;p&gt;Every one of these is a coherent engineering position. But they aren’t
interchangeable, and this is why an EDR agent cannot have one unified
sensor abstraction that “handles platform differences.” The differences
aren’t in the API surface. They’re in what the platform believes about you,
and belief doesn’t refactor.&lt;&#x2F;p&gt;
&lt;p&gt;What &lt;em&gt;can&lt;&#x2F;em&gt; be unified is everything downstream. Once an event exists — a
process started, a file was written, a task port was acquired — the
questions become identical everywhere: how do you not lose it, how do you
name the thing it happened to, who is allowed to act on it. Three sensors,
three trust models, one contract from there on.&lt;&#x2F;p&gt;
&lt;p&gt;The next essay follows that contract downstream and asks why telemetry
pipelines keep converging on the same shape.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Building a Mini-EDR Agent - Monitoring Process Creation with ETW in C</title>
        <published>2026-03-26T00:00:00+00:00</published>
        <updated>2026-03-26T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://adasumizox.github.io/posts/building-a-mini-edr-agent-monitoring-process-creation-with-etw-in-c/"/>
        <id>https://adasumizox.github.io/posts/building-a-mini-edr-agent-monitoring-process-creation-with-etw-in-c/</id>
        
        <content type="html" xml:base="https://adasumizox.github.io/posts/building-a-mini-edr-agent-monitoring-process-creation-with-etw-in-c/">&lt;p&gt;In my previous post, we built a mini-SIEM lab using Sysmon to gather high-quality telemetry from a Windows host. But how can an endpoint sensor observe process creation in real time without relying on Sysmon?&lt;&#x2F;p&gt;
&lt;p&gt;One useful source is &lt;a rel=&quot;noopener external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;learn.microsoft.com&#x2F;en-us&#x2F;windows&#x2F;win32&#x2F;etw&#x2F;event-tracing-portal&quot;&gt;&lt;strong&gt;Event Tracing for Windows (ETW)&lt;&#x2F;strong&gt;&lt;&#x2F;a&gt;, Windows’ general-purpose tracing infrastructure. Production EDR products normally combine several user-mode and kernel-mode telemetry sources; ETW is one source rather than the mechanism behind every product.&lt;&#x2F;p&gt;
&lt;p&gt;In this post, we will ditch the pre-built collection tools and write a small EDR-style sensor in C. It will subscribe to the Windows Kernel Process provider and print an event whenever a new process starts.&lt;&#x2F;p&gt;
&lt;p&gt;Before diving into the code, let’s look at the three main parts of ETW:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Providers&lt;&#x2F;strong&gt; - The components generating logs (e.g. the Windows Kernel).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Controllers&lt;&#x2F;strong&gt; - Applications that start and configure tracing sessions.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Consumers&lt;&#x2F;strong&gt; - Applications that read events from real-time sessions or &lt;code&gt;.etl&lt;&#x2F;code&gt; files.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Our C program will act as both a controller (creating the trace session) and a consumer (parsing and printing events).&lt;&#x2F;p&gt;
&lt;p&gt;Let’s build it step-by-step.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;phase-1-setting-up-the-etw-session-the-controller&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#phase-1-setting-up-the-etw-session-the-controller&quot; aria-label=&quot;Anchor link for: phase-1-setting-up-the-etw-session-the-controller&quot;&gt;Phase 1: Setting up the ETW Session (The Controller)&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;To receive events, we first need to start an ETW trace session and define the GUID of the provider we want to enable.&lt;&#x2F;p&gt;
&lt;p&gt;In our case, it is &lt;code&gt;Microsoft-Windows-Kernel-Process&lt;&#x2F;code&gt;, whose GUID is &lt;a rel=&quot;noopener external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;repnz&#x2F;etw-providers-docs&#x2F;blob&#x2F;master&#x2F;Manifests-Win10-18990&#x2F;Microsoft-Windows-Kernel-Process.xml&quot;&gt;&lt;code&gt;{22FB2CD6-0E7B-422B-A0C7-2FAD1F0E716}&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;c&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #72F1B8;&quot;&gt;#define&lt;&#x2F;span&gt;&lt;span style=&quot;color: #36F9F6;&quot;&gt; _WIN32_WINNT&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97E72;&quot;&gt; 0x0600&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #72F1B8;&quot;&gt;#include&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF8B39;&quot;&gt; &amp;lt;windows.h&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #72F1B8;&quot;&gt;#include&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF8B39;&quot;&gt; &amp;lt;stdio.h&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #72F1B8;&quot;&gt;#include&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF8B39;&quot;&gt; &amp;lt;evntrace.h&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #72F1B8;&quot;&gt;#include&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF8B39;&quot;&gt; &amp;lt;evntcons.h&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #72F1B8;&quot;&gt;#include&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF8B39;&quot;&gt; &amp;lt;tdh.h&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt;#pragma comment&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt;lib&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF8B39;&quot;&gt;&amp;quot;tdh.lib&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt;#pragma comment&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt;lib&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF8B39;&quot;&gt;&amp;quot;advapi32.lib&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FE4450;&quot;&gt;static const&lt;&#x2F;span&gt;&lt;span&gt; GUID ProviderGuid &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFFFFFEE;&quot;&gt;=&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97E72;&quot;&gt; 0x22FB2CD6&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97E72;&quot;&gt; 0x0E7B&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97E72;&quot;&gt; 0x422B&lt;&#x2F;span&gt;&lt;span&gt;, {&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97E72;&quot;&gt; 0xA0&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97E72;&quot;&gt; 0xC7&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97E72;&quot;&gt; 0x2F&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97E72;&quot;&gt; 0xAD&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97E72;&quot;&gt; 0x1F&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97E72;&quot;&gt; 0xD0&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97E72;&quot;&gt; 0xE7&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97E72;&quot;&gt; 0x16&lt;&#x2F;span&gt;&lt;span&gt; } };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;In &lt;code&gt;main&lt;&#x2F;code&gt;, we configure the &lt;code&gt;EVENT_TRACE_PROPERTIES&lt;&#x2F;code&gt; structure. &lt;code&gt;EVENT_TRACE_REAL_TIME_MODE&lt;&#x2F;code&gt; makes events available to a real-time consumer instead of writing them to a disk file.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;c&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F; Setup session properties&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;TRACE_PROPERTIES sessionProps &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFFFFFEE;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97E72;&quot;&gt; 0&lt;&#x2F;span&gt;&lt;span&gt; };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;sessionProps.Properties.Wnode.BufferSize &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFFFFFEE;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt; sizeof&lt;&#x2F;span&gt;&lt;span&gt;(TRACE_PROPERTIES);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;sessionProps.Properties.Wnode.Flags &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFFFFFEE;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt; WNODE_FLAG_TRACED_GUID;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;sessionProps.Properties.Wnode.ClientContext &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFFFFFEE;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97E72;&quot;&gt; 1&lt;&#x2F;span&gt;&lt;span&gt;; &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;sessionProps.Properties.LogFileMode &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFFFFFEE;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt; EVENT_TRACE_REAL_TIME_MODE;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt; &#x2F;&#x2F; Crucial for EDR&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;sessionProps.Properties.LoggerNameOffset &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFFFFFEE;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #36F9F6;&quot;&gt; offsetof&lt;&#x2F;span&gt;&lt;span&gt;(TRACE_PROPERTIES, SessionName);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;&#x2F;&#x2F; Start the trace session (Requires Administrator privileges!)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;ULONG status &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFFFFFEE;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #36F9F6;&quot;&gt; StartTraceW&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt;&amp;amp;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF7EDB;font-style: italic;&quot;&gt;g_hSession&lt;&#x2F;span&gt;&lt;span&gt;, g_SessionName,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt; &amp;amp;&lt;&#x2F;span&gt;&lt;span&gt;sessionProps.Properties);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;phase-2-subscribing-to-kernel-events&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#phase-2-subscribing-to-kernel-events&quot; aria-label=&quot;Anchor link for: phase-2-subscribing-to-kernel-events&quot;&gt;Phase 2: Subscribing to Kernel Events&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;Starting a session is not enough. We must enable our provider for that session with &lt;code&gt;EnableTraceEx2&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;c&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;status &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFFFFFEE;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #36F9F6;&quot;&gt; EnableTraceEx2&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    g_hSession,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt;    &amp;amp;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF7EDB;font-style: italic;&quot;&gt;ProviderGuid&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    EVENT_CONTROL_CODE_ENABLE_PROVIDER,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    TRACE_LEVEL_VERBOSE,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97E72;&quot;&gt;    0x10&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt; &#x2F;&#x2F; WINEVENT_KEYWORD_PROCESS&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97E72;&quot;&gt;    0&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97E72;&quot;&gt;    0&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97E72;&quot;&gt;    NULL&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;phase-3-consuming-the-events-the-consumer&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#phase-3-consuming-the-events-the-consumer&quot; aria-label=&quot;Anchor link for: phase-3-consuming-the-events-the-consumer&quot;&gt;Phase 3: Consuming the Events (The Consumer)&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;Now that events are flowing into the session, we need to read them. We set up an &lt;code&gt;EVENT_TRACE_LOGFILEW&lt;&#x2F;code&gt; structure, point it to our session name, and provide an &lt;code&gt;EventRecordCallback&lt;&#x2F;code&gt; function that ETW calls for each event.&lt;&#x2F;p&gt;
&lt;p&gt;Finally, we call &lt;code&gt;ProcessTrace()&lt;&#x2F;code&gt;. This function blocks while it processes events and invokes our callback, until the trace is stopped.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;c&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;EVENT_TRACE_LOGFILEW logFile &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFFFFFEE;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97E72;&quot;&gt; 0&lt;&#x2F;span&gt;&lt;span&gt; };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;logFile.LoggerName &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFFFFFEE;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt; g_SessionName;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;logFile.ProcessTraceMode &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFFFFFEE;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt; PROCESS_TRACE_MODE_REAL_TIME &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt;|&lt;&#x2F;span&gt;&lt;span&gt; PROCESS_TRACE_MODE_EVENT_RECORD;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;logFile.EventRecordCallback &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFFFFFEE;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt; EventRecordCallback;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt; &#x2F;&#x2F; The function that handles data&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;TRACEHANDLE hTrace &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFFFFFEE;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #36F9F6;&quot;&gt; OpenTraceW&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt;&amp;amp;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF7EDB;font-style: italic;&quot;&gt;logFile&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #36F9F6;&quot;&gt;printf&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF8B39;&quot;&gt;&amp;quot;Listening for Process Starts... (Press Ctrl+C to stop)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #36F9F6;&quot;&gt;\n&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF8B39;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;status &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFFFFFEE;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #36F9F6;&quot;&gt; ProcessTrace&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt;&amp;amp;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF7EDB;font-style: italic;&quot;&gt;hTrace&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97E72;&quot;&gt; 1&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97E72;&quot;&gt; NULL&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97E72;&quot;&gt; NULL&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt; &#x2F;&#x2F; Blocks and processes events&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;phase-4-parsing-the-payload-with-tdh&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#phase-4-parsing-the-payload-with-tdh&quot; aria-label=&quot;Anchor link for: phase-4-parsing-the-payload-with-tdh&quot;&gt;Phase 4: Parsing the Payload with TDH&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;Raw ETW data is an unformatted binary payload. Parsing it manually is error-prone, and its layout can change with Windows updates. Thankfully, Microsoft provides the &lt;strong&gt;Trace Data Helper (TDH)&lt;&#x2F;strong&gt; API, which uses the provider’s manifest to parse the binary payload into readable strings and integers.&lt;&#x2F;p&gt;
&lt;p&gt;In &lt;code&gt;EventRecordCallback&lt;&#x2F;code&gt;, we verify the provider and select event ID &lt;code&gt;1&lt;&#x2F;code&gt;, which this provider’s manifest defines as process start. We then use &lt;code&gt;TdhGetProperty&lt;&#x2F;code&gt; to extract fields such as &lt;code&gt;ProcessID&lt;&#x2F;code&gt; and &lt;code&gt;ImageName&lt;&#x2F;code&gt;. Event schemas can vary by provider version, so production code should inspect the event metadata rather than assume every Windows build exposes an identical payload.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;c&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;VOID WINAPI &lt;&#x2F;span&gt;&lt;span style=&quot;color: #36F9F6;&quot;&gt;EventRecordCallback&lt;&#x2F;span&gt;&lt;span&gt;(PEVENT_RECORD &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF7EDB;font-style: italic;&quot;&gt;pEvent&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt;    if&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #36F9F6;&quot;&gt;memcmp&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt;&amp;amp;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF7EDB;&quot;&gt;pEvent&lt;&#x2F;span&gt;&lt;span&gt;-&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF7EDB;&quot;&gt;EventHeader&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF7EDB;&quot;&gt;ProviderId&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt; &amp;amp;&lt;&#x2F;span&gt;&lt;span&gt;ProviderGuid,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt; sizeof&lt;&#x2F;span&gt;&lt;span&gt;(GUID))&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt; !=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97E72;&quot;&gt; 0&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt; return&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;    &#x2F;&#x2F; Event ID 1 == Process Start&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt;    if&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF7EDB;&quot;&gt;pEvent&lt;&#x2F;span&gt;&lt;span&gt;-&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF7EDB;&quot;&gt;EventHeader&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF7EDB;&quot;&gt;EventDescriptor&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF7EDB;&quot;&gt;Id&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt; ==&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97E72;&quot;&gt; 1&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #36F9F6;&quot;&gt;        printf&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF8B39;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #36F9F6;&quot;&gt;\n&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF8B39;&quot;&gt;[+] Process Started!&lt;&#x2F;span&gt;&lt;span style=&quot;color: #36F9F6;&quot;&gt;\n&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF8B39;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #36F9F6;&quot;&gt;        printf&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF8B39;&quot;&gt;&amp;quot;  ProcessID: &lt;&#x2F;span&gt;&lt;span style=&quot;color: #72F1B8;font-style: italic;&quot;&gt;%lu&lt;&#x2F;span&gt;&lt;span style=&quot;color: #36F9F6;&quot;&gt;\n&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF8B39;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #36F9F6;&quot;&gt; GetUint32Property&lt;&#x2F;span&gt;&lt;span&gt;(pEvent, L&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF8B39;&quot;&gt;&amp;quot;ProcessID&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;));&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #36F9F6;&quot;&gt;        printf&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF8B39;&quot;&gt;&amp;quot;  ParentProcessID: &lt;&#x2F;span&gt;&lt;span style=&quot;color: #72F1B8;font-style: italic;&quot;&gt;%lu&lt;&#x2F;span&gt;&lt;span style=&quot;color: #36F9F6;&quot;&gt;\n&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF8B39;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #36F9F6;&quot;&gt; GetUint32Property&lt;&#x2F;span&gt;&lt;span&gt;(pEvent, L&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF8B39;&quot;&gt;&amp;quot;ParentProcessID&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;));&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #36F9F6;&quot;&gt;        PrintStringProperty&lt;&#x2F;span&gt;&lt;span&gt;(pEvent, L&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF8B39;&quot;&gt;&amp;quot;ImageName&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #36F9F6;&quot;&gt;        printf&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF8B39;&quot;&gt;&amp;quot;--------------------------------------------------&lt;&#x2F;span&gt;&lt;span style=&quot;color: #36F9F6;&quot;&gt;\n&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF8B39;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;em&gt;The complete program should include &lt;code&gt;GetUint32Property&lt;&#x2F;code&gt; and &lt;code&gt;PrintStringProperty&lt;&#x2F;code&gt; helpers that wrap &lt;code&gt;TdhGetProperty&lt;&#x2F;code&gt;, plus error handling for missing properties. The excerpts here focus on the ETW control flow.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;graceful-shutdown&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#graceful-shutdown&quot; aria-label=&quot;Anchor link for: graceful-shutdown&quot;&gt;Graceful Shutdown&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;Real-time ETW sessions are system-wide objects. If our program exits without stopping its session, that session can remain active. To prevent this, we handle &lt;code&gt;Ctrl+C&lt;&#x2F;code&gt; and stop it with &lt;code&gt;ControlTraceW&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;c&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;BOOL WINAPI &lt;&#x2F;span&gt;&lt;span style=&quot;color: #36F9F6;&quot;&gt;ConsoleHandler&lt;&#x2F;span&gt;&lt;span&gt;(DWORD &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF7EDB;font-style: italic;&quot;&gt;signal&lt;&#x2F;span&gt;&lt;span&gt;) {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt;    if&lt;&#x2F;span&gt;&lt;span&gt; (signal &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt;==&lt;&#x2F;span&gt;&lt;span&gt; CTRL_C_EVENT &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt;&amp;amp;&amp;amp;&lt;&#x2F;span&gt;&lt;span&gt; g_hSession) {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #36F9F6;&quot;&gt;        printf&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF8B39;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #36F9F6;&quot;&gt;\n&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF8B39;&quot;&gt;Stopping trace session gracefully...&lt;&#x2F;span&gt;&lt;span style=&quot;color: #36F9F6;&quot;&gt;\n&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF8B39;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #848BBD;font-style: italic;&quot;&gt;        &#x2F;&#x2F; Tell the kernel to stop our session&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #36F9F6;&quot;&gt;        ControlTraceW&lt;&#x2F;span&gt;&lt;span&gt;(g_hSession, g_SessionName,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt; &amp;amp;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF7EDB;&quot;&gt;stopProps&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF7EDB;&quot;&gt;Properties&lt;&#x2F;span&gt;&lt;span&gt;, EVENT_TRACE_CONTROL_STOP);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt;        return&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97E72;&quot;&gt; TRUE&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FEDE5D;&quot;&gt;    return&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97E72;&quot;&gt; FALSE&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;compiling-and-running&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#compiling-and-running&quot; aria-label=&quot;Anchor link for: compiling-and-running&quot;&gt;Compiling and Running&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;To compile this sensor, use the MSVC compiler included with Visual Studio. Open the &lt;strong&gt;x64 Native Tools Command Prompt&lt;&#x2F;strong&gt; and run:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;vb&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;cl.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF7EDB;&quot;&gt;exe&lt;&#x2F;span&gt;&lt;span&gt; &#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF7EDB;&quot;&gt;O2&lt;&#x2F;span&gt;&lt;span&gt; mini_edr.c&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;Testing the Agent:&lt;&#x2F;strong&gt;
Because ETW interacts directly with the Windows Kernel, &lt;strong&gt;you must run this executable as an Administrator&lt;&#x2F;strong&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Open an elevated command prompt, run &lt;code&gt;mini_edr.exe&lt;&#x2F;code&gt;, and then open an application like &lt;code&gt;notepad.exe&lt;&#x2F;code&gt; or &lt;code&gt;calc.exe&lt;&#x2F;code&gt;. You will see your mini-EDR catch the execution:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;PS &amp;gt; .\mini_edr.exe&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Trace Session &amp;#39;ProcMonTrace_18872&amp;#39; Started.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Listening for Process Starts... (Press Ctrl+C to stop)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[+] Process Started!&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ProcessID: 18956&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ParentProcessID: 1600&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ImageName: \Device\HarddiskVolume3\Windows\System32\dllhost.exe&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--------------------------------------------------&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[+] Process Started!&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ProcessID: 19012&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ParentProcessID: 6692&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ImageName: \Device\HarddiskVolume3\Program Files\WindowsApps\Microsoft.WindowsNotepad_11.2512.26.0_x64__8wekyb3d8bbwe\Notepad\Notepad.exe&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--------------------------------------------------&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[+] Process Started!&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ProcessID: 19200&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ParentProcessID: 2040&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ImageName: \Device\HarddiskVolume3\Program Files\WindowsApps\Microsoft.WindowsCalculator_11.2508.4.0_x64__8wekyb3d8bbwe\CalculatorApp.exe&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--------------------------------------------------&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;conclusion&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#conclusion&quot; aria-label=&quot;Anchor link for: conclusion&quot;&gt;Conclusion&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;Congratulations! You’ve just written a core component of an EDR engine.&lt;&#x2F;p&gt;
&lt;p&gt;Commercial EDRs add many layers: additional telemetry sources, tamper protection, buffering, enrichment, correlation, and remote analysis. This small consumer is not an EDR by itself, but it demonstrates one practical way to collect process telemetry on Windows.&lt;&#x2F;p&gt;
&lt;p&gt;In a future lab, we can compare this approach with a signed kernel driver that uses process-creation callbacks.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Building a Mini-SIEM Lab: Sysmon, Vector, Redpanda, ClickHouse &amp; Grafana</title>
        <published>2026-02-22T00:00:00+00:00</published>
        <updated>2026-02-22T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://adasumizox.github.io/posts/crc-building-mini-siem-lab-sysmon-vector-redpanda-clickhouse-grafana/"/>
        <id>https://adasumizox.github.io/posts/crc-building-mini-siem-lab-sysmon-vector-redpanda-clickhouse-grafana/</id>
        
        <content type="html" xml:base="https://adasumizox.github.io/posts/crc-building-mini-siem-lab-sysmon-vector-redpanda-clickhouse-grafana/">&lt;p&gt;Building a Security Information and Event Management (SIEM) system from scratch is one of the best ways to understand how those enterprise system works.&lt;&#x2F;p&gt;
&lt;p&gt;In this lab, we will build a modern log pipeline using Windows host and WSL2 (Debian). Before diving into the code, let’s understand core architecture of our pipeline:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Ingestion (&lt;a rel=&quot;noopener external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;learn.microsoft.com&#x2F;pl-pl&#x2F;sysinternals&#x2F;downloads&#x2F;sysmon&quot;&gt;Sysmon&lt;&#x2F;a&gt; &amp;amp; &lt;a rel=&quot;noopener external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;vector.dev&#x2F;&quot;&gt;Vector&lt;&#x2F;a&gt;):&lt;&#x2F;strong&gt; Gathering logs from the operating system is crucial step. Usually we gather logs from different system so we need to transform them into a schema format (ECS&#x2F;OCSF)&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Buffering (&lt;a rel=&quot;noopener external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.redpanda.com&#x2F;&quot;&gt;Redpanda&lt;&#x2F;a&gt;):&lt;&#x2F;strong&gt; A messaging queue that temporarily hold logs. This prevent database from crashing if a sudden spike in log volume occurs.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Storage (&lt;a rel=&quot;noopener external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;clickhouse.com&#x2F;&quot;&gt;ClickHouse&lt;&#x2F;a&gt;):&lt;&#x2F;strong&gt; An OLAP Database which is adjusted to handling Real-Time Analytics. It is column-oriented and optimized for analyzing massive amounts of log data.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Visualization (&lt;a rel=&quot;noopener external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;grafana.com&#x2F;&quot;&gt;Grafana&lt;&#x2F;a&gt;):&lt;&#x2F;strong&gt; The dashboard where security analysts run queries and visualize threats.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;Let’s build it step-by-step&lt;&#x2F;p&gt;
&lt;h2 id=&quot;phase-1-ingestion-sysmon-vector&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#phase-1-ingestion-sysmon-vector&quot; aria-label=&quot;Anchor link for: phase-1-ingestion-sysmon-vector&quot;&gt;Phase 1: Ingestion (Sysmon &amp;amp; Vector)&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;First we need something to generate high-quality security logs. For that we can use &lt;strong&gt;Sysmon&lt;&#x2F;strong&gt; Windows service that logs process creation, network connection and more.
We can use some already existing config e.g. &lt;a rel=&quot;noopener external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;SwiftOnSecurity&#x2F;sysmon-config&quot;&gt;SwitftOnSecurity&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Open an elevated PowerShell prompt on your Windows machine and run:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;# Download config to temp&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Invoke-WebRequest -Uri https:&#x2F;&#x2F;raw.githubusercontent.com&#x2F;SwiftOnSecurity&#x2F;sysmon-config&#x2F;master&#x2F;sysmonconfig-export.xml -OutFile C:\Windows\Temp\sysmonconfig.xml&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;# Install sysmon with config&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;sysmon64.exe -accepteula -i C:\Windows\Temp\sysmonconfig.xml&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Next step is installing &lt;code&gt;Vector&lt;&#x2F;code&gt; by DataDog to read those logs and send them to our pipeline.
&lt;em&gt;Note: You need to compile Vector from source to include &lt;a rel=&quot;noopener external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;vectordotdev&#x2F;vector&#x2F;pull&#x2F;24305&quot;&gt;PR #24305&lt;&#x2F;a&gt;. If you are compiling from Windows, ensure you have &lt;a rel=&quot;noopener external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;releases.llvm.org&#x2F;download.html&quot;&gt;LLVM&lt;&#x2F;a&gt; and follow the &lt;a rel=&quot;noopener external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;vector.dev&#x2F;docs&#x2F;setup&#x2F;installation&#x2F;manual&#x2F;from-source&#x2F;&quot;&gt;Vector manual installation guide&lt;&#x2F;a&gt;&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Create your Vector configuration file. This config maps raw Windows Event Logs to the Elastic Common Schema (ECS) format, making the data clean and structured.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;# vector.yaml&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;data_dir: C:\Users\USERNAME\AppData\Local\Temp\vector-data&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;api:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  enabled: true&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  address: 127.0.0.1:8686&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;sources:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  sysmon_logs:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    type: windows_event_log&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    channels:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      - Microsoft-Windows-Sysmon&#x2F;Operational&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      - Application&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      - System&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      - Security&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;transforms:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  remap_ecs:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    type: remap&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    inputs:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      - sysmon_logs&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    source: |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      # Metadata&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      .ecs.version = &amp;quot;8.11.0&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      .event.kind = &amp;quot;event&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      .event.code = .event_id&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      .event.provider = .provider_name&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      .event.dataset = .channel&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      # Map Log Level&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      .log.level = .level&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      # Timestamp&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      .timestamp = parse_timestamp!(.timestamp, format: &amp;quot;%Y-%m-%dT%H:%M:%S.%fZ&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      .@timestamp = .timestamp&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      # Host&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      .host.name = .computer&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      .host.os.family = &amp;quot;windows&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      .host.os.platform = &amp;quot;windows&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      # Event_data as primary source of truth&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      ed = .event_data&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      if .channel == &amp;quot;Microsoft-Windows-Sysmon&#x2F;Operational&amp;quot; &amp;amp;&amp;amp; !is_null(ed) {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        .event.module = &amp;quot;sysmon&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        # Sysmon Event 1: Process Creation&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        if .event_id == 1 {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          .event.category = &amp;quot;process&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          .event.type = [&amp;quot;start&amp;quot;]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          .process.pid = to_int!(ed.ProcessId)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          .process.entity_id = ed.ProcessGuid&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          .process.executable = ed.Image&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          .process.command_line = ed.CommandLine&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          .process.working_directory = ed.CurrentDirectory&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          .process.integrity = ed.IntegrityLevel&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          if !is_null(ed.Image) {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            path_parts = split!(ed.Image, &amp;quot;\\&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            .process.name = path_parts[-1]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          # User Parsing&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          if !is_null(ed.User) {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            user_parts = split!(ed.User, &amp;quot;\\&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            if length(user_parts) == 2 {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;              .user.domain = user_parts[0]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;              .user.name = user_parts[1]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            } else {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;              .user.name = ed.User&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          # Hash Parsing&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          if !is_null(ed.Hashes) {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            hashes = parse_key_value!(ed.Hashes, key_value_delimiter: &amp;quot;=&amp;quot;, field_delimiter: &amp;quot;,&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            if !is_null(hashes.MD5) { .process.hash.md5 = hashes.MD5 }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            if !is_null(hashes.SHA1) { .process.hash.sha1 = hashes.SHA1 }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            if !is_null(hashes.SHA256) { .process.hash.sha256 = hashes.SHA256 }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            if !is_null(hashes.IMPHASH) { .process.hash.imphash = hashes.IMPHASH }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          # Parent Process&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          .process.parent.pid = to_int!(ed.ParentProcessId)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          .process.parent.entity_id = ed.ParentProcessGuid&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          .process.parent.executable = ed.ParentImage&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          .process.parent.command_line = ed.ParentCommandLine&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          if !is_null(ed.ParentImage) {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            parent_parts = split!(ed.ParentImage, &amp;quot;\\&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            .process.parent.name = parent_parts[-1]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        # Sysmon Event 3: Network Connection&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        if .event_id == 3 {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          .event.category = &amp;quot;network&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          .event.type = [&amp;quot;connection&amp;quot;, &amp;quot;start&amp;quot;]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          .source.ip = ed.SourceIp&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          .source.port = to_int!(ed.SourcePort)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          .source.domain = ed.SourceHostname&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          .destination.ip = ed.DestinationIp&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          .destination.port = to_int!(ed.DestinationPort)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          .destination.domain = ed.DestinationHostname&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          .network.transport  = ed.Protocol&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          .process.pid = to_int!(ed.ProcessId)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          .process.entity_id = ed.ProcessGuid&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          .process.executable = ed.Image&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          if !is_null(ed.Image) {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            img_parts = split!(ed.Image, &amp;quot;\\&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            .process.name = img_parts[-1]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          if !is_null(ed.User) {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            user_parts = split!(ed.User, &amp;quot;\\&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            if length(user_parts) == 2 {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;              .user.domain = user_parts[0]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;              .user.name = user_parts[1]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      } else {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        # Handle standard Windows logs (Security, System, App)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        if .channel == &amp;quot;Security&amp;quot; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          .event.category = &amp;quot;authentication&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            if .event_id == 4624 {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;              .event.type = [&amp;quot;start&amp;quot;, &amp;quot;authentication_success&amp;quot;]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;              .event.outcome = &amp;quot;success&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            if .event_id == 4625 {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;              .event.type = [&amp;quot;start&amp;quot;, &amp;quot;authentication_failure&amp;quot;]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;              .event.outcome = &amp;quot;failure&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        if .channel == &amp;quot;System&amp;quot; { .event.category = &amp;quot;host&amp;quot; }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        if .channel == &amp;quot;Application&amp;quot; { .event.category = &amp;quot;process&amp;quot; }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        if !is_null(ed) {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          if !is_null(ed.TargetUserName) { .user.name = ed.TargetUserName }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          if !is_null(ed.TargetDomainName) { .user.domain = ed.TargetDomainName }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          if !is_null(ed.SubjectUserName) &amp;amp;&amp;amp; ed.SubjectUserName != &amp;quot;-&amp;quot; { .user.effective.name = ed.SubjectUserName }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          if !is_null(ed.IpAddress) &amp;amp;&amp;amp; ed.IpAddress != &amp;quot;-&amp;quot; { .source.ip = ed.IpAddress }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          if !is_null(ed.IpPort) &amp;amp;&amp;amp; ed.IpPort != &amp;quot;-&amp;quot; { .source.port = to_int(ed.IpPort) ?? null }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          if !is_null(ed.ProcessName) &amp;amp;&amp;amp; ed.ProcessName != &amp;quot;-&amp;quot; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            .process.executable = ed.ProcessName&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            p_parts = split!(ed.ProcessName, &amp;quot;\\&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            .process.name = p_parts[-1]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          if !is_null(ed.ProcessId) { .process.pid = to_int(ed.ProcessId) ?? null }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      # Clean up raw fields to save bandwidth&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      del(.event_data)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      del(.message)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      del(.task)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      del(.level_value)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      del(.record_id)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      del(.version)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;# Send to Redpanda (Kafka)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;sinks:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  to_redpanda:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    type: kafka&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    inputs:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      - remap_ecs&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    bootstrap_servers: 127.0.0.1:29092&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    topic: event-logs&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    encoding:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      codec: json&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;phase-2-3-4-buffering-storage-ui&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#phase-2-3-4-buffering-storage-ui&quot; aria-label=&quot;Anchor link for: phase-2-3-4-buffering-storage-ui&quot;&gt;Phase 2, 3 &amp;amp; 4: Buffering, Storage, UI&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;We will run our backend services in WSL2 (Debian) using Podman. Save following as &lt;code&gt;docker-compose.yml&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;# docker-compose.yml&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;version: &amp;#39;3.8&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;services:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  redpanda:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    image: docker.redpanda.com&#x2F;redpandadata&#x2F;redpanda:latest&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    container_name: redpanda&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    command:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      - redpanda&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      - start&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      - --kafka-addr internal:&#x2F;&#x2F;0.0.0.0:9092,external:&#x2F;&#x2F;0.0.0.0:29092&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      # Advertise external to localhost so Windows Vector can reach it. Internal to redpanda:9092 so Clickhouse can reach it.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      - --advertise-kafka-addr internal:&#x2F;&#x2F;redpanda:9092,external:&#x2F;&#x2F;127.0.0.1:29092&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      - --smp 1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      - --memory 1G&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      - --mode dev-container&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    ports:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      - &amp;quot;29092:29092&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      - &amp;quot;9092:9092&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      - &amp;quot;8081:8081&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      - &amp;quot;9644:9644&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    volumes:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      - redpanda_data:&#x2F;var&#x2F;lib&#x2F;redpanda&#x2F;data&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    networks:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      - siem_net&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  clickhouse:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    image: docker.io&#x2F;clickhouse&#x2F;clickhouse-server:latest&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    container_name: clickhouse&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    environment:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      - CLICKHOUSE_PASSWORD=siemlab&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    ports:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      - &amp;quot;8123:8123&amp;quot; # HTTP port (Grafana uses this)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      - &amp;quot;9000:9000&amp;quot; # Native client port&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    volumes:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      - clickhouse_data:&#x2F;var&#x2F;lib&#x2F;clickhouse&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      - .&#x2F;init-db.sql:&#x2F;docker-entrypoint-initdb.d&#x2F;init-db.sql # Auto-initializes tables&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    depends_on:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      - redpanda&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    networks:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      - siem_net&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  grafana:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    image: docker.io&#x2F;grafana&#x2F;grafana:latest&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    container_name: grafana&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    ports:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      - &amp;quot;3000:3000&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    environment:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      # Automatically install the ClickHouse plugin&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      - GF_INSTALL_PLUGINS=grafana-clickhouse-datasource&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    volumes:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      - grafana_data:&#x2F;var&#x2F;lib&#x2F;grafana&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    depends_on:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      - clickhouse&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    networks:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      - siem_net&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;networks:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  siem_net:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    driver: bridge&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;volumes:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  redpanda_data:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  clickhouse_data:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  grafana_data:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;RedPanda does not require additional configuration but ClickHouse and Grafana does.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;initializing-database&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#initializing-database&quot; aria-label=&quot;Anchor link for: initializing-database&quot;&gt;Initializing Database&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;ClickHouse needs to know how to read the logs from Redpanda and where to store them. We do this using a three-step architecture.&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Target Table:&lt;&#x2F;strong&gt; Where the data permanently lives.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Kafka Engine Table:&lt;&#x2F;strong&gt; A temporary consumer that pulls data from Redpanda&lt;&#x2F;li&gt;
&lt;li&gt;Materialized View: The pipeline connecting the consumer to the target table.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;Let’s save this as &lt;code&gt;init-db.sql&lt;&#x2F;code&gt; in the same directory as previous file so it will be initialized when database is initialized.
Here ingestion is smaller than our Event source, but we can extend that according to our use case.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;-- init-db.sql&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;CREATE DATABASE IF NOT EXISTS siem;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;-- 1. Storage Table: The actual storage for your ECS logs&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;CREATE TABLE IF NOT EXISTS siem.logs&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    `@timestamp` DateTime64(3),&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    `event.kind` String,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    `event.category` String,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    `event.type` Array(String),&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    `event.code` UInt32,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    `host.name` String,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    `process.name` String,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    `process.pid` UInt32,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    `source.ip` String,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    `destination.ip` String,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    `raw_message` String -- Storing the raw JSON is great for students to dig into missing fields&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;) ENGINE = MergeTree()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;ORDER BY `@timestamp`;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;-- 2. Consumer Table: Pulls raw JSON from Redpanda.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;CREATE TABLE IF NOT EXISTS siem.logs_queue&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    message String&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;) ENGINE = Kafka&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;SETTINGS kafka_broker_list = &amp;#39;redpanda:9092&amp;#39;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         kafka_topic_list = &amp;#39;event-logs&amp;#39;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         kafka_group_name = &amp;#39;clickhouse_consumer_group&amp;#39;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         kafka_format = &amp;#39;JSONAsString&amp;#39;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;-- 3. Pipeline: The Materialized View translating the Queue into Storage&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;CREATE MATERIALIZED VIEW IF NOT EXISTS siem.logs_mv TO siem.logs AS&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;SELECT&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -- Safely parse timestamp, default to current time if missing so the pipeline doesn&amp;#39;t break&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    ifNull(parseDateTime64BestEffortOrNull(JSONExtractString(message, &amp;#39;@timestamp&amp;#39;), 3), now64(3)) AS `@timestamp`,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    JSONExtractString(message, &amp;#39;event&amp;#39;, &amp;#39;kind&amp;#39;) AS `event.kind`,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    JSONExtractString(message, &amp;#39;event&amp;#39;, &amp;#39;category&amp;#39;) AS `event.category`,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -- Convert raw JSON array strings to actual ClickHouse Arrays&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    arrayMap(x -&amp;gt; replaceAll(x, &amp;#39;&amp;quot;&amp;#39;, &amp;#39;&amp;#39;), JSONExtractArrayRaw(message, &amp;#39;event&amp;#39;, &amp;#39;type&amp;#39;)) AS `event.type`,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    JSONExtractUInt(message, &amp;#39;event&amp;#39;, &amp;#39;code&amp;#39;) AS `event.code`,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    JSONExtractString(message, &amp;#39;host&amp;#39;, &amp;#39;name&amp;#39;) AS `host.name`,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    JSONExtractString(message, &amp;#39;process&amp;#39;, &amp;#39;name&amp;#39;) AS `process.name`,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    JSONExtractUInt(message, &amp;#39;process&amp;#39;, &amp;#39;pid&amp;#39;) AS `process.pid`,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    JSONExtractString(message, &amp;#39;source&amp;#39;, &amp;#39;ip&amp;#39;) AS `source.ip`,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    JSONExtractString(message, &amp;#39;destination&amp;#39;, &amp;#39;ip&amp;#39;) AS `destination.ip`,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    message AS `raw_message`&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;FROM siem.logs_queue;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Now we need to install &lt;code&gt;podman&lt;&#x2F;code&gt; and &lt;code&gt;podman-compose&lt;&#x2F;code&gt; then start the stack&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;sudo apt install podman podman-compose&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;sudo podman-compose up -d&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;troubleshooting-wsl2-podman-issues&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#troubleshooting-wsl2-podman-issues&quot; aria-label=&quot;Anchor link for: troubleshooting-wsl2-podman-issues&quot;&gt;Troubleshooting WSL2 &amp;amp; Podman Issues&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;When running Podman on WSL2, you might encounter a few known issue. Here is how to fix them:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Error:&lt;&#x2F;strong&gt; &lt;code&gt;WARN: The cgroupv2 manager is set to systemd but there is no systemd user session available&lt;&#x2F;code&gt;
&lt;em&gt;Fix:&lt;&#x2F;em&gt; You need to enable systemd inside WSL2. Edit or create &lt;code&gt;&#x2F;etc&#x2F;wsl.conf&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[boot]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;systemd=true&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;Error:&lt;&#x2F;strong&gt; &lt;code&gt;netavark: nftables error: nft did not return successfully while applying ruleset&lt;&#x2F;code&gt;
&lt;em&gt;Fix:&lt;&#x2F;em&gt; Switch your firewall to legacy &lt;code&gt;iptables&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;sudo update-alternatives --set iptables &#x2F;usr&#x2F;sbin&#x2F;iptables-legacy&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;sudo update-alternatives --set ip6tables &#x2F;usr&#x2F;sbin&#x2F;ip6tables-legacy&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;If that fails, force Podman to use iptables via its config&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;sudo mkdir -p &#x2F;etc&#x2F;containers&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;echo -e &amp;quot;[network]\nfirewall_driver = \&amp;quot;iptables\&amp;quot;&amp;quot; | sudo tee -a &#x2F;etc&#x2F;containers&#x2F;containers.conf&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;Start Fresh&lt;&#x2F;strong&gt;
If you mess up your configuration and need to wipe your state clean, run:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;# Powershell&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;wsl --shutdown&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;# WSL2&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;podman system reset -f&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;sudo podman network prune -f&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;sudo podman-compose down&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;phase-4-ui-and-threat-hunting-with-grafana&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#phase-4-ui-and-threat-hunting-with-grafana&quot; aria-label=&quot;Anchor link for: phase-4-ui-and-threat-hunting-with-grafana&quot;&gt;Phase 4: UI and Threat Hunting with Grafana&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;If all services are running, it’s time to connect Grafana to ClickHouse&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Open browser and go to &lt;code&gt;http:&#x2F;&#x2F;localhost:3000&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Navigate to Connections &amp;gt; Data Sources &amp;gt; Add data source&lt;&#x2F;li&gt;
&lt;li&gt;Select ClickHouse and fill in the details:
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Server Address:&lt;&#x2F;strong&gt; clickhouse&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Protocol:&lt;&#x2F;strong&gt; HTTP&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Secure (TLS):&lt;&#x2F;strong&gt; Off&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Username:&lt;&#x2F;strong&gt; default&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Password:&lt;&#x2F;strong&gt; siemlab&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Default Database (Advanced):&lt;&#x2F;strong&gt; siem&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;Click “Save &amp;amp; Test”. If it fails, check the logs with &lt;code&gt;sudo podman logs grafana&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;h4 id=&quot;example-queries&quot;&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#example-queries&quot; aria-label=&quot;Anchor link for: example-queries&quot;&gt;Example Queries&lt;&#x2F;a&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;Create a new dashboard, add a visualization, and try these queries to analyze your ingested data.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Query 1: Find Noisy Processes&lt;&#x2F;strong&gt;
Great for establishing a baseline of normal activity on host&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;SELECT&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    `process.name`,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    count() AS total_events&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;FROM siem.logs&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;WHERE $__timeFilter(`@timestamp`) AND `process.name` != &amp;#39;&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;GROUP BY `process.name`&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;ORDER BY total_events DESC&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;LIMIT 10&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;adasumizox.github.io&#x2F;posts&#x2F;crc-building-mini-siem-lab-sysmon-vector-redpanda-clickhouse-grafana&#x2F;query_popular.png&quot; alt=&quot;result of query&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Query 2: Raw Event Log View&lt;&#x2F;strong&gt;
A tabular view of recent events&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #FFFFFF; background-color: #262335;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;SELECT&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    `@timestamp` AS Time,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    `host.name` AS Host,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    `event.code` AS EventID,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    `process.name` AS Process,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    `source.ip` AS SourceIP,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    `destination.ip` AS DestIP&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;FROM siem.logs&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;WHERE $__timeFilter(`@timestamp`)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;ORDER BY `@timestamp` DESC&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;LIMIT 100&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;adasumizox.github.io&#x2F;posts&#x2F;crc-building-mini-siem-lab-sysmon-vector-redpanda-clickhouse-grafana&#x2F;query_all.png&quot; alt=&quot;result of query&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Now you have local SIEM processing real-time Windows logs.&lt;&#x2F;p&gt;
</content>
        
    </entry>
</feed>
