<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Saint Zero Day</title>
    <description>The latest articles on DEV Community by Saint Zero Day (@saint_zero_day).</description>
    <link>https://dev.to/saint_zero_day</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3825959%2F719209bf-6c59-4942-b7ef-e126f1b10b26.jpg</url>
      <title>DEV Community: Saint Zero Day</title>
      <link>https://dev.to/saint_zero_day</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/saint_zero_day"/>
    <language>en</language>
    <item>
      <title>One install, many customers: building airtight multi-tenancy into a self-hosted security platform</title>
      <dc:creator>Saint Zero Day</dc:creator>
      <pubDate>Tue, 02 Jun 2026 04:39:38 +0000</pubDate>
      <link>https://dev.to/saint_zero_day/one-install-many-customers-building-airtight-multi-tenancy-into-a-self-hosted-security-platform-kl2</link>
      <guid>https://dev.to/saint_zero_day/one-install-many-customers-building-airtight-multi-tenancy-into-a-self-hosted-security-platform-kl2</guid>
      <description>&lt;p&gt;I build a self-hosted security platform in Go. Today I shipped one of its biggest capabilities: one install that can host many customers, each one fully sealed off from the others. Multi-tenancy, done so the walls between customers are airtight.&lt;/p&gt;

&lt;p&gt;Here's what I built and how it works.&lt;/p&gt;

&lt;h2&gt;
  
  
  The goal: two deployment models from one codebase
&lt;/h2&gt;

&lt;p&gt;I wanted both of these to be true at the same time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;One install per customer — the simple, self-contained mode (everything defaults to a single tenant).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;One install, many customers — a central server hosting a whole book of clients, each isolated.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The second is just the first with more than one customer in it. The hard part is the word airtight: Customer A must never read or touch Customer B's data, by any path. So I designed isolation in two directions — the read side (what an operator sees) and the write side (what agents on customer machines send in).&lt;/p&gt;

&lt;h2&gt;
  
  
  Read side: every query knows its customer
&lt;/h2&gt;

&lt;p&gt;Every table already carried a tenant_id. The work was making sure every by-id lookup is scoped to the customer you're actually viewing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="n"&gt;DB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;GetIncident&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tenantID&lt;/span&gt; &lt;span class="kt"&gt;int64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IncidentRow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// ... WHERE id = ? AND tenant_id = ?&lt;/span&gt;
    &lt;span class="c"&gt;// an id from another customer simply returns "not found"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The operator's current customer lives in their session; the dashboard and the API both resolve it the same way and thread it down to the store. Pick a customer in the switcher and the entire dashboard — every page, every alert, every agent — follows it. Default to a single tenant and the behavior is identical to the single-install mode, so nothing about the simple path changes.&lt;/p&gt;

&lt;p&gt;I locked this down at the data layer rather than trusting handlers to remember, and I proved it with an adversarial test: stand up two customers, then assert one can't read or mutate the other across every domain. That test is the real deliverable — the scoping is just what makes it pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  Write side: bind the agent to the customer at install time
&lt;/h2&gt;

&lt;p&gt;Agents run on a customer's machines (think a ZDS agent on their domain controller) and report back. Their data has to land in their tenant and nowhere else. Before building this I went and read how the people who do it at scale handle it.&lt;/p&gt;

&lt;p&gt;Some folks bind an agent to a customer at install time with a token — a per-organization secret baked into the deploy. The agent never gets to say which customer it belongs to; the token already decided. While others pins its agent channel with a certificate so nothing can impersonate it. That's the model, and it's a good one, so I built the same thing.&lt;/p&gt;

&lt;p&gt;Each customer gets a per-tenant enrollment secret. An agent can only register into the customer whose secret it presents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// constant-time compare; empty secret = "no token required" (single-install back-compat)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;enrollSecret&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
   &lt;span class="n"&gt;subtle&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConstantTimeCompare&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EnrollSecret&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;enrollSecret&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;writeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusForbidden&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"invalid or missing enrollment secret"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every agent also carries its own mTLS client certificate, issued at enrollment. So when an agent reports in, the body's agent_id has to match the certificate's common name, and the tenant always comes from the enrolled record — never from anything the request claims:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// the agent id is client-supplied; bind it to the verified cert so an agent&lt;/span&gt;
&lt;span class="c"&gt;// can't report (and write) as another agent / another customer&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;agentCertMatches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AgentID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;writeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusForbidden&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"client certificate does not match agent id"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The control plane gets the same treatment: an operator can only see and command agents that belong to the customer they're viewing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// 404 unless the agent belongs to the caller's tenant — you can't read, control,&lt;/span&gt;
&lt;span class="c"&gt;// or even confirm the existence of another customer's machines&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="n"&gt;Server&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;agentInTenant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;agentID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Agent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetAgentByAgentID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agentID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TenantID&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;tenantID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;writeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusNotFound&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"agent not found"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Products per customer
&lt;/h2&gt;

&lt;p&gt;The same platform sells differently to different customers, so add-on capabilities — EDR, identity threats, attack-surface, patch management, and the rest — are toggled per customer. The nav, the route gating, and the settings toggles all key off the current customer. A new customer starts with the core platform and nothing else; you turn on what they bought. There's a Customers screen to create a customer (which hands you its one-time enrollment token), a switcher to move between them, and a one-click way to rotate a token if it's ever exposed — old agents keep running, only new enrollments need the new one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proving it, not assuming it
&lt;/h2&gt;

&lt;p&gt;Isolation isn't a property you design in — it's a claim you have to attack until it stops being true. So there are two layers of adversarial tests: a store-level suite that proves cross-tenant reads and writes fail across every domain, and an end-to-end HTTP test that stands up two customers and an agent and confirms one can't read another's data or dispatch a command to another's machine. A couple of deliberate trade-offs are written down rather than left implicit — the operator API key is the admin credential, not a tenant boundary, and mTLS has to be on for the agent-identity binding to hold.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tamper-evidence: the records can't be quietly rewritten
&lt;/h2&gt;

