<?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: Elise Vance</title>
    <description>The latest articles on DEV Community by Elise Vance (@shecantcode).</description>
    <link>https://dev.to/shecantcode</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3878897%2F6ebcddbc-3637-4adf-a77d-741d02c2cd41.jpeg</url>
      <title>DEV Community: Elise Vance</title>
      <link>https://dev.to/shecantcode</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shecantcode"/>
    <language>en</language>
    <item>
      <title>Static vs Semantic: how a security scanner reads AI-generated code</title>
      <dc:creator>Elise Vance</dc:creator>
      <pubDate>Wed, 15 Apr 2026 01:52:21 +0000</pubDate>
      <link>https://dev.to/shecantcode/static-vs-semantic-how-a-security-scanner-reads-ai-generated-code-cj6</link>
      <guid>https://dev.to/shecantcode/static-vs-semantic-how-a-security-scanner-reads-ai-generated-code-cj6</guid>
      <description>&lt;p&gt;Three days ago &lt;a href="https://dev.to/vibedoctor_io/i-scanned-the-most-famous-ai-coding-repos-on-github-heres-what-i-found-469l"&gt;VibeDoctor launched a scanner&lt;/a&gt; for AI-generated apps. They scan by running six tools in parallel: SonarQube, Gitleaks, Trivy, Lighthouse, plus custom checks. They scanned open-lovable, devika, and bolt.new and found hundreds of issues. It's good work.&lt;/p&gt;

&lt;p&gt;But there's a whole class of bug their approach can't see. I built &lt;a href="https://chat-api-19ij.onrender.com" rel="noopener noreferrer"&gt;Vibe Check&lt;/a&gt; to catch that class. Here's what's different.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap
&lt;/h2&gt;

