<?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: CopperSunDev</title>
    <description>The latest articles on DEV Community by CopperSunDev (@coppersundev).</description>
    <link>https://dev.to/coppersundev</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%2F3659025%2F67b7af33-5040-4848-9b99-f2b9ccf2e6c3.png</url>
      <title>DEV Community: CopperSunDev</title>
      <link>https://dev.to/coppersundev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/coppersundev"/>
    <language>en</language>
    <item>
      <title>Command Injection in AI-Generated Express.js: A Real Scan</title>
      <dc:creator>CopperSunDev</dc:creator>
      <pubDate>Tue, 11 Aug 2026 22:53:17 +0000</pubDate>
      <link>https://dev.to/coppersundev/command-injection-in-ai-generated-expressjs-a-real-scan-4a54</link>
      <guid>https://dev.to/coppersundev/command-injection-in-ai-generated-expressjs-a-real-scan-4a54</guid>
      <description>&lt;p&gt;AI coding assistants generate Express.js routes that work. They also generate routes where user input reaches &lt;code&gt;execSync()&lt;/code&gt; without sanitization. Those two facts coexist in the same file.&lt;/p&gt;

&lt;p&gt;Here's the scan of a five-route Express.js user API built from four AI prompts. The prompts were specific and reasonable: user lookup, username search, avatar upload with ImageMagick, and data export. The scanner returned three findings in the first pass. Here's what they were, why they appeared, and what the scan missed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What BrassCoders Returns on an AI-Generated Express.js API
&lt;/h2&gt;

&lt;p&gt;BrassCoders scanned the five-route Express.js user API and returned three findings: two on the hardcoded JWT secret on line 10 (flagged by the JS/TS scanner as HIGH and by SecretsScanner as MEDIUM), and one CRITICAL command injection flagged by Semgrep taint analysis on line 73. The scan ran offline in under two seconds with no data leaving the machine.&lt;/p&gt;

&lt;p&gt;The raw YAML output for the three findings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;js_ts_c01129e7&lt;/span&gt;
  &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;high&lt;/span&gt;
  &lt;span class="na"&gt;file_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;src/users.js&lt;/span&gt;
  &lt;span class="na"&gt;line_number&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;JS/TS&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Security:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;hardcoded_password"&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Potential&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;hardcoded&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;credential&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;assigned&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;JWT_SECRET&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
  &lt;span class="na"&gt;code_snippet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;const&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;JWT_SECRET&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'';"&lt;/span&gt;
  &lt;span class="na"&gt;detected_by&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;JavaScriptTypeScriptScanner&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;secret_secret_keyword_f72527467796_10&lt;/span&gt;
  &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;medium&lt;/span&gt;
  &lt;span class="na"&gt;file_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;src/users.js&lt;/span&gt;
  &lt;span class="na"&gt;line_number&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Possible&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Secret&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Keyword"&lt;/span&gt;
  &lt;span class="na"&gt;detected_by&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SecretsScanner&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;semgrep-command_injection-67dfad9b34c1&lt;/span&gt;
  &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;critical&lt;/span&gt;
  &lt;span class="na"&gt;file_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;src/users.js&lt;/span&gt;
  &lt;span class="na"&gt;line_number&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;73&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Tainted&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;dataflow:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;command&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;injection"&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Tainted&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;HTTP&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;request&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;reaches&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;shell&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;execution&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;sink."&lt;/span&gt;
  &lt;span class="na"&gt;detected_by&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SemgrepTaintScanner&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The JS/TS scanner and SecretsScanner both flagged line 10 independently — two different detection mechanisms, same source line. The Semgrep taint scanner traced the request body field &lt;code&gt;filename&lt;/code&gt; from the HTTP handler into the &lt;code&gt;execSync()&lt;/code&gt; call on line 73. That's the CRITICAL finding.&lt;/p&gt;

&lt;h2&gt;
  
  
  The CRITICAL Finding: Command Injection via execSync
&lt;/h2&gt;

&lt;p&gt;BrassCoders's Semgrep taint scanner flagged CRITICAL command injection at line 73 of &lt;code&gt;src/users.js&lt;/code&gt;: user-controlled HTTP request data flowing into a shell execution sink. The code was generated from the prompt "Write an Express route to resize a user's uploaded avatar with ImageMagick":&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="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/:id/avatar&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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;gt&lt;/span&gt;&lt;span class="p"&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;filename&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&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;outputFile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`/var/uploads/avatars/&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="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_thumb.jpg`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Resize image using ImageMagick convert&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;execSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`convert /var/uploads/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; -resize 150x150 &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;outputFile&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&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="na"&gt;thumbnail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;outputFile&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;&lt;code&gt;filename&lt;/code&gt; comes from &lt;code&gt;req.body&lt;/code&gt; — a field the caller controls. The string template passes it directly to &lt;code&gt;execSync()&lt;/code&gt;, which runs the result through a shell. An attacker sending &lt;code&gt;filename: "a.jpg; rm -rf /var/uploads"&lt;/code&gt; in the request body gets the semicolon interpreted as a command separator. The &lt;code&gt;rm -rf&lt;/code&gt; runs as the server process user.&lt;/p&gt;

&lt;p&gt;The ImageMagick use case is exactly where this pattern appears in AI-generated code: the model knows convert takes a filename argument, uses template literals for string assembly (the simplest approach), and produces code that works for legitimate filenames. The shell metacharacter path never appears in a test that sends valid image filenames.&lt;/p&gt;

&lt;p&gt;Two fixes are available. The safer one avoids the shell entirely by passing an argument array to &lt;code&gt;spawn()&lt;/code&gt;:&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;spawn&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="s1"&gt;child_process&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/:id/avatar&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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;gt&lt;/span&gt;&lt;span class="p"&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;filename&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&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;inputPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`/var/uploads/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;outputFile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`/var/uploads/avatars/&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="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_thumb.jpg`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Argument array — no shell interpolation&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;proc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;convert&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="nx"&gt;inputPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-resize&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="s1"&gt;150x150&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;outputFile&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="nx"&gt;proc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;close&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="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&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="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Conversion failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&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="na"&gt;thumbnail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;outputFile&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;&lt;code&gt;spawn()&lt;/code&gt; with an argument array doesn't invoke a shell — each argument is passed directly to the process. Shell metacharacters in &lt;code&gt;filename&lt;/code&gt; are treated as literal characters, not syntax. The additional fix is input validation: reject filenames containing path separators or characters outside the expected character set before reaching the spawn call at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The HIGH Finding: Hardcoded JWT Signing Key
&lt;/h2&gt;

&lt;p&gt;BrassCoders's JavaScript/TypeScript scanner flagged HIGH on line 10 via Babel AST analysis — a credential string assigned to a variable named after its security function. SecretsScanner independently flagged the same line via detect-secrets entropy analysis. Two detectors, one source line.&lt;/p&gt;

&lt;p&gt;The code:&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;// JWT signing secret&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;JWT_SECRET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-super-secret-jwt-key-do-not-share&lt;/span&gt;&lt;span class="dl"&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 comment confirms the developer knew this was a credential. The AI generated a literal string because a literal string satisfies the prompt "write a signing key setup" in the simplest way. The string works during development. The problem is what happens when the file commits: the secret is now in version control, and git history doesn't forget it. Deleting the line on the next commit leaves the credential in every prior commit.&lt;/p&gt;

&lt;p&gt;BrassCoders's YAML output notes that the credential value is redacted — &lt;code&gt;''&lt;/code&gt; appears in &lt;code&gt;code_snippet&lt;/code&gt; rather than the actual string. The file path, line number, and variable name appear; the secret value doesn't leave the machine.&lt;/p&gt;

&lt;p&gt;The fix is straightforward:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;JWT_SECRET&lt;/span&gt; &lt;span class="o"&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;JWT_SECRET&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;JWT_SECRET&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;JWT_SECRET environment variable is required&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 explicit throw on startup catches misconfigured deployments before they can serve requests with a null or undefined signing key. Without it, &lt;code&gt;jwt.sign(payload, undefined)&lt;/code&gt; produces tokens that verify against any undefined key — a silent failure mode worse than the startup crash.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Scan Missed: SQL Injection via Template Literals
&lt;/h2&gt;

&lt;p&gt;BrassCoders didn't flag the two SQL queries in the API. Both use template literal interpolation — the same pattern that generates CRITICAL SQL injection findings in Python:&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;// Line 35 — unflagged&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`SELECT id, username, email FROM users WHERE id = &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&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="c1"&gt;// Line 57 — unflagged&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`SELECT id, username, email FROM users WHERE username LIKE '%&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;%'`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;BrassCoders's SQL taint rules are in its Python scanner set — Bandit B608 and Semgrep's &lt;code&gt;brass.python.taint.sql-injection&lt;/code&gt; rule trace string interpolation into SQL queries in Python. The JavaScript Semgrep ruleset currently covers command injection; SQL template literal injection isn't in it. The two queries went unflagged in this scan.&lt;/p&gt;

&lt;p&gt;This is a real coverage gap. The fix is the same regardless of whether the scanner catches it: use parameterized queries. &lt;code&gt;better-sqlite3&lt;/code&gt;'s prepared statement API takes placeholders:&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;// Parameterized — not injectable&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SELECT id, username, email FROM users WHERE id = ?&lt;/span&gt;&lt;span class="dl"&gt;'&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="nx"&gt;userId&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;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SELECT id, username, email FROM users WHERE username LIKE ?&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="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`%&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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 &lt;code&gt;?&lt;/code&gt; placeholder form passes &lt;code&gt;userId&lt;/code&gt; and &lt;code&gt;username&lt;/code&gt; as bound parameters rather than interpolating them into the query string. The driver handles escaping; no manual sanitization is needed. This pattern is the authoritative fix for SQL injection regardless of what the scanner reports.&lt;/p&gt;

&lt;p&gt;The scan caught what it could detect structurally. The SQL queries require a rule that doesn't yet exist in the JavaScript taint ruleset. Until it does, parameterized queries are the defensive default.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding BrassCoders to a Node.js Project
&lt;/h2&gt;

&lt;p&gt;BrassCoders scans JavaScript and TypeScript source files alongside Python — the same &lt;code&gt;pip install brasscoders &amp;amp;amp;&amp;amp;amp; brasscoders scan .&lt;/code&gt; command covers both languages. The JS/TS scanner uses Babel AST analysis for credential patterns and Semgrep taint rules for injection vulnerabilities. No separate install, no separate invocation.&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;brasscoders
brasscoders scan /path/to/express-project
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The scan emits YAML to &lt;code&gt;.brass/ai_instructions.yaml&lt;/code&gt;, structured for Claude Code or Cursor consumption. Each finding includes severity, detector, file path, line number, and (for JS/TS findings) a code snippet with the credential value redacted. Claude Code reads the file and source-verifies each finding against the source.&lt;/p&gt;

&lt;p&gt;For CI integration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/brasscoders.yml&lt;/span&gt;
&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;BrassCoders scan&lt;/span&gt;
    &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install brasscoders &amp;amp;amp;&amp;amp;amp; brasscoders scan .&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command exits non-zero if the scan produces CRITICAL findings above your configured threshold. The three findings from this scan — two on the JWT secret, one CRITICAL command injection — would fail the CI step before the code reaches review.&lt;/p&gt;

&lt;p&gt;The SQL injection in JavaScript template literals remains a manual review item until the JavaScript SQL taint rules ship. The command injection and credential findings are caught by the current scanner. Both categories of bugs appear consistently in AI-generated Express.js code because both patterns satisfy the prompt and pass local tests.&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;brasscoders
brasscoders scan &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="c"&gt;# CRITICAL: command injection at src/users.js:73&lt;/span&gt;
&lt;span class="c"&gt;# HIGH: hardcoded credential at src/users.js:10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>security</category>
      <category>ai</category>
      <category>engineering</category>
    </item>
    <item>
      <title>AI Coding Tool Security Risk for Engineering Leaders</title>
      <dc:creator>CopperSunDev</dc:creator>
      <pubDate>Tue, 11 Aug 2026 22:44:55 +0000</pubDate>
      <link>https://dev.to/coppersundev/ai-coding-tool-security-risk-for-engineering-leaders-25mh</link>
      <guid>https://dev.to/coppersundev/ai-coding-tool-security-risk-for-engineering-leaders-25mh</guid>
      <description>&lt;p&gt;The research on AI-generated code security reaches engineering teams as anecdote before it reaches them as data. Here's the data.&lt;/p&gt;

&lt;p&gt;Three independent studies from the last 24 months document a consistent pattern: AI coding assistants produce code that passes developer review at higher rates than it deserves, the security gap doesn't close as models get larger, and most engineering organizations have no formal process for managing the risk. All three problems have known solutions. The first step is understanding the actual scope.&lt;/p&gt;

&lt;h2&gt;
  
  
  What AI-Generated Code Security Research Shows
&lt;/h2&gt;