&lt;p&gt;Isolation keeps customers apart. The other half of trust is making sure the record of what happened can't be silently altered — including by someone who's already inside the box. For that, the platform anchors its important events to an immutable, hash-chained, signed ledger I built called SaintChain.&lt;/p&gt;

&lt;p&gt;Four kinds of event get anchored: audit actions (who did what), finding creation and state changes, backup events (including each scan run's Merkle root), and chain-of-custody evidence handling. Each one is a hash chained onto the last and signed, so any after-the-fact edit breaks the chain and chain verify catches it.&lt;/p&gt;

&lt;p&gt;It runs in two modes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# embedded ledger — zero config, on by default&lt;/span&gt;
zds-core serve &lt;span class="nt"&gt;--chain&lt;/span&gt;
&lt;span class="c"&gt;# tamper-PROOF — anchor to a separate SaintChain daemon (its own OS user/process)&lt;/span&gt;
zds-core serve &lt;span class="nt"&gt;--chain-endpoint&lt;/span&gt; http://127.0.0.1:7433
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Embedded is the convenient default. Remote is the strong one: because the SaintChain daemon runs as its own process and owns the signing key, a compromised app can append to history but can't rewrite it — the thing holding the pen isn't the thing being audited. Add an off-host checkpoint of the chain head and even a fully-compromised host can't quietly edit the past without the discrepancy surfacing somewhere it doesn't control.&lt;/p&gt;

&lt;p&gt;So multi-tenancy gives you walls between customers; SaintChain gives you a record of what happened inside those walls that you can actually trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then I made it teach itself
&lt;/h2&gt;

&lt;p&gt;A capability you can't explain isn't worth much, so the second half of the day was documentation — an architecture write-up and a full operator manual for the dashboard.&lt;/p&gt;

&lt;p&gt;Then I built a pipeline to turn that manual into narrated how-to videos, entirely locally, no paid services:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Voice — Piper TTS, a neural voice rendered offline.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Screenshots — headless Chrome driving the real dashboard, seeded with demo data.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Assembly — ffmpeg: clean slides with soft fades, a mouse pointer that glides to whatever the narration is describing, and live subtitles synced sentence-by-sentence (I render each sentence's audio separately so the timing is exact).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A few hard-won ffmpeg notes, in case they save you an evening:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;ffmpeg inside a while read loop eats the loop's stdin and corrupts your file list — pass -nostdin.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;An unbounded -loop 1 overlay input makes ffmpeg encode frames forever — bound it with -t and overlay=...:shortest=1.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The Ken Burns zoompan jitters on slow moves because it crops on integer pixels; static slides look smoother than bad motion.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;TTS that peaks at 0 dB clips and sounds harsh — loudnorm to -16 LUFS with headroom fixes it.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nine videos, about five and a half minutes, the whole dashboard explained in its own voice. The pipeline is four small scripts; adding a section is editing a spec file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this leaves the platform
&lt;/h2&gt;

&lt;p&gt;One server can now run a whole roster of customers with real walls between them, agents that can only ever belong to the customer they were deployed for, products sold per customer, and a documentation set that explains the whole thing. It's the difference between a tool I run for myself and a platform I could run for many — built the way I want to build everything: airtight, and proven.&lt;/p&gt;

&lt;p&gt;You can find the platform at &lt;a href="https://szdsecurity.com/" rel="noopener noreferrer"&gt;szdsecurity.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>security</category>
      <category>go</category>
      <category>architecture</category>
      <category>devjournal</category>
    </item>
    <item>
      <title>It ran it works: I audited my own security platform and found a detection engine that never ran</title>
      <dc:creator>Saint Zero Day</dc:creator>
      <pubDate>Mon, 01 Jun 2026 06:56:43 +0000</pubDate>
      <link>https://dev.to/saint_zero_day/it-ran-it-works-i-audited-my-own-security-platform-and-found-a-detection-engine-that-never-ran-15fc</link>
      <guid>https://dev.to/saint_zero_day/it-ran-it-works-i-audited-my-own-security-platform-and-found-a-detection-engine-that-never-ran-15fc</guid>
      <description>&lt;p&gt;I build a security platform. Last night I stopped adding features and did something less fun and more honest: I sat down to make every capability prove it actually works — end to end, with real data, demanding a real pass or fail.&lt;/p&gt;

&lt;p&gt;"It ran" is not a pass. A page that renders is not a feature. A green checkmark is a claim, not evidence. So I went capability by capability and tried to break each one.&lt;/p&gt;

&lt;p&gt;I found four real bugs and one of them was a gut-punch: a whole detection engine that was wired into the UI, unit-tested, and never actually ran in production.&lt;/p&gt;

&lt;p&gt;Here's how the night went.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule: drive it, don't admire it
&lt;/h2&gt;

&lt;p&gt;My method was boring on purpose. For each capability:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Feed it real input through the real entry point (CLI or API), not a test fixture.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Check the data actually landed (query the DB, don't trust the success message).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Feed it a malicious input and a benign input — it has to fire on one and stay quiet on the other.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The detection engine passed cleanly. I threw a PsExec process event at it and it lit up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;zds-core detection &lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="nt"&gt;--event&lt;/span&gt; &lt;span class="s1"&gt;'{"event_type":"process_create","process_name":"psexec.exe"}'&lt;/span&gt;
1 alert&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;:
  &lt;span class="o"&gt;[&lt;/span&gt;high] PsExec Execution — &lt;span class="o"&gt;(&lt;/span&gt;matched: map[process_name:psexec.exe]&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A wevtutil cl Security event tripped a critical "Log Clearing" rule. A plain notepad.exe matched nothing. Good — it detects, and it doesn't cry wolf.&lt;/p&gt;

&lt;p&gt;(Small UX papercut I fixed while I was there: if you forgot the event_type field, the engine silently matched nothing and printed "no rules matched" — which reads exactly like "you're safe." Now it warns you that the event can't match any rule. Silence that looks like safety is the most dangerous output a security tool can produce.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The one that hurt: ITDR
&lt;/h2&gt;

&lt;p&gt;Identity Threat Detection and Response. The engine has detectors for impossible travel, credential spraying, brute force, privilege escalation. All unit-tested. All green.&lt;/p&gt;

&lt;p&gt;I ran the real flow: POST two login events for one user — New York, then London thirty minutes later. That's ~5,500 km in half an hour. Textbook impossible travel. Then I asked for the alerts.&lt;/p&gt;

&lt;p&gt;Nothing. Zero alerts.&lt;/p&gt;

&lt;p&gt;The events were in the database. The detector logic was correct in isolation. But the alerts endpoint was empty. So I read the ingest handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="n"&gt;Server&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;handleRecordITDREvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;evt&lt;/span&gt; &lt;span class="n"&gt;itdr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IdentityEvent&lt;/span&gt;
    &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewDecoder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;evt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InsertIdentityEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;evt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c"&gt;// store it...&lt;/span&gt;
    &lt;span class="c"&gt;// ...and return. That's it.&lt;/span&gt;
    &lt;span class="n"&gt;writeJSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCreated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;evt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It stored the event and returned. It never called the detector. Every identity event in the system was being filed and forgotten. The engine that was the entire point of the feature had no caller. In production the alerts list would have been empty forever, and it would have looked like good news.&lt;/p&gt;

&lt;p&gt;The fix was small once I saw it — hold a persistent detector on the server and run every event through it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InsertIdentityEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;evt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c"&gt;// ...&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;itdrDetector&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RecordEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;evt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InsertIdentityAlert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;alert&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Re-ran the exact same two logins:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ALERT: impossible_travel  high  "impossible travel detected: 5570 km in 30m0s"
risk: critical (80)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the moment the night paid for itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The other three
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;CSAF export (vulnerability advisory) returned zero vulnerabilities even when findings carried CVEs. It built the advisory only from the vulnerabilities table, but a CVE imported from a scanner lands on the finding first. I ran a real Nuclei scan, it found a Log4Shell template match (CVE-2021-44228), imported clean — and the export dropped it. Fix: synthesize the advisory entry from the finding when the vuln table hasn't caught up. Now the CVE shows up where it belongs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;GRC compliance reported 0% against every framework. The "supported frameworks" list and the controls being scored were two different control-ID namespaces that never intersected. The command was structurally incapable of returning anything but zero. Pointed it at the frameworks the controls are actually tagged with — now it scores.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Workflow executions stored started_at in UTC and completed_at in local time. Same row, off by my timezone offset, quietly corrupting every duration. One column was using the SQLite CURRENT_TIMESTAMP default while the other came from the app.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What actually held up
&lt;/h2&gt;

&lt;p&gt;Plenty did, and I made it prove it: incident lifecycle and timelines, the workflow engine, Diamond Model events, threat-hunting hypotheses, forensic hashing + YARA validation, case management with a real state machine, UEBA (I trained a baseline on nine normal logins and it correctly flagged a 3am login from Russia pulling 900 events as unusual hour + location + volume), and the Active Directory scanner against a real Windows Server 2022 domain controller — real findings, real MITRE mappings, no invented kerberoasting that wasn't there.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lesson I keep relearning
&lt;/h2&gt;

&lt;p&gt;The bugs that scare me aren't the ones that crash. They're the ones that return success while doing nothing. A detector with no caller. An advisory that silently drops the one CVE that matters. A compliance score that's structurally pinned to zero. Each one renders fine. Each one would demo fine. Each one is a lie your own dashboard tells you.&lt;/p&gt;

&lt;p&gt;If you ship anything people rely on, spend a night being your own adversary. Don't ask "does it run." Ask "show me." Feed it the bad input. Query the table. Make the green checkmark earn it.&lt;/p&gt;

&lt;p&gt;The detector runs now.&lt;/p&gt;

</description>
      <category>security</category>
      <category>go</category>
      <category>testing</category>
      <category>devjournal</category>
    </item>
    <item>
      <title>My test suite was green. My software was lying to me.</title>
      <dc:creator>Saint Zero Day</dc:creator>
      <pubDate>Sat, 30 May 2026 05:22:16 +0000</pubDate>
      <link>https://dev.to/saint_zero_day/my-test-suite-was-green-my-software-was-lying-to-me-2c7n</link>
      <guid>https://dev.to/saint_zero_day/my-test-suite-was-green-my-software-was-lying-to-me-2c7n</guid>
      <description>&lt;p&gt;My CI was green. 1,885 tests, 66 packages, zero failures. go vet clean. The build was a single self-contained binary. By every signal a Go project gives you, it worked.&lt;/p&gt;

&lt;p&gt;Then I pointed it at something real, and watched it lie to my face.&lt;/p&gt;

&lt;p&gt;This is the story of six bugs I found in my own security platform — &lt;strong&gt;ZDS Core&lt;/strong&gt; — by refusing to trust a green checkmark. Five of the six belonged to the same scary family: the code reported success and stored nothing. No error. No stack trace. A 200 OK and an empty database.&lt;/p&gt;

&lt;p&gt;If you ship anything that ingests data from the outside world, you have at least one of these right now. Let me show you what they look like.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup: test against reality, not fixtures
&lt;/h2&gt;

&lt;p&gt;ZDS Core is a security platform written in Go — vulnerability scanning, an endpoint agent, EDR integrations, vuln-data feeds, compliance exports, the works. My unit tests were thorough. They were also, I realized, all talking to fixtures and in-memory SQLite. They proved my logic. They proved nothing about what happens when a real Wazuh server, a real OpenSearch cluster, or a real CVE feed shows up with data shaped slightly differently than I assumed.&lt;/p&gt;

&lt;p&gt;So I spent a weekend wiring up the real things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;podman containers for an nginx target, a Wazuh 4.7 manager, an OpenSearch node&lt;/li&gt;
&lt;li&gt;nmap for actual scanning&lt;/li&gt;
&lt;li&gt;Live feeds: CISA KEV, FIRST.org EPSS, Google OSV, NVD&lt;/li&gt;
&lt;li&gt;The endpoint agent running on my actual Fedora box&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rule for the weekend: &lt;strong&gt;"it ran" is not a pass.&lt;/strong&gt; Data has to land, and it has to be correct. Then I went looking for lies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug #1 — The endpoint agent that threw everything away
&lt;/h2&gt;

&lt;p&gt;The agent registered with the server. It collected 2,343 software packages and a pile of open ports off my machine. The server logged results accepted for every batch. The agent was happy. I was happy.&lt;/p&gt;

&lt;p&gt;The software table had zero rows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;failed to insert port finding 38810/udp: FOREIGN KEY constraint failed
failed to insert port finding 40319/udp: FOREIGN KEY constraint failed
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The handler looked up the agent's asset by IP to get a parent asset_id. But nothing ever created that asset. So agentAssetID was 0, every insert violated the foreign key, and the error was logged-and-swallowed inside a loop while the endpoint cheerfully returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"accepted"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"accepted software_inventory results"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix was to create the asset on first sight (idempotently), and — embarrassingly — to actually set AssetID on the port finding, which the original code never did. After that: 2,343 packages and 96 findings landed. The entire endpoint telemetry pipeline had been a no-op, and the API was reporting victory the whole time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; an accepted: true you control is worth nothing. The only proof is the row.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug #2 — "Synced 1" / stored 0
&lt;/h2&gt;

&lt;p&gt;Same family, different corner. My EDR alert sync said alerts synced: 1. The findings table was empty.&lt;/p&gt;

&lt;p&gt;This one was a landmine hiding in a helper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="n"&gt;DB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;UpsertFinding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Finding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FingerprintHash&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;   &lt;span class="c"&gt;// &amp;lt;-- no error. no insert. nothing.&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A finding with no fingerprint returns (0, false, nil). No error. The caller increments synced++, logs success, moves on. Four different EDR code paths built findings without ever setting a fingerprint. Every alert and every vulnerability from those paths evaporated, silently, with a success message.&lt;/p&gt;

&lt;p&gt;If you take one thing from this post: &lt;strong&gt;a function that returns nil for "I did nothing" is a trap.&lt;/strong&gt; At minimum it should be loud. Ideally the type system shouldn't let you build the thing without the required field.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug #3 — The CVE that wasn't there
&lt;/h2&gt;

&lt;p&gt;The OSV feed connector queried Google's OSV API for a CVE and reported Loaded 0 vulnerabilities. The API was returning 200 with a full record. I checked by hand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://api.osv.dev/v1/vulns/CVE-2021-44228 | jq &lt;span class="s1"&gt;'{id, aliases}'&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"id"&lt;/span&gt;: &lt;span class="s2"&gt;"CVE-2021-44228"&lt;/span&gt;,
  &lt;span class="s2"&gt;"aliases"&lt;/span&gt;: &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"GHSA-jfh8-c2jp-5v3q"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The parser only scanned aliases for a CVE- prefix. But when you query OSV by CVE, the CVE is the record's id, and the alias is the GHSA. So the parser found no CVE, mapped to an empty ID, and dropped the result. Every CVE-keyed OSV sync had been a quiet no-op. One-line shape mismatch, total data loss.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug #4 — A removed endpoint nobody noticed
&lt;/h2&gt;

&lt;p&gt;edr sync against a real Wazuh 4.7 manager: agents and vulns synced fine, alerts 404'd.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;wazuh api GET /alerts?limit=500&amp;amp;offset=0 returned status 404
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The code's own comment admitted the truth — "Wazuh 4.x alerts come from the indexer API" — and then queried the Manager API's /alerts endpoint anyway. That endpoint existed in Wazuh 3.x and was removed in 4.x; alerts now live in the Indexer (OpenSearch). The integration could never have worked against any modern Wazuh.&lt;/p&gt;

&lt;p&gt;Fixing it meant actually implementing the Indexer query path (wazuh-alerts-*/_search), wiring an --indexer-url, and skipping gracefully when it isn't set instead of faceplanting on a dead URL. Then I stood up a real OpenSearch node, seeded an alert, and watched it flow all the way through to a stored finding:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;scanner&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;wazuh  category=alert  severity=high&lt;/span&gt;
&lt;span class="py"&gt;title&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;[5710] sshd: Attempt to login using a non-existent user&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That end-to-end run is the only reason I believe it works. A unit test with a mocked response would never have caught that the endpoint itself was gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug #5 — The connector with no front door
&lt;/h2&gt;

&lt;p&gt;I went to test the Nessus/Nuclei/OpenVAS importer and discovered there was no way to call it. The parsers existed, were unit-tested, and were registered in a nice little plugin registry — and absolutely nothing in the CLI or API ever invoked them. A whole feature with no door.&lt;/p&gt;

&lt;p&gt;So I added one (connector import --tool nuclei --file ...), ran a real Nuclei scan against my nginx target, imported it — and immediately found:&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug #6 — Nuclei findings, minus the CVE
&lt;/h2&gt;

&lt;p&gt;Nuclei tags its CVE-template matches with a classification block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"classification"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"cve-id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"CVE-2021-44228"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"cwe-id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"cwe-502"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"cvss-score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My parser's struct didn't have a classification field, so encoding/json quietly dropped it. Every CVE that Nuclei detected lost its CVE on import — no correlation, no KEV matching, no CSAF export. The findings landed, but stripped of the one field that makes a vuln finding useful.&lt;/p&gt;

&lt;p&gt;After the fix, the full chain finally worked: &lt;strong&gt;Nuclei scan → import → CVE finding → OSV enrich → correlate → CSAF-VEX 2.0 advisory.&lt;/strong&gt; Six tools, one pipeline, and it took running every link to trust the chain.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Green tests are a floor, not a ceiling.&lt;/strong&gt; Mine proved the logic and hid every integration assumption I'd gotten wrong.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Silent success is the most dangerous failure mode.&lt;/strong&gt; A crash gets fixed in an hour. A 200 OK that stores nothing survives to production and costs you a customer's trust.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;"Accepted" is not "stored."&lt;/strong&gt; Verify the row, not the response.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Field-shape mismatches are invisible in Go.&lt;/strong&gt; encoding/json drops unknown fields without a peep. The CVE-in-id bug and the dropped classification block were both this. Decode strictly, or assert on the parsed result.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Run the whole chain against the real thing at least once.&lt;/strong&gt; Every one of these six was only catchable with a live target.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All six are fixed, each with a regression test, and I wrote the evidence down — actual commands, actual output — in a VERIFICATION.md in the repo. If I'm going to ask people to trust this thing with their security, "trust me, the tests pass" isn't good enough. "Here's exactly what I ran and what it returned" is the bar.&lt;/p&gt;

&lt;h2&gt;
  
  
  The other thing that happened this week
&lt;/h2&gt;

&lt;p&gt;In between bug hunts, I got the company site live: &lt;strong&gt;&lt;a href="https://szdsecurity.com" rel="noopener noreferrer"&gt;szdsecurity.com&lt;/a&gt;&lt;/strong&gt;. Zero Day Security is the company I'm building this under — security and AI-adoption work, with the platform you just read about as the backbone. It's early. I'm pre-customer and building in the open, which is exactly why I'd rather publish the weekend I spent finding my own bugs than a glossy feature list.&lt;/p&gt;

&lt;p&gt;If you build tools that ingest data, go try to break one this weekend. Point it at something real. I promise it's lying to you about something — the only question is whether you find out before your users do.&lt;/p&gt;

&lt;p&gt;Building &lt;a href="https://szdsecurity.com" rel="noopener noreferrer"&gt;Zero Day Security&lt;/a&gt;. More war stories to come.&lt;/p&gt;

</description>
      <category>go</category>
      <category>security</category>
      <category>testing</category>
      <category>devops</category>
    </item>
    <item>
      <title>Has this site just turned into who can make the best stuff with Gemini?</title>
      <dc:creator>Saint Zero Day</dc:creator>
      <pubDate>Sat, 18 Apr 2026 03:53:37 +0000</pubDate>
      <link>https://dev.to/saint_zero_day/has-this-site-just-turned-into-who-can-make-the-best-stuff-with-gemini-3cci</link>
      <guid>https://dev.to/saint_zero_day/has-this-site-just-turned-into-who-can-make-the-best-stuff-with-gemini-3cci</guid>
      <description></description>
    </item>
    <item>
      <title>I'm Building a Full Security Operations Platform. Solo.</title>
      <dc:creator>Saint Zero Day</dc:creator>
      <pubDate>Wed, 25 Mar 2026 03:16:39 +0000</pubDate>
      <link>https://dev.to/saint_zero_day/im-building-a-full-security-operations-platform-solo-12el</link>
      <guid>https://dev.to/saint_zero_day/im-building-a-full-security-operations-platform-solo-12el</guid>
      <description>&lt;p&gt;I quit waiting for someone else to build the thing I needed.&lt;/p&gt;

&lt;p&gt;For twenty years I've watched small organizations — government contractors, healthcare providers, critical infrastructure operators — get caught between two options: enterprise security tooling they can't afford, or nothing at all.&lt;/p&gt;

&lt;p&gt;The enterprise stack costs six figures. It takes a team of twelve to deploy. It assumes you have a SOC, a SIEM, a dedicated compliance officer, and a budget that doesn't make your CFO cry.&lt;/p&gt;

&lt;p&gt;Most organizations have none of that. So they run with nothing. Maybe an antivirus license and a prayer.&lt;/p&gt;

&lt;p&gt;I'm building the third option.&lt;/p&gt;




&lt;h2&gt;
  
  
  What It Is
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Zero Day Security (ZDS)&lt;/strong&gt; is a full security operations platform designed for the organizations that the enterprise vendors forgot.&lt;/p&gt;

&lt;p&gt;One platform. One deployment. Everything an organization needs to actually defend itself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vulnerability scanning&lt;/strong&gt; — know what's exposed before an attacker does&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EDR integration&lt;/strong&gt; — pull endpoint telemetry from the agents you already run&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Incident management&lt;/strong&gt; — detect, investigate, respond, document&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compliance automation&lt;/strong&gt; — NIST 800-171, CMMC Level 2, OSCAL exports that auditors can actually read&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Threat intelligence&lt;/strong&gt; — ingest feeds, correlate IOCs, know what's hunting you&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attack path analysis&lt;/strong&gt; — see how an attacker chains vulnerabilities to reach crown jewels&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identity threat detection&lt;/strong&gt; — catch credential abuse before it becomes a breach&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;External attack surface monitoring&lt;/strong&gt; — know what you look like from the outside&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This isn't a dashboard that shows you pretty graphs. This is the platform that does the work.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Solo
&lt;/h2&gt;

&lt;p&gt;Because nobody else was going to build it.&lt;/p&gt;

&lt;p&gt;I spent two decades in security. I've seen what works and what doesn't. I've watched vendors sell shelfware to organizations that needed real protection. I've watched compliance become a checkbox exercise instead of an actual security posture.&lt;/p&gt;

&lt;p&gt;The problem isn't that the technology doesn't exist. The problem is that it's locked behind contracts, sales calls, and price tags that start at "call us."&lt;/p&gt;

&lt;p&gt;So I'm building it myself. Every package. Every test. Every line.&lt;/p&gt;

&lt;p&gt;Is that the smart way to do it? Probably not.&lt;/p&gt;

&lt;p&gt;Is it getting done? Yes.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;p&gt;Go. SQLite for portability. PostgreSQL when you need scale. HTMX dashboard because nobody needs another React app for a security tool.&lt;/p&gt;

&lt;p&gt;No Kubernetes. No microservices. No twelve-factor ceremony. One binary. Deploy it. Use it.&lt;/p&gt;

&lt;p&gt;The security industry has a complexity addiction. Every vendor adds layers because layers justify pricing. ZDS strips that out. You shouldn't need a platform team to run your security platform.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where It Stands
&lt;/h2&gt;

&lt;p&gt;The platform is built. Not "MVP built" — built built. The kind of built where you run the test suite and everything passes. The kind where the OSCAL export actually validates against the NIST schema.&lt;/p&gt;

&lt;p&gt;What's next is production hardening. Real-world deployments. The unsexy work that turns software into a product.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'm Writing About
&lt;/h2&gt;

&lt;p&gt;I'm going to document this build in public. Not the sanitized version — the real one.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How to design a security platform that doesn't require a team of twelve to operate&lt;/li&gt;
&lt;li&gt;Integration patterns for EDR vendors who don't want you integrating with them&lt;/li&gt;
&lt;li&gt;What NIST 800-171 actually requires vs. what compliance vendors tell you it requires&lt;/li&gt;
&lt;li&gt;Building AI-driven security tooling that isn't just a ChatGPT wrapper with a vulnerability database&lt;/li&gt;
&lt;li&gt;What it's like to build something this big alone and why I keep going&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're a security engineer, a compliance professional, a solo founder, or just someone who thinks the security industry is broken — follow along.&lt;/p&gt;

&lt;p&gt;This is Zero Day Security.&lt;/p&gt;

&lt;p&gt;This is what I'm building.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm Saint Zero Day — GWOT veteran and security engineer. Find me here on DEV at &lt;a href="https://dev.to/saintzeroday"&gt;@saintzeroday&lt;/a&gt; or on &lt;a href="https://github.com/saintzeroday" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you're interested in what I'm building with Notion MCP, check out &lt;a href="https://dev.to/saint_zero_day/i-built-a-tamper-proof-security-blockchain-inside-notion-5f74"&gt;SaintChain — a tamper-proof security blockchain inside Notion&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>devjournal</category>
      <category>ai</category>
    </item>
    <item>
      <title>I Built a Tamper-Proof Security Blockchain Inside Notion</title>
      <dc:creator>Saint Zero Day</dc:creator>
      <pubDate>Wed, 25 Mar 2026 02:13:37 +0000</pubDate>
      <link>https://dev.to/saint_zero_day/i-built-a-tamper-proof-security-blockchain-inside-notion-5f74</link>
      <guid>https://dev.to/saint_zero_day/i-built-a-tamper-proof-security-blockchain-inside-notion-5f74</guid>
      <description>&lt;p&gt;Everyone else built todo apps. We built a security blockchain inside Notion.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;SaintChain turns Notion into a tamper-proof security event ledger. Vulnerability scans, incident reports, compliance checks, policy violations -- every event gets cryptographically hashed into blocks, chained with SHA-256 double hashing, signed with Ed25519, and stored directly in Notion databases via MCP.&lt;/p&gt;

&lt;p&gt;Modify a single character in any record. The chain breaks. SaintChain proves exactly where.&lt;/p&gt;

&lt;p&gt;This is the same integrity architecture that NIST 800-171 and SOC 2 require for tamper-evident audit logging -- running inside Notion, orchestrated by Claude Code, with Solana-style checkpoint anchoring as a proof of concept.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The pitch:&lt;/strong&gt; What if Notion wasn't just a wiki, but a block explorer for your security audit trail?&lt;/p&gt;

&lt;h3&gt;
  
  
  How It Works
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;13 security events&lt;/strong&gt; (real CVEs, incidents, compliance checks, policy violations) get ingested and sealed into 3 blocks, plus a genesis block that bootstraps the chain&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full chain verification&lt;/strong&gt; -- double-SHA-256 hash linkage, Merkle root validation, Ed25519 signature checks on every block&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tamper detection&lt;/strong&gt; -- modify one hash and every downstream block breaks. SaintChain pinpoints the exact failure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI-driven forensics&lt;/strong&gt; -- when tampering is detected, Claude writes a forensic incident narrative directly into Notion: what changed, what it means cryptographically, what to do about it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solana-style checkpoint anchoring&lt;/strong&gt; -- chain state gets signed and recorded for third-party verification. This is a local simulation -- no live Solana transactions. I'd rather build that correctly with real Solana developers than ship something half-broken for a contest&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The Notion Workspace
&lt;/h3&gt;

&lt;p&gt;Three databases and four pages, all created and populated by Claude Code via MCP:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Blocks DB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;4 blocks with double-SHA-256 hashes, previous hashes, Merkle roots, Ed25519 signatures, verification status&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Events DB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;14 security events with type, severity, source, target, payload hash, block reference&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Checkpoints DB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Solana-style anchor point with chain hash, Merkle root, TX signature&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Dashboard&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Chain health at a glance -- block count, event breakdown by type and severity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Verification Report (PASSED)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Block-by-block integrity check -- all green&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Incident Narrative&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;CRITICAL alert -- forensic timeline of chain tampering at block 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Verification Report (FAILED)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Post-tamper report showing the exact failure point&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4xpvlj3ymg6l3mrk38in.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4xpvlj3ymg6l3mrk38in.png" alt="SaintChain Audit Dashboard"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Full SaintChain workspace -- 3 databases, 4 pages, all populated via MCP&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftcvpjs64ntny2w79f9zj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftcvpjs64ntny2w79f9zj.png" alt="Blocks database"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Blocks database -- each row is a sealed block with its double-SHA-256 hash, previous hash, Merkle root, and event count&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy1hp38b2hlyk7g9d85do.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy1hp38b2hlyk7g9d85do.png" alt="Events database"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Events database -- 14 security events across scanning, incidents, compliance, and remediation. Color-coded severity selects.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbqzk9ftvqvahdda3koyn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbqzk9ftvqvahdda3koyn.png" alt="Checkpoints database"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Checkpoints database -- Solana-style anchor with chain hash, Merkle root, and TX signature&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fteedgoozhhi07cn7b2z7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fteedgoozhhi07cn7b2z7.png" alt="Security Dashboard"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Security Dashboard -- chain health status, event breakdown by type, severity distribution&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flopfu41nmptmumoaon5y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flopfu41nmptmumoaon5y.png" alt="Verification Report PASSED"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Verification Report -- all 4 blocks pass integrity checks&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy8rqmhb2qtgjddopl70r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy8rqmhb2qtgjddopl70r.png" alt="Incident Narrative"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Tamper detected at block 2. CRITICAL callout, incident timeline, forensic analysis, and remediation steps -- all written by Claude into Notion.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcsilzmwdedbnoj2hq3n3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcsilzmwdedbnoj2hq3n3.png" alt="Verification Report FAILED"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Post-tamper verification -- block 2 FAILED with hash mismatch and invalid signature. Exact failure details shown.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/UePGQTWlsQ0"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;The demo walks through the full pipeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Generate Ed25519 keypair, create genesis block&lt;/li&gt;
&lt;li&gt;Ingest 13 security events across scanning, incident response, and remediation&lt;/li&gt;
&lt;li&gt;Batch events into 3 signed blocks with Merkle trees&lt;/li&gt;
&lt;li&gt;Run full chain integrity verification (passes)&lt;/li&gt;
&lt;li&gt;Sync all chain state to Notion databases and pages via MCP&lt;/li&gt;
&lt;li&gt;Tamper with block 2's hash (simulating direct database manipulation)&lt;/li&gt;
&lt;li&gt;Re-verify -- chain breaks at block 2, all downstream blocks invalidated&lt;/li&gt;
&lt;li&gt;Claude writes a forensic incident narrative into Notion&lt;/li&gt;
&lt;li&gt;Post-tamper verification report shows the exact failure point&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Show us the Code
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/saintzeroday/saintchain-notion" rel="noopener noreferrer"&gt;&lt;strong&gt;GitHub: saintzeroday/saintchain-notion&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Architecture
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;saintchain/
├── crypto.py            # Ed25519 keys + SHA-256 double hashing (PyNaCl)
├── merkle.py            # Merkle tree construction + inclusion proofs
├── events.py            # Security event model, canonical JSON serialization
├── block.py             # Block model, double-SHA-256 hashing, chain linkage
├── chain.py             # Append-only ledger, verification, tamper detection
├── solana_anchor.py     # Solana checkpoint anchoring (local simulation)
├── notion_sync.py       # Chain state -&amp;gt; Notion database property payloads
├── notion_dashboard.py  # Dashboard page generator
└── notion_reporter.py   # Verification reports + incident narratives
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;65 tests across 6 files. Crypto, Merkle trees, blocks, chain integrity, event serialization, and Notion payload generation all tested independently.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Crypto Engine
&lt;/h3&gt;

&lt;p&gt;Ported from &lt;a href="https://github.com/saintzeroday/saintblockchain" rel="noopener noreferrer"&gt;Saint Blockchain&lt;/a&gt;, my Rust implementation of an immutable security event ledger.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Double SHA-256&lt;/strong&gt; -- hash the hash to prevent length-extension attacks. Same pattern Bitcoin uses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;double_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;first&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# raw 32 bytes, NOT hex
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Ed25519 signatures&lt;/strong&gt; -- every block is signed. Deterministic signatures mean no nonce reuse risk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;signed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_signing_key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;signed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Merkle trees&lt;/strong&gt; -- events within a block form a binary tree. The root hash commits to all events at once. You can prove a single event exists in a block without downloading the others:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;merkle_root&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hashes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;hashes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;hash_str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;empty&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hashes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hashes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;current_level&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hashes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current_level&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;next_level&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current_level&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current_level&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;combined&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current_level&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;current_level&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;combined&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current_level&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;current_level&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="n"&gt;next_level&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;hash_str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;combined&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;current_level&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;next_level&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;current_level&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Chain Verification
&lt;/h3&gt;

&lt;p&gt;The verification engine walks every block from genesis, checking three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hash linkage&lt;/strong&gt; -- does &lt;code&gt;previous_hash&lt;/code&gt; match the prior block's hash?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Merkle root&lt;/strong&gt; -- does the recomputed event tree match the stored root?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ed25519 signature&lt;/strong&gt; -- was this block signed by the chain authority?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When any check fails, verification stops and reports the exact failure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;verify_chain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;from_height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to_height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ChainVerification&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# ...
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from_height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;block&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_blocks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;previous_hash&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;prev_hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ChainVerification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;valid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;blocks_checked&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;blocks_checked&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;first_invalid_height&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;previous hash mismatch at height &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;validation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;public_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;validation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;merkle_valid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ChainVerification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;valid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;blocks_checked&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;blocks_checked&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;first_invalid_height&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invalid merkle root at height &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;validation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;signature_valid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ChainVerification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;valid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;blocks_checked&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;blocks_checked&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;first_invalid_height&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invalid block signature at height &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;prev_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;hash&lt;/span&gt;
        &lt;span class="n"&gt;blocks_checked&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ChainVerification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;valid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;blocks_checked&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;blocks_checked&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five independent integrity checks per block -- hash, Merkle root, signature, event payload hashes, and event count. Any single modification anywhere in the chain cascades into a verification failure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Notion Sync via MCP
&lt;/h3&gt;

&lt;p&gt;The sync layer converts chain state into Notion database rows. Each block becomes a database entry with its hash, previous hash, Merkle root, signature, and real-time verification status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;block_to_notion_properties&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Block&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;verified&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Height&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;number&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rich_text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}}]},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Previous Hash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rich_text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;previous_hash&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;previous_hash&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;genesis&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
        &lt;span class="p"&gt;]},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Merkle Root&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rich_text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;merkle_root&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}}]},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;start&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Event Count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;number&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_count&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Verified&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;checkbox&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;verified&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Signature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rich_text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}}]},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sync payload generator runs real-time validation on every block before pushing to Notion. If a block has been tampered with, its &lt;code&gt;Verified&lt;/code&gt; checkbox shows up unchecked -- no manual flagging needed, the crypto does it.&lt;/p&gt;

