<?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: OBI EBUKA DAVID</title>
    <description>The latest articles on DEV Community by OBI EBUKA DAVID (@techstrtupninja).</description>
    <link>https://dev.to/techstrtupninja</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%2F630530%2F98c815f7-9800-47cd-b1d3-f8483636ff01.png</url>
      <title>DEV Community: OBI EBUKA DAVID</title>
      <link>https://dev.to/techstrtupninja</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/techstrtupninja"/>
    <language>en</language>
    <item>
      <title>83% of Financial Apps Ship a CSP That Doesn't Work</title>
      <dc:creator>OBI EBUKA DAVID</dc:creator>
      <pubDate>Thu, 13 Aug 2026 17:44:48 +0000</pubDate>
      <link>https://dev.to/techstrtupninja/83-of-financial-apps-ship-a-csp-that-doesnt-work-5hh5</link>
      <guid>https://dev.to/techstrtupninja/83-of-financial-apps-ship-a-csp-that-doesnt-work-5hh5</guid>
      <description>&lt;p&gt;We scanned 618 public apps from 347 African banks, fintechs and payment companies across 21 countries. One passive request each. No logins, no probing, nobody named.&lt;/p&gt;

&lt;p&gt;Half graded C or below.&lt;/p&gt;

&lt;p&gt;83% exposed to XSS. 39% with no clickjacking protection on the page customers log in through. 79 running WordPress under a bank brand. 14 on expired certificates today.&lt;/p&gt;

&lt;p&gt;It took one afternoon from a laptop.&lt;/p&gt;

&lt;p&gt;That is the same view an attacker has of your estate right now. The difference is they are running it with AI, across a whole market, before lunch.&lt;/p&gt;

&lt;p&gt;And a third of these institutions publish a public API. Nothing in this report protects that part. No header stops a request that is perfectly well formed and simply should not have been allowed.&lt;/p&gt;

&lt;p&gt;The research: &lt;a href="https://www.nemesislabs.xyz/state-of-app-security/" rel="noopener noreferrer"&gt;https://www.nemesislabs.xyz/state-of-app-security/&lt;/a&gt;&lt;br&gt;
Check your own app, free: &lt;a href="https://www.nemesislabs.xyz/protect/" rel="noopener noreferrer"&gt;https://www.nemesislabs.xyz/protect/&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Autogon #Omniguard #Nemesis #ApplicationSecurity #Finance #AI
&lt;/h1&gt;

</description>
    </item>
    <item>
      <title>Your WAF was never built to see this: positive security vs signature WAFs, and how it stops IDOR/BOLA</title>
      <dc:creator>OBI EBUKA DAVID</dc:creator>
      <pubDate>Wed, 12 Aug 2026 01:22:09 +0000</pubDate>
      <link>https://dev.to/techstrtupninja/your-waf-was-never-built-to-see-this-positive-security-vs-signature-wafs-and-how-it-stops-1acl</link>
      <guid>https://dev.to/techstrtupninja/your-waf-was-never-built-to-see-this-positive-security-vs-signature-wafs-and-how-it-stops-1acl</guid>
      <description>&lt;p&gt;You are logged in. You hit your own billing page and the browser fires:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="nf"&gt;GET&lt;/span&gt; &lt;span class="nn"&gt;/api/invoices/1043&lt;/span&gt; &lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;
&lt;span class="na"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app.acme.com&lt;/span&gt;
&lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Bearer eyJhbGciOi...&lt;/span&gt;
&lt;span class="na"&gt;Cookie&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;session=valid&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Back comes your invoice. Fine. Now you change one digit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="nf"&gt;GET&lt;/span&gt; &lt;span class="nn"&gt;/api/invoices/1044&lt;/span&gt; &lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;
&lt;span class="na"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app.acme.com&lt;/span&gt;
&lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Bearer eyJhbGciOi...&lt;/span&gt;
&lt;span class="na"&gt;Cookie&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;session=valid&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Invoice 1044 belongs to a different customer. If the response is their invoice, you just walked out with someone else's data using nothing but your own valid session and the up-arrow key. That is IDOR (Insecure Direct Object Reference), and in API terms it is BOLA (Broken Object Level Authorization). It is the number one item on the OWASP API Security Top 10.&lt;/p&gt;

&lt;p&gt;Here is the part that catches people. Look at that second request again. There is no SQL in it. No &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt;. No &lt;code&gt;../../etc/passwd&lt;/code&gt;. No union select, no encoded payload, no oversized header. It is a clean, well-formed, authenticated GET to a route your API absolutely serves. Every byte of it looks exactly like the first request, because it is exactly like the first request. One integer changed.&lt;/p&gt;

&lt;p&gt;So walk it through your defenses. Your WAF looks at it and sees a valid method, a known path shape, a real bearer token, and a body with nothing suspicious in it. Pass. Your rate limiter sees one request. Pass. Your TLS terminates fine. Pass. The only thing in your entire stack that had the information needed to stop that request was the handler behind &lt;code&gt;/api/invoices/{id}&lt;/code&gt;, which needed to check "does the caller who owns this session also own invoice 1044?" and did not. The authorization check was the one guard that could have caught it, and it was missing. Everything else waved it through because, to everything else, there was nothing to catch.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a signature WAF actually does
&lt;/h2&gt;

&lt;p&gt;A signature WAF (Cloudflare WAF, AWS WAF, ModSecurity with the OWASP Core Rule Set) works by matching your traffic against a list of known-bad patterns. The OWASP CRS is thousands of these rules: regexes and heuristics for SQL injection, cross-site scripting, path traversal, command injection, known scanner user-agents, malicious file uploads. Plus rate limiting and IP reputation on top. When a request matches enough badness, it gets blocked or scored up.&lt;/p&gt;

&lt;p&gt;This model is genuinely good at what it is for. It filters out the constant background radiation of the internet: the automated bots spraying Log4Shell strings at every host they can find, the SQLi fuzzers, the WordPress exploit kits hammering &lt;code&gt;/wp-login.php&lt;/code&gt;. If you run an API on the public internet without something soaking up that noise, your logs are unreadable and your odds are worse. I am not telling you to turn your WAF off.&lt;/p&gt;