&lt;p&gt;BrassCoders's N=15 AI-generated code corpus detected at least one security finding in all 15 files when scanned with the full 12-scanner stack — a controlled research sample, not a deployment-scale dataset. The corpus is published at coppersun.dev/benchmarks: 15 Python files generated from security-relevant prompts, with reproducible scan instructions. The external research operates at substantially larger scale and reaches similar conclusions.&lt;/p&gt;

&lt;p&gt;Veracode's &lt;a href="https://www.veracode.com/resources/analyst-reports/2025-genai-code-security-report/" rel="noopener noreferrer"&gt;July 2025 GenAI Code Security Report&lt;/a&gt; tested over 100 large language models across 80 distinct security-relevant tasks. The headline number: 45% of AI-generated code samples fail security tests. Broken down by language, Java failed at 72%, C# at 45%, JavaScript at 43%, and Python at 38%. Cross-site scripting tests failed at 86% of relevant test cases across the tested models. The finding that matters most for engineering leaders: newer and larger models showed no consistent security improvement over earlier models. Scaling the model doesn't close the vulnerability gap.&lt;/p&gt;

&lt;p&gt;Perry et al. (&lt;a href="https://dl.acm.org/doi/10.1145/3576915.3623157" rel="noopener noreferrer"&gt;CCS 2023&lt;/a&gt;, 47 participants) ran a controlled experiment assigning developers either an AI coding assistant or no assistant. On an ECDSA signing task, 3% of the AI-assisted group produced secure code versus 21% of controls (p=0.039). The ECDSA task is a useful probe because correct implementation requires parameter choices subtle to non-cryptographers — the nonce k must be random and never reused, and an incorrect implementation looks structurally correct to reviewers who don't know that constraint. The study used codex-davinci-002, a 2022-era model; frontier models today produce more fluent code. Whether they've closed the cryptographic correctness gap is an open research question. Veracode's 2025 findings suggest the gap persists across model generations.&lt;/p&gt;

&lt;p&gt;Checkmarx's Future of AppSec 2025 report (1,519 respondents, August 2025) measured the organizational response: 98% of organizations reported a security breach attributable to vulnerable code, and only 18% had governance policies in place for AI coding tools. Adoption is ahead of governance by 82 percentage points.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why AI Assistants Produce Insecure Code
&lt;/h2&gt;

&lt;p&gt;BrassCoders's N=15 corpus shows three bug categories recurring across the AI-generated files: SQL injection via format-string query assembly, hardcoded credentials in configuration and initialization code, and unsafe cryptographic calls. All three share the same structural origin — the AI generates code that satisfies the stated prompt and passes local tests, with no model of the production attack surface the code will face.&lt;/p&gt;

&lt;p&gt;SQL injection through format-string assembly is the clearest case. A Flask database query generated from "write a user lookup by ID" produces:&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="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT id, name, email FROM users WHERE id = %s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This query returns correct rows during development. Tests pass. The vulnerability — SQL injection via user-controlled &lt;code&gt;user_id&lt;/code&gt; — only manifests when an attacker sends a crafted string to the endpoint. Standard tests don't exercise that path unless the suite was written specifically to test injection resistance. BrassCoders flags this as CRITICAL via Bandit B608 and Semgrep taint analysis. The fix is parameterization:&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="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT id, name, email FROM users WHERE id = ?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hardcoded credentials follow the same pattern. A configuration module generated from "write an HMAC signing key setup" produces &lt;code&gt;SECRET_KEY = "s3cr3t-signing-key-change-me"&lt;/code&gt;. A literal string satisfies the prompt; it works during development; it commits to version control when the developer runs &lt;code&gt;git add&lt;/code&gt;. BrassCoders's SecretsScanner flags it HIGH and redacts the credential value from YAML output — the code snippet and file path appear, but not the literal secret. The fix: &lt;code&gt;SECRET_KEY = os.environ.get("HMAC_SECRET_KEY", "")&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The cryptographic case requires more context-reading. &lt;code&gt;hashlib.md5()&lt;/code&gt; without &lt;code&gt;usedforsecurity=False&lt;/code&gt; triggers Bandit B324 because MD5 is broken for authentication and signing — collision attacks let an attacker substitute a different input that produces the same hash. For content deduplication (asking "are these bytes identical?"), collision attacks are irrelevant. The AI generates code correct for the stated use case. The scanner correctly flags the structural pattern. Whether the finding is a real vulnerability depends on what the digest is used for downstream. That's context the scanner can't read. The AI reviewer handles it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Overconfidence Problem
&lt;/h2&gt;

&lt;p&gt;BrassCoders catches SQL injection, hardcoded credentials, and command injection patterns at commit time — before the code enters a review cycle where Perry et al.'s overconfidence finding becomes expensive. The study's core result was explicit: AI-assisted developers rated their code as more secure than controls rated theirs, while actually writing less secure code. The model's apparent confidence transfers to the developers reviewing its output.&lt;/p&gt;

&lt;p&gt;This is a review-calibration problem, not a one-time training fix. Fluent, well-structured AI-generated code carries a lower friction signal than code that looks uncertain or inconsistent. The hesitation that catches a suspicious SQL interpolation — "wait, is that injectable?" — is quieter when the surrounding code looks professional. The mechanism isn't carelessness. It's what well-formatted, syntactically correct code communicates to reviewers trained to trust that signal.&lt;/p&gt;

&lt;p&gt;Two practical implications follow for engineering teams. First, peer review processes for AI-generated PRs should include an explicit scanning step, not only visual review. The scanner catches structural patterns that visual review misses under overconfidence conditions. Second, PR reviewers should know they're reviewing AI-generated code — the Perry overconfidence effect is partly a calibration issue, and reviewers who know the source may apply more scrutiny to the output.&lt;/p&gt;

&lt;p&gt;Veracode's 2025 State of Software Security report found average remediation time for security flaws has increased 47% over five years, to 252 days. A bug that ships in an AI-generated commit runs the same remediation sequence as any other production security bug: detection, log audit to bound the exposure window, credential rotation if the window is uncertain, compliance notification if regulated data was in scope. The commit velocity AI coding tools provide doesn't compress that timeline. What compresses the timeline is when the bug is caught. At commit, it's seconds. At production, it's months.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Governance Gap
&lt;/h2&gt;

&lt;p&gt;BrassCoders's scanner output goes to Claude Code or Cursor for AI-assisted triage — the AI reviewer reads the YAML, source-verifies each finding against the original file, and classifies each as real vulnerability or false positive with fix-ready advice. That division of labor (BrassCoders as deterministic pattern-matcher, AI reviewer as context-aware triage layer) is the technical architecture. The governance question is whether that architecture is required before AI-generated code can merge.&lt;/p&gt;

&lt;p&gt;The Checkmarx 2025 data makes the gap concrete: 82% of organizations with active AI coding tool usage have no defined process for what review AI-generated commits require. The tooling problem is solvable in an afternoon. The governance problem is a management decision about what process is required.&lt;/p&gt;

&lt;p&gt;Three decisions define a minimal AI coding governance policy:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What scans run before merge.&lt;/strong&gt; At minimum, a static-analysis scan covering the categories AI assistants structurally miss: SQL injection, hardcoded credentials, unsafe subprocess calls, and phantom imports from AI hallucinations. BrassCoders OSS core covers all of these with no account required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who reviews scanner findings.&lt;/strong&gt; Automated scanning produces findings; a human closes the loop. The AI reviewer (Claude Code, Cursor) does context-aware classification. The developer confirms. Without a defined owner, findings queue unreviewed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the verified false-positive process is.&lt;/strong&gt; Some findings are false positives in context. &lt;code&gt;.brassignore&lt;/code&gt; captures these decisions so they don't consume review cycles on future scans. A glob rule like &lt;code&gt;file_dedupe.py&lt;/code&gt; suppresses all findings from a specific file; a type rule like &lt;code&gt;:brass.python.taint.sql-injection&lt;/code&gt; suppresses a specific Semgrep check project-wide. Without a suppression process, teams tune out the scanner after the first wave of noise.&lt;/p&gt;

&lt;p&gt;These three decisions don't require new tooling. They require the management decision that AI-generated code has a defined review process. That decision is what the 18% who have governance policies have made.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building a Security Gate for AI-Generated Code
&lt;/h2&gt;

&lt;p&gt;BrassCoders installs in one command and runs offline with no account required — the OSS core catches the structural bugs AI coding assistants produce without any data leaving the developer's machine.&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;brasscoders
brasscoders scan /path/to/project
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The scan emits YAML to &lt;code&gt;.brass/findings.yaml&lt;/code&gt;, structured for Claude Code or Cursor consumption. Each finding includes severity, detector, file path, line number, and code snippet. The AI reviewer reads the file, verifies each finding against the source, and produces a triage output: real bug with fix advice, or false positive with reasoning. The scanner is the deterministic first layer. The AI reviewer is the context-aware second layer.&lt;/p&gt;

&lt;p&gt;CI integration runs the scan on every commit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/brasscoders.yml&lt;/span&gt;
&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;BrassCoders scan&lt;/span&gt;
    &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install brasscoders &amp;amp;&amp;amp; brasscoders scan .&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;BrassCoders Paid ($12/developer/month) adds a semantic deduplication pass and project-signature reranking that reduces a 1500+ finding raw scan to approximately 300 prioritized findings. The enrichment pass processes already-redacted findings and a project signature derived from README, manifest, and entrypoint metadata — no raw source code leaves the machine. For teams where 1500 raw findings make AI-assisted triage impractical at scale, the enrichment pass is what makes the process work.&lt;/p&gt;

&lt;p&gt;The engineering leadership version of this is binary: the gate runs in CI before merge or it doesn't. Perry, Veracode, and Checkmarx describe what happens when it doesn't. The gate costs one afternoon to wire and runs in seconds per commit.&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>engineering</category>
    </item>
    <item>
      <title>Tuning .brassignore: Suppressing a False Positive in Three Steps</title>
      <dc:creator>CopperSunDev</dc:creator>
      <pubDate>Sat, 08 Aug 2026 19:46:28 +0000</pubDate>
      <link>https://dev.to/coppersundev/tuning-brassignore-suppressing-a-false-positive-in-three-steps-4keb</link>
      <guid>https://dev.to/coppersundev/tuning-brassignore-suppressing-a-false-positive-in-three-steps-4keb</guid>
      <description>&lt;p&gt;BrassCoders scans a file-deduplication script and returns a CRITICAL finding: MD5 used for security. The pattern match is correct — &lt;code&gt;hashlib.md5()&lt;/code&gt; without &lt;code&gt;usedforsecurity=False&lt;/code&gt; triggers Bandit B324 regardless of context. The problem is that content fingerprinting for deduplication is not a security use of MD5. Here's how to tell BrassCoders that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Starting Point: What the Scan Shows
&lt;/h2&gt;

&lt;p&gt;BrassCoders's published N=15 AI-code-findings corpus includes &lt;code&gt;file_dedupe.py&lt;/code&gt;, generated from the prompt "Write a script that finds duplicate files in a directory tree by hashing their contents." The model produced a working deduplication script using &lt;code&gt;hashlib.md5()&lt;/code&gt;:&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;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;find_duplicates&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;seen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="n"&gt;duplicates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;dirpath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filenames&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;walk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;filenames&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;path&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;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dirpath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;digest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;digest&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;duplicates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;duplicates&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running &lt;code&gt;brasscoders --offline scan /path/to/project&lt;/code&gt; on a project containing this file produces the following at line 19:&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="n"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;critical&lt;/span&gt;
&lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;file_dedupe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt;
&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Use&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;weak&lt;/span&gt; &lt;span class="n"&gt;MD5&lt;/span&gt; &lt;span class="nb"&gt;hash&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;security&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="n"&gt;Consider&lt;/span&gt; &lt;span class="n"&gt;usedforsecurity&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;
&lt;span class="n"&gt;detected_by&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;bandit&lt;/span&gt;
&lt;span class="n"&gt;also_detected_by&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AstGrepScanner&lt;/span&gt;
&lt;span class="n"&gt;line_number&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;19&lt;/span&gt;
&lt;span class="n"&gt;code_snippet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;digest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;digest&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Total findings for the project: 57. Three of those findings are on &lt;code&gt;file_dedupe.py&lt;/code&gt; — the B324 CRITICAL, the AstGrepScanner confirmation, and one additional pattern overlap. All three are on the MD5 call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Finding Is a False Positive in Context
&lt;/h2&gt;

&lt;p&gt;BrassCoders's Bandit scanner fires B324 on any &lt;code&gt;hashlib.md5()&lt;/code&gt; call that doesn't explicitly declare &lt;code&gt;usedforsecurity=False&lt;/code&gt;. The rule doesn't inspect how the digest is used after the call — whether it's checked against a stored credential, used to sign a token, or compared to another file's fingerprint for deduplication.&lt;/p&gt;

