Go — Deep-Dive Guide
Go is a primary language for Gadriel Code, with a dedicated taint analyzer,
an entry-point policy tuned to Go web frameworks, cross-file (workspace)
tracking on by default, and optional gosec corroboration. This guide goes
below the summary in languages.md — read that first for the
capability matrix, and scan-coverage.md for the six scan
types.
The Go front-end (go.rs) binds tree-sitter-go; the taint engine lives in
taint_go/ with a cross-file coordinator in workspace_taint/go.rs.
Which scans apply, and how deep
| Scan type | Applies? | Depth |
|---|---|---|
| SAST | ✅ | Dedicated Go taint analyzer with a framework-aware entry-point policy; intra-file N-hop interprocedural + cross-file workspace pass. |
| SCA | ✅ | go.sum parser (see limits below). |
| Secrets | ✅ | Language-agnostic. |
| Config | ✅ | Language-agnostic. |
| API | ✅ | OpenAPI/GraphQL + source route-handler extraction. |
The taint engine
Sources it recognizes
Known-input calls (GO_KNOWN_INPUT_CALLS) — substring-matched on an
assignment RHS, covering the standard library and the major frameworks:
net/http: r.URL, r.URL.Query, r.FormValue, r.PostFormValue, r.Body,
r.Header, r.Cookie, r.RemoteAddr, r.Host, req.*, request.*
gorilla: mux.Vars
gin: c.Query, c.Param, c.PostForm, c.GetRawData, c.ShouldBindJSON, c.BindJSON
echo: c.QueryParam, c.FormValue, c.Bind
fiber: c.Query, c.Params, c.Body, c.BodyParser
chi: chi.URLParam
Lambda: event.QueryStringParameters, event.Body, event.PathParameters
std: os.Getenv, os.Args, os.Stdin, bufio.NewScanner
Provenance tiering: os.Getenv → Env; os.Args/os.Stdin/bufio.NewScanner
→ LocalCli; everything else (request-bound values) → Remote.
Deserialization propagators (GO_UNMARSHAL_CALLS) — the (src, &dst) shape
taints dst: Unmarshal, UnmarshalJSON/YAML/XML, Decode, BodyParser,
BindJSON, Bind, ShouldBindJSON, ShouldBind.
Entry-point policy
Because Go handlers are ordinary functions, the default heuristic policy
(DecoratorAndNameHeuristic) classifies entry points several ways, and every
parameter of a classified function is seeded as Remote taint:
- Exact names —
ServeHTTP,Handler,main,lambdaHandler,index. - Name affixes — prefix
Handle/handle, suffixHandler/Handle. - Request-shaped parameter — a parameter named
r/req/request/c/ctxwhose annotation contains a known request type (http.Request,gin.Context,echo.Context,fiber.Ctx,events.APIGatewayProxyRequest, …).context.Contextis explicitly excluded unless it is an API Gateway event. - Registered closures — a closure passed to
app.HandleFunc,r.GET,router.Use, etc. - gRPC — a method whose receiver type contains
Serverand whose parameter is a package-qualified...Requestmessage.
A deliberate FP fix: bare local Request/*Request types are not
request-bound — only package-qualified (protobuf) messages are — so a helper
named exportCurl(req *Request) is not treated as an HTTP entry point.
Sink families and finding classes
Sinks live in the GVL rule corpus, not the Go source; the engine surfaces the dotted callee name so a rule can match it. The exception is the open-redirect gate, which is hard-coded because it needs argument-position logic.
| Class | Sink shape | CWE |
|---|---|---|
| SQL injection | db.Exec(fmt.Sprintf("... %s", uid)) | CWE-89 |
| Command injection | exec.Command("sh", "-c", cmd) (os/exec) | CWE-78 |
| Open redirect | http.Redirect(w, r, target, code) / c.Redirect(code, loc) | CWE-601 |
| Path traversal | http.ServeFile(w, r, p) with a request-bound p | CWE-22 |
| Insecure deserialization | json.Unmarshal(body, &u) from r.Body | CWE-502 |
gofunc handleUser(w http.ResponseWriter, r *http.Request) { // entry pointuid := r.URL.Query().Get("id") // Remote sourcedb.Exec(fmt.Sprintf("SELECT * FROM u WHERE id=%s", uid)) // CWE-89}
gocmd := r.FormValue("cmd")exec.Command("sh", "-c", cmd) // CWE-78
Taint propagates through fmt.Sprintf/Errorf, string concatenation, method
calls on tainted receivers, and multi-return destructuring — all via a lexical
identifier scan, not typed sink modeling. Every go fn() surfaces as a
synthetic goroutine call so concurrency rules can match.
Cross-file (workspace) taint — on by default
Go has package-level cross-file taint, on by default for directory scans
under the same --no-workspace-taint switch as the other workspace languages
(active banner: SAST: workspace-aware (cross-file Python + JS/TS + Rust + Go taint enabled)).
- Cross-package calls (
helpers.BuildQuery(uid)) are recorded and resolved through a workspace function index built fromimportspecs. - The fixed-point loop is bounded: 12 rounds, ceilings of 1,200 files / 12,000 functions, and a 10-second wall-clock budget; exceeding a ceiling falls back to per-file taint.
- A callee parameter inherits the caller's dominant provenance tier, so
os.Args/os.Getenvsources keepLocalCli/Envacross the boundary rather than being upgraded toRemote.
Honest cross-file limits: only ./ and ../ relative import paths resolve
— github.com/..., GOPATH, vendor/, and stdlib are unresolved; only
function_declaration targets are indexed (cross-package methods are out of
scope); and only two-segment pkg.Func() selector shapes are handled.
Sanitizer / guard recognition
Structural dominance-proof guard analysis exists (taint_go/guard.rs) with
early-exit kinds return, break, continue, goto, but ships inert in
production: no shipped Go rule declares dominating_guards:, and the guard runs
only as a log-only observational signal — the token-scan stays authoritative.
Sanitized reassignments are recognized at the matcher sink gate via each rule's
sanitizers: list and sanitize_*/escape_*/html.EscapeString heuristics.
SCA — go.sum
Go dependency analysis parses go.sum only, emitting Ecosystem::Go,
feeding OSV/license/health/reachability. Entries are deduped on
(module, version), with the /go.mod suffix stripped.
Limit: go.mod is not parsed, so there is no direct-vs-transitive
distinction — every dependency is marked Direct. A go.mod cross-check is a
documented deferred item.
Tier-1 corroboration with gosec
Go is the best example of Gadriel's Tier-1 aggregation model (see
tiers.md). When you pass --corroborate (opt-in, off by default;
also [scan] corroborate in .gadriel.toml), Gadriel runs your locally
installed gosec over the scan root (gosec -fmt=sarif ./...) and folds its
findings in:
- gosec findings are tagged
Tier1External, kept at Medium confidence (never High by assumption), and mapped to CWE/OWASP codes (G201/G202→SQLi, G204→command injection, G304→path traversal, G401→weak crypto, …). - Merge is additive, not suppressive. On overlap (same path, line ±3, CWE
family) the higher-confidence finding is kept and the external one is attached
as
corroborated_by; a strictly-higher-confidence external finding replaces the in-house one. Disjoint gosec findings are appended — most of the value is coverage the in-house Go engine does not have (e.g. weak-crypto G401, hardcoded-credential G101). - Tier-1 is advisory — in
code doctora missing or too-oldgosecis a warning, never a gate failure. This is the framework generalizing the clang precedent to gosec/bandit/eslint.
False positives, provenance, and test scope
Go carries some of the most targeted FP gates in the scanner:
- Open-redirect three-gate — the tainted value must occupy the actual
destination argument; there must be a genuine
Remoterequest source (a sibling of an*http.Requestparameter does not count); and a same-origin.URL.String()target whose host/scheme are never assigned is treated as same-origin by construction. has_genuine_request_sourcetier check — env/CLI sources (os.Getenv,os.Args,os.Stdin) do not satisfy the open-redirect gate.context.Contextexclusion and bare-local-Requestexclusion cut common false entry points.- Test scope — Go test detection is path-based.
**/*_test.gois a hard skip (Go excludes_test.gofrom production builds, so a vuln there is unexploitable);**/*.pb.go(generated) is also skipped. Other test paths are demoted a band. - Provenance —
dominant_tier()takes the most-dangerous tier present;demote: [local_cli]/[env]rules down-weightos.Args/os.Getenvflows. - Low-confidence SAST is advisory, as for every language.
Honest scope limits
go.modnot parsed — no direct/transitive distinction in SCA.- Flow-insensitive — a re-clean is not tracked; relies on matcher sanitizers.
- Interprocedural — intra-file N-hop for
function_declarationbare-identifier callees only; method calls, index/closure callees, and goroutine-captured closures are not followed intra-file (cross-package only via the workspace coordinator). - Cross-file — only relative imports; remote/vendored packages and cross-package methods are out of scope.
- Return-value back-propagation is not modeled cross-function; multi-return is approximated by lexical over-tagging.
- No Go rule ships
dominating_guards— structural guard analysis is inert (log-only) in production. - Argument extraction is Phase-1 stubbed (only positional argument shapes).
- Sink coverage is rule-defined, not engine-defined.
- gosec corroboration is experimental, opt-in, advisory, and Medium-capped.
Related documentation
- languages.md — the cross-language capability matrix.
- scan-coverage.md — the six scan types and eight pillars.
- false-positives.md — confidence, effective risk, provenance, waivers.
- tiers.md — the three-tier model and
gosecTier-1 corroboration via--corroborate.