&lt;p&gt;I am telling you what it structurally cannot see. A signature WAF answers one question: &lt;em&gt;does this request look like a known attack?&lt;/em&gt; IDOR does not look like a known attack. Neither does broken authentication, where a valid token is used in a context it should not be allowed in. Neither does business-logic abuse, like applying a coupon a thousand times, or calling a state-transition endpoint out of order to skip a payment step. These are the attacks that breach real APIs, and they are all made of well-formed requests. The pattern-matching model has nothing to match, because the maliciousness is not in the shape of the request. It is in the relationship between the caller and the object, and that relationship lives in your data, not in the bytes on the wire.&lt;/p&gt;

&lt;p&gt;OWASP made this explicit. In the API Security Top 10 (the 2023 list), API1:2023 is Broken Object Level Authorization (BOLA), and API5:2023 is Broken Function Level Authorization (BFLA). BOLA is "I can access an object I do not own." BFLA is "I can call a function I am not entitled to call," like a normal user hitting an admin-only endpoint that happens to accept their token. The two authorization failures sit at the very top of the list precisely because they are so common and so invisible to the tooling most teams already run. A signature WAF is not on that list of controls because it was never designed to be.&lt;/p&gt;

&lt;h2&gt;
  
  
  The inversion: allow-list instead of deny-list
&lt;/h2&gt;

&lt;p&gt;Positive security flips the question. Instead of &lt;em&gt;does this request match something bad&lt;/em&gt;, it asks &lt;em&gt;does this request match this app's own normal behavior?&lt;/em&gt; You build an allow-list of what your application actually does, then you block whatever deviates from it.&lt;/p&gt;

&lt;p&gt;The trick is that "normal" is not a rule someone writes. It is learned from your app's real behavior. A positive-security layer watches traffic and builds a model: these are the routes that exist, this is the shape each one takes (method, path structure, which query params appear and of what kind), this is whether the caller is usually authenticated, and, importantly, these are the per-tenant access patterns. What does a normal session touch? Whose objects does a given identity normally reach?&lt;/p&gt;

&lt;p&gt;Now replay the IDOR. Your session has a learned baseline: over hundreds of requests, this identity reads invoices that belong to this identity. Then a request comes in for an object outside that pattern. It is still a well-formed GET to a known route. But it deviates from &lt;em&gt;this caller's&lt;/em&gt; learned normal, and deviation is exactly what a positive-security model is built to flag. It does not need a rule that says "invoice 1044 is off-limits to user 7." It just needs to have learned that user 7 does not reach into that neighborhood, and then notice when user 7 suddenly does. The catch comes from the baseline, not from a signature.&lt;/p&gt;

&lt;p&gt;That is the whole difference in one sentence. A deny-list needs someone to have anticipated the attack. An allow-list only needs to know what your app normally does, and every real IDOR is, by definition, your app doing something it normally does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this looks like in practice
&lt;/h2&gt;

&lt;p&gt;This is the model behind Nemesis Shield, which is what I build, so I will show you the real integration rather than hand-wave. It is a per-app SDK you mount as outermost middleware. It computes a privacy-preserving &lt;em&gt;shape&lt;/em&gt; of each request locally (method, the normalized path like &lt;code&gt;/invoices/{int}&lt;/code&gt;, query-param structure, status, and whether the caller was authenticated) and never ships your bodies, responses, or secrets.&lt;/p&gt;

&lt;p&gt;FastAPI, one line:&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;nemesis_shield.asgi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SentinelMiddleware&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SentinelMiddleware&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NEMESIS_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Express, one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;sentinel&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@nemesis-shield-autogon/sentinel/express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sentinel&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEMESIS_TOKEN&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install is the obvious &lt;code&gt;pip install nemesis-shield&lt;/code&gt; or &lt;code&gt;npm install @nemesis-shield-autogon/sentinel&lt;/code&gt;, token comes from the environment, and there are the same one-liners for Go, Ruby, PHP, Java, .NET, Rust, and the edge runtimes. Mount it first so it sees every route, not just your API prefix.&lt;/p&gt;

&lt;p&gt;Every app starts in &lt;strong&gt;observe&lt;/strong&gt; mode. It blocks nothing. It only learns, building the per-app, per-tenant baseline from real traffic. That matters because the whole model rests on knowing your normal, and you do not want to enforce against a baseline that has not seen your app yet.&lt;/p&gt;

&lt;p&gt;If you are shipping a brand-new API, there is no traffic to learn from, so you can drive it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @nemesis-shield-autogon/learn &lt;span class="nt"&gt;--target&lt;/span&gt; http://localhost:3000 &lt;span class="nt"&gt;--app-token&lt;/span&gt; nsk_... &lt;span class="nt"&gt;--repo&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nemesis Learn exercises every route (it can read your repo to discover them, log in to reach protected routes, and chain CRUD flows) so the baseline completes in minutes instead of waiting weeks on organic traffic. It reports coverage back so the console shows when you are actually ready.&lt;/p&gt;

&lt;p&gt;Then you review. Learned behaviors show up in a queue with the evidence behind them, you approve the legitimate ones, and when you are satisfied you flip the app to &lt;strong&gt;enforce&lt;/strong&gt; in the console. No redeploy. From that point, requests whose shape falls outside the approved, per-tenant baseline get a &lt;code&gt;403&lt;/code&gt; and land as findings. The key word is per-tenant: this is not one global ruleset pretending every customer looks the same. The baseline is scoped to your app and its access patterns, which is the only way "this caller does not normally reach that object" means anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest limits
&lt;/h2&gt;

&lt;p&gt;I am not going to tell you this replaces fixing your authorization. It does not, and anyone who says their WAF-shaped product does is selling you a nightlight and calling it a lock.&lt;/p&gt;

