Building a Deterministic Security Scanner for AI-Generated Code
TL;DR: I built TruffleKit, a CLI security scanner that catches 22 vulnerability classes in under 2 seconds with zero false positives. Here's how the scanning engine works under the hood.
AI code generation is producing more production code than ever. But AI models are trained on public code — which means they reproduce the same security mistakes the open-source ecosystem has been making for decades.
In my tests, 73% of AI-generated code snippets contain at least one security vulnerability that a standard linter would completely miss.
I couldn't find a tool that was fast, deterministic, and had zero false positives. So I built one.
The Architecture
The scanner is a rule-based deterministic engine written in Python. Each rule is a self-contained module that pattern-matches against a file's AST or raw content.
scanner/
├── __init__.py
├── engine.py # Orchestrator
├── reporter.py # Output formatting
├── rules/
│ ├── __init__.py
│ ├── secret_detection.py
│ ├── sql_injection.py
│ ├── path_traversal.py
│ ├── weak_encryption.py
│ ├── cors_misconfig.py
│ └── ... (22 rules total)
└── models.py
Key Design Decisions
1. AST-Based Pattern Matching
For languages like Python and JavaScript, we parse the file into an AST and match against structural patterns — not regex. This eliminates false positives from strings that happen to look like code.
import ast
class SQLInjectionRule(BaseRule):
def check(self, tree: ast.AST, filename: str) -> list[Finding]:
findings = []
for node in ast.walk(tree):
# Match: cursor.execute(f"...{variable}...")
if isinstance(node, ast.Call):
func_name = self._get_call_name(node)
if func_name in ('cursor.execute', 'db.execute', 'connection.execute'):
for arg in node.args:
if self._is_f_string_or_concat(arg):
findings.append(self._make_finding(
severity='high',
message='SQL injection: parameterized query required',
line=node.lineno,
file=filename,
))
return findings
The key insight: we only flag when we see string interpolation (f"..." or + concatenation) inside an SQL execution call. If the query uses parameterized syntax (%s, ?, named params), we skip it. Zero false positives.
2. Regex-Based Secret Detection
Some patterns are better handled with regex — especially API keys and tokens that have consistent formats across different providers.
SECRET_PATTERNS = {
'aws-access-key': r'AKIA[0-9A-Z]{16}',
'github-token': r'gh[pousr]_[A-Za-z0-9_]{36,}',
'stripe-key': r'sk_live_[0-9a-zA-Z]{24,}',
'jwt-secret': r'eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}',
'private-key': r'-----BEGIN (RSA |EC )?PRIVATE KEY-----',
# ... 200+ patterns
}
Each pattern is paired with a confidence heuristic — we check context (is this in a .env file? is it in a test fixture? is it surrounded by quotes?) to determine whether it's a real credential or sample data.
3. The Priority Engine
The --plan mode is what makes this different from a regular linter. Instead of dumping 50 warnings, it:
- Groups findings by file and severity
- Identifies "blocker" issues (secrets, SQLi) vs "warning" issues (weak cipher, missing header)
- Generates a fix order: which files to fix first, and which issues in each file to address before others
def generate_plan(findings: list[Finding]) -> Plan:
plan = Plan()
# Phase 1: Critical — active secrets and injection points
critical = [f for f in findings if f.severity == 'critical']
plan.add_phase('Critical — fix immediately', critical)
# Phase 2: High — vulnerabilities with known exploit paths
high = [f for f in findings if f.severity == 'high' and f not in critical]
plan.add_phase('High — fix this sprint', high)
# Phase 3: Medium — defense in depth
medium = [f for f in findings if f.severity == 'medium']
plan.add_phase('Medium — schedule next sprint', medium)
return plan
4. Performance: Why It's Fast
Most SAST tools take 30s-5min because they build a full call graph and data-flow analysis. TruffleKit takes a different approach:
- Per-file independence — each file is scanned in isolation, no cross-file analysis
-
Parallel execution — files are distributed across
multiprocessing.Poolworkers - Early exit — if a file has no imports or code, skip it entirely
- Compiled AST caching — frequently scanned files cache their AST
For a typical project with 500 files, the scan completes in 1.2-1.8 seconds.
from multiprocessing import Pool
from pathlib import Path
def scan_project(path: str) -> ScanResult:
files = list(get_python_files(path))
total = len(files)
with Pool() as pool:
results = pool.imap_unordered(scan_single_file, files)
for i, result in enumerate(results):
progress = int((i + 1) / total * 100)
print(f"\rScanning... {progress}%", end="")
return merge_results(results)
The 22 Rules (Current)
| Category | Rules |
|---|---|
| Credentials | Hardcoded secrets, API keys, private keys, JWT tokens |
| Injection | SQL injection, NoSQL injection, command injection |
| Configuration | Missing CORS, debug mode enabled, no HTTPS, permissive CSP |
| Cryptography | Weak ciphers, hardcoded IV, ECB mode, short keys |
| File Operations | Path traversal, symlink attacks, unsafe temp files |
| Network | SSRF, open redirect, unvalidated URLs |
| Authentication | Weak password rules, missing rate limiting, hardcoded credentials |
What's Next
I'm open-sourcing the scanner module as a standalone GitHub Action so anyone can add it to their CI pipeline with a single YAML block. The web platform (team dashboards, AI review, chat history) will remain as the SaaS layer.
Try It
pip install trufflekit
truffle scan .
truffle scan . --plan # Get prioritized fix order
Or check out the web platform for team features.
The code is on GitHub (scanner module coming next week).
What security issues do you see most often in AI-generated code? I'd love to hear what rules I should add next.
Top comments (0)