&lt;p&gt;Two uses of MD5 are structurally identical in source:&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="c1"&gt;# Authentication use (real vulnerability — MD5 is broken for this)
&lt;/span&gt;&lt;span class="n"&gt;stored_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;stored_hash&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;db_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;password_hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# attacker can find collisions
&lt;/span&gt;
&lt;span class="c1"&gt;# Content fingerprinting (not a security use — collision attacks are irrelevant)
&lt;/span&gt;&lt;span class="n"&gt;digest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;digest&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# duplicate detection
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;MD5 collision attacks matter when an attacker can substitute a different input that produces the same hash — replacing a document with a forged one that has the same signature, or finding a password that matches a stored hash. For deduplication, you're asking "are these bytes identical?" A collision that produces the same hash for different content is irrelevant — the code would incorrectly report two different files as duplicates, not allow unauthorized access.&lt;/p&gt;

&lt;p&gt;The scanner correctly reports the pattern. The context — &lt;code&gt;if digest in seen&lt;/code&gt; for duplicate detection — makes it a false positive for security purposes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Ways to Clear the Finding
&lt;/h2&gt;

&lt;p&gt;BrassCoders surfaces the finding so you can decide: fix the code or suppress it. Both clear the finding from scan output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 1 — Code fix (preferred when you own the file):&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Python 3.9 added the &lt;code&gt;usedforsecurity&lt;/code&gt; parameter to explicitly declare that a hash function is not being used for security purposes:&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="n"&gt;digest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;usedforsecurity&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this flag, Bandit B324 no longer fires. The intent is documented in the source itself — any future reader, scanner, or AI reviewer sees the declaration. This is the right call when you control the file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 2 — .brassignore glob (when you can't change the file):&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;file_dedupe.py&lt;/code&gt; is a benchmark corpus file, vendored code, or a third-party script you're scanning but not modifying, add a glob rule to &lt;code&gt;.brassignore&lt;/code&gt; at the project root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# file_dedupe.py uses MD5 for content deduplication only — not a security use.
# usedforsecurity=False would be the code fix; suppressed here because this
# is a benchmark corpus reference file, not production code we own.
file_dedupe.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bare filenames in &lt;code&gt;.brassignore&lt;/code&gt; match that file at any depth in the project tree. No anchoring needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Re-Scan: From 57 to 54
&lt;/h2&gt;

&lt;p&gt;After adding &lt;code&gt;file_dedupe.py&lt;/code&gt; to &lt;code&gt;.brassignore&lt;/code&gt;, the re-scan shows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;🙈 .brassignore: dropped 3 findings (57 → 54)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All three findings tied to &lt;code&gt;file_dedupe.py&lt;/code&gt; are gone. The remaining 54 findings are from other files in the project — real bugs including SQL injection, command injection via &lt;code&gt;shell=True&lt;/code&gt;, and unsafe subprocess calls that warrant review. The MD5 pattern no longer appears in the triage queue.&lt;/p&gt;

&lt;p&gt;BrassCoders reads &lt;code&gt;.brassignore&lt;/code&gt; before the noise-reduction pass. Suppressed findings never enter the ranking pipeline. For the Paid plan, they also never reach the enrichment step — suppressed findings don't consume enrichment tokens.&lt;/p&gt;

&lt;p&gt;The CLI message is the confirmation to check for. If the message doesn't appear, either the &lt;code&gt;.brassignore&lt;/code&gt; file wasn't found at the project root or the glob rule didn't match the finding's file path. The &lt;code&gt;--offline&lt;/code&gt; flag goes before &lt;code&gt;scan&lt;/code&gt; in the command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brasscoders &lt;span class="nt"&gt;--offline&lt;/span&gt; scan /path/to/project
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  When to Suppress vs When to Fix
&lt;/h2&gt;

&lt;p&gt;BrassCoders's &lt;code&gt;.brassignore&lt;/code&gt; is the right tool for findings in code you don't own or can't change. The decision is straightforward:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix the code when you own it.&lt;/strong&gt; The &lt;code&gt;usedforsecurity=False&lt;/code&gt; parameter is the explicit declaration that removes the finding and documents intent. A future reader — or another scanner — sees the decision in the source without needing to cross-reference a suppression file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use .brassignore when you can't change the file.&lt;/strong&gt; Vendored libraries, auto-generated code, third-party integrations, and benchmark corpus reference files are all cases where you've verified the finding is a false positive but can't or shouldn't modify the source. The glob rule suppresses the finding without touching the file.&lt;/p&gt;

&lt;p&gt;For patterns that appear across many files — a privacy rule, a Semgrep check ID — &lt;code&gt;.brassignore&lt;/code&gt; supports type rules via the &lt;code&gt;:rule_id&lt;/code&gt; syntax. Semgrep findings store their check ID as &lt;code&gt;rule_id&lt;/code&gt; in metadata; the type rule &lt;code&gt;:brass.python.taint.sql-injection&lt;/code&gt; suppresses BrassCoders's SQL injection taint rule project-wide. Bandit findings store the test ID as &lt;code&gt;bandit_test_id&lt;/code&gt; rather than &lt;code&gt;rule_id&lt;/code&gt;, so per-file glob rules are the supported suppression path for Bandit.&lt;/p&gt;

&lt;p&gt;The fuller catalog of &lt;code&gt;.brassignore&lt;/code&gt; patterns for Django projects, FastAPI projects, and test fixture directories is at &lt;a href="https://coppersun.dev/blog/tuning-brasscoders-brassignore/" rel="noopener noreferrer"&gt;/blog/tuning-brasscoders-brassignore/&lt;/a&gt;. The distinction between &lt;code&gt;.brassignore&lt;/code&gt; and &lt;code&gt;.gitignore&lt;/code&gt; — two independent files with different jobs — is at &lt;a href="https://coppersun.dev/blog/what-is-the-difference-between-brassignore-and-gitignore/" rel="noopener noreferrer"&gt;/blog/what-is-the-difference-between-brassignore-and-gitignore/&lt;/a&gt;.&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;brasscoders
brasscoders &lt;span class="nt"&gt;--offline&lt;/span&gt; scan /path/to/project
&lt;span class="c"&gt;# Review findings. For confirmed false positives: fix the code or add to .brassignore.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>opensource</category>
      <category>ai</category>
      <category>engineering</category>
    </item>
    <item>
      <title>From HIGH to Clean: Removing a Hardcoded HMAC Key in AI-Generated Python</title>
      <dc:creator>CopperSunDev</dc:creator>
      <pubDate>Sat, 08 Aug 2026 03:12:41 +0000</pubDate>
      <link>https://dev.to/coppersundev/from-high-to-clean-removing-a-hardcoded-hmac-key-in-ai-generated-python-56j1</link>
      <guid>https://dev.to/coppersundev/from-high-to-clean-removing-a-hardcoded-hmac-key-in-ai-generated-python-56j1</guid>
      <description>&lt;p&gt;The model wrote what you asked for. The prompt said "include a usable example so I can run it" — and the model gave you one, complete with &lt;code&gt;SECRET_KEY = "s3cr3t-signing-key-change-me"&lt;/code&gt; hardcoded in source. BrassCoders flags that assignment as HIGH severity on the first scan: confidence 0.85, impact_score 0.9, secret_type "Secret Keyword."&lt;/p&gt;

&lt;p&gt;Two lines of code fix it. Here's the full walkthrough, with real scan output.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Starting Point: The Prompt Asked for a Runnable Example
&lt;/h2&gt;

&lt;p&gt;BrassCoders's published corpus includes &lt;code&gt;token_check.py&lt;/code&gt;, generated from the prompt "Write a function that signs and verifies a session token using HMAC. Include a usable example so I can run it." The phrase "Include a usable example" is the instruction that produces the credential — the model needs a key to sign with, so it fills one in.&lt;/p&gt;

&lt;p&gt;This pattern recurs across AI-generated auth and signing code. The demo needs a working value. The model produces a placeholder-looking string, the code runs, the developer commits it. &lt;code&gt;"s3cr3t-signing-key-change-me"&lt;/code&gt; reads like a reminder to swap it out — but it's still a hardcoded literal in version-controlled source until someone does.&lt;/p&gt;

&lt;p&gt;A placeholder key with "change-me" in the name signals intent without enforcing it. No runtime error fires. Deploys succeed. HMAC signatures verify correctly in tests. The production deploy looks identical to the local demo. There's nothing to alert on until someone checks whether the key is actually loaded from a safe source.&lt;/p&gt;

&lt;p&gt;The file that came out of the model:&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;import&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;

&lt;span class="n"&gt;SECRET_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s3cr3t-signing-key-change-me&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;mac&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SECRET_KEY&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;mac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compare_digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;sig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user=42&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user=42&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The HMAC logic is correct. &lt;code&gt;hmac.compare_digest&lt;/code&gt; is the right comparison function — timing-safe, no short-circuit on mismatch. The bug isn't the signing implementation. It's the key source.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running the Scan: What BrassCoders Surfaces
&lt;/h2&gt;

&lt;p&gt;BrassCoders's SecretsScanner — built on detect-secrets, the upstream credential-pattern library from Yelp — catches the &lt;code&gt;SECRET_KEY&lt;/code&gt; assignment at line 11 with HIGH severity. The finding title is "Possible hardcoded credential" and the exact value is redacted in the &lt;code&gt;.brass/ai_instructions.yaml&lt;/code&gt; output, so the findings file doesn't re-expose the secret.&lt;/p&gt;

&lt;p&gt;The scan command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brasscoders scan /path/to/corpus/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The finding in the YAML output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Possible&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;hardcoded&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;credential&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;(value&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;redacted)"&lt;/span&gt;
  &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;token_check.py&lt;/span&gt;
  &lt;span class="na"&gt;line&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;11&lt;/span&gt;
  &lt;span class="na"&gt;detector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SecretsScanner (detect-secrets)&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="s"&gt;Variable assignment: SECRET_KEY. A credential-shaped literal was&lt;/span&gt;
    &lt;span class="s"&gt;detected at this location. The exact value has been redacted to&lt;/span&gt;
    &lt;span class="s"&gt;prevent the brass output from re-exposing it.&lt;/span&gt;
  &lt;span class="na"&gt;confidence&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.85&lt;/span&gt;
  &lt;span class="na"&gt;impact_score&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.9&lt;/span&gt;
  &lt;span class="na"&gt;secret_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Secret&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Keyword"&lt;/span&gt;
  &lt;span class="na"&gt;also_detected_by&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bandit (B105)&lt;/span&gt;
  &lt;span class="na"&gt;remediation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="s"&gt;Confirm whether this value is a real credential. If yes: rotate it&lt;/span&gt;
    &lt;span class="s"&gt;immediately, remove it from history, and load it from a secrets&lt;/span&gt;
    &lt;span class="s"&gt;manager or environment variable.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice what's absent: no &lt;code&gt;code_snippet&lt;/code&gt; field. BrassCoders redacts credential-shaped values from the findings file by design. The output points to the file and line number — enough to locate and fix the credential — without reproducing the string in the YAML. If the findings file contained the literal, the findings file would be another place the secret lives.&lt;/p&gt;

&lt;p&gt;The severity classification comes from two signals. The &lt;a href="https://github.com/Yelp/detect-secrets" rel="noopener noreferrer"&gt;detect-secrets&lt;/a&gt; "Secret Keyword" pattern scores based on variable name: &lt;code&gt;SECRET_KEY&lt;/code&gt; is a high-confidence credential signal. The impact_score of 0.9 reflects what this credential class controls — an HMAC signing key governs session token validity, so a compromise breaks the authentication boundary entirely, not just leaks a piece of data.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;also_detected_by: bandit (B105)&lt;/code&gt; field shows &lt;a href="https://bandit.readthedocs.io/en/latest/" rel="noopener noreferrer"&gt;Bandit&lt;/a&gt; fired on this file too. B105 is Bandit's generic hardcoded-password-string rule, which triggers on obviously-named credential assignments. In this case, both tools fire. The SecretsScanner is the primary signal because detect-secrets also applies entropy analysis, covering cases where the variable name is less obvious but the value's entropy is high enough to classify as a credential. B105 alone would miss those.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: Load From Environment
&lt;/h2&gt;

&lt;p&gt;BrassCoders's remediation note for the credential finding is direct: rotate if real, then load from a secrets manager or environment variable. For &lt;code&gt;token_check.py&lt;/code&gt;, &lt;code&gt;os.environ.get("HMAC_SECRET_KEY", "")&lt;/code&gt; replaces the literal with a runtime-loaded value. The key never appears in source, in git history, or in build artifacts once the commit is cleaned.&lt;/p&gt;

&lt;p&gt;The fix is two lines added to the import block:&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;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="n"&gt;SECRET_KEY&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="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HMAC_SECRET_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The empty-string default lets the module import cleanly. Add a startup guard if you want the process to fail fast on a missing key rather than at signing time:&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;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;SECRET_KEY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HMAC_SECRET_KEY not set&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;One step matters before removing the literal from source: check whether the key was ever deployed. If &lt;code&gt;"s3cr3t-signing-key-change-me"&lt;/code&gt; reached production, any party with the source can forge tokens signed with it. Rotate first. Then clean the history. For git history removal, &lt;code&gt;git filter-repo&lt;/code&gt; is the current recommended tool — it rewrites history, which requires a force push and coordination with anyone who has cloned the repo. The safer path, when the key was never deployed, is to make the environment-variable change in a new commit and leave the history in place.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;.brassignore&lt;/code&gt; suppresses the BrassCoders finding. It does not remove the string from past commits. That's not the right tool here.&lt;/p&gt;

&lt;p&gt;The full fixed file:&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;import&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="n"&gt;SECRET_KEY&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="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HMAC_SECRET_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;mac&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SECRET_KEY&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;mac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compare_digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;sig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user=42&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user=42&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Re-Scan: Zero Credential Findings
&lt;/h2&gt;

&lt;p&gt;BrassCoders's re-scan of the fixed file shows SecretsScanner and Bandit B105 both silent — the credential-shaped literal is no longer present in source, so neither tool fires. The only remaining findings are Pylint code quality items for missing docstrings: zero security findings, zero HIGH or CRITICAL severity.&lt;/p&gt;

&lt;p&gt;The scanner summary after the fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Secrets detection:  0 findings  ✓
Bandit (security):  0 credential findings
Pylint (quality):   2 findings  (missing docstrings — code quality, not security)

Critical issues:    0
High issues:        0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Pylint warnings are legitimate. The functions have no docstrings, and Pylint will note that. Worth addressing in production code — but these are in a different category from the credential finding, and the security surface is clean.&lt;/p&gt;

&lt;p&gt;Two credential findings before the fix. Zero after. The Pylint items were always present; SecretsScanner took priority in the first output.&lt;/p&gt;

&lt;h2&gt;
  
  
  What BrassCoders Doesn't Verify
&lt;/h2&gt;

&lt;p&gt;BrassCoders catches the presence of a credential-shaped literal — it cannot determine whether the value in source was ever used to sign real session tokens in production. That determination belongs to the incident response workflow: check deployment history, determine the exposure window, decide whether to rotate.&lt;/p&gt;

&lt;p&gt;Detection time is when you know to start checking. If &lt;code&gt;"s3cr3t-signing-key-change-me"&lt;/code&gt; was copied directly into a production deploy, any party who reads the source can verify or forge tokens signed with it. The scan tells you to look; it doesn't tell you what was looked at before you scanned.&lt;/p&gt;

&lt;p&gt;BrassCoders catches 20+ secret formats through detect-secrets plus 7 custom patterns — AWS access keys, GitHub PATs, Stripe live keys, OpenAI API keys, Slack tokens, PEM-formatted private keys, JWTs, and high-entropy strings, among others. The HMAC key here triggers the "Secret Keyword" pattern: variable name contains a credential-signal word, assigned a string literal. The same pattern fires on &lt;code&gt;API_KEY&lt;/code&gt;, &lt;code&gt;TOKEN&lt;/code&gt;, &lt;code&gt;PASSWORD&lt;/code&gt;, and similar assignments across a codebase.&lt;/p&gt;

&lt;p&gt;Run &lt;code&gt;brasscoders scan&lt;/code&gt; before you commit. The scan takes seconds. The credential investigation, after the fact, takes longer.&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>From SQL Injection to Clean: Fixing a B608 Finding in AI-Generated Flask Code</title>
      <dc:creator>CopperSunDev</dc:creator>
      <pubDate>Sat, 08 Aug 2026 01:45:51 +0000</pubDate>
      <link>https://dev.to/coppersundev/from-sql-injection-to-clean-fixing-a-b608-finding-in-ai-generated-flask-code-3m7e</link>
      <guid>https://dev.to/coppersundev/from-sql-injection-to-clean-fixing-a-b608-finding-in-ai-generated-flask-code-3m7e</guid>
      <description>&lt;p&gt;The endpoint is 15 lines of Python. It queries a user by ID and returns the record as JSON. claude-opus-4-8, given the prompt "Write a small Flask endpoint that returns a user record by id from a sqlite database as JSON," produced it in one shot. The code compiles. The route works. The SQL injection runs without a second prompt.&lt;/p&gt;

&lt;p&gt;BrassCoders finds it on the first scan — two CRITICAL findings and one HIGH, all pointing at the same line.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Starting Point: What the Model Generated
&lt;/h2&gt;

&lt;p&gt;BrassCoders's published N=15 AI-code-findings corpus includes user_lookup.py, generated from the prompt "Write a small Flask endpoint that returns a user record by id from a sqlite database as JSON." The model produced a working Flask endpoint that connects to a SQLite database and returns user records as JSON. It also produced SQL injection at line 22, where the query string is built by Python string formatting.&lt;/p&gt;

&lt;p&gt;Here's the generated file:&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;import&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flask&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jsonify&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;Flask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_db&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;app.db&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/user/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_db&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;cur&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT id, name, email FROM users WHERE id = %s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;not found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]})&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&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;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The injection is on the &lt;code&gt;cur.execute&lt;/code&gt; line. Python's &lt;code&gt;%s&lt;/code&gt; substitution runs before the query reaches SQLite — &lt;code&gt;user_id&lt;/code&gt; arrives from the URL as a raw string, and Flask passes it through without modification. An attacker sends &lt;code&gt;/user/1 OR 1=1&lt;/code&gt; and the formatted query becomes &lt;code&gt;SELECT id, name, email FROM users WHERE id = 1 OR 1=1&lt;/code&gt;. Every row in the users table comes back. A more destructive payload — &lt;code&gt;/user/1; DROP TABLE users; --&lt;/code&gt; — produces a multi-statement string that some driver configurations will execute.&lt;/p&gt;

&lt;p&gt;The corpus file is one of 15 files in BrassCoders's published AI-code-findings set, each generated by a frontier model responding to a plausible engineering prompt, each scanned and committed with full provenance. user_lookup.py is one of the cleaner cases. The prompt was specific, the implementation was idiomatic Flask, and the vulnerability is textbook.&lt;/p&gt;

&lt;p&gt;The model got the route shape right. The JSON response right. One thing wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running the Scan: What BrassCoders Reports
&lt;/h2&gt;

&lt;p&gt;BrassCoders catches the injection on the first scan — two CRITICAL findings at line 22 from input_validation_analyzer and SemgrepTaintScanner, both confirmed by Bandit as well. The &lt;code&gt;code_snippet&lt;/code&gt; field in the YAML output shows the exact injected line, not just the file or function name.&lt;/p&gt;

&lt;p&gt;Run the scan against the corpus with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brasscoders &lt;span class="nt"&gt;--offline&lt;/span&gt; scan /corpus/files &lt;span class="nt"&gt;--no-enrich&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three findings surface:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;input_validation_analyzer — CRITICAL — line 22&lt;/strong&gt;&lt;br&gt;
Title: Input Validation Vulnerability: Sql Injection Risk&lt;br&gt;
Description: User input appears to be directly used in SQL queries without proper sanitization&lt;br&gt;
Code snippet: &lt;code&gt;cur.execute("SELECT id, name, email FROM users WHERE id = %s" % user_id)&lt;/code&gt;&lt;br&gt;
Also detected by: bandit&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SemgrepTaintScanner — CRITICAL — line 22&lt;/strong&gt;&lt;br&gt;
Title: Tainted dataflow: sql injection&lt;br&gt;
Description: Tainted HTTP request data reaches a SQL execution sink without parameterization&lt;br&gt;
Code snippet: &lt;code&gt;cur.execute("SELECT id, name, email FROM users WHERE id = %s" % user_id)&lt;/code&gt;&lt;br&gt;
Also detected by: bandit&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;bandit — HIGH — line 31&lt;/strong&gt;&lt;br&gt;
Title: Flask app run with debug=True&lt;/p&gt;

&lt;p&gt;Two independent detectors landing on the same line, both confirmed by a third, is the pattern worth acting on immediately. Bandit's B608 rule — documented at &lt;a href="https://bandit.readthedocs.io/en/latest/plugins/b608_hardcoded_sql_expressions.html" rel="noopener noreferrer"&gt;bandit.readthedocs.io&lt;/a&gt; — fires specifically on formatted strings (&lt;code&gt;%s&lt;/code&gt;, f-strings, &lt;code&gt;.format()&lt;/code&gt;) appearing in SQL query contexts. The &lt;code&gt;also_detected_by&lt;/code&gt; field in the BrassCoders YAML tells Claude Code that multiple scanners agreed on the same line; that cross-scanner confirmation is the signal to weight heavily in triage.&lt;/p&gt;

&lt;p&gt;The debug=True finding is separate. Flask's debug mode activates the Werkzeug interactive debugger; any unhandled exception exposes an in-browser Python shell. That's a code execution surface if the app reaches production.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Fix: Parameterized Query
&lt;/h2&gt;

&lt;p&gt;BrassCoders's remediation note for both CRITICAL findings says the same thing: use prepared statements. SQLite's positional placeholder is &lt;code&gt;?&lt;/code&gt;, passed as a tuple in the second argument to &lt;code&gt;cursor.execute()&lt;/code&gt; — the sqlite3 module sends the query string and the bound value to the database library separately, so no user-controlled data ever interpolates into the query text.&lt;/p&gt;

&lt;p&gt;The fixed 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="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT id, name, email FROM users WHERE id = ?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The query string stays fixed at &lt;code&gt;SELECT id, name, email FROM users WHERE id = ?&lt;/code&gt;. The &lt;code&gt;user_id&lt;/code&gt; value binds inside the sqlite3 module, not in Python string formatting. Python's sqlite3 documentation at &lt;a href="https://docs.python.org/3/library/sqlite3.html" rel="noopener noreferrer"&gt;docs.python.org/3/library/sqlite3.html&lt;/a&gt; calls this "DB-API 2.0 parameter substitution" — the value is transmitted to SQLite's C library as a typed parameter, never concatenated into the SQL text. No amount of SQL syntax in the &lt;code&gt;user_id&lt;/code&gt; value changes the query structure.&lt;/p&gt;

&lt;p&gt;Note the driver difference: psycopg2 (PostgreSQL) uses &lt;code&gt;%s&lt;/code&gt; as its placeholder, with a values tuple rather than &lt;code&gt;%&lt;/code&gt;-string formatting. &lt;code&gt;cursor.execute("SELECT ... WHERE id = %s", (user_id,))&lt;/code&gt; is safe in psycopg2; &lt;code&gt;cursor.execute("SELECT ... WHERE id = %s" % user_id)&lt;/code&gt; is not. The placeholder syntax varies by driver; the principle of binding values separately from the query string applies everywhere. This distinction is how the bug survives code review — the two forms look similar at a glance.&lt;/p&gt;

&lt;p&gt;Fix the debug flag at the same time:&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="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full fixed file:&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;import&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flask&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jsonify&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;Flask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_db&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;app.db&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/user/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_db&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;cur&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT id, name, email FROM users WHERE id = ?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,))&lt;/span&gt;
    &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;not found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]})&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&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;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two characters change in the query string. One argument added. The route logic and the response format are identical.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Re-Scan: What Changes and What Remains
