<?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: Nishaad Gangal</title>
    <description>The latest articles on DEV Community by Nishaad Gangal (@the_one_who).</description>
    <link>https://dev.to/the_one_who</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%2F4190937%2F601409a0-702b-4bfa-9cdb-706d5f7b1608.png</url>
      <title>DEV Community: Nishaad Gangal</title>
      <link>https://dev.to/the_one_who</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/the_one_who"/>
    <language>en</language>
    <item>
      <title>"No path found" is the most useful thing a security agent can say.</title>
      <dc:creator>Nishaad Gangal</dc:creator>
      <pubDate>Sat, 17 Oct 2026 17:44:22 +0000</pubDate>
      <link>https://dev.to/the_one_who/no-path-found-is-the-most-useful-thing-a-security-agent-can-say-20c8</link>
      <guid>https://dev.to/the_one_who/no-path-found-is-the-most-useful-thing-a-security-agent-can-say-20c8</guid>
      <description>&lt;p&gt;&lt;em&gt;Building Blastradius for Graph Hacks with FalkorDB&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp5uw94amq6y5j6m06z7c.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%2Fp5uw94amq6y5j6m06z7c.png" alt="Blastradius tracing a critical advisory through the dependency graph to an internet-facing service" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://13-203-50-195.sslip.io" rel="noopener noreferrer"&gt;https://13-203-50-195.sslip.io&lt;/a&gt; · &lt;strong&gt;Code:&lt;/strong&gt; &lt;a href="https://github.com/NishaadG/blastradius" rel="noopener noreferrer"&gt;https://github.com/NishaadG/blastradius&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every time a new CVE lands, someone asks the same four questions: &lt;strong&gt;Are we affected? Through which dependency? Who owns that service? Did we already deal with something like this?&lt;/strong&gt; Most tools answer with a list. The honest answer is a &lt;em&gt;path&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with "similar text"
&lt;/h2&gt;

&lt;p&gt;Most agent demos retrieve context by embedding similarity. That is fine for "find the doc that sounds like this". It is the wrong tool for &lt;em&gt;"does this vulnerable package reach a service that faces the internet?"&lt;/em&gt;, because that is a question about &lt;strong&gt;connections&lt;/strong&gt;, and similarity can't prove a connection exists or doesn't.&lt;/p&gt;

