June 12, 2026 · Fafa Agbetsise · 0.5.7 → 0.5.8
Four languages, eight repos: cst-rails closes the Tier C corpus
The last repo of the run is Ruby on Rails — the fourth language. By now the routine is mechanical: a new language scores ~6% on first scan, a .rb detector path closes it, the corpus mean ticks up. cst-rails went 6% to 100%. Here's the Ruby wave, the three comment-poisoning bugs the first scan caught (again), and what the whole eight-repo arc actually proved.
cst-rails is the eighth and final repo in the Tier C corpus, and the fourth language: Ruby on Rails with the ruby-openai gem. Rails AI apps express the six categories through Ruby idioms nothing in the scanner had seen — "...#{user_input}" interpolation, messages = [{ role: params[:role] }], backtick command execution, File.read("personas/#{name}.txt"), user.to_json, ActionController::Live streaming, OpenAI::Client.new with no request_timeout. First scan: 6% recall, the hardcoded key and nothing else.
0.5.8 adds aiapp_regex_rb.go — fourteen Ruby detectors, gated on an LLM-SDK marker so a plain Rails app stays silent. cst-rails went to 100% (16/16, 0 false positives), and the corpus mean moved from 64% to 76%.
The same bug, three more times
Every cycle of this run has caught the same class of bug on the first scan, and Ruby caught three at once:
- The gate was case-sensitive. The activation marker matched
OpenAIandopenaibut notOPENAI_API_KEY— so the admin controller that leaks the key (and every tool file that saysLLMin caps) was gated out. Made it case-insensitive. - The
request_timeoutcheck matched a comment. The streaming service’s comment literally said “client is built with norequest_timeout.” The suppression saw the word and suppressed the finding. Anchored it onrequest_timeout:(the actual kwarg). - The
ensure/stream.closecheck matched a comment. The SSE controller’s comment said “noensure response.stream.close.” Same trap. Anchored it on a real^\s*ensureblock.
This is the comment-poisoning pattern from the Python cycle (the is_disconnected mention) and the Go cycle (the allowlist mention), a third time. At this point it’s a known failure mode with a known fix: never let prose decide a verdict — strip comments or anchor on code. The reason we keep catching it is that the fixtures explain their own bugs in comments, which is exactly what real well-documented vulnerable code does too. A detector that’s fooled by a comment in the fixture would be fooled by a comment in production.
The numbers
cst-rails 0.5.7 0.5.8
─────────────────────────────────────────
TOTAL recall 6% → 100% (16/16, 0 FP, 0 hallucinations)
Across the full eight-repo corpus:
Category 0.5.7 0.5.8 Delta
prompt-injection 60% → 75% +15pp
unsafe-role-merge 50% → 65% +15pp
pii-in-prompt 63% → 75% +13pp
unsafe-tool-output 73% → 85% +12pp
unbounded-stream 65% → 75% +10pp
client-side-llm-key 80% → 87% +7pp
TOTAL corpus recall 64% → 76% +12pp
What eight repos and four cycles actually proved
When this run started, the AI-app detectors were JavaScript- and TypeScript-shaped, and unsafe-role-merge recall was 8%. Four cycles later:
start end
unsafe-role-merge 8% → 65%
unbounded-stream 38% → 75%
prompt-injection ~42% → 75%
overall ~48% → 76%
Languages covered: 2 → 4 (TS, Python, Go, Ruby)
Frameworks: 4 → 7
Precision: 100% on all eight repos, every cycle
But the recall numbers aren’t really the point. The point is the loop. Every cycle was identical in shape: build a repo that stresses a known weakness (a category or a whole language), scan it, watch it fail, ship the detectors that close the gap, re-score all repos to prove no regression, and write down exactly what moved. Four times, including twice for languages the detectors had never seen. The benchmark caught a bad fixture before it could teach a bad lesson (the CrewAI Task-description trap), and it caught the same comment-poisoning bug in three different languages before it could ship.
A SAST tool’s recall number is a claim. The corpus is the receipt. Eight repos, four languages, 128 labeled vulnerabilities, one scorer, every result reproducible from the truth files in the public repo. That’s the deliverable — not the 76%, but the fact that you can check it.
Previous in this series: A whole new language: cst-go-agent · The agent-framework blind spot: cst-crewai-multiagent · The first Python target: cst-fastapi-tools · cst-express-agent · SvelteKit + Anthropic · The first afternoon.