&lt;p&gt;Positive security has real costs you should go in knowing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It needs a clean learning period.&lt;/strong&gt; If you learn from traffic that already contains abuse, you bake the abuse into "normal." Learn in dev/staging or a known-good window, and review what it learned before you trust it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is a complement, not a substitute.&lt;/strong&gt; The actual fix for the invoice IDOR is an authorization check in the handler: does this caller own this object? Write that check. Positive security is defense in depth that catches the ones you missed and the new ones you have not written yet. It is not permission to skip the check.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;False positives are real, which is why observe-first exists.&lt;/strong&gt; A legitimate new feature is, to the model, a deviation. That is what the approve/deny review queue is for, and it is why enforcement is a deliberate flip and not a default. Managed well, the noise is bounded. Ignored, it will annoy you.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Business-logic abuse that stays entirely inside normal behavior (a real user doing a real thing too many times, within their own scope) is also not something an access-pattern baseline catches on its own. Different problem, different control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Close
&lt;/h2&gt;

&lt;p&gt;Your signature WAF is doing its job. It just was never built to see the request that changed one digit and walked off with someone else's invoice, because that request was never malformed. It was well-formed and unauthorized, and those are different problems that need different defenses.&lt;/p&gt;

&lt;p&gt;Positive security is the defense for the second one, and it is free to start, one line to add, and safe by default because it observes before it ever blocks. If you build with an AI editor, you do not even add it yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx &lt;span class="nt"&gt;-y&lt;/span&gt; @nemesis-shield-autogon/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Point Cursor or Claude Code at the Nemesis MCP server and it wires the SDK in as it builds. Then go write the authorization check too.&lt;/p&gt;

&lt;p&gt;Author: David Obi&lt;/p&gt;

</description>
      <category>idor</category>
      <category>owasp</category>
      <category>webdev</category>
      <category>bola</category>
    </item>
    <item>
      <title>Your Supabase Edge Function needs a WAF too</title>
      <dc:creator>OBI EBUKA DAVID</dc:creator>
      <pubDate>Wed, 12 Aug 2026 01:18:00 +0000</pubDate>
      <link>https://dev.to/techstrtupninja/your-supabase-edge-function-needs-a-waf-too-263p</link>
      <guid>https://dev.to/techstrtupninja/your-supabase-edge-function-needs-a-waf-too-263p</guid>
      <description>&lt;p&gt;Here is the function that keeps me up at night. It is a Supabase Edge Function called &lt;code&gt;checkout&lt;/code&gt;, maybe 60 lines of Deno, and it does what every checkout does: takes an order id, looks up the cart, confirms the price, writes a row. To read the cart across users it was given the &lt;code&gt;service_role&lt;/code&gt; key, because RLS was getting in the way during a Friday afternoon and the fastest unblock was to step outside RLS entirely. That decision shipped. The function is now public at &lt;code&gt;https://&amp;lt;project&amp;gt;.supabase.co/functions/v1/checkout&lt;/code&gt;, and the key inside it can read and write every row in every table, ignoring every policy you wrote.&lt;/p&gt;

&lt;p&gt;Now the part people miss. Your app has a WAF. Cloudflare, or the Vercel firewall, or whatever sits in front of your Next.js origin. That WAF is doing real work on &lt;code&gt;app.yourcompany.com&lt;/code&gt;. It is doing zero work on the checkout function, because the checkout function does not run behind it. It runs on Supabase's edge, at a different hostname, on infrastructure your CDN never sees. An attacker who hits the function URL directly walks straight past the thing you think is protecting you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The edge is the blind spot on purpose
&lt;/h2&gt;

&lt;p&gt;The whole selling point of edge and serverless functions is that they run close to the request and independent of your origin. Supabase Edge Functions, Vercel Edge, Cloudflare Workers: same shape. They spin up in a lightweight isolate, handle the request, and disappear. That independence is exactly why the WAF in front of your main app does not cover them. Different host, different path, different network. The firewall config you spent a day tuning applies to routes that terminate at your origin, not to a Deno isolate answering on &lt;code&gt;functions.supabase.co&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And these functions are not the low-value part of your stack. They are where you put the logic too sensitive for the browser: signing URLs, charging cards, minting tokens, reading other users' data with elevated privileges. Supabase Edge Functions are public by default. There is no implicit login gate. Anybody who knows the URL can send a request, and the function itself is the only thing deciding whether that request is legitimate. It is the trust boundary, and most of the time there is nothing sitting on it except your own &lt;code&gt;if&lt;/code&gt; statements.&lt;/p&gt;

&lt;p&gt;I have watched teams add a WAF, feel covered, and never notice that the three functions holding the &lt;code&gt;service_role&lt;/code&gt; key have no protection at all. The monolith got the guard. The functions that can drop a table got nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  A signature WAF would not save you here anyway
&lt;/h2&gt;

&lt;p&gt;Say you did route the function through a classic WAF. It still would not catch the attacks that actually hit these things. A signature WAF (the negative-security model) works off a list of known-bad patterns. SQL injection strings, XSS payloads, the log4shell probe, a directory of CVE fingerprints. It is a bouncer with a list of banned faces.&lt;/p&gt;

&lt;p&gt;The attacks against a checkout function do not have banned faces. &lt;code&gt;GET /checkout/1001&lt;/code&gt; when you are user 1002 is a textbook IDOR/BOLA, and every byte of it looks like a normal request, because it is one. It is a valid method, a valid path, valid syntax. There is no payload to match. Broken authentication looks like a request missing a header. Business-logic abuse (replaying a discount, ordering a negative quantity, calling steps out of order) is made entirely of legal requests in an illegal sequence. A pattern matcher has nothing to match on. The OWASP API Security Top 10 has been led by BOLA and broken auth for years precisely because signature tools are blind to them.&lt;/p&gt;

&lt;p&gt;The model that works is the inverse. Positive security. Instead of listing what is bad, you learn what is normal for this specific function and block everything else. The function serves &lt;code&gt;GET /products&lt;/code&gt; unauthenticated and &lt;code&gt;POST /checkout&lt;/code&gt; authenticated with an integer order id. That is the allow-list. &lt;code&gt;GET /checkout/1001&lt;/code&gt; from an unauthenticated caller is not on it, so it does not get through, and nobody had to predict that particular attack in advance. The unknown request fails because it deviates from the baseline, not because someone wrote a rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix is one line around your handler
&lt;/h2&gt;