&lt;p&gt;A graph can. In Blastradius the question is one traversal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;a:&lt;/span&gt;&lt;span class="n"&gt;Advisory&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;id:&lt;/span&gt;&lt;span class="n"&gt;$id&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="py"&gt;e:&lt;/span&gt;&lt;span class="n"&gt;AFFECTS&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;p:&lt;/span&gt;&lt;span class="n"&gt;Package&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;s:&lt;/span&gt;&lt;span class="n"&gt;Service&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;shortestPath&lt;/span&gt;&lt;span class="ss"&gt;((&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:DEPENDS_ON&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="m"&gt;15&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="ss"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="ow"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;s.name&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p.name&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p.version&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e.fixed&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;length&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="ss"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="ow"&gt;IN&lt;/span&gt; &lt;span class="nf"&gt;nodes&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nf"&gt;coalesce&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n.key&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n.name&lt;/span&gt;&lt;span class="ss"&gt;)]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it on a real project (Google's Online Boutique, with real lockfiles and real OSV advisories) and a critical &lt;code&gt;protobufjs&lt;/code&gt; RCE resolves, in tens of milliseconds, to:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;frontend → checkoutservice → paymentservice → @opentelemetry/exporter-otlp-grpc → @grpc/proto-loader → protobufjs@6.11.4&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;with the internet-facing entry point, the owning team, and the fixed version, all from the same graph.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proof by absence
&lt;/h2&gt;

&lt;p&gt;Ask about Log4Shell and the same query returns &lt;strong&gt;zero rows&lt;/strong&gt;. That is not a failure; it is the answer. Nothing in the graph depends on a Maven package, so the verdict is &lt;code&gt;NOT_AFFECTED&lt;/code&gt;, with the empty traversal on screen as the evidence. Ask about an advisory for a package you &lt;em&gt;do&lt;/em&gt; run, but at a patched version, and the graph explains &lt;em&gt;why&lt;/em&gt; it's safe: the package is present, the version is outside the affected range.&lt;/p&gt;

&lt;h2&gt;
  
  
  The LLM doesn't get to decide
&lt;/h2&gt;

&lt;p&gt;The verdict (&lt;code&gt;AFFECTED / NOT_AFFECTED / NEEDS_HUMAN&lt;/code&gt;) comes from plain rules over graph evidence. The LLM, optional and free-tier, only chooses which tool to call and phrases the result. It never sees a database credential or writes Cypher; the read path is rejected for writes and enforced read-only by the database too. Remediation actions are proposals that wait for a human.&lt;/p&gt;

&lt;h2&gt;
  
  
  Memory that is also a graph
&lt;/h2&gt;

&lt;p&gt;When a human approves a fix, the decision is written back as a &lt;code&gt;Case&lt;/code&gt; node linked to the advisory, the affected services and the playbook used. Days later a &lt;em&gt;different&lt;/em&gt; &lt;code&gt;protobufjs&lt;/code&gt; advisory arrives, and a three-hop traversal &lt;code&gt;(Case)-[:ABOUT]-&amp;gt;(Advisory)-[:AFFECTS]-&amp;gt;(Package)&lt;/code&gt; finds the earlier case. The agent cites it and reuses the playbook; the playbook's success counter ticks up. Memory, dependency data and ownership live in one graph, so the agents' handoffs (&lt;code&gt;Step-[:HANDS_OFF_TO]-&amp;gt;Step&lt;/code&gt;) are queryable too.&lt;/p&gt;

&lt;p&gt;Each tenant gets its &lt;strong&gt;own FalkorDB graph&lt;/strong&gt;, so isolation is structural rather than a &lt;code&gt;WHERE tenant = …&lt;/code&gt; you hope nobody forgets.&lt;/p&gt;

&lt;h2&gt;
  
  
  A company brain with receipts
&lt;/h2&gt;

&lt;p&gt;Ownership comes from &lt;code&gt;CODEOWNERS&lt;/code&gt;, expertise from commit history, and dependency decisions link to the exact commit that made them. A &lt;code&gt;SUPERSEDES&lt;/code&gt; edge marks decisions that were later overtaken, so the answer to "why do we pin this?" says whether the reasoning is still current.&lt;/p&gt;

&lt;p&gt;Commits say what changed, not why, so a build step also reads the pull request behind each dependency change and has Gemini extract the decision and the stated reason. The model is boxed in: it may only restate what the PR says, an empty rationale stays empty, and a package is linked to a decision only if its name literally appears in the PR or commit text. Two evals enforce this, and the second one caught the model inventing a subject (&lt;code&gt;otlp-exporters&lt;/code&gt;, a paraphrase, not a package) on the first run. Now "why did we upgrade golang.org/x/net?" is answered with the CVEs and the PR links.&lt;/p&gt;

&lt;h2&gt;
  
  
  The topology is discovered, not typed in
&lt;/h2&gt;

&lt;p&gt;Early on I hand-wrote which service calls which. That's the weakest part of any demo, so I replaced it: Blastradius now reads the project's own deployment files. In Online Boutique's Kubernetes manifests, &lt;code&gt;PRODUCT_CATALOG_SERVICE_ADDR=productcatalogservice:3550&lt;/code&gt; is a call and a &lt;code&gt;LoadBalancer&lt;/code&gt; Service is an internet exposure. In the OpenTelemetry demo's Compose file, bare env names resolve through &lt;code&gt;.env&lt;/code&gt;, and only a real &lt;code&gt;host:container&lt;/code&gt; port mapping counts as exposure. Checked against my old hand-written list, discovery recovered 100% of the OpenTelemetry edges (and found 7 the docs never mention), 100% of Bank of Anthos's (whose addresses hide inside ConfigMaps and &lt;code&gt;jdbc:&lt;/code&gt; URLs) and 94% of Boutique's; the one it misses is in no deployment file at all, and the eval report says so.&lt;/p&gt;

&lt;h2&gt;
  
  
  Any public repo, including the paths manifests hide
&lt;/h2&gt;

&lt;p&gt;Paste &lt;code&gt;owner/name&lt;/code&gt; and Blastradius finds the manifests, asks &lt;a href="https://deps.dev" rel="noopener noreferrer"&gt;deps.dev&lt;/a&gt; for the resolved dependency tree when a manifest lists only direct dependencies, and builds a tenant graph. Pointed at a deliberately vulnerable Java app, it finds Log4Shell as &lt;code&gt;app → spring-boot-starter-log4j2 → log4j-core@2.14.1&lt;/code&gt;. The build file never mentions log4j-core; a static parser sees nothing. A graph of resolved dependencies sees the whole chain.&lt;/p&gt;

&lt;h2&gt;
  
  
  One graph, many AI clients
&lt;/h2&gt;

&lt;p&gt;The same toolkit is exposed as an MCP server, so Claude Desktop, Claude Code or any MCP client can ask the graph directly, and as a tool-calling loop for the chat box. The model can only call validated, read-only tools; it never sees Cypher.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do we know it's right?
&lt;/h2&gt;

&lt;p&gt;Ground truth is computed &lt;em&gt;without&lt;/em&gt; the graph from the raw lockfiles, cached OSV data, the discovered topology and &lt;code&gt;CODEOWNERS&lt;/code&gt;, then compared to the product: affected-service sets and verdicts for every advisory in three real projects (the OpenTelemetry demo, Online Boutique and Bank of Anthos), that every returned path is a real chain of lockfile edges, exposure distances, ownership, transitive callers, memory reuse, and tenant and session isolation. 57 of 57 pass, plus 42 unit tests and guardrail checks (write queries are rejected on the agent path).&lt;/p&gt;

&lt;h2&gt;
  
  
  Under load, honestly
&lt;/h2&gt;

&lt;p&gt;Track 2 mentions high-concurrency shared state, so I benchmarked it: 10, 50 and 100 agents hammering one graph. Reads stay fast; FalkorDB serialises writes, so write latency queues. At 100 agents my first run saw about 2% of operations rejected with &lt;code&gt;Max pending queries exceeded&lt;/code&gt;. That's FalkorDB protecting itself, so the client now retries with jittered backoff and counts every retry in the results instead of hiding them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "is the graph decoration?" test
&lt;/h2&gt;

&lt;p&gt;Stop the FalkorDB container. The UI turns red and nothing answers: no cache, no fallback, by design. Start it, and everything resumes. If removing the database leaves the product working, the database was decoration. Here, it is the product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reachable, not just present
&lt;/h2&gt;

&lt;p&gt;For Go services, Blastradius runs Google's &lt;code&gt;govulncheck&lt;/code&gt; and stores the call-graph result on the edge between service and advisory. A P0 whose vulnerable function is never called is demoted with the reason; one that is called shows the actual stack, from &lt;code&gt;main&lt;/code&gt; down to the vulnerable function.&lt;/p&gt;

&lt;h2&gt;
  
  
  It notices, and it blocks
&lt;/h2&gt;

&lt;p&gt;A watcher re-queries OSV on a schedule and flags advisories published since the graph was built. A CLI and GitHub Action analyse a checkout, write SARIF and fail a pull request only for findings that change &lt;em&gt;introduces&lt;/em&gt; compared to the base branch.&lt;/p&gt;

&lt;h2&gt;
  
  
  SBOM in, SBOM out
&lt;/h2&gt;

&lt;p&gt;Not every team can point a tool at its source. So Blastradius also takes a CycloneDX or SPDX file: each component's purl becomes a &lt;code&gt;Package&lt;/code&gt; node, the SBOM's dependency list becomes &lt;code&gt;DEPENDS_ON&lt;/code&gt; edges, and from there it is the same graph and the same traversals. It goes the other way too: any tenant exports as CycloneDX 1.5 with the advisories that apply. The eval exports each of the three projects, re-imports the file, and checks that the packages and edges come back identical.&lt;/p&gt;

&lt;h2&gt;
  
  
  A free-tier model, shared with strangers
&lt;/h2&gt;

&lt;p&gt;The hosted demo runs on Gemini's free tier, and every visitor shares one quota. Instead of letting a busy minute turn into errors, the server budgets model calls across everyone and backs off for a short while after a failure. When the model is out, the same tools answer through a deterministic router, and the UI says which one answered and why. Because the verdicts never depended on the model, the answer to "are we affected?" is the same either way.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;Function-level reachability beyond Go, and building the &lt;code&gt;CALLS&lt;/code&gt; edges from OpenTelemetry traces instead of reading them from deployment files.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Open the &lt;a href="https://13-203-50-195.sslip.io" rel="noopener noreferrer"&gt;live demo&lt;/a&gt; and take the guided tour, or paste a public &lt;code&gt;owner/name&lt;/code&gt; repository. To run it yourself, it is &lt;code&gt;docker compose up&lt;/code&gt; plus one build command, and it works offline from a committed snapshot. The README has the data model, every Cypher query the product depends on, and the eval report.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Built with FalkorDB, Cypher, FastAPI, Next.js and Cytoscape. Everything runs locally for $0. Developed with Claude Code as a coding assistant (disclosed per the hackathon rules).&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>falkordb</category>
      <category>security</category>
      <category>graphdatabase</category>
    </item>
  </channel>
</rss>
