June 12, 2026 · Fafa Agbetsise · 0.5.6 → 0.5.7
A whole new language: cst-go-agent and the .go detector path that moved every category
Seven repos in, the corpus was entirely Python and TypeScript. The detectors had never seen a line of Go. cst-go-agent forces a .go AI-app scanner path to exist at all — and because the Go target stresses all six categories at once, building it moved every single one. Here's the new detector file, the LLM-SDK gate that keeps getdebug's own CLI silent, and two precise bugs the first scan surfaced.
For seven repos, every CodeSecBench target was Python or TypeScript, so the detectors only ever learned Python and TypeScript idioms. cst-go-agent is the first non-Python/JS target, and it exposes something the previous six couldn’t: there was no .go AI-app detector path at all. A Go AI service could leak its key, shell out a tool argument, and stream on a dead context, and getdebug — running its regex pass — would see none of it. First scan: 6% recall, and the only thing caught was the hardcoded key (the secret scanner is language-agnostic).
Go expresses the same six categories through idioms no Python or JS regex touches:
Category Go idiom
────────────────────────────────────────────────────────────────────────
prompt-injection systemPrompt := fmt.Sprintf("...%s", userInput)
prompt-injection prompt := "..." + strings.Join(chunks, "\n") + q
unsafe-role-merge System: anthropic.F(fmt.Sprintf("...%s", role))
unsafe-role-merge openai.ChatCompletionMessage{Role: incomingRole}
unsafe-role-merge os.ReadFile(fmt.Sprintf("personas/%s.txt", name))
client-side-llm-key map[string]string{"api_key": h.cfg.APIKey}
unsafe-tool-output exec.Command("sh", "-c", args.Command)
unsafe-tool-output db.Query(fmt.Sprintf("...%s", args.UserID))
pii-in-prompt json.Marshal(user) → prompt
pii-in-prompt fmt.Sprintf("...%+v", profileRow)
unbounded-stream client.Messages.NewStreaming(context.Background())
unbounded-stream &http.Client{} // no Timeout
The .go path, and the gate that keeps it polite
0.5.7 adds aiapp_regex_go.go and routes .go files to it. But there’s a hazard a Go scanner has that a SvelteKit scanner doesn’t: getdebug’s own CLI is written in Go. A naive Go pass would scan getdebug scanning itself, and getdebug’s code is full of fmt.Sprintf, exec.Command, and the literal strings "openai" and "anthropic" (they’re in the detector patterns).
So the whole Go pass is gated on goLlmMarkerRe — it only runs on files that actually import an LLM SDK or hit a provider URL. getdebug’s CLI doesn’t import anthropic-sdk-go; it just mentions the word. The gate distinguishes the two. We verified it: a self-scan of the CLI produces zero Go AI-app findings. The detector path is real, and it stays silent on the 99% of Go that isn’t an AI app.
Two bugs the first scan surfaced
The benchmark earned its keep again. First pass after writing the detectors: 88%, two misses.
-
admin.goleaked key, not caught. The handler doesEncode(map[string]string{"api_key": h.cfg.APIKey}). Our regex matchedcfg.APIKeybut noth.cfg.APIKey— it allowed one dot, the real code had two. Fixed the field matcher to span segments ([\w.]+Key). -
router.godynamic role, not caught. The detector fires onRole: incomingRoleunless the file validates against an allowlist — and the suppression check matched the bare word “allowlist”. The vulnerable file’s comment said “role taken from an untrusted string variable with no allowlist.” The word in the comment suppressed the finding. Same comment-poisoning trap we hit in the Python cycle. Fixed by anchoring the suppression on anallowed* :=assignment — real code, not prose.
Both are the kind of bug you only find by running the detector against a corpus that’s trying to be representative rather than convenient.
The numbers
cst-go-agent 0.5.6 0.5.7
─────────────────────────────────────────
TOTAL recall 6% → 100% (16/16, 0 FP, 0 hallucinations)
Across the now-seven-repo corpus — and this is the part that’s different from every prior cycle — every category moved:
Category 0.5.6 0.5.7 Delta
prompt-injection 47% → 71% +24pp ← biggest (Go carries 4 PI)
unsafe-role-merge 41% → 59% +18pp
pii-in-prompt 57% → 71% +14pp
unbounded-stream 61% → 72% +11pp
unsafe-tool-output 74% → 83% +9pp
client-side-llm-key 77% → 85% +8pp
TOTAL corpus recall 59% → 73% +14pp
Earlier cycles moved one category — the one the repo was built to stress — and held the rest flat. This one moved all six, because a new language target isn’t a category stressor; it’s a coverage stressor across the board. Adding Go didn’t make the prompt-injection detector smarter about Python. It gave the corpus a seventh data point in a language where the prompt-injection recall had been zero, and the new Go detectors pulled it up.
What’s next
cst-rails (Ruby on Rails + ruby-openai, repo #8) is the fourth language and the last repo in this run. The pattern is now a routine: a new language scores near-zero on first scan, a .rb detector path closes it, and the corpus mean ticks up. The interesting part isn’t whether it works — it’s that the framework for proving it works, end to end, is the same every time.
Previous in this series: The agent-framework blind spot: cst-crewai-multiagent · The first Python target: cst-fastapi-tools · Targeting unsafe-tool-output: cst-express-agent · SvelteKit + Anthropic · The first afternoon.