&lt;p&gt;When tamper detection fires, Claude generates a forensic incident narrative -- not a template fill, but a contextual analysis of what the hash change means, which blocks are affected, and what the response should be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate_incident_narrative&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Chain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tampered_height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;original_hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;block&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_block&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tampered_height&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;current_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;hash&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="c1"&gt;# Builds a Notion page with:
&lt;/span&gt;    &lt;span class="c1"&gt;# - CRITICAL callout block
&lt;/span&gt;    &lt;span class="c1"&gt;# - Numbered incident timeline (original hash -&amp;gt; modified hash -&amp;gt; detection -&amp;gt; cascade)
&lt;/span&gt;    &lt;span class="c1"&gt;# - Forensic analysis: what the hash change means cryptographically
&lt;/span&gt;    &lt;span class="c1"&gt;# - Remediation: isolate, restore from checkpoint, investigate, rotate keys, re-anchor
&lt;/span&gt;    &lt;span class="c1"&gt;# ...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How I Used Notion MCP
&lt;/h2&gt;

&lt;p&gt;Notion is not a display layer here. It is the &lt;strong&gt;block explorer&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The same way Solana Explorer lets you browse transactions on Solana, the SaintChain Notion workspace lets you browse a security audit blockchain:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Blocks database = block explorer&lt;/strong&gt; -- click any block to see its hash, previous hash, Merkle root, signature, event count, and verification status&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Events database = transaction ledger&lt;/strong&gt; -- every security event is a row with type, severity, source, target, payload hash, and block reference&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Checkpoints database = anchor registry&lt;/strong&gt; -- Solana-style checkpoints with TX signatures for third-party verification&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dashboard page = chain health monitor&lt;/strong&gt; -- overall chain status, event breakdown by type and severity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verification reports = integrity proofs&lt;/strong&gt; -- block-by-block results that auditors can read without running code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Incident narratives = AI forensics&lt;/strong&gt; -- when tampering is detected, Claude writes the investigation report directly into Notion&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Claude Code drives the entire pipeline: key generation, event signing, block sealing, chain verification, Notion database creation, data population, dashboard generation, tamper simulation, and incident narrative writing. It is not calling a single API endpoint. It is orchestrating cryptographic operations and reasoning about what the results mean.&lt;/p&gt;

