← All guides

SAST: Static Application Security Testing

Code & dependencies
On this page

In plain terms

Static Application Security Testing (SAST) reads the source code you write and looks for insecure patterns, without running the program. It's the security equivalent of a proofreader who knows the ways code tends to go wrong: building a SQL query by gluing strings together, using a broken hash function, shelling out with unsanitized input.

Where SCA checks the code you reuse, SAST checks the code you author.

Why it matters

Whole categories of serious bugs are recognizable by shape: SQL injection, command injection, cross-site scripting, path traversal, hardcoded credentials, weak cryptography. Catching them while the code is still in a pull request is dramatically cheaper than after it ships. SAST is a core part of "shift-left" security (more here).

Where it fits

  • Artifact: your source code.
  • Stage: in the editor and on every pull request. SAST needs no deployment, so it's a natural continuous-integration (CI) gate on the diff.

How it works

Early SAST tools were simple pattern matchers (glorified grep). Modern ones parse code into an abstract syntax tree and reason about it, and the best perform taint analysis: tracking whether untrusted input (a request parameter) can flow into a dangerous sink (a database query) without being sanitized. That data-flow awareness is what separates a useful finding from noise.

The same sink, two paths to it A request parameter flows to two different calls to the same database query function. Along the first path it passes through an escaping function, so the value that arrives has been sanitized and there is no vulnerability. Along the second it arrives unchanged, and that is the injection. A tool that matches on the shape of the query call reports both call sites; a tool that tracks where the value came from reports the second only. WHY A MATCH IS NOT YET A FINDING req.query.id untrusted input escape(id) db.query(sql) reaches it escaped not a finding req.query.id untrusted input nothing on this path db.query(sql) reaches it whole this is the finding Both rows call the same function on the same line of a query builder. A tool matching the shape of the call reports two. A tool tracking where the value came from reports one. The second row is the one anybody fixes, and the first is how a team learns to ignore the tool.
Taint analysis asks a different question rather than a deeper version of the same one, replacing
does this look dangerous with can attacker-controlled input reach it.
ToolNotes
SemgrepOSS, polyglot, fast; rules are readable patterns anyone can write
gosecOSS, Go-specialized; type-aware, deep on Go idioms
BanditOSS, Python-specific
CodeQLPowerful semantic analysis; free for open source, via GitHub
SonarQube, Checkmarx, Snyk CodeCommercial platforms with broad language + policy coverage

Common pitfalls

  • False positives. SAST's biggest tax. A pattern that looks injectable may be safe in context; too much noise trains people to ignore it. Tuning rules to your codebase matters.
  • Language depth varies. A generic polyglot ruleset is shallower than a language-specialized analyzer (e.g. gosec for Go). Best coverage often means the right tool per language.
  • It can't see runtime. Logic flaws and configuration issues that only appear when the app runs are DAST territory, not SAST.
How Draugr fits. Draugr's sast control runs Semgrep over each component's source and normalizes findings to SARIF. Semgrep is the default because it needs no build step and covers many languages, which is the right trade for a control that runs on every pull request.

The pitfall above is answered by running the specialist alongside the generalist rather than instead of it. gosec is type-aware and deep on Go idioms, and turning it on is gosec: {enabled: true} under the control. Both scanners' findings arrive in one report ranked by one model, and a flaw both of them report is counted once, with the row naming the other tool. Depth on one language costs a line rather than a second pipeline. See running two scanners on one control and the control catalog.

Semgrep leaves out a file over 1 MB, and abandons a rule that times out on a file, and still exits clean. Each such file is listed under Caveats as unread rather than counted as checked. Exclude generated files with ignore:, or fail the run on them with failOnCaveats: [unread-files] in the descriptor's gate.

Keep learning