&lt;/h2&gt;

&lt;p&gt;BrassCoders's re-scan of the fixed file shows the input_validation_analyzer and Bandit findings gone — the &lt;code&gt;%s&lt;/code&gt; format pattern no longer appears in a SQL context, so neither rule fires. The debug=True finding is gone. One finding remains: Semgrep's taint rule fires at line 23 with the title "Tainted dataflow: sql injection."&lt;/p&gt;

&lt;p&gt;This is a known false positive on correctly parameterized SQLite calls. Semgrep's python taint rule tracks data flows from user-controlled sources — in this case, the &lt;code&gt;user_id&lt;/code&gt; Flask route parameter — to SQL execution functions like &lt;code&gt;cursor.execute()&lt;/code&gt;. The rule fires whenever it sees user-controlled data reaching the call, parameterized or not. It doesn't distinguish &lt;code&gt;cur.execute(query % user_id)&lt;/code&gt; from &lt;code&gt;cur.execute(query, (user_id,))&lt;/code&gt;. To the taint rule, both forms deliver user-controlled data to a SQL execution sink.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;(user_id,)&lt;/code&gt; tuple form is safe. The fix is correct.&lt;/p&gt;

&lt;p&gt;This is where the BrassCoders + Claude workflow handles the residual. The scanner narrows scope from 3 findings to 1 — both CRITICALs gone, the HIGH gone. Claude Code reads the surviving Semgrep finding, sees the parameterized tuple syntax in the &lt;code&gt;code_snippet&lt;/code&gt; field, and marks it a false positive. Three findings reduced to one triage decision, and the triage takes seconds.&lt;/p&gt;

&lt;p&gt;The reduction matters in practice. Before the fix, Claude reading the YAML sees two CRITICAL entries with identical code snippets, plus a HIGH for debug=True. After the fix, one taint finding on a parameterized call. The cognitive surface drops; the reasoning path is shorter.&lt;/p&gt;

&lt;h2&gt;
  
  
  What BrassCoders Doesn't Check
&lt;/h2&gt;

&lt;p&gt;BrassCoders's SQL injection findings confirm the structural pattern: a string-formatted query reaching an SQL execution sink. They don't verify whether the authenticated user is authorized to look up the requested &lt;code&gt;user_id&lt;/code&gt; — that authorization question requires business-logic context no scanner can infer from source alone.&lt;/p&gt;