&lt;h3&gt;
  
  
  Technical Stack
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Crypto&lt;/td&gt;
&lt;td&gt;SHA-256 double hash, Ed25519 (PyNaCl), Merkle trees&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chain&lt;/td&gt;
&lt;td&gt;In-memory append-only ledger with full verification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anchor&lt;/td&gt;
&lt;td&gt;Solana-style checkpoint signing (local simulation -- not live)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interface&lt;/td&gt;
&lt;td&gt;Notion databases + pages via MCP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Orchestration&lt;/td&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tests&lt;/td&gt;
&lt;td&gt;65 tests across 6 files&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Why This Matters
&lt;/h3&gt;

&lt;p&gt;SaintChain is not a hackathon toy. It is a piece of something I have been building for over a year.&lt;/p&gt;

&lt;p&gt;I am building &lt;strong&gt;Zero Day Security (ZDS)&lt;/strong&gt; -- a full security operations platform for small and mid-size organizations. Government contractors, healthcare providers, critical infrastructure operators -- they need real security tooling but cannot afford the enterprise stack. They are stuck choosing between "too expensive" and "nothing."&lt;/p&gt;

&lt;p&gt;ZDS is 130,000 lines of Go. Vulnerability scanning, EDR integration (Wazuh, CrowdStrike, SentinelOne, Defender), attack path analysis, incident management, workflow automation, OSCAL compliance exports for NIST 800-171 and CMMC Level 2, threat intelligence feeds, external attack surface monitoring, identity threat detection. 62 packages, 1,163 tests. One engineer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SaintChain is the audit backbone of that platform.&lt;/strong&gt; Every scan result, every incident, every compliance check that flows through ZDS needs a tamper-proof record. When you submit a NIST 800-171 assessment, an auditor needs to know your logs have not been modified after the fact. That is what SaintChain does -- cryptographic proof that the record is intact.&lt;/p&gt;