&lt;p&gt;This is &lt;a href="https://nemesislabs.xyz/shield" rel="noopener noreferrer"&gt;Nemesis Shield&lt;/a&gt;. The edge SDK is a single function, &lt;code&gt;withShield&lt;/code&gt;, that wraps your Deno handler and puts the allow-list in front of it. Here is the checkout function before:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Deno&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;serve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;orderId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;carts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;single&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And after:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;withShield&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;jsr:@nemesis-shield/edge&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;Deno&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;serve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;withShield&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;orderId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;carts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;single&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Deno&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;NEMESIS_TOKEN&lt;/span&gt;&lt;span class="dl"&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 import is the JSR package &lt;code&gt;jsr:@nemesis-shield/edge&lt;/code&gt; (on npm runtimes like Cloudflare Workers or Vercel Edge it is &lt;code&gt;@nemesis-shield-autogon/edge&lt;/code&gt;, same &lt;code&gt;withShield&lt;/code&gt; signature). The token is read from an environment variable, never hardcoded. You set it once with the Supabase CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;supabase secrets &lt;span class="nb"&gt;set &lt;/span&gt;&lt;span class="nv"&gt;NEMESIS_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;nsk_your_app_token
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Grab a free token at &lt;a href="https://shield.nemesislabs.xyz" rel="noopener noreferrer"&gt;shield.nemesislabs.xyz&lt;/a&gt;. That is the whole integration. &lt;code&gt;withShield(handler, { token })&lt;/code&gt; returns a &lt;code&gt;(Request) =&amp;gt; Response&lt;/code&gt; handler, which is exactly what &lt;code&gt;Deno.serve&lt;/code&gt; wants, so it drops in with no other changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually happens after you deploy
&lt;/h2&gt;

&lt;p&gt;The thing I want to be clear about, because it is what makes this safe to ship on a Friday: it does not start blocking. On first deploy the function is in &lt;strong&gt;observe&lt;/strong&gt; mode. Every request runs your handler exactly as before, and the SDK records the &lt;em&gt;shape&lt;/em&gt; of the request (method, normalized route, the structure of query params, whether the caller was authenticated) so it can learn what normal looks like. Nothing is blocked. You cannot break production by adding it, because in observe mode there is no enforcement to break.&lt;/p&gt;

&lt;p&gt;Learning from live traffic takes as long as it takes to see all your routes, which for a quiet function could be a while. So there is an agent that does it in minutes. Nemesis Learn exercises every route of your app in dev or staging and lets the baseline complete fast:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @nemesis-shield-autogon/learn &lt;span class="nt"&gt;--target&lt;/span&gt; http://localhost:54321 &lt;span class="nt"&gt;--app-token&lt;/span&gt; nsk_... &lt;span class="nt"&gt;--repo&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It sends representative, benign traffic (never attack payloads), can read your repo to discover routes from source, and posts a coverage report back so the console shows a baseline-readiness meter. When it looks complete, you open the app in the console, approve the learned behaviors, and flip the mode to &lt;strong&gt;enforce&lt;/strong&gt;. No redeploy. The SDK refreshes its compiled policy lazily on the request path with a short TTL, so the next request on a cold isolate picks up the new mode on its own.&lt;/p&gt;

&lt;p&gt;One property I lean on hard: it is fail-open. Look at the actual handler in the SDK and the entire policy-refresh-and-decide block is wrapped so that any internal error falls straight through to your code. If Shield is unreachable, if the policy fetch times out, if anything in the middleware throws, your function still runs. The only path that returns a 403 is an explicit, deliberate block in enforce mode. A security tool that can take down your checkout when its own backend has a bad day is worse than no tool. This one cannot.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest gotchas
&lt;/h2&gt;

&lt;p&gt;It learns from the traffic you give it, so give it clean traffic. If you run Nemesis Learn against a staging environment that is already full of junk, or you leave observe mode running while someone is actively fuzzing the function, that noise becomes part of the baseline and you will approve behaviors you did not mean to. Exercise the real routes, review what it learned before you approve, and do not enforce on a baseline you did not look at.&lt;/p&gt;

&lt;p&gt;It models structure, not values. The shape it records is method plus normalized route plus param names and &lt;em&gt;kinds&lt;/em&gt; (is this an integer, a uuid, an email) plus an auth flag plus status. It never ships request bodies, and it never ships your users' data. That is a deliberate privacy choice, and it is also a limit: an attack that stays entirely inside your normal request shapes (a valid authenticated request that abuses logic the function itself permits) is not something a behavioral allow-list catches. That class needs business-logic scoring, which is a different tool. Do not let anyone sell you an allow-list as if it catches everything. It catches deviation from learned normal, which is a large and painful category, and not the whole of it.&lt;/p&gt;

&lt;p&gt;And the free tier has limits on volume and app count. For a couple of functions it is genuinely free. If you are running a fleet, you will hit the paid tiers eventually. I would rather tell you that now than have you find out at request one million.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add it from your editor
&lt;/h2&gt;

&lt;p&gt;If you build with an AI editor (Cursor, Claude Code, Windsurf, anything that speaks MCP), you can have the agent wire this in for you. There is a local MCP server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx &lt;span class="nt"&gt;-y&lt;/span&gt; @nemesis-shield-autogon/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Point your editor at it and "add Nemesis Shield to my checkout function" becomes a thing your agent can actually do, in observe mode, reading the token from the environment, mounted first so it sees every route. The free tools need no account.&lt;/p&gt;

&lt;p&gt;The checkout function that reads every row with the &lt;code&gt;service_role&lt;/code&gt; key is not going away. It is how you build on Supabase. Put something on the boundary it lives on.&lt;/p&gt;

&lt;p&gt;David Obi&lt;/p&gt;

