CodeSecBench
← All posts

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.

ai-app-security benchmark calibration go golang

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.

  1. admin.go leaked key, not caught. The handler does Encode(map[string]string{"api_key": h.cfg.APIKey}). Our regex matched cfg.APIKey but not h.cfg.APIKey — it allowed one dot, the real code had two. Fixed the field matcher to span segments ([\w.]+Key).

  2. router.go dynamic role, not caught. The detector fires on Role: incomingRole unless 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 an allowed* := 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.