<?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: Ayaan Malik</title>
    <description>The latest articles on DEV Community by Ayaan Malik (@ayanmalik_1212).</description>
    <link>https://dev.to/ayanmalik_1212</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%2F4051071%2Ff1e6de3a-6408-489d-9dec-98c0e78c2656.png</url>
      <title>DEV Community: Ayaan Malik</title>
      <link>https://dev.to/ayanmalik_1212</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ayanmalik_1212"/>
    <language>en</language>
    <item>
      <title>Reachability analysis: why most of your dependency CVEs don't matter</title>
      <dc:creator>Ayaan Malik</dc:creator>
      <pubDate>Tue, 28 Jul 2026 10:23:49 +0000</pubDate>
      <link>https://dev.to/ayanmalik_1212/reachability-analysis-why-most-of-your-dependency-cves-dont-matter-3cci</link>
      <guid>https://dev.to/ayanmalik_1212/reachability-analysis-why-most-of-your-dependency-cves-dont-matter-3cci</guid>
      <description>&lt;p&gt;A lockfile is not an attack surface&lt;/p&gt;

&lt;p&gt;Traditional dependency scanning works off the manifest: parse the lockfile, look up every package@version against a CVE database (NVD, OSV, GHSA), report matches. It's fast and it's complete — every known vulnerability in every resolved package gets flagged.&lt;/p&gt;

&lt;p&gt;The problem is that &lt;em&gt;resolved&lt;/em&gt; and &lt;em&gt;used&lt;/em&gt; are different things. A typical Node.js project pulls in hundreds of transitive dependencies for a handful of direct ones. Python projects carry dev-only extras. A CVE in a package deep in your dependency tree that nothing in your code ever imports cannot be exploited through your application — there's no code path that reaches it. It's still a real CVE. It's just not &lt;em&gt;your&lt;/em&gt; problem this week, and a report that can't tell you which is which trains people to stop reading it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What reachability analysis actually checks
&lt;/h2&gt;

&lt;p&gt;Reachability analysis answers one narrow, useful question: &lt;em&gt;does any code in this repository import the vulnerable package at all?&lt;/em&gt; It's static analysis, not dynamic — no code runs. The mechanics vary by ecosystem but the shape is consistent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Python&lt;/strong&gt; — parse import/from statements across the source tree, resolve them against the dependency graph from requirements.txt, pyproject.toml, or poetry/pipenv lockfiles&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JavaScript/TypeScript&lt;/strong&gt; — walk import/require statements (including re-exports) against package.json and the lockfile's resolved tree&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Go&lt;/strong&gt; — cross-reference go.mod requires against actual package imports, since Go's module system already prunes a lot of this at build time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ruby&lt;/strong&gt; — match require/require_relative calls against Gemfile.lock&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Java/Kotlin&lt;/strong&gt; — resolve import statements against Maven/Gradle dependency trees, which is the noisiest case given how deep enterprise dependency trees typically run&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A package with zero matching imports anywhere in the source tree is &lt;strong&gt;unreachable&lt;/strong&gt;. That doesn't delete the finding — it demotes it. Severity drops a rung, it stops occupying a slot in the top-of-queue list, and it stays fully visible and reversible on demand. The goal is triage, not suppression.&lt;/p&gt;

&lt;h2&gt;
  
  
  Package-level vs. function-level reachability
&lt;/h2&gt;

&lt;p&gt;It's worth being honest about the limits, because overclaiming here is how security tooling loses trust. What's described above is &lt;strong&gt;package-level reachability&lt;/strong&gt; — it answers "is this package imported at all." It does not answer the stronger question, &lt;strong&gt;function-level reachability&lt;/strong&gt;: does an actual call path reach the specific vulnerable function, with attacker-influenced data flowing into it? That requires a call graph and often taint tracking, and it runs into real limits fast:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic imports&lt;/strong&gt; — &lt;code&gt;importlib.import_module(name)&lt;/code&gt;-style loading where the module name is computed at runtime is invisible to static import parsing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reflection and eval&lt;/strong&gt; — Java reflection, Ruby's &lt;code&gt;send&lt;/code&gt;, JavaScript's &lt;code&gt;eval&lt;/code&gt; — all can reach code no static graph sees&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plugin/middleware architectures&lt;/strong&gt; — frameworks that load handlers by convention (file path, decorator registry) rather than explicit import obscure the real call graph&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Partial reachability&lt;/strong&gt; — a package can be imported for one harmless function while the vulnerable function in the same package is never called; package-level analysis can't see that far&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The honest framing: package-level reachability is a high-confidence &lt;strong&gt;noise filter&lt;/strong&gt;, not a soundness proof. It will essentially never mark something reachable when it isn't (false negatives on the safe side), but it can occasionally miss a genuinely exploitable path reached only through dynamic loading. That asymmetry is exactly why the output should be a demotion with a visible toggle, not a silent deletion — the deeper call-path analysis that would close this gap is a meaningfully larger project (real call graphs, taint tracking across ecosystem-specific frameworks), and most teams get more value from a fast, conservative filter today than a slow, complete one eventually.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do with the "unreachable" pile
&lt;/h2&gt;

&lt;p&gt;Don't delete it, and don't ignore it forever either. A sane workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Let reachable CVEs drive this week's fix queue — that's the actionable set&lt;/li&gt;
&lt;li&gt;Batch unreachable CVEs into a periodic dependency-hygiene pass (monthly is usually enough) rather than a per-scan fire drill&lt;/li&gt;
&lt;li&gt;If a package shows up unreachable but you know it's loaded dynamically somewhere (plugin registries, dependency-injection containers), flag that pattern once — it's a systemic blind spot worth a manual note, not a per-CVE re-review&lt;/li&gt;
&lt;li&gt;Re-check reachability whenever you actually start using a previously-unused package — the demotion is a snapshot of current usage, not a permanent verdict&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How Virgil applies this
&lt;/h2&gt;

&lt;p&gt;I've been building &lt;a href="https://virgilhq.app" rel="noopener noreferrer"&gt;Virgil&lt;/a&gt;, a security audit tool, and reachability analysis is one stage in its pipeline — after normalizing dependency findings, before ranking. Unreachable CVEs drop a severity rung and collapse out of the default view, with a one-click toggle to see them and a note on why each was demoted. It's combined with finding clustering (grouping duplicate findings by shared root cause) so the queue you actually read is short enough to act on.&lt;/p&gt;

&lt;p&gt;Full writeup on the mechanism (with example output) is on the &lt;a href="https://virgilhq.app/docs/reachability" rel="noopener noreferrer"&gt;docs page&lt;/a&gt; if you want more detail than fits here.&lt;/p&gt;

&lt;p&gt;Curious how other teams are handling this — especially the dynamic-loading blind spot. What's worked for you?&lt;/p&gt;

</description>
      <category>security</category>
      <category>opensource</category>
      <category>devops</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