&lt;p&gt;The route in user_lookup.py has no authentication decorator. No &lt;code&gt;@login_required&lt;/code&gt;, no session check, no ownership validation. Any caller who hits &lt;code&gt;/user/&lt;/code&gt; gets the record if it exists — including records that don't belong to them. Whether that's a bug or a deliberate public endpoint depends on the application design, and BrassCoders has no way to know from source structure alone.&lt;/p&gt;

&lt;p&gt;Claude Code, reading the BrassCoders YAML output in context, can surface this. The scanner reports the structural pattern; the AI triage layer reads the findings alongside the actual code and flags concerns the scanner didn't touch. That division of labor is built into how BrassCoders formats its output — the YAML is structured for AI consumption, so Claude can reason about what the scanner found and note what it didn't reach.&lt;/p&gt;




&lt;p&gt;The corpus is Apache 2.0 licensed and the scan is reproducible. Install &lt;code&gt;brasscoders&lt;/code&gt; via &lt;code&gt;pip install brasscoders&lt;/code&gt;, clone the OSS repo at &lt;code&gt;github.com/CopperSunDev/brasscoders&lt;/code&gt;, and run &lt;code&gt;brasscoders scan docs/benchmarks/ai-code-findings-corpus/files&lt;/code&gt;. The SQL injection finding appears at user_lookup.py:22. The re-scan behavior after the &lt;code&gt;?&lt;/code&gt; fix matches exactly what's described above.&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>What Static Analysis Cannot See: A Complete Map of the Limit</title>
      <dc:creator>CopperSunDev</dc:creator>
      <pubDate>Sat, 08 Aug 2026 00:23:37 +0000</pubDate>
      <link>https://dev.to/coppersundev/what-static-analysis-cannot-see-a-complete-map-of-the-limit-30lo</link>
      <guid>https://dev.to/coppersundev/what-static-analysis-cannot-see-a-complete-map-of-the-limit-30lo</guid>
      <description>&lt;p&gt;SQL injection through a formatted string: BrassCoders catches it on every scan, byte-identical, at the exact line. Missing authentication on an &lt;code&gt;/admin&lt;/code&gt; route: no static analyzer catches it without knowing which routes require authentication — a fact that lives in the spec, not the source file. The gap between those two cases is not a tool limitation. It's a category limit.&lt;/p&gt;

&lt;p&gt;Static analysis matches patterns against source code structure. It can report only what the structure encodes. Every bug that depends on runtime state, caller intent, architectural policy, business rules, or deployment configuration sits outside that boundary — not because the tool is weak, but because those facts are not encoded in the source.&lt;/p&gt;

&lt;p&gt;That distinction matters for teams shipping AI-generated code. AI assistants produce code that looks structurally sound while burying context-dependent bugs in the logic. A scanner catches the structural part. An AI assistant doing triage catches the rest. Neither layer replaces the other, and confusing their roles wastes both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Navigation
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;What "Can't See" Means for a Pattern Scanner&lt;/li&gt;
&lt;li&gt;What BrassCoders Catches Deterministically&lt;/li&gt;
&lt;li&gt;What Requires Context the Scanner Can't Have&lt;/li&gt;
&lt;li&gt;The Bug-Category Visibility Table&lt;/li&gt;
&lt;li&gt;What False Positives Reveal About the Boundary&lt;/li&gt;
&lt;li&gt;How the Two Layers Compose&lt;/li&gt;
&lt;li&gt;What BrassCoders's Findings File Gives the AI Assistant&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What "Can't See" Means for a Pattern Scanner
&lt;/h2&gt;

&lt;p&gt;BrassCoders reports what has a structural marker in the source — patterns its 12 scanners can match deterministically on every run. What it cannot see is everything that depends on context the source code doesn't encode: intent, runtime state, caller provenance, business rules, production data distribution.&lt;/p&gt;

&lt;p&gt;A structural marker is something an AST traversal finds without executing the code. The string &lt;code&gt;f"SELECT * FROM users WHERE id={user_id}"&lt;/code&gt; is a structural marker for SQL injection: its AST contains a formatted string with a SQL keyword prefix and an interpolated variable, a pattern the rule matches deterministically on every run, regardless of where &lt;code&gt;user_id&lt;/code&gt; came from or what database is running.&lt;/p&gt;

&lt;p&gt;When there's no structural marker, the scanner has nothing to match against. A route handler that calls an admin-only query carries no "requires authentication" annotation in the AST. The requirement exists in a product document or in the developer's intent at the time of writing. Pattern rules evaluate syntax, not intent.&lt;/p&gt;

&lt;p&gt;The implication is that the boundary isn't fuzzy. It's precise: a pattern with a fixed structural shape can be matched; a fact that requires executing the code, reading documentation, or knowing the deployment environment cannot. That boundary is the fundamental property that makes the scanner deterministic, not a shortcoming to engineer away. Give up the pattern-only constraint and you give up the guarantee of identical results on identical code.&lt;/p&gt;

&lt;h2&gt;
  
  
  What BrassCoders Catches Deterministically
&lt;/h2&gt;

&lt;p&gt;BrassCoders's 12 scanners — Bandit, Pylint, Pyre/Pysa, Semgrep, ast-grep, detect-secrets, and six custom detectors — catch what has a fixed structural shape: SQL injection via string formatting (Bandit B608), shell injection (B602/B603), hardcoded credentials (detect-secrets), unsafe deserialization (B506), and phantom imports that don't exist on PyPI.&lt;/p&gt;

&lt;p&gt;Each of these fires on every scan, byte-identical for the same commit. A formatted SQL string on line 42 today is the same finding on line 42 after twenty commits to unrelated files. That repeatability is what a CI gate requires: a check that fails consistently on the same code and passes consistently on clean code. A 2025 study by Gnieciak and Szandala, &lt;a href="https://arxiv.org/abs/2508.04448" rel="noopener noreferrer"&gt;Large Language Models Versus Static Code Analysis Tools&lt;/a&gt;, measured this gap directly: deterministic analyzers pointed at the exact line while LLM reviewers mislocated findings due to tokenization artifacts. The LLMs led on recall; precision and exact localization lagged.&lt;/p&gt;

&lt;p&gt;BrassCoders's JavaScript and TypeScript scanner (one of the six custom detectors) extends the same approach to six JS and TS file types via Babel AST parsing. The patterns it targets — &lt;code&gt;eval()&lt;/code&gt; calls, dangerous &lt;code&gt;innerHTML&lt;/code&gt; assignments, &lt;code&gt;document.write&lt;/code&gt; usage, hardcoded API keys, and hardcoded passwords — are structural markers, the same category as the Python findings. Semgrep also runs against those file types; Bandit is Python-only and does not.&lt;/p&gt;

&lt;p&gt;The structural coverage across categories is documented in BrassCoders's published benchmark: across 12 AI-generated bug categories, BrassCoders's scanners catch 11; Bandit running alone catches 6. For the corpus and scoring, see &lt;a href="https://coppersun.dev/blog/ai-coder-bug-benchmark/" rel="noopener noreferrer"&gt;the full benchmark at /blog/ai-coder-bug-benchmark/&lt;/a&gt;. What makes these findings useful in CI is stability: the same code produces the same result on every run. That property is what &lt;a href="https://coppersun.dev/blog/deterministic-gate-vs-llm-reviewer/" rel="noopener noreferrer"&gt;Why AI Code Needs a Deterministic Gate, Not Just an LLM&lt;/a&gt; argues for with the Gnieciak benchmark as evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Requires Context the Scanner Can't Have
&lt;/h2&gt;

&lt;p&gt;BrassCoders cannot determine whether a division operation will receive a zero denominator, whether a route requires authentication, or whether a query runs in O(N²) time against production-scale data — because these answers depend on runtime facts, architectural decisions, and domain knowledge that aren't encoded in the source file.&lt;/p&gt;

&lt;p&gt;Four categories account for most of the scanner's blind spots.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Runtime behavior.&lt;/strong&gt; A division that fails on a zero denominator only fails when that input arrives. The source shows the operation; it doesn't show the call path or what values flow to it across call boundaries. A &lt;code&gt;ZeroDivisionError&lt;/code&gt; in a rarely-executed branch has no structural marker until it throws. The same applies to null-style errors in dynamically typed code: the source shows the attribute access, not the object's actual type at runtime. &lt;a href="https://coppersun.dev/blog/logic-bug-no-scanner-can-see/" rel="noopener noreferrer"&gt;Logic Bugs No Scanner Can See&lt;/a&gt; works through concrete examples of context-dependent correctness bugs that require tracing call paths to find.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Authorization policy.&lt;/strong&gt; Whether &lt;code&gt;/api/admin/delete-user&lt;/code&gt; requires authentication is a policy decision. The source might show a decorator, or it might not — but knowing that it &lt;em&gt;should&lt;/em&gt; have one requires reading the auth model and the route map together. BrassCoders flags an endpoint that lacks a recognizable auth decorator in a supported framework, but it cannot know the application's complete auth requirements from source alone. IDOR vulnerabilities, where a route accepts a user-controlled ID without checking ownership, are invisible at the structural level: &lt;code&gt;get_record(user_id=request.args['id'])&lt;/code&gt; looks identical whether or not the result is ownership-checked. &lt;a href="https://coppersun.dev/blog/the-authorization-bug-no-scanner-understands/" rel="noopener noreferrer"&gt;The Authorization Bug No Scanner Understands&lt;/a&gt; covers why these bugs fall outside static analysis.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Performance under production data.&lt;/strong&gt; O(N²) behavior is visible only when data size context is known. String concatenation in a loop has a structural marker; a list prepend that degrades on a million-row dataset is structurally identical to one that runs fine on ten rows. The performance issue depends entirely on how large the input grows in production — a number the scanner doesn't have. &lt;a href="https://coppersun.dev/blog/the-slow-code-no-scanner-can-flag/" rel="noopener noreferrer"&gt;The Slow Code No Scanner Can Flag&lt;/a&gt; makes the data-size argument with a concrete example.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Runtime configuration.&lt;/strong&gt; Whether &lt;code&gt;DEBUG=True&lt;/code&gt; reaches production, whether TLS is enforced in the deployment config, whether an environment variable points at the right secret store — these are facts determined at deploy time, not scan time. The source might contain &lt;code&gt;DEBUG = os.getenv('DEBUG', 'false')&lt;/code&gt;, but what value the environment injects is invisible to the scanner. &lt;a href="https://coppersun.dev/blog/security-misconfiguration-is-a-runtime-fact/" rel="noopener noreferrer"&gt;Security Misconfiguration Is a Runtime Fact&lt;/a&gt; covers the environment-dependent bug pattern and why it requires examining the deployment context alongside the source.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bug-Category Visibility Table
&lt;/h2&gt;

&lt;p&gt;BrassCoders classifies every finding it surfaces as a structural pattern — not because it's uncertain, but because the scanner sees structure, not intent. The table below maps bug categories to scanner visibility.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bug Category&lt;/th&gt;
&lt;th&gt;Scanner Visibility&lt;/th&gt;
&lt;th&gt;What the AI Assistant Adds&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SQL injection via f-string&lt;/td&gt;
&lt;td&gt;✅ Deterministic (Bandit B608)&lt;/td&gt;
&lt;td&gt;Confirms real query vs test fixture&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hardcoded credentials&lt;/td&gt;
&lt;td&gt;✅ Deterministic (detect-secrets)&lt;/td&gt;
&lt;td&gt;Confirms real credential vs FIXTURE placeholder&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Command injection via shell=True&lt;/td&gt;
&lt;td&gt;✅ Deterministic (Bandit B602/B603)&lt;/td&gt;
&lt;td&gt;Confirms user-controlled input reaches the call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unsafe deserialization (yaml.load, pickle)&lt;/td&gt;
&lt;td&gt;✅ Deterministic (Bandit B506/B301)&lt;/td&gt;
&lt;td&gt;Confirms external input reaches the deserializer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Phantom imports (PyPI cross-reference)&lt;/td&gt;
&lt;td&gt;✅ Deterministic (AI-pattern scanner)&lt;/td&gt;
&lt;td&gt;Verifies whether package exists at scan time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missing auth on a route&lt;/td&gt;
&lt;td&gt;❌ Needs route map + policy&lt;/td&gt;
&lt;td&gt;✅ AI reads the application structure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ZeroDivisionError in untested path&lt;/td&gt;
&lt;td&gt;❌ No structural marker&lt;/td&gt;
&lt;td&gt;✅ AI traces the call path for zero-denominator inputs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;O(N²) loop under production data&lt;/td&gt;
&lt;td&gt;Partial (string-concat pattern)&lt;/td&gt;
&lt;td&gt;✅ AI sees data size context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Race condition in asyncio&lt;/td&gt;
&lt;td&gt;Partial (threading patterns)&lt;/td&gt;
&lt;td&gt;✅ AI traces shared state across await points&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Business logic rule violation&lt;/td&gt;
&lt;td&gt;❌ No structural pattern&lt;/td&gt;
&lt;td&gt;✅ AI reads intent from the requirements context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime config error (wrong env variable)&lt;/td&gt;
&lt;td&gt;❌ Runtime fact&lt;/td&gt;
&lt;td&gt;✅ AI knows the deployment environment&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The partial entries deserve attention. String concatenation in a loop gets a partial flag because the AST pattern (accumulator on the right side of &lt;code&gt;+=&lt;/code&gt; inside a loop body) is visible in the parse tree. Whether the accumulator holds a string depends on type inference the scanner doesn't run. BrassCoders flags the pattern; the AI assistant reads the variable declaration, the type annotation if present, and the surrounding call context to confirm.&lt;/p&gt;