</description>
      <category>edgefunctions</category>
      <category>security</category>
      <category>deno</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Add a real WAF to your FastAPI, Express, or Next.js app in one line</title>
      <dc:creator>OBI EBUKA DAVID</dc:creator>
      <pubDate>Wed, 12 Aug 2026 01:06:27 +0000</pubDate>
      <link>https://dev.to/techstrtupninja/add-a-real-waf-to-your-fastapi-express-or-nextjs-app-in-one-line-4c0c</link>
      <guid>https://dev.to/techstrtupninja/add-a-real-waf-to-your-fastapi-express-or-nextjs-app-in-one-line-4c0c</guid>
      <description>&lt;p&gt;The first time an app of mine got popped, it was not a clever attack. Someone changed a number in a URL. &lt;code&gt;/api/orders/8123&lt;/code&gt; became &lt;code&gt;/api/orders/8124&lt;/code&gt;, and they were looking at a stranger's order. No SQL injection, no payload, nothing my WAF had ever been trained to recognize. Just a well-formed GET request for an object that was not theirs.&lt;/p&gt;

&lt;p&gt;That is the part nobody tells you when you buy a WAF. The WAF you are paying for was built to catch a different decade's attacks.&lt;/p&gt;

&lt;h2&gt;
  
  
  What your WAF is actually doing
&lt;/h2&gt;

&lt;p&gt;A traditional WAF is a signature engine. It carries a list of known-bad patterns, most of them descended from the OWASP Core Rule Set, and it matches every request against that list. SQL injection strings, cross-site scripting payloads, path traversal, a few thousand regexes. If a request matches, it blocks. If it does not, it passes.&lt;/p&gt;

&lt;p&gt;This works great for the noise. Automated scanners hammering your login page with &lt;code&gt;' OR 1=1&lt;/code&gt; get stopped cold, and that is worth something.&lt;/p&gt;

&lt;p&gt;But look at what it cannot see. The order-number attack I described has no bad pattern in it. It is a legitimate request shape hitting a legitimate route with a legitimate session. The only thing wrong is that this user should not have access to that object, and a signature engine has no idea what any user should have access to. Broken object-level authorization sits at the very top of the OWASP API Security Top 10 for a reason. It is the most common way APIs get breached, and it is invisible to the model your WAF is built on.&lt;/p&gt;

&lt;p&gt;Same story for broken auth, for business-logic abuse, for the endpoint you shipped last Tuesday that leaks a little more than it should. None of it looks like an attack. All of it is an attack.&lt;/p&gt;

&lt;h2&gt;
  
  
  The other way to do this
&lt;/h2&gt;

&lt;p&gt;There is a different model, and it is old enough that the idea is not controversial: instead of listing everything bad and blocking that, list everything good and block the rest. Allow-list instead of deny-list. In security terms it is called positive security.&lt;/p&gt;

&lt;p&gt;The reason nobody did it for web apps is that writing the allow-list by hand is miserable. Your app has hundreds of routes, each with its own request shapes, auth requirements, and access patterns, and they change every sprint. Nobody is going to maintain that by hand, and they were right not to try.&lt;/p&gt;

&lt;p&gt;The thing that changed is that you no longer have to write it. You let the app teach you what normal looks like. Every route it actually serves, the shape of the requests that hit it, whether the caller was authenticated, what a real user's access pattern looks like. Learn that, and then anything that deviates from it gets flagged. The order-number attack deviates, because this user has never accessed that object and the access pattern does not fit. It gets caught for what it is, not because someone wrote a rule.&lt;/p&gt;

&lt;p&gt;That is what I have been building with Nemesis Shield, so the rest of this is how you actually wire it up. It is genuinely one line, and it is free to start.&lt;/p&gt;

&lt;h2&gt;
  
  
  FastAPI
&lt;/h2&gt;

&lt;p&gt;Install the package and add the middleware. That is the whole integration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;nemesis-shield
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;nemesis_shield.asgi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SentinelMiddleware&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SentinelMiddleware&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nsk_your_app_token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your routes do not change. Your handlers do not change. The middleware sits in front, watches the request and response shapes, and reports them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Express
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @nemesis-shield-autogon/sentinel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;sentinel&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@nemesis-shield-autogon/sentinel/express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sentinel&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEMESIS_TOKEN&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Register it before your routes so it sees every request, including the ones that match no handler. Fastify and Koa have the same one-liner if that is your stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next.js
&lt;/h2&gt;

&lt;p&gt;For Next, guard everything from the middleware:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// middleware.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;withShield&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@nemesis-shield-autogon/edge&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;matcher&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/:path*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;withShield&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEMESIS_TOKEN&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 same &lt;code&gt;withShield&lt;/code&gt; wrapper works for Vercel Edge, Cloudflare Workers, and Supabase Edge Functions, which matters more than it sounds like, because those are usually the parts of an app that have no protection at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that keeps you from getting paged
&lt;/h2&gt;

&lt;p&gt;Here is the thing that makes this safe to ship, and the thing I would have wanted to hear before turning any WAF loose on production traffic.&lt;/p&gt;

&lt;p&gt;It starts in observe mode. Blocks nothing. It watches real traffic and builds the baseline, and everything it sees shows up in a review queue with the evidence attached. You look at what it learned, you approve it, and only then do you flip it to enforce. There is no moment where you deploy a wall and pray it does not brick your checkout flow.&lt;/p&gt;

&lt;p&gt;If you do not want to wait on real traffic to fill out the baseline, point the Nemesis Learn agent at your app in staging and it exercises your routes for you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @nemesis-shield-autogon/learn &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--target&lt;/span&gt; http://localhost:3000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--app-token&lt;/span&gt; nsk_your_app_token &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--repo&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It reads your repo to find routes, drives them over HTTP, and reports coverage, so you can see exactly when the baseline is ready to enforce instead of guessing.&lt;/p&gt;

&lt;p&gt;And it is fail-open. If the Nemesis service is ever unreachable, the SDK gets out of the way and your app serves the request. A security tool that can take your app down when its own backend has a bad day is not a security tool, it is a second outage waiting to happen. I have watched that movie. The SDK ships behavioral shapes only, the method, the shape of the path, the status, whether the caller was authenticated. Not your request bodies, not your secrets, not your source.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this fits, honestly
&lt;/h2&gt;