&lt;p&gt;The Notion MCP integration adds something ZDS does not have on its own: &lt;strong&gt;a universal interface anyone can read.&lt;/strong&gt; The ZDS dashboard is built for security engineers. But compliance officers, executives, auditors, and clients need to see the audit trail too -- and they already use Notion. SaintChain via MCP puts integrity proofs where non-technical stakeholders can browse them without VPN access or training on a new tool.&lt;/p&gt;

&lt;p&gt;And when something goes wrong -- when a record gets tampered with -- Claude does not just log a hash mismatch to stderr. It writes a forensic incident narrative in plain English, directly into Notion, explaining what changed, what it means, and what to do about it. That is the difference between a security alert nobody reads and an actionable response.&lt;/p&gt;

&lt;p&gt;Every compliance framework requires this. NIST 800-171, SOC 2, CMMC, HIPAA, FedRAMP. Tamper-evident logging is not optional. SaintChain makes it real. Notion MCP makes it accessible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Team
&lt;/h2&gt;

&lt;p&gt;Solo build.&lt;/p&gt;

&lt;p&gt;I'm Saint Zero Day -- GWOT veteran and security engineer, building Zero Day Security.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DEV:&lt;/strong&gt; &lt;a href="https://dev.to/saintzeroday"&gt;@saintzeroday&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/saintzeroday/saintchain-notion" rel="noopener noreferrer"&gt;saintzeroday/saintchain-notion&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cryptographic engine is ported from my &lt;a href="https://github.com/saintzeroday/saintblockchain" rel="noopener noreferrer"&gt;Saint Blockchain&lt;/a&gt; Rust codebase. The Notion integration, orchestrator, dashboard, verification reports, and incident narratives are new for this challenge. The security domain knowledge -- the event types, the compliance frameworks, the incident response workflow -- comes from building ZDS and two decades of doing this for real.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>notionchallenge</category>
      <category>mcp</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