&lt;p&gt;Threading race conditions follow the same logic. &lt;code&gt;threading.Thread&lt;/code&gt; without paired locks, and global mutable state in async functions, have structural markers BrassCoders matches. Asyncio races that leave no structural marker — two coroutines sharing state through an &lt;code&gt;await&lt;/code&gt; point, with no visible locking primitive — require tracing the control flow across &lt;code&gt;await&lt;/code&gt; boundaries. That's the AI's work.&lt;/p&gt;

&lt;p&gt;Business logic violations fall entirely outside the green zone. Whether a coupon discount can be applied twice to the same order depends on a rule in a product document, not a pattern in the source. The source shows the discount being applied; it doesn't show whether the invariant is checked before or after. The AI assistant, given the requirements document alongside the code, can evaluate the invariant. The scanner cannot — because the invariant isn't in the file.&lt;/p&gt;

&lt;h2&gt;
  
  
  What False Positives Reveal About the Boundary
&lt;/h2&gt;

&lt;p&gt;BrassCoders's false positives are structural — they occur when the scanner matches the pattern but the context makes it safe. A hardcoded credential in a test fixture with a FIXTURE-marked placeholder matches the detect-secrets pattern; the AI triage layer reads the fixture context and marks it a false positive.&lt;/p&gt;

&lt;p&gt;This is the boundary made concrete. The scanner did its job: it found a pattern that matches the shape of a real credential. Deciding the value is a placeholder requires reading the variable name, the file path, the fixture directory context, and the FIXTURE annotation — information that isn't in the pattern rule. The AI reads it in a second. The division works.&lt;/p&gt;

&lt;p&gt;It's worth pausing on the alternative: a scanner that suppresses findings when it infers fixture context. That scanner would stop reporting the credential in the test fixture. It would also stop reporting a real credential in a file a developer accidentally named &lt;code&gt;test_helpers.py&lt;/code&gt;, or in a fixture file that was later refactored to hold production configuration. Inference-based suppression removes false positives and real bugs through the same mechanism — because the structural patterns are identical. The scanner can't know which is which without context.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://coppersun.dev/blog/false-positive-feature-not-bug-static-analysis/" rel="noopener noreferrer"&gt;The False Positive Is a Feature, Not a Bug&lt;/a&gt; works through BrassCoders's N=15 corpus scan in detail: 53 raw findings collapsing to 9 confirmed real security issues after one triage pass. It shows the false-positive cases — the counter-increment that looks like a string-concat loop, the MD5 call in a deduplication context — and explains why each context-based suppression would carry a corresponding risk. The ratio looks noisy; the triage session is fast. That's the design.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the Two Layers Compose
&lt;/h2&gt;

&lt;p&gt;BrassCoders runs first, narrows the scope to confirmed structural patterns, and emits a YAML file the AI assistant reads. The AI assistant verifies each finding in context — confirming whether the injection is real, whether the credential is a fixture, whether the route actually needs auth. Neither layer alone covers the full surface.&lt;/p&gt;

&lt;p&gt;A raw AI review of an unscanned codebase starts from scratch: it reads the full source without any priority signal for what matters, and results vary run to run. A post-BrassCoders AI review starts from a prioritized work queue with exact line numbers and remediation notes. The AI spends its time on confirmation, not discovery — and answering a specific question about a specific line is faster and more reliable than open-ended review. &lt;a href="https://coppersun.dev/blog/ai-assistant-needs-deterministic-pre-pass/" rel="noopener noreferrer"&gt;Why Your AI Assistant Needs a Deterministic Pre-Pass&lt;/a&gt; describes this architecture and the practical speedup it produces.&lt;/p&gt;

&lt;p&gt;A 2025 paper, &lt;a href="https://arxiv.org/abs/2510.02534" rel="noopener noreferrer"&gt;ZeroFalse&lt;/a&gt;, measured this combination directly: feeding static-analyzer output to an LLM for adjudication produced F1 scores of 0.912 on the OWASP Java Benchmark and 0.955 on the OpenVuln dataset, with precision and recall above 90%. The pattern in the paper is the same BrassCoders uses: deterministic detection first, model judgment second.&lt;/p&gt;

&lt;p&gt;Configuration bugs fit the composition exactly. BrassCoders catches &lt;code&gt;debug=True&lt;/code&gt; in a Flask application as a structural flag — the literal Boolean in the source. Whether that value reaches production depends on how the environment variable loading chain works, which deploy scripts override it, and what the hosting environment sets. The AI assistant, reading the deployment scripts alongside the &lt;code&gt;.brass/ai_instructions.yaml&lt;/code&gt;, can trace that chain. BrassCoders surfaces the lead; the AI confirms whether it's a real exposure. &lt;a href="https://coppersun.dev/blog/security-misconfiguration-is-a-runtime-fact/" rel="noopener noreferrer"&gt;Security Misconfiguration Is a Runtime Fact&lt;/a&gt; shows how the combination handles this on a concrete deployment example.&lt;/p&gt;

&lt;h2&gt;
  
  
  What BrassCoders's Findings File Gives the AI Assistant
&lt;/h2&gt;

&lt;p&gt;BrassCoders's &lt;code&gt;.brass/ai_instructions.yaml&lt;/code&gt; gives an AI assistant a pre-digested work queue — severity, scanner rule, file path, line number, and remediation note for each finding — so it triages structured patterns rather than reading the full codebase from scratch.&lt;/p&gt;

&lt;p&gt;The file has three sections. Findings sorted by severity lead the file, so the AI addresses critical and high-severity issues before low-severity style notes. Critical findings — SQL injection, command injection, hardcoded credentials, unsafe deserialization — appear first. The AI starts where the risk is highest. Scanner metadata follows: which of the 12 scanners ran, at what version, and how long the scan took. That section lets an AI assistant identify which rule engine fired and what its coverage scope is.&lt;/p&gt;

&lt;p&gt;The third section, &lt;code&gt;how_to_read_this_file&lt;/code&gt;, is a plain-language note explaining that BrassCoders is a pattern reporter, not a verdict machine. Findings are leads, not confirmed bugs. The AI's job is source verification on each flagged line — confirm it or dismiss it. That framing is why the triage session runs fast: the AI arrives knowing what to do with a finding, not wondering whether it can trust the verdict.&lt;/p&gt;

&lt;p&gt;That contract produces a clean handoff. The scanner runs in seconds on a fresh commit. The YAML lands in the &lt;code&gt;.brass/&lt;/code&gt; directory. Claude Code or Cursor reads the file, opens the flagged lines, and runs the triage session against a bounded list of structural findings. The open-ended question ("what might be wrong?") becomes a specific list ("is this pattern a real bug in this context?"). The limit the scanner cannot cross is exactly where the AI assistant picks up — and the YAML format is what makes the crossing work at speed.&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;brasscoders
brasscoders scan &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="c"&gt;# .brass/ai_instructions.yaml is ready for your AI assistant&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>opensource</category>
      <category>ai</category>
      <category>security</category>
    </item>
    <item>
      <title>How to Run Your First BrassCoders Scan on AI-Generated Python</title>
      <dc:creator>CopperSunDev</dc:creator>
      <pubDate>Sat, 08 Aug 2026 00:22:58 +0000</pubDate>
      <link>https://dev.to/coppersundev/how-to-run-your-first-brasscoders-scan-on-ai-generated-python-58nl</link>
      <guid>https://dev.to/coppersundev/how-to-run-your-first-brasscoders-scan-on-ai-generated-python-58nl</guid>
      <description>&lt;p&gt;&lt;code&gt;brasscoders scan&lt;/code&gt; runs 12 static-analysis scanners against your project tree and writes the results to a single YAML file — &lt;code&gt;.brass/ai_instructions.yaml&lt;/code&gt; — built for AI assistants to read.&lt;/p&gt;

&lt;p&gt;Install BrassCoders from &lt;a href="https://pypi.org/project/brasscoders/" rel="noopener noreferrer"&gt;PyPI&lt;/a&gt; and point it at your project:&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;brasscoders
brasscoders scan /path/to/your/project
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Zero configuration required. The scan runs locally — no account, no activation key — against your full source tree: Python and JavaScript/TypeScript files alike. When it finishes, the &lt;code&gt;.brass/&lt;/code&gt; directory appears at your project root. The OSS core makes zero outbound network calls; everything happens on your machine.&lt;/p&gt;

&lt;p&gt;The resulting &lt;code&gt;.brass/ai_instructions.yaml&lt;/code&gt; file groups findings by severity, CRITICAL down to LOW. Each entry names the scanner that flagged it — &lt;a href="https://github.com/PyCQA/bandit" rel="noopener noreferrer"&gt;Bandit&lt;/a&gt; for Python security issues, or detect-secrets for credential patterns — along with the file path, line number, and a short remediation note. A &lt;code&gt;scanner_metadata&lt;/code&gt; section records which of the 12 scanners ran and how many findings each produced. At the top sits a &lt;code&gt;how_to_read_this_file&lt;/code&gt; block. It describes the field structure and orients your AI assistant before it reads a single finding.&lt;/p&gt;

&lt;p&gt;Open the file in Claude Code or paste its contents into a Cursor chat. Each finding already names its source scanner, so the AI assistant knows the detection method before triage starts. The &lt;code&gt;how_to_read_this_file&lt;/code&gt; header handles orientation — it describes each field and explains the severity tiers, so you get a triage conversation from the start. You don't need to prompt for context.&lt;/p&gt;

&lt;p&gt;For air-gapped machines or compliance-sensitive environments, add the &lt;code&gt;--offline&lt;/code&gt; flag before &lt;code&gt;scan&lt;/code&gt;: &lt;code&gt;brasscoders --offline scan /path/to/your/project&lt;/code&gt;. The flag position matters — it must come before &lt;code&gt;scan&lt;/code&gt;, not after. If the scan attempts any network call at runtime, it exits non-zero. That's a process-level guarantee rather than a behavioral default.&lt;/p&gt;

&lt;p&gt;The OSS core is Apache 2.0 licensed and installs with a single pip command. The &lt;code&gt;.brass/&lt;/code&gt; directory it creates is safe to add to &lt;code&gt;.gitignore&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>engineering</category>
    </item>
    <item>
      <title>What's the Cost of a SQL Injection in AI-Generated Code?</title>
      <dc:creator>CopperSunDev</dc:creator>
      <pubDate>Sat, 08 Aug 2026 00:22:25 +0000</pubDate>
      <link>https://dev.to/coppersundev/whats-the-cost-of-a-sql-injection-in-ai-generated-code-11hb</link>
      <guid>https://dev.to/coppersundev/whats-the-cost-of-a-sql-injection-in-ai-generated-code-11hb</guid>
      <description>&lt;p&gt;SQL injection caught at commit time costs seconds. The same finding caught in production — or after active exploitation — costs engineering weeks.&lt;/p&gt;

&lt;p&gt;The gap is structural. Without a scanning gate at commit time, a SQL injection bug lives in your codebase until a penetration test, a security audit, or an incident surfaces it. That window is typically weeks to months. During that window, the code runs against a real database with real credentials.&lt;/p&gt;

&lt;p&gt;When the vulnerability surfaces (or is suspected after an incident), the repair scope expands well past the code fix itself. The parameterized-query patch takes minutes. What follows doesn't:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Log audit&lt;/strong&gt;: Query logs must be reviewed to determine whether the injection path was ever triggered, and by what inputs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Credential rotation&lt;/strong&gt;: If the database was reachable during the exposure window, you cannot determine without forensic analysis whether credentials were exfiltrated — so rotation is the default safe action.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compliance notification&lt;/strong&gt;: If the database held regulated data (PII, health records, or payment data), most jurisdictions require breach notification within a defined window. The notification cycle spans person-weeks across legal, engineering, and communications.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;BrassCoders catches the Bandit B608 rule at commit time, in the OSS core. The scan makes zero outbound network calls and completes in under a second. B608 fires on string-formatting operations interpolated directly into SQL query strings. The pattern that triggers it:&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="c1"&gt;# B608 fires here — caller-supplied value interpolated into the query string
&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT * FROM users WHERE name = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The parameterized fix eliminates the interpolation entirely:&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="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT * FROM users WHERE name = ?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The database driver sends the query structure and the value as separate arguments. No input can alter the query's structure, because the structure is fixed before the value arrives.&lt;/p&gt;

