Safeguard
Security Guides

The gosec Static Analysis Guide: Rules, CI, and Taming False Positives

gosec catches hardcoded secrets, weak crypto, unsafe SQL, and command injection in Go — but only if you run it well and triage it honestly. A practical guide to the rules that matter and the noise that doesn't.

Daniel Osei
Security Researcher
6 min read

gosec is the closest thing Go has to a standard security linter, and most teams that adopt it fall into one of two traps: they run it once, drown in findings that include a pile of false positives, and disable it — or they never tune it and let a wall of low-severity noise train developers to ignore the output entirely. Used well, gosec is genuinely valuable: it catches hardcoded credentials, weak cryptography, unsafe SQL construction, and command-injection surfaces before they merge, mapping each finding to a CWE. Used badly, it's security theater. This guide is about the difference — which rules carry real weight, how to run it in CI without blocking on noise, and how to suppress the false positives correctly so the true ones still stand out.

What is gosec actually checking, and how do you run it?

gosec parses your Go AST and abstract call information looking for known-dangerous patterns, then emits findings tagged with a rule ID (Gxxx), a severity, a confidence, and a CWE.

go install github.com/securego/gosec/v2/cmd/gosec@latest
gosec ./...

Each finding carries two axes worth internalizing: severity (how bad if real) and confidence (how sure gosec is it's real). A HIGH-severity/HIGH-confidence hit is where you look first; a MEDIUM-severity/LOW-confidence hit is often a pattern that's fine in context. You can filter directly:

gosec -severity medium -confidence medium ./...   # cut low-value noise
gosec -fmt sarif -out results.sarif ./...         # for code-scanning UIs

The SARIF output matters because it plugs into GitHub code scanning and most security dashboards, turning findings into tracked, deduplicated issues instead of console spew.

Which gosec rules carry the most weight?

A handful map to the vulnerabilities that actually cause incidents. Learn these by ID:

RuleCatchesCWE
G101Hardcoded credentials / secrets798
G201 / G202SQL built via format string / concatenation89
G204Subprocess with variable command/args78
G304File path from a variable (traversal)22
G401 / G501Weak crypto: MD5/SHA1/DES for security use327
G402TLS InsecureSkipVerify / weak config295
G404math/rand where crypto randomness is needed338
G601Implicit memory aliasing in range loops

G101 alone justifies running gosec in most shops — a hardcoded API key or password caught pre-merge is a breach that never happened. G402 (InsecureSkipVerify: true) and G404 (math/rand for tokens or IDs) are the two that developers most often introduce "temporarily" and forget. G304 pairs naturally with adopting os.Root (Go 1.24+) as the structural fix for path traversal rather than ad-hoc cleaning.

How do you run it in CI without crying wolf?

Gate on high-severity, high-confidence findings; report the rest without failing the build.

# GitHub Actions
- name: gosec
  uses: securego/gosec@master
  with:
    args: '-severity high -confidence medium -fmt sarif -out gosec.sarif ./...'
- uses: github/codeql-action/upload-sarif@v3
  with:
    sarif_file: gosec.sarif

The principle is progressive enforcement. Day one, if you fail the build on every finding in a mature codebase, you'll get an override commit within the hour and lose the check forever. Instead: block on HIGH/HIGH (the ones that are almost always real and serious), surface everything else as non-blocking annotations in the PR, and ratchet the threshold down as the backlog clears. Track findings in a dashboard via SARIF so a suppressed or accepted finding stays visible rather than vanishing.

How do you suppress a false positive without hiding real ones?

Use a narrowly scoped #nosec annotation with the specific rule ID and a justification comment — never a blanket disable.

gosec will flag patterns that are safe in context — an allowlisted exec.Command (G204), or MD5 used for a non-security checksum (G401). The correct suppression is surgical:

// This hash is a cache key, not a security control.
h := md5.Sum(data) // #nosec G401 -- non-cryptographic cache key

Three rules make suppression trustworthy: name the exact rule ID (#nosec G401, not bare #nosec, which silences everything on that line), include a reason a reviewer can evaluate, and require that any #nosec be approved in code review like any other security decision. A bare #nosec sprinkled to make the linter quiet is how real findings disappear. Audit for annotation drift periodically — grep your codebase for #nosec and confirm each one still has a valid justification.

Where does gosec stop, and what fills the gap?

gosec is a pattern matcher on a single repository's source — it doesn't do cross-service dataflow, dependency CVEs, or confirm exploitability at runtime.

gosec will tell you a Query is built with Sprintf (G201), but it won't trace whether that Sprintf input actually comes from an untrusted request across function boundaries, and it knows nothing about vulnerabilities in your dependencies — that's govulncheck and software composition analysis territory. It also can't confirm a finding is reachable and exploitable; a G204 hit on thoroughly allowlisted code is a true pattern match but not a vulnerability. Treat gosec as your fast, in-editor and in-CI first pass, and layer dataflow-aware SAST and runtime validation on top for the findings that need proof rather than pattern-matching.

The gosec adoption checklist

  • Install and run gosec ./...; adopt SARIF output from day one.
  • Gate CI on HIGH severity + HIGH/MEDIUM confidence; report the rest.
  • Prioritize G101, G201/G202, G204, G304, G401/G501, G402, G404.
  • Require rule-scoped #nosec with a justification, reviewed like code.
  • Ratchet the gate stricter as the backlog clears.
  • Layer govulncheck and dataflow SAST for what gosec can't see.

How Safeguard Helps

gosec finds the pattern; Safeguard tells you whether it's a real, reachable vulnerability. Run static analysis through the Safeguard CLI alongside dataflow analysis that traces tainted input across function and package boundaries, so a G204 or G201 hit is confirmed as exploitable or dismissed with evidence rather than left as a maybe. Dynamic testing (DAST) exercises the running service to prove it out, and Griffin, the AI analysis engine, triages each finding and drafts the fix — cutting the false-positive burden that makes teams abandon their linter. Comparing SAST platforms for Go? See our Checkmarx comparison.

Run security analysis on your Go code free at app.safeguard.sh/register, with docs at docs.safeguard.sh.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.