&lt;p&gt;Positive security is not a replacement for writing correct authorization code. If your handler does not check that the order belongs to the user, fix the handler. The WAF is the layer that catches the mistake you did not know you made, and the zero-day in a dependency you cannot patch until Tuesday, and the endpoint a teammate shipped without telling you. It buys you time and it catches the class of attack a signature engine was never going to see. That is the whole pitch.&lt;/p&gt;

&lt;p&gt;It also needs a clean learning window. If you first turn it loose on an app that is already under active attack, it will learn the attack as normal, so do the learning in staging or during a quiet period and approve what it shows you. That is the one real gotcha, and observe-first exists precisely so you never skip it.&lt;/p&gt;

&lt;p&gt;If you are building with an AI editor, you do not even have to add the line yourself. The Nemesis MCP server gives your coding agent the tools to do it while it writes the code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx &lt;span class="nt"&gt;-y&lt;/span&gt; @nemesis-shield-autogon/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ask it to protect your app and it installs the SDK, runs the learn agent, and walks the baseline to enforce for you.&lt;/p&gt;

&lt;p&gt;The free tier covers one app and does not need a card, which is enough to protect the side project that is quietly holding real user data. Change one number in one of your own URLs sometime and see what comes back. If it is somebody else's data, you already know which layer was supposed to stop that, and you already know it did not.&lt;/p&gt;

</description>
      <category>fastapi</category>
      <category>express</category>
      <category>nextjs</category>
      <category>positivesecurity</category>
    </item>
    <item>
      <title>We built an LLM that runs real pentests. The hard part was stopping it from lying</title>
      <dc:creator>OBI EBUKA DAVID</dc:creator>
      <pubDate>Wed, 08 Jul 2026 05:49:54 +0000</pubDate>
      <link>https://dev.to/techstrtupninja/we-built-an-llm-that-runs-real-pentests-the-hard-part-was-stopping-it-from-lying-57m5</link>
      <guid>https://dev.to/techstrtupninja/we-built-an-llm-that-runs-real-pentests-the-hard-part-was-stopping-it-from-lying-57m5</guid>
      <description>&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F41litujlg2hsfli9ggvm.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F41litujlg2hsfli9ggvm.png" alt=" " width="799" height="451"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We built an autonomous pentester on top of a large language model. It runs recon, picks and drives real offensive tools, exploits what it finds, and writes the report. The engineering that actually mattered was not teaching a model to hack. Models will happily "hack." The hard part was stopping it from telling us it found things that were not there.&lt;/p&gt;

&lt;p&gt;This is the build story of Nemesis Red, and specifically the problem everyone building LLM security tooling smashes into: hallucinated findings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why most LLM pentest demos are demoware
&lt;/h2&gt;

&lt;p&gt;Point a model at a target, ask it to find vulnerabilities, and it will hand you a confident, well-formatted list. Some of it is real. Some of it is invented. From the text alone you cannot tell which, and in security a false positive is not a rounding error. It burns an analyst's afternoon, it erodes trust in the tool, and at scale it makes the output worthless.&lt;/p&gt;

&lt;p&gt;A model that says "the login form is vulnerable to SQL injection" has told you nothing you can act on. A model that says "here is the injection, here is the exact request I sent, here is the database version it returned" has told you everything. The gap between those two sentences is the entire product.&lt;/p&gt;

&lt;h2&gt;
  
  
  ReasonChain: prove it or it did not happen
&lt;/h2&gt;

&lt;p&gt;The core of Nemesis Red is an engine we call ReasonChain. The rule it enforces is easy to state and annoying to build: the model is never trusted on its own word. Every claim it makes gets executed against the real target and checked against ground truth before it counts as a finding.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr8456y142zvvrhlzryun.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr8456y142zvvrhlzryun.png" alt=" " width="800" height="458"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The loop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The model forms a hypothesis. "This parameter looks injectable."&lt;/li&gt;
&lt;li&gt;It selects and runs a real tool to test that hypothesis, against the actual target, not a simulation.&lt;/li&gt;
&lt;li&gt;The result is checked against an objective signal. Did the payload return the data. Did the shell open. Did the scoreboard flip.&lt;/li&gt;
&lt;li&gt;Only a hypothesis that survives step 3 becomes a finding. Everything else is discarded, including the things the model was completely sure about.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is closer to how a human pentester works than to how a chatbot works. You do not write "SQLi confirmed" in a report because the app looked suspicious. You write it because you pulled a row out of the database. ReasonChain holds the model to that same standard.&lt;/p&gt;

&lt;p&gt;The effect on output quality is the whole point. The model is allowed to be wrong in private as many times as it likes. It is not allowed to be wrong in the report.&lt;/p&gt;

&lt;h2&gt;
  
  
  Driving 290 tools instead of reimplementing them
&lt;/h2&gt;

&lt;p&gt;We did not try to teach the model to exploit things from scratch. The offensive tooling already exists and is battle-tested. Nemesis Red orchestrates around 290 tools from the Kali ecosystem across 17 categories, and the model's job is to be the operator: pick the right tool for the current phase and target type, run it, read the output, and decide the next move.&lt;/p&gt;

&lt;p&gt;That framing matters. The model is not a hacker in a vacuum, it is a decision layer on top of tools that already work. Recon feeds exploitation feeds reporting in a closed loop, with the model choosing the path and the tools doing the actual work. When the model picks wrong, the verification step catches it, because a tool that finds nothing produces no ground-truth signal to promote into a finding.&lt;/p&gt;

&lt;h2&gt;
  
  
  We measured it, and kept the methodology attached
&lt;/h2&gt;

&lt;p&gt;Claims about "AI finds more" are cheap, so here is ours with the method attached.&lt;/p&gt;

&lt;p&gt;In a controlled ablation, Nemesis Red produced &lt;strong&gt;24.4 times more valid findings&lt;/strong&gt; than fixed-script automation on the same targets. The setup: 30 OWASP-class targets, 120 runs, across four conditions. The full system, then three ablations that each remove one capability: no replanning, no fusion of results across steps, and randomized tool order instead of reasoned order. Wilcoxon signed-rank, W = 465, p &amp;lt; 0.001.&lt;/p&gt;