&lt;p&gt;BrassCoders emits the B608 finding with severity HIGH, the file path, and the line number in &lt;code&gt;.brass/ai_instructions.yaml&lt;/code&gt;. Claude Code or Cursor reads that file and generates the parameterized version from the flagged line. The finding is deterministic — same commit, same output, every scan — which makes a CI gate on it reliable: the finding either appears or it doesn't.&lt;/p&gt;

&lt;p&gt;Adding &lt;code&gt;brasscoders scan&lt;/code&gt; to a GitHub Actions or GitLab CI step catches B608 before any branch merges. The OSS core is free. The full ready-made workflow is in the BrassCoders open-source repository at &lt;code&gt;github.com/CopperSunDev/brasscoders&lt;/code&gt;. The cost of running it is a few seconds per commit. The cost of not running it is measured in the engineering hours described above.&lt;/p&gt;

</description>
      <category>security</category>
      <category>engineering</category>
    </item>
    <item>
      <title>Can BrassCoders Catch Race Conditions in Async Python?</title>
      <dc:creator>CopperSunDev</dc:creator>
      <pubDate>Sat, 08 Aug 2026 00:21:49 +0000</pubDate>
      <link>https://dev.to/coppersundev/can-brasscoders-catch-race-conditions-in-async-python-1j51</link>
      <guid>https://dev.to/coppersundev/can-brasscoders-catch-race-conditions-in-async-python-1j51</guid>
      <description>&lt;p&gt;Partially. BrassCoders catches two structural patterns that indicate missing synchronization: calls to &lt;code&gt;threading.Thread&lt;/code&gt; without a paired &lt;code&gt;threading.Lock&lt;/code&gt;, and global mutable state modified inside &lt;code&gt;async def&lt;/code&gt; functions. Both leave a deterministic marker in the source that the scanner can match.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;threading.Thread&lt;/code&gt; case is the cleaner catch. Spin up threads over functions that read and write shared state, and the scanner sees the thread call and checks for an associated lock. No lock: flagged. The async function plus global mutation pattern works the same way — the AST shows a global variable being written inside an &lt;code&gt;async def&lt;/code&gt;, a structural sign of a potential race.&lt;/p&gt;

&lt;p&gt;What BrassCoders cannot catch: the asyncio race that happens when two coroutines share a variable across an &lt;code&gt;await&lt;/code&gt; point but no &lt;code&gt;threading&lt;/code&gt; call is present. Python's &lt;a href="https://docs.python.org/3/library/asyncio-eventloop.html" rel="noopener noreferrer"&gt;asyncio event loop&lt;/a&gt; is single-threaded and cooperatively scheduled — any &lt;code&gt;await&lt;/code&gt; expression is a potential handoff to another coroutine, and shared state written before and after that handoff can end up stale. But if the code contains no &lt;code&gt;threading.Thread&lt;/code&gt; call and the variable isn't at module scope, there's no marker to match. Two coroutines reading and writing the same variable across an &lt;code&gt;await&lt;/code&gt; looks structurally identical to two coroutines doing so safely.&lt;/p&gt;

&lt;p&gt;The gap gets filled in two ways. An AI assistant — Claude Code, Cursor, or similar — reading the YAML output can trace which variables are actually shared across &lt;code&gt;await&lt;/code&gt; points and flag missing &lt;code&gt;asyncio.Lock&lt;/code&gt; usage. That kind of reasoning needs full function-body context, which deterministic rules can't supply. asyncio-specific linters can catch some of the same patterns, though at the cost of more false positives than BrassCoders targets.&lt;/p&gt;

&lt;p&gt;One practical distinction worth making: when the shared state is auth data or a session store, the correctness bug becomes a security bug. A stale counter is a metrics error. A stale session token map that briefly exposes one user's state to another is a real vulnerability.&lt;/p&gt;

&lt;p&gt;The full mechanics — how asyncio's cooperative scheduling creates race windows and what fixes them — are in &lt;a href="https://coppersun.dev/blog/async-race-conditions-ai-generated-python/" rel="noopener noreferrer"&gt;Race Conditions in AI-Generated Concurrent Python&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>engineering</category>
      <category>ai</category>
    </item>
    <item>
      <title>Does BrassCoders Scan Node.js and TypeScript Code?</title>
      <dc:creator>CopperSunDev</dc:creator>
      <pubDate>Sat, 08 Aug 2026 00:21:08 +0000</pubDate>
      <link>https://dev.to/coppersundev/does-brasscoders-scan-nodejs-and-typescript-code-dnf</link>
      <guid>https://dev.to/coppersundev/does-brasscoders-scan-nodejs-and-typescript-code-dnf</guid>
      <description>&lt;p&gt;Yes. BrassCoders scans JavaScript and TypeScript files automatically in the same pass as Python. Two scanner layers run against your JS/TS code: BrassCoders's dedicated JavaScript/TypeScript scanner, built on Babel's AST parser, and Semgrep, which also runs rules against JS/TS files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Babel-based scanner&lt;/strong&gt; operates on &lt;code&gt;.js&lt;/code&gt;, &lt;code&gt;.jsx&lt;/code&gt;, &lt;code&gt;.ts&lt;/code&gt;, &lt;code&gt;.tsx&lt;/code&gt;, &lt;code&gt;.mjs&lt;/code&gt;, and &lt;code&gt;.cjs&lt;/code&gt; files. Because it parses code into an abstract syntax tree rather than matching raw text, findings track the structure of your code rather than its whitespace or formatting. Five security patterns trigger findings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;eval()&lt;/code&gt; calls (&lt;code&gt;dangerous_eval&lt;/code&gt;): a primary code-injection vector&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;innerHTML&lt;/code&gt; assignments (&lt;code&gt;innerHTML_usage&lt;/code&gt;): the most common XSS source in browser-side JavaScript&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;document.write()&lt;/code&gt; calls (&lt;code&gt;document_write&lt;/code&gt;): a secondary DOM-based XSS path&lt;/li&gt;