&lt;p&gt;Static scanners pattern-match. They look for &lt;code&gt;eval(&lt;/code&gt;, &lt;code&gt;os.system(&lt;/code&gt;, hardcoded API keys, outdated dependencies. They're good at this, and they catch a lot.&lt;/p&gt;

&lt;p&gt;What they can't catch is &lt;strong&gt;intent&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Consider this real function from a repo I scanned this week:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_auth_ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Accept multiple common secret formats so GHL/curl both work.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;WEBHOOK_SECRET&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;  &lt;span class="c1"&gt;# no secret set -&amp;gt; allow all
&lt;/span&gt;    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Grep for known vuln patterns: nothing. SonarQube: clean. Gitleaks: no secrets here (that's the point). Trivy: no CVEs. Every static tool I threw at it said OK.&lt;/p&gt;

&lt;p&gt;The bug is that when &lt;code&gt;WEBHOOK_SECRET&lt;/code&gt; is unset in the environment, the function returns &lt;code&gt;True&lt;/code&gt; and the webhook is &lt;strong&gt;fully open&lt;/strong&gt;. In development &lt;code&gt;WEBHOOK_SECRET&lt;/code&gt; is often unset. In production, a simple env-var typo becomes an unauthenticated remote action vector.&lt;/p&gt;

&lt;p&gt;This is a semantic bug. You only catch it by reading the code and asking "what happens when the inputs are missing?" That's a human pen-tester mindset, not a regex.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Vibe Check reads code
&lt;/h2&gt;

&lt;p&gt;Vibe Check sends files to Claude via the Anthropic API with a custom prompt focused on six categories: secrets, auth, injection, data exposure, dependencies, config. The prompt has specific guardrails for LLM failure modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Default-allow patterns.&lt;/strong&gt; Explicit instructions to flag &lt;code&gt;if not secret: return True&lt;/code&gt; as critical.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic SQL in column names.&lt;/strong&gt; Parameterized queries are safe until the column name or &lt;code&gt;ORDER BY&lt;/code&gt; clause comes from an f-string. The prompt flags this explicitly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy invariant.&lt;/strong&gt; Claude is told to never echo actual secret values in findings. The scanner itself never persists source code, raw responses, or the secrets it finds.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Output is structured via Anthropic's tool-use API. Every finding has category, severity, file, line, title, description, suggested_fix, and a verbatim &lt;code&gt;code_snippet&lt;/code&gt; field that Vibe Check uses to auto-correct line numbers post-hoc (Claude hallucinates line numbers; the snippet search fixes that).&lt;/p&gt;

&lt;h2&gt;
  
  
  False positives are the hard part
&lt;/h2&gt;

&lt;p&gt;LLMs are noisy. Over a day of self-scanning my own repo, I watched Claude emit gems like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"render.yaml contains &lt;code&gt;sync: false&lt;/code&gt; for secrets — could be misconfigured"&lt;br&gt;
(&lt;code&gt;sync: false&lt;/code&gt; is the &lt;em&gt;correct&lt;/em&gt; Render.com setting)&lt;/p&gt;

&lt;p&gt;"compare_digest is correctly implemented"&lt;br&gt;
(flagged as a finding even though it's literally the fix)&lt;/p&gt;

&lt;p&gt;"SQL query logging during development could expose sensitive data"&lt;br&gt;
(it's development; that's the whole point of &lt;code&gt;echo=True&lt;/code&gt;)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The solution isn't more prompting. Claude ignores prompt guardrails about a third of the time. The real fix is a &lt;strong&gt;hard post-parse filter&lt;/strong&gt; that drops findings matching known false-positive patterns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Infrastructure config files (&lt;code&gt;render.yaml&lt;/code&gt;, &lt;code&gt;.github/workflows/*&lt;/code&gt;, &lt;code&gt;Dockerfile&lt;/code&gt;, &lt;code&gt;*.tf&lt;/code&gt;) at low/medium severity&lt;/li&gt;
&lt;li&gt;Template files (&lt;code&gt;.env.example&lt;/code&gt;) for secrets-category findings&lt;/li&gt;
&lt;li&gt;Test files (&lt;code&gt;tests/&lt;/code&gt;, &lt;code&gt;conftest.py&lt;/code&gt;) entirely&lt;/li&gt;
&lt;li&gt;Self-contradictory descriptions ("X is correct, but...")&lt;/li&gt;
&lt;li&gt;Speculative hedging at low/medium ("could be a", "if this were", "potentially allowing")&lt;/li&gt;
&lt;li&gt;Dev-environment-only warnings ("during development", "in non-production")&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Critical severity &lt;strong&gt;never&lt;/strong&gt; gets filtered. We want false positives at critical to surface for human review, not be silently dropped.&lt;/p&gt;

&lt;p&gt;After this filter, my own repo scans as &lt;strong&gt;100/100 with zero findings&lt;/strong&gt;. Without it, the score bounced between 79 and 94 across seven runs with completely different findings each time. &lt;em&gt;Filtering is the product.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I found in the wild
&lt;/h2&gt;

&lt;p&gt;I scanned a 24K-star AI-coding repo (responsible disclosure in flight; this post will be updated with the repo name after Thursday 2026-04-17 EOD UTC). Top findings:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Finding&lt;/th&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Why static scanners miss it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Unauthenticated command execution&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;app/api/run-command-v2/route.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No auth gate before shelling out. No &lt;code&gt;eval()&lt;/code&gt; or &lt;code&gt;child_process.exec()&lt;/code&gt; with obviously-tainted input. Just a route handler that trusts any caller.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Arbitrary file write via AI-generated content&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;app/api/apply-ai-code-stream/route.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Writes to disk. Static tools see &lt;code&gt;fs.writeFile&lt;/code&gt; and don't flag it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Missing auth on package installation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;app/api/install-packages-v2/route.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The action is "install arbitrary npm package". No auth.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;API key leaked in error responses&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;app/api/search/route.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Regex scanners look for hardcoded keys in source. This one is &lt;em&gt;echoed&lt;/em&gt; back to the client on failure paths.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Combined, these form a complete "submit code → write it → execute it" chain. A separate scanner ran against this same repo three days ago and flagged hundreds of issues. None of these four. They require reading the route handlers end-to-end and asking what's actually authenticated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;I'm a non-technical founder. I can't write code. I built two production apps using AI coding tools and realized I had no way to know if they were safe. 65% of vibe-coded apps have security vulnerabilities. 35 CVEs were traced to AI-generated code in March 2026 alone.&lt;/p&gt;

&lt;p&gt;So I built the tool I needed. Vibe Check uses Claude to understand what code is &lt;em&gt;trying&lt;/em&gt; to do, and catches when it silently fails. Built by someone who can't code, for everyone who can't code.&lt;/p&gt;

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

&lt;p&gt;Vibe Check is free, no signup required for basic scans. Sign in with GitHub for scan history. Your code is never stored.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://chat-api-19ij.onrender.com" rel="noopener noreferrer"&gt;https://chat-api-19ij.onrender.com&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Code is open at &lt;a href="https://github.com/evance1227/chat" rel="noopener noreferrer"&gt;evance1227/chat&lt;/a&gt;. Feedback welcome, especially on the prompt and the false-positive filter.&lt;/p&gt;

&lt;p&gt;— Elise Vance (&lt;a href="https://twitter.com/shecantcode" rel="noopener noreferrer"&gt;@shecantcode&lt;/a&gt;)&lt;/p&gt;

</description>
      <category>ai</category>
      <category>codequality</category>
      <category>security</category>
      <category>vibecoding</category>
    </item>
  </channel>
</rss>