&lt;p&gt;The ablations are the interesting part, because they say where the lift comes from. Removing replanning hurts. Removing cross-step fusion hurts. Randomizing the order the model would otherwise reason its way to hurts. The gain is not "we called an LLM," it is the reasoning loop wrapped around the tools. Strip the reasoning and you are back to scripted scanning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Receipts
&lt;/h2&gt;

&lt;p&gt;On known-vulnerable targets during the study, Nemesis Red autonomously identified and exploited three critical vulnerabilities rated CVSS 9.8, and confirmed each with a working exploit rather than a scanner flag:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CVE-2024-38476 (Apache mod_rewrite SSRF)&lt;/li&gt;
&lt;li&gt;CVE-2024-38474 (Apache mod_rewrite substitution)&lt;/li&gt;
&lt;li&gt;CVE-2023-25690 (HTTP request smuggling)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To be precise about what that is and is not: these are known, published N-day CVEs, not novel zero-days. The result is that the system chained a full exploit against each on its own and proved it, with no human steering the steps. Novel zero-day discovery is a different and harder problem, and we treat it as a separate thing rather than blurring the two.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local by default, because pentest data is sensitive
&lt;/h2&gt;

&lt;p&gt;A detail that matters for anyone who would actually run this: you should not have to ship a client's live vulnerabilities to a third-party API to use the tool. Nemesis Red is bring-your-own-key for the frontier models (Anthropic, OpenAI, Gemini) if you want their reasoning, but it also runs fully local against an Ollama model (Qwen, Llama, or your own fine-tune). Local is slower and a little less sharp, but for engagements where the target data cannot leave the environment, "a little less sharp and it never leaves the box" is the correct trade.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three ways to run it
&lt;/h2&gt;

&lt;p&gt;The same engine runs at three levels of autonomy, depending on how much you want to hand over:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Copilot.&lt;/strong&gt; A human drives, the model reads tool output and pre-fills the next moves. For testers who want speed without giving up the wheel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Autonomous vulnerability assessment.&lt;/strong&gt; The model runs the discovery and verification loop and hands you a verified findings list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Autonomous penetration test.&lt;/strong&gt; Recon to exploit to report, hands off.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it gets wrong, honestly
&lt;/h2&gt;

&lt;p&gt;Before verification, the model is exactly as overconfident as every other LLM. The entire architecture exists because raw model output is not trustworthy, and if you deleted the verification layer you would have the same demoware problem as everyone else. Verification also costs time and tool executions, so a run is not free. And autonomy needs guardrails: a system driving offensive tools on its own has to be scoped hard to the authorized target set, which is a first-class design concern, not an afterthought.&lt;/p&gt;

&lt;p&gt;None of that is a reason not to build it. It is the reason to build it with proof in the loop instead of vibes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this fits
&lt;/h2&gt;

&lt;p&gt;Nemesis Red is the offensive half of what we are building. The defensive half, Nemesis Blue, is a Rust EDR with a completely different build story that I wrote up separately.&lt;/p&gt;

&lt;p&gt;If you want to see it run or get your hands on it, we are at &lt;a href="//nemesislabs.xyz"&gt;nemesislabs.xyz&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>python</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>We built a Rust EDR. macOS notarization tried to kill it.</title>
      <dc:creator>OBI EBUKA DAVID</dc:creator>
      <pubDate>Wed, 08 Jul 2026 02:19:08 +0000</pubDate>
      <link>https://dev.to/techstrtupninja/we-built-a-rust-edr-macos-notarization-tried-to-kill-it-363f</link>
      <guid>https://dev.to/techstrtupninja/we-built-a-rust-edr-macos-notarization-tried-to-kill-it-363f</guid>
      <description>&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp6ctd604zlu5g16fip7e.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp6ctd604zlu5g16fip7e.png" alt=" " width="800" height="514"&gt;&lt;/a&gt;&lt;br&gt;
Over the last few months we shipped Nemesis Blue, an endpoint detection and response agent written in Rust, notarized and running on macOS. This is the build story: why Rust, what notarization actually costs you, the entitlement bug that ate a week, and how you do detection engineering inside Apple's hardened runtime.&lt;/p&gt;

&lt;p&gt;No marketing here. Just the parts that were hard.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Rust for an EDR
&lt;/h2&gt;

&lt;p&gt;An EDR runs on every machine you protect, with high privilege, all the time. That job description rules a lot of things out. A garbage-collected runtime that pauses mid-scan is not acceptable. A memory-safety bug in an agent that sits just above the kernel is an incident waiting to happen, on your customer's box, with your signature on it.&lt;/p&gt;

&lt;p&gt;Rust gave us three things that mattered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No GC pauses. Scans run in bounded time, every time.&lt;/li&gt;
&lt;li&gt;Memory safety without a runtime. The agent ships as a single static binary.&lt;/li&gt;
&lt;li&gt;One codebase across platforms. Our crates (&lt;code&gt;nemesis-core&lt;/code&gt;, &lt;code&gt;nemesis-scan&lt;/code&gt;, &lt;code&gt;nemesis-detect&lt;/code&gt;, and friends) build for macOS, Linux, and Windows sensors out of the same Cargo workspace.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last decision paid off later: once the macOS agent was solid, the Linux build came up end to end in days instead of weeks, because everything below the platform layer was already shared and tested.&lt;/p&gt;
&lt;h2&gt;
  
  
  Notarization is the part nobody warns you about
&lt;/h2&gt;

&lt;p&gt;If you want a macOS binary to run on someone else's machine without a scary Gatekeeper wall, you notarize it. To notarize, Apple requires the &lt;strong&gt;hardened runtime&lt;/strong&gt;. And the hardened runtime quietly changes the rules your process runs under: no unsigned executable memory, no loading unsigned libraries, restricted debugging, and by default, no JIT.&lt;/p&gt;

&lt;p&gt;That "no JIT" line is where we lost a week.&lt;/p&gt;
&lt;h2&gt;
  
  
  The SIGKILL that ate a week
&lt;/h2&gt;