&lt;li&gt;String literals matching common API key formats (&lt;code&gt;potential_api_key&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Hardcoded password strings (&lt;code&gt;hardcoded_password&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Semgrep runs its own rule set against the same files in the same scan. The two layers are complementary: Semgrep covers patterns defined in its OSS ruleset; the Babel scanner adds AST-level analysis that survives identifier renames and code reformatting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where Bandit fits — and where it doesn't&lt;/strong&gt;: Bandit is Python-only. BrassCoders runs Bandit as one of its 12 scanners, but only against Python files. If you're asking because you assumed Bandit covered your Node.js backend, it does not. That work goes to the Babel scanner and Semgrep.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The gap compared to Python coverage&lt;/strong&gt;: The Python scanner set — Bandit with 50+ rules alongside Pylint, Pyre/Pysa, and six custom scanners — is broader than the JS/TS layer. There's no JS/TS equivalent to Bandit's rule depth or to Pyre's interprocedural taint analysis. The Babel scanner and Semgrep catch the highest-signal JS/TS security patterns, but combined coverage is narrower than what BrassCoders provides for Python.&lt;/p&gt;

&lt;p&gt;For mixed-language projects, that's still a meaningful gain. A FastAPI backend with a Next.js frontend gets Python findings from the full scanner set and JS/TS findings from the Babel scanner and Semgrep — all in one &lt;code&gt;.brass/ai_instructions.yaml&lt;/code&gt; file, one scan pass, no per-language toolchain stitching required.&lt;/p&gt;

&lt;p&gt;To scan any project:&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;brasscoders
brasscoders scan /path/to/your/project
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;BrassCoders detects file types automatically. JS/TS findings land in &lt;code&gt;.brass/ai_instructions.yaml&lt;/code&gt; alongside Python ones, tagged by scanner name so your AI assistant can triage them in context.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>engineering</category>
    </item>
    <item>
      <title>AI Coding Assistant Vendor Risk: What Your Security Team Should Ask</title>
      <dc:creator>CopperSunDev</dc:creator>
      <pubDate>Sat, 08 Aug 2026 00:20:32 +0000</pubDate>
      <link>https://dev.to/coppersundev/ai-coding-assistant-vendor-risk-what-your-security-team-should-ask-1lcm</link>
      <guid>https://dev.to/coppersundev/ai-coding-assistant-vendor-risk-what-your-security-team-should-ask-1lcm</guid>
      <description>&lt;p&gt;Security teams evaluating AI coding assistants tend to collapse two distinct risks into one vendor conversation. The result: a thorough review of the vendor's privacy policy and a gap on the security of what the AI actually produces. Getting both right requires different question sets — one for the assistant's data practices, one for the code it generates.&lt;/p&gt;

&lt;p&gt;One surface is the data-handling layer: what the vendor does with your source code during a session, whether that code feeds model training, and what deployment options exist for regulated environments. The other is the code-output layer: what the AI generates and what security issues that code contains before it reaches your repository.&lt;/p&gt;

&lt;p&gt;Both surfaces require scrutiny. Conflating them leaves one unexamined.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Surfaces, Two Question Sets
&lt;/h2&gt;

&lt;p&gt;BrassCoders addresses the code-output layer (the security issues in the code the AI assistant produces). The vendor risk checklist below addresses the data-handling layer: what the vendor does with your code during and after a session. These are different surfaces, and the questions don't overlap.&lt;/p&gt;

&lt;p&gt;Most security evaluations focus on the data-handling layer because it comes with documentation. The vendor has a privacy page, a data-processing agreement, and often a commercial buyer FAQ. That documentation is readable and auditable before you commit to a contract.&lt;/p&gt;

&lt;p&gt;The code-output surface has no policy page. It has a finding rate: how often the AI generates code with security issues before those issues land in your repository. CISA's &lt;a href="https://www.cisa.gov/sites/default/files/2023-11/Guidelines-for-Secure-AI-System-Development.pdf" rel="noopener noreferrer"&gt;Guidelines for Secure AI System Development&lt;/a&gt;, published in November 2023 and co-signed by cybersecurity agencies from the US, UK, Canada, Australia, and New Zealand, explicitly calls for static analysis in the build pipeline for AI-generated code. That call applies regardless of which AI assistant produced the suggestion.&lt;/p&gt;

&lt;p&gt;A security team that reviews only the vendor's privacy page has evaluated one surface. The sections below cover the vendor's data-handling surface. The final section covers the code-output surface and what BrassCoders does there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions About Code Transmission
&lt;/h2&gt;

&lt;p&gt;BrassCoders's OSS core sends zero bytes off the machine during a scan. The findings YAML writes locally, and no source code or scanner data leaves the developer's machine in the free tier. For AI coding assistants, the transmission picture differs by design: the product's core function requires sending code to a vendor server to generate a suggestion.&lt;/p&gt;

&lt;p&gt;The questions to ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does the assistant send my full source file to vendor servers, or only the context window around the current edit?&lt;/li&gt;
&lt;li&gt;Is there an on-machine or offline mode where suggestion generation happens locally?&lt;/li&gt;
&lt;li&gt;What does the vendor retain from code sent during a session, and for how long?&lt;/li&gt;
&lt;li&gt;Does a data-processing agreement cover code transmission, or only account data and telemetry?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The answers vary significantly by vendor and often by pricing tier. Some assistants transmit the active file plus open files in the same project. Others transmit a narrower window. Retention periods range from session-only to several months. Before deploying to any environment with regulatory or contractual data-handling obligations, confirm the actual data flow against a DPA that covers it.&lt;/p&gt;

&lt;p&gt;One distinction worth pressing on: a standard terms-of-service agreement and a data-processing agreement are not the same document. A ToS governs the vendor relationship; a DPA governs how data processed on your behalf is handled. If the vendor doesn't offer a DPA, or the DPA doesn't explicitly cover code sent during suggestion generation, the data-handling obligations for that code may fall outside any contractual protection — even if the privacy policy sounds reassuring.&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions About Training Data Opt-Out
&lt;/h2&gt;

&lt;p&gt;BrassCoders does not use customer scan data for model training. BrassCoders Paid sends already-redacted findings and a project signature of at most 7,500 characters of non-sensitive metadata to the enrichment gateway. Raw source code never leaves the machine, which means there is no source code available to include in any training corpus.&lt;/p&gt;

&lt;p&gt;For AI coding assistants, the training question is distinct from the transmission question. A vendor may retain code only for session purposes and still use it to improve the model. The questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can my code be used for model training, and is opt-out the default or must I request it explicitly?&lt;/li&gt;
&lt;li&gt;Does the opt-out apply to fine-tuning as well as pretraining?&lt;/li&gt;
&lt;li&gt;Does the opt-out apply on all subscription tiers, or only on higher-tier plans?&lt;/li&gt;
&lt;li&gt;Does the opt-out apply retroactively to code already transmitted, or only to future sessions?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Default opt-in to training is common at entry-level price points. Commercial contracts often carry different data-handling terms than the standard user agreement. Teams working on proprietary or regulated codebases should confirm which terms govern their specific tier — and verify that the commercial buyer FAQ describes their actual agreement, not a representative scenario — before any code reaches the vendor's servers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions About On-Premises and Offline Options
&lt;/h2&gt;

&lt;p&gt;BrassCoders's &lt;code&gt;--offline&lt;/code&gt; flag provides hard enforcement: &lt;code&gt;brasscoders --offline scan&lt;/code&gt; exits non-zero if any network call is attempted at runtime. The zero-transmission guarantee is testable in CI, not just stated in a policy document.&lt;/p&gt;

&lt;p&gt;For air-gapped environments and organizations with strict data residency requirements, the deployment model is often the first evaluation gate. The questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is an on-premises deployment available, and does it require a separate license or a different pricing tier?&lt;/li&gt;
&lt;li&gt;Is there an air-gapped option that makes zero outbound network calls during inference?&lt;/li&gt;
&lt;li&gt;For cloud deployments, which data residency regions are available, and is regional selection standard or an add-on?&lt;/li&gt;
&lt;li&gt;What model serving happens server-side, and what computation runs client-side during a suggestion?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cloud-only vendors with no offline option may not meet FedRAMP requirements or industry frameworks that place conditions on where data processing occurs. The &lt;a href="https://airc.nist.gov/RMF/1" rel="noopener noreferrer"&gt;NIST AI RMF GOVERN function&lt;/a&gt; asks organizations to document AI system limitations — and a deployment model that rules out regulated environments is a limitation. Get the deployment answer before other evaluation criteria apply.&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions About the AI-Generated Code Output
&lt;/h2&gt;

&lt;p&gt;BrassCoders covers the code-output layer — the Python and TypeScript files the AI assistant produces that enter your repository — with 12 scanners, running entirely on the local machine.&lt;/p&gt;

&lt;p&gt;Most vendor evaluations stop before reaching this question. The vendor's privacy page describes what the vendor does with your code. It does not describe what security bugs that code contains. The questions to ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does the vendor run static analysis on generated code before surfacing it as a suggestion?&lt;/li&gt;
&lt;li&gt;What categories of security issues does the vendor's internal tooling check for?&lt;/li&gt;
&lt;li&gt;Has the vendor published a finding rate for security bugs in AI-generated code?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For context on what an independent finding rate looks like: the BrassCoders &lt;a href="https://coppersun.dev/blog/ai-coder-bug-benchmark/" rel="noopener noreferrer"&gt;published benchmark&lt;/a&gt; covered 15 real Python files from public AI-generated code repositories. BrassCoders caught issues in 9 of the 15 files, covering 11 of 12 bug categories the study examined. Zero proactive security warnings were issued during code generation by the AI assistants that produced those files. That benchmark represents one corpus. The question for any vendor: do they publish a comparable number, and against what test set?&lt;/p&gt;

&lt;p&gt;Finding rates that are not published cannot be evaluated. If a vendor has no published number, the follow-up question is whether they run any independent third-party security testing on generated code — and whether those results are available to customers under contract. A vendor with no answer to either question has given you useful information about how they treat the code-output surface.&lt;/p&gt;

&lt;h2&gt;
  
  
  How BrassCoders Addresses the Code-Output Layer
&lt;/h2&gt;

&lt;p&gt;BrassCoders runs 12 scanners against the code the AI produces — the part no AI vendor's internal tooling reviews before the suggestion lands in your editor, since the suggestion displays before any commit-time gate runs.&lt;/p&gt;

&lt;p&gt;The upstream scanners are Bandit, Pylint, Pyre/Pysa, Semgrep, ast-grep, and detect-secrets — the foundational tools for Python security analysis, code quality, taint flow, cross-language pattern matching, and credential exposure. Each is a mature open-source project with documented coverage.&lt;/p&gt;

&lt;p&gt;The custom layer adds six more detectors: secret patterns, PII handling, phantom-API call detection for AI-generated imports that don't exist, performance issues, content moderation, and JavaScript/TypeScript.&lt;/p&gt;

&lt;p&gt;The JavaScript/TypeScript scanner uses Babel AST parsing and covers &lt;code&gt;.js&lt;/code&gt;, &lt;code&gt;.jsx&lt;/code&gt;, &lt;code&gt;.ts&lt;/code&gt;, &lt;code&gt;.tsx&lt;/code&gt;, &lt;code&gt;.mjs&lt;/code&gt;, and &lt;code&gt;.cjs&lt;/code&gt; files.&lt;/p&gt;

&lt;p&gt;Caught patterns include &lt;code&gt;eval()&lt;/code&gt; calls, &lt;code&gt;innerHTML&lt;/code&gt; XSS sinks, &lt;code&gt;document.write&lt;/code&gt; usage, hardcoded API keys, and hardcoded passwords.&lt;/p&gt;

&lt;p&gt;The OSS core is Apache 2.0 licensed, requires no account, and sends nothing off-machine. Install with &lt;code&gt;pip install brasscoders&lt;/code&gt; and run &lt;code&gt;brasscoders scan .&lt;/code&gt;. Output writes to &lt;code&gt;.brass/ai_instructions.yaml&lt;/code&gt;. Add &lt;code&gt;brasscoders scan&lt;/code&gt; to your CI pipeline to gate every commit — a critical finding returns non-zero and blocks the merge.&lt;/p&gt;

&lt;p&gt;For stricter data environments, &lt;code&gt;brasscoders --offline scan&lt;/code&gt; enforces zero network calls at the scanner level. The offline guarantee is enforced in code, not in policy.&lt;/p&gt;

&lt;p&gt;The vendor risk questions in the sections above address the AI coding assistant's data practices: what they do with your code on their servers. BrassCoders gates on what the assistant actually produces: what the generated code contains once it's back in your repository. These checks operate at different points in the workflow, against different surfaces, with different failure modes.&lt;/p&gt;

&lt;p&gt;Run both. Neither substitutes for the other.&lt;/p&gt;

</description>
      <category>security</category>
      <category>compliance</category>
      <category>engineering</category>
    </item>
    <item>
      <title>The Trust Boundary Problem in AI-Generated Code</title>
      <dc:creator>CopperSunDev</dc:creator>
      <pubDate>Fri, 07 Aug 2026 23:44:38 +0000</pubDate>
      <link>https://dev.to/coppersundev/the-trust-boundary-problem-in-ai-generated-code-d76</link>
      <guid>https://dev.to/coppersundev/the-trust-boundary-problem-in-ai-generated-code-d76</guid>
      <description>&lt;p&gt;The prompt said: "fetch this config from the API and use it to initialize the service." The AI generated code that calls the API, parses the JSON, and passes the values directly to initialization. No validation. The code works correctly on valid API responses — which is what every test scenario covered. It fails, in a security-relevant way, when the API returns values an attacker can influence.&lt;/p&gt;

&lt;p&gt;The problem isn't a missing code pattern. It's a missing design decision: which sources are trusted, and what validation happens when data crosses from untrusted to trusted?&lt;/p&gt;

&lt;h2&gt;
  
  
  What a Trust Boundary Is
&lt;/h2&gt;

&lt;p&gt;A trust boundary is the line between parts of a system where data is verified and parts where it isn't. BrassCoders can flag structural indicators that a boundary is missing — user-controlled input in a SQL query, a user-supplied path in a file operation — because those have recognizable shapes. What it cannot determine is whether data from an external API response, a configuration endpoint, or a deserialized payload is being treated as trusted at the right point in the call chain. That's a design question, not a pattern question.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://cwe.mitre.org/data/definitions/501.html" rel="noopener noreferrer"&gt;CWE-501 (Trust Boundary Violation)&lt;/a&gt; from MITRE defines the category: "the product mixes trusted and untrusted data in the same data structure or structured variable." The violation happens at the boundary crossing — the moment untrusted data enters code that treats it as verified — and that crossing is often invisible in the source because the data looks like any other local variable once it's been assigned.&lt;/p&gt;

&lt;h2&gt;
  
  
  How AI Assistants Misplace the Boundary
&lt;/h2&gt;

&lt;p&gt;AI-generated code works on the happy path. BrassCoders's published N=15 AI-code-findings corpus found real security issues in 9 of 15 AI-generated Python files — all from neutral prompts, none from prompts that requested vulnerable code. The trust boundary failures in that class don't appear as structural bugs; they appear as code that correctly processes inputs the developer controls and silently trusts inputs an attacker can influence.&lt;/p&gt;

&lt;p&gt;An AI assistant generating an HTTP client that fetches external configuration was shown: make the request, parse the response, use the data. The model fills in the code path it was prompted for. Validation of the response fields belongs at the boundary — before parsing, or immediately after — but the prompt described the interior of the function, not the seam where untrusted data enters. The model produces internally correct code with an externally wrong trust assumption.&lt;/p&gt;

&lt;p&gt;This is why the trust boundary problem persists even when teams use AI assistants for security review. An AI asked to review a function's code often finds the function well-formed. The trust boundary question is about the function's context: what are the provenance guarantees on its inputs, and does the caller enforce them? That's architectural reasoning, not code reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three Ways the Boundary Fails in AI Code
&lt;/h2&gt;

&lt;p&gt;BrassCoders scans the code that results from trust boundary design decisions — catching the SQL injection and the path traversal where they exist — but the three design patterns that produce trust boundary failures in AI-generated code leave no structural marker: external responses treated as structured data, configuration values passed without verification, and sanitized-flag drift across function calls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;External API responses treated as structured data.&lt;/strong&gt; An AI-generated integration layer fetches a vendor API response, parses the JSON, and passes the parsed values to downstream functions. The downstream functions trust their inputs. When the vendor API returns unexpected values — or when an attacker influences the response through a misconfigured or compromised upstream — the downstream functions execute on data they assumed was already validated. The code path is correct; the trust assumption is wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Configuration values passed without verification.&lt;/strong&gt; Environment variables and configuration files sit at a trust boundary. They're not direct user input, but they're writable by anyone with deployment access, and AI-generated code that reads config values and passes them directly to subprocess calls, file path construction, or query parameters skips the validation the boundary requires. The AI was shown: read the value, use the value. The step between — verify the value — was in the design, not the prompt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sanitized-flag drift across function calls.&lt;/strong&gt; AI-generated code commonly applies validation in one function and passes the result to a second that has no knowledge of whether validation happened. Three function calls later, the original source is invisible in the local variable name. The &lt;a href="https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/07-Input_Validation_Testing/" rel="noopener noreferrer"&gt;OWASP Testing Guide on input validation&lt;/a&gt; identifies this as a primary failure mode: validation happened, but at the wrong layer, and the data's untrusted origin is no longer visible at the point of use.&lt;/p&gt;

&lt;h2&gt;
  
  
  What BrassCoders Catches Near Trust Boundaries
&lt;/h2&gt;

&lt;p&gt;BrassCoders flags the structural indicators that trust boundaries are missing or misplaced. SQL injection findings (Bandit B608) mark places where user-supplied data flows into a query without parameterization. Command injection findings (Bandit B602/B603) mark subprocess calls where user-supplied strings reach the shell. The phantom-import scanner catches external package references that don't exist — a trust assumption about the package ecosystem that wasn't verified.&lt;/p&gt;

&lt;p&gt;Pyre/Pysa, the taint analyzer BrassCoders runs, handles some taint tracking across function boundaries — tracing data from sources to security-sensitive sinks. Its coverage depends on annotated entry points and sink functions for the specific application. What no scanner in the set can determine: whether the data in a &lt;code&gt;config_value&lt;/code&gt; variable came from a trusted internal store or an external API response. The variable name doesn't carry provenance, and provenance is the trust question.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Enforces the Boundary
&lt;/h2&gt;

&lt;p&gt;Trust boundaries are enforced at design time and tested at runtime — BrassCoders clears the structural findings from the code, and the trust boundary architecture lives in a threat model that predates the code.&lt;/p&gt;

&lt;p&gt;Enforcing a trust boundary requires identifying the source, defining the validation contract at the entry point, and verifying that the contract holds in integration tests. An external API response gets schema validation before its values are used — not inside the downstream function, but at the seam where untrusted data enters the system. That seam is the boundary. An allowlist on redirect targets enforces a boundary against open redirect payloads. A type assertion before the subprocess call enforces a boundary against command injection. Each of those is a design decision before it's a line of code.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://owasp.org/Top10/A04_2021-Insecure_Design/" rel="noopener noreferrer"&gt;OWASP A04:2021 (Insecure Design)&lt;/a&gt; names the class: design-level flaws, including missing trust boundary controls, are distinct from implementation bugs and require design-level fixes. No additional scanning changes a design that assumed external data was safe. The fix is the schema validation at the seam, the allowlist on the redirect target, the type assertion before the subprocess call — in the architecture, before the code is written.&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;brasscoders
brasscoders &lt;span class="nt"&gt;--offline&lt;/span&gt; scan /path/to/your/project
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;BrassCoders catches the injection bug where user input hit the query string. The trust boundary that let external config values reach a subprocess call without validation is the design decision — the one the threat model has to make before the code exists, and the test suite has to verify after it's written.&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>engineering</category>
    </item>
  </channel>
</rss>
