<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
    <channel>
      <title>Strafo - suricata</title>
      <link>https://strafo.net</link>
      <description>Personal blog of a cat disguised as a human</description>
      <generator>Zola</generator>
      <language>en</language>
      <atom:link href="https://strafo.net/tags/suricata/rss.xml" rel="self" type="application/rss+xml"/>
      <lastBuildDate>Wed, 23 Sep 2026 00:00:00 +0000</lastBuildDate>
      <item>
          <title>Shleemypants gets a pager: reactive Suricata triage in the homelab</title>
          <pubDate>Wed, 23 Sep 2026 00:00:00 +0000</pubDate>
          <author>Andrea Straforini</author>
          <link>https://strafo.net/blog/suricata-agent-webhook/</link>
          <guid>https://strafo.net/blog/suricata-agent-webhook/</guid>
          <description xml:base="https://strafo.net/blog/suricata-agent-webhook/">&lt;p&gt;In &lt;a rel=&quot;noopener external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;strafo.net&#x2F;blog&#x2F;shleemypants-sre-agent&#x2F;&quot;&gt;&lt;em&gt;AI SRE&lt;&#x2F;em&gt;&lt;&#x2F;a&gt; I gave the homelab an on-call engineer: a disposable VM running &lt;a rel=&quot;noopener external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;NousResearch&#x2F;hermes-agent&quot;&gt;Hermes&lt;&#x2F;a&gt;, scoped credentials, one cron job auditing dependencies every three days. Bounded, useful, entirely &lt;em&gt;scheduled&lt;&#x2F;em&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;My home router runs OPNsense with &lt;a rel=&quot;noopener external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;docs.opnsense.org&#x2F;manual&#x2F;ips.html&quot;&gt;Suricata&lt;&#x2F;a&gt;, and Monit emails me about interesting events:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Content match Service suricata_alert&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;    Date:        Tue, 22 Sep 2026 13:47:38&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    Action:      alert&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    Host:        opnsense.example.lan&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    Description: content match:&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;{&amp;quot;timestamp&amp;quot;:&amp;quot;2026-09-22T13:46:30.838863+0000&amp;quot;,&amp;quot;in_iface&amp;quot;:&amp;quot;vtnet1&amp;quot;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; &amp;quot;event_type&amp;quot;:&amp;quot;alert&amp;quot;,&amp;quot;vlan&amp;quot;:[1],&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; &amp;quot;src_ip&amp;quot;:&amp;quot;10.0.0.42&amp;quot;,&amp;quot;src_port&amp;quot;:35485,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; &amp;quot;dest_ip&amp;quot;:&amp;quot;10.0.0.1&amp;quot;,&amp;quot;dest_port&amp;quot;:53,&amp;quot;proto&amp;quot;:&amp;quot;UDP&amp;quot;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; &amp;quot;alert&amp;quot;:{&amp;quot;action&amp;quot;:&amp;quot;allowed&amp;quot;,&amp;quot;signature_id&amp;quot;:2012811,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          &amp;quot;signature&amp;quot;:&amp;quot;ET DNS Query to a .tk domain - Likely Hostile&amp;quot;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          &amp;quot;category&amp;quot;:&amp;quot;Potentially Bad Traffic&amp;quot;,&amp;quot;severity&amp;quot;:2,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          &amp;quot;metadata&amp;quot;:{&amp;quot;confidence&amp;quot;:[&amp;quot;High&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;Your faithful employee,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Monit&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Turning one of those into an actual investigation requires several planets to align:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;I have to be at home, in front of my PC&lt;&#x2F;li&gt;
&lt;li&gt;I have to have nothing else going on&lt;&#x2F;li&gt;
&lt;li&gt;I have to feel like it&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;Most of the time I just skipped them. Not sustainable.&lt;&#x2F;p&gt;
&lt;figure&gt;
  &lt;img src=&quot;lenny-suricata-alert.jpg&quot; alt=&quot;Lenny from The Simpsons holding up the &amp;quot;0 days without&amp;quot; workplace safety sign, captioned &amp;quot;days without a Suricata alert&amp;quot;&quot;&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;And when I did investigate, the interesting question was never &quot;did a rule fire&quot; but &quot;did this