&lt;p&gt;Our scanning engine is built on &lt;strong&gt;YARA-X&lt;/strong&gt;, the Rust rewrite of YARA. YARA-X compiles rules down and runs them on &lt;strong&gt;wasmtime&lt;/strong&gt;. Wasmtime is a JIT: to execute, it allocates memory, marks it executable, and jumps into it.&lt;/p&gt;

&lt;p&gt;Under a normal build, this is invisible. Under the hardened runtime, the kernel refuses to hand you executable-writable pages. The moment the scanner tried to run its first rule, the whole daemon died. Not an exception we could catch. An immediate &lt;code&gt;SIGKILL&lt;/code&gt;, with Console showing a code-signature violation.&lt;/p&gt;

&lt;p&gt;The symptom was maddening because everything worked in development and died the instant it was signed and hardened. Same binary, different fate, depending on entitlements.&lt;/p&gt;

&lt;p&gt;The fix is one entitlement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;com.apple.security.cs.allow-jit&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;true/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add it, re-sign, re-notarize, and wasmtime can allocate its JIT pages again. Obvious in hindsight. Not obvious at 2am when your only clue is a process that vanishes without a stack trace.&lt;/p&gt;

&lt;p&gt;The lesson we took from it: on macOS, your entitlements are part of your runtime behavior, not a checkbox at the end. If a dependency touches executable memory, dynamic libraries, or the network, that shows up here or your process dies in ways your test suite never sees.&lt;/p&gt;

&lt;h2&gt;
  
  
  Detection engineering under the hardened runtime
&lt;/h2&gt;

&lt;p&gt;With the engine actually running, the real work is detection. Nemesis Blue runs three layers:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Signatures.&lt;/strong&gt; YARA-X rules for known families and behaviors. Fast, explainable, and the thing an analyst can read and trust.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Machine learning.&lt;/strong&gt; An XGBoost model trained on the EMBER feature set for static classification of executables. The model is the interesting supply-chain problem: if an attacker can swap your model file, they own your verdicts. So model bundles are signed with &lt;strong&gt;Ed25519&lt;/strong&gt;, and the agent verifies the signature before it will load a model. A model that does not verify does not run. Retraining happens weekly in CI, and it only publishes when the labeled corpus is large enough to be worth shipping. New models roll out to a canary slice of the fleet first, so a bad training run degrades a handful of machines instead of all of them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IOC matching and quarantine.&lt;/strong&gt; Indicator feeds for the known-bad, and quarantine to pull a file out of harm's way once something fires.&lt;/p&gt;

&lt;p&gt;None of these are novel on their own. The engineering is in making all three run in bounded time, on someone else's laptop, without being the reason their fan spins up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 78-second freeze
&lt;/h2&gt;

&lt;p&gt;Memory scanning is where we hit the ugliest performance bug. Our first pass at walking a process's memory regions shelled out and parsed the output. On small processes it was fine. On a large one, it serialized behind a single call that blocked for &lt;strong&gt;78 seconds&lt;/strong&gt;. For an agent that is supposed to be invisible, 78 seconds of a pegged process is not a bug, it is an eviction notice.&lt;/p&gt;

&lt;p&gt;The fix was to stop treating memory enumeration as a subprocess and read the regions directly, with hard caps on region size and early skips for the mappings we never scan. Bounded work, off the hot path. The 78 seconds became milliseconds.&lt;/p&gt;

&lt;p&gt;This is the recurring theme of EDR work: correctness is table stakes, but the thing that gets you uninstalled is being heavy. Every scan has to answer "what did this cost the user."&lt;/p&gt;

&lt;h2&gt;
  
  
  Shipping, and staying shipped
&lt;/h2&gt;

&lt;p&gt;The agent ships as a notarized &lt;code&gt;.pkg&lt;/code&gt;. But shipping once is not the hard part. Keeping a fleet current is.&lt;/p&gt;

&lt;p&gt;Nemesis Blue agents check for updates at boot and every few hours after, on a stable channel, and self-install a newer signed and notarized package when one is published. Pushing a release is: bump the version, build the package, notarize it, publish the manifest. Because updates are gated behind signing and notarization, a compromised update server still cannot push a payload the agents will accept. The agents only run what Apple and our signing key both vouch for.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it honestly does today
&lt;/h2&gt;

&lt;p&gt;Here is the part most product posts skip. As of today, Nemesis Blue does detection: scanning, YARA-X, the ML classifier, IOC matching, quarantine, and monitoring, all in a notarized agent you can install right now.&lt;/p&gt;

&lt;p&gt;What it does not do yet is real-time blocking. Full inline enforcement (killing a malicious process before it executes, streaming kernel events, process attribution) depends on Apple's &lt;strong&gt;Endpoint Security&lt;/strong&gt; entitlement, which is a manual review with a multi-week queue. We have it submitted. Until it clears, enforce mode stays behind the flag, and we would rather say that plainly than imply a capability we cannot ship. The moment the entitlement lands, the blocking path is already built and waiting for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;If you are building a signed, notarized native agent on macOS:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Treat entitlements as runtime behavior. &lt;code&gt;allow-jit&lt;/code&gt; for anything wasm or JIT, and audit every dependency for what the hardened runtime will forbid.&lt;/li&gt;
&lt;li&gt;Sign your models, not just your binary. A swapped model is a swapped verdict.&lt;/li&gt;
&lt;li&gt;Measure what every scan costs the user. Heavy gets you uninstalled faster than wrong.&lt;/li&gt;
&lt;li&gt;Be honest about what is gated. The entitlement queue is real, and pretending otherwise just burns trust with the exact people who will read your Console logs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nemesis Blue is the defensive half of what we are building at Nemesis Labs. The offensive half, Nemesis Red, is an autonomous pentest engine, and it has its own build story that is nothing like this one. That post is next.&lt;/p&gt;

&lt;p&gt;If you want to follow along or kick the tires, we are at nemesislabs.xyz.&lt;/p&gt;

</description>
      <category>security</category>
      <category>rust</category>
      <category>devops</category>
      <category>macos</category>
    </item>
  </channel>
</rss>