host do anything else weird around then&quot;, and answering that means cross-referencing the
firewall log, the DNS log and the ARP table by hand.&lt;&#x2F;p&gt;
&lt;p&gt;So: time to give the SRE agent a new job.&lt;&#x2F;p&gt;
&lt;p&gt;The idea is the following:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;┌─ OPNsense router ───────────────────────────────────┐&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;│  Suricata ─▶ eve.json ─▶ Monit ─▶ push script       │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│                        (content match)   │          │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│  firewall log ◀──┐                       │          │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│  DNS log      ◀──┤            signed POST│          │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│  ARP table    ◀──┤                       │          │&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;                   │  REST API             │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;┌─ shleemypants-sre-agent VM ──────────────▼──────────┐&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;│                  └──── agent run ◀── Hermes webhook │&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;                          Telegram&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The interesting bit is what it costs while nothing is happening: zero. That rules out the
obvious design, where the agent polls the log on a cron and burns an LLM call every few
minutes to say &quot;no&quot;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-hermes-side&quot;&gt;The Hermes side&lt;a class=&quot;post-anchor&quot; href=&quot;#the-hermes-side&quot; aria-label=&quot;Anchor link for: the-hermes-side&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Hermes has a &lt;a rel=&quot;noopener external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;hermes-agent.nousresearch.com&#x2F;docs&#x2F;user-guide&#x2F;messaging&#x2F;webhooks&quot;&gt;webhook platform&lt;&#x2F;a&gt;: named routes under &lt;code&gt;&#x2F;webhooks&#x2F;&amp;lt;name&amp;gt;&lt;&#x2F;code&gt;, each with its own prompt, toolset and delivery target.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;routes&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: #F92672;&quot;&gt;  suricata-alerts&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: #F92672;&quot;&gt;    toolsets&lt;&#x2F;span&gt;&lt;span&gt;: [&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt;&amp;quot;terminal&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; &amp;quot;file&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; &amp;quot;web&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #AE81FF;&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 style=&quot;color: #F92672;&quot;&gt;    deliver&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt; telegram&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F92672;&quot;&gt;    prompt&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F92672;&quot;&gt; |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt;      Suricata fired an alert on the OPNsense router. Read the OPNsense section&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt;      of workspace&#x2F;INSTRUCTIONS.md, then use the OPNsense api to investigate.&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: #E6DB74;&quot;&gt;      Give a verdict: real, or noise. If the logs do not support a conclusion,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt;      say that instead of inventing one.&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: #E6DB74;&quot;&gt;      signature: {signature}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt;      source:    {src_ip}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #E6DB74;&quot;&gt;      target:    {dest_ip}:{dest_port}&#x2F;{proto}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;monit-as-the-trigger&quot;&gt;Monit as the trigger&lt;a class=&quot;post-anchor&quot; href=&quot;#monit-as-the-trigger&quot; aria-label=&quot;Anchor link for: monit-as-the-trigger&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;That endpoint now sits there waiting. Something on the router has to notice a new alert and
call it.&lt;&#x2F;p&gt;
&lt;p&gt;OPNsense ships Monit, and it already had a file check emailing me on new alerts. A file check matches &lt;em&gt;newly appended&lt;&#x2F;em&gt; content:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #272822;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;check file suricata_alert path &#x2F;var&#x2F;log&#x2F;suricata&#x2F;eve.json&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    if content = &amp;quot;event_type...alert&amp;quot; then exec &amp;quot;&#x2F;usr&#x2F;local&#x2F;bin&#x2F;suricata-webhook.py&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;what-the-payload-leaves-out&quot;&gt;What the payload leaves out&lt;a class=&quot;post-anchor&quot; href=&quot;#what-the-payload-leaves-out&quot; aria-label=&quot;Anchor link for: what-the-payload-leaves-out&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;The obvious implementation forwards the eve.json record. It&#x27;s already JSON, and the interesting
fields are right there: &lt;code&gt;dns.rrname&lt;&#x2F;code&gt;, &lt;code&gt;http.hostname&lt;&#x2F;code&gt;, &lt;code&gt;http.user_agent&lt;&#x2F;code&gt;. An alert reading
&lt;code&gt;ET DNS Query to a .tk domain&lt;&#x2F;code&gt; is much more useful when you can see &lt;em&gt;which&lt;&#x2F;em&gt; domain.&lt;&#x2F;p&gt;
&lt;p&gt;Don&#x27;t. Those fields are written by whoever sent the packet, and the prompt interpolates the
payload verbatim, so they land inside the agent&#x27;s own instructions. The rule fires &lt;em&gt;because&lt;&#x2F;em&gt; a
stranger picked the domain, and registering &lt;code&gt;ignore-previous-instructions-and.tk&lt;&#x2F;code&gt; isn&#x27;t a
sophisticated attack; it&#x27;s a DNS lookup.&lt;&#x2F;p&gt;
&lt;p&gt;So the payload is ten flat fields (signature, category, severity, addresses, ports, timestamp),
every one of them chosen by Suricata or the IP stack rather than by the packet&#x27;s author. The
agent gets the domain anyway, by querying the DNS log itself.&lt;&#x2F;p&gt;
&lt;p&gt;That&#x27;s a reduction, not a wall: the domain still reaches the model, it just arrives as one log
row among hundreds instead of as a labelled field the prompt told it to read. So the real
backstop is the account. A hijacked run reads a handful of logs, writes nothing, and can talk
to one Telegram channel.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wiring-it-up&quot;&gt;Wiring it up&lt;a class=&quot;post-anchor&quot; href=&quot;#wiring-it-up&quot; aria-label=&quot;Anchor link for: wiring-it-up&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;The script lands in &lt;code&gt;&#x2F;usr&#x2F;local&#x2F;bin&lt;&#x2F;code&gt;, root-owned and mode &lt;code&gt;700&lt;&#x2F;code&gt;, since Monit executes it as
root. The shared secret goes in a mode-&lt;code&gt;600&lt;&#x2F;code&gt; file under &lt;code&gt;&#x2F;usr&#x2F;local&#x2F;etc&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Then, in &lt;strong&gt;Services ‣ Monit&lt;&#x2F;strong&gt;, the content condition and its &lt;code&gt;Exec&lt;&#x2F;code&gt; action become a Service
Test attached to the existing &lt;code&gt;eve.json&lt;&#x2F;code&gt; file check.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;the-api-user&quot;&gt;The API user&lt;a class=&quot;post-anchor&quot; href=&quot;#the-api-user&quot; aria-label=&quot;Anchor link for: the-api-user&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;The agent authenticates to OPNsense as a dedicated non-root account holding eight privileges,
which is the whole of its reach into the router:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Diagnostics: ARP Table&lt;&#x2F;strong&gt; and &lt;strong&gt;NDP Table&lt;&#x2F;strong&gt;, for IP to MAC to hostname&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Diagnostics: Logs: Firewall&lt;&#x2F;strong&gt; (Live, Plain and Summary View), which together back the
parsed firewall log endpoint&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Services: Dnsmasq DNS&#x2F;DHCP: Log File&lt;&#x2F;strong&gt;, the DNS queries the payload omits&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Interfaces: Diagnostics: DNS Lookup&lt;&#x2F;strong&gt;, for resolving a name found during an investigation&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;System: Deny config write&lt;&#x2F;strong&gt;, not a read grant at all but a hard block on configuration
writes regardless of what else is checked&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;One caveat if you copy this shape: &lt;strong&gt;the log privilege isn&#x27;t quite read-only.&lt;&#x2F;strong&gt; A &lt;code&gt;&#x2F;clear&lt;&#x2F;code&gt;
endpoint sits in the same granted namespace and works by &lt;code&gt;GET&lt;&#x2F;code&gt;, so whatever reads the log can
also delete it. There&#x27;s no narrower privilege to withhold, which leaves it where the other
forbidden actions live: a rule in the agent&#x27;s instructions rather than a wall.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;closing&quot;&gt;Closing&lt;a class=&quot;post-anchor&quot; href=&quot;#closing&quot; aria-label=&quot;Anchor link for: closing&quot;&gt;&lt;span aria-hidden=&quot;true&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;The engineering here is small: one stdlib-only Python file, a config block, and an API account
with eight privileges. As with the agent itself, most of the work went into bounding it rather
than building it: last time by deciding what it was allowed to do, this time by deciding what
it can be told, and by whom.&lt;&#x2F;p&gt;
&lt;p&gt;And the proof isn&#x27;t that a report arrives; it&#x27;s what the report says. On the first real run the
agent named &lt;code&gt;maps.google.tk&lt;&#x2F;code&gt;, a domain that appeared in none of the ten fields it had been
handed. It found the name by taking the source address and searching the DNS log, closing the
correlation loop on its own for the cost of one extra query.&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;section class=&quot;alert note&quot; role=&quot;note&quot; aria-labelledby=&quot;Yo8JWS1N&quot;&gt;
    &lt;div class=&quot;alert-icon alert-icon-info&quot;&gt;&lt;&#x2F;div&gt;
    &lt;div class=&quot;alert-content&quot; role=&quot;presentation&quot;&gt;
        &lt;strong id=&quot;Yo8JWS1N&quot; class=&quot;alert-title&quot; aria-hidden=&quot;true&quot;&gt;License&lt;&#x2F;strong&gt;
        &lt;p&gt;This article is licensed under the &lt;a rel=&quot;noopener external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;creativecommons.org&#x2F;licenses&#x2F;by-sa&#x2F;4.0&#x2F;&quot;&gt;CC BY-SA 4.0 license&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;

    &lt;&#x2F;div&gt;
&lt;&#x2F;section&gt;
</description>
      </item>
    </channel>
</rss>
