Skip to content
Français
Account: not signed in, sync available My learning
Select theme
Français

SAST: analysing source code to find flaws

By updated

Improve this page

SAST (Static Application Security Testing) reads the source code of an application without running it and spots the flaws that you wrote yourself: injections, unvalidated data, dangerous calls. This lesson is for a DevOps engineer or a developer who wants to understand what a SAST scanner really sees, what it does not see, and how to place it in a pipeline without drowning the team in alerts. The examples rely on Semgrep 1.178.0, run against a small, deliberately flawed Flask application.

What you will learn

  • Understand how a scanner follows data from its source to its sink
  • Distinguish what SAST, SCA and DAST see, and what escapes them
  • Interpret a real SAST result and spot the noise it contains
  • Place SAST in a pipeline with graduated blocking
  • Handle a false positive without disabling the control for everyone
  • Link this control to requirement SOCLE-APP-AST-1 (in French) of the SOCLE framework

What is SAST?

SAST is a static analysis: the tool reads the code like a very patient reviewer, without launching the application. It answers a precise question, "does my code contain dangerous constructs?", and that precision defines both its strength and its limits. It sees all the code, including a branch never executed by the tests, but it sees neither the running application nor the code of the libraries it imports.

How does a SAST scanner reason?

A modern scanner does not just look for words in the text. It builds a representation of the program, then follows the data path. The vocabulary is simple: a source is an entry point controlled from the outside (an HTTP parameter), a sink is a dangerous call (an SQL query, a system command), and taint tracking consists in checking whether data "tainted" by a source reaches a sink without being sanitised.

The diagram shows this reasoning on the SQL injection case: the data starts from an HTTP parameter, goes through a variable, and only produces a finding if it reaches the sink without having been sanitised along the way.

Data flow tracking by a SAST scanner: data starts from a source, an HTTP parameter, and goes through an intermediate variable; if it is not sanitised, it reaches a sink, an SQL query or a system command, and the taint rule produces a finding; if it is sanitised, there is no finding.noyesSource:HTTP parameterIntermediatevariableDatasanitised?Sink: SQL query,system commandSAST finding(taint rule)No finding
Source · SVG
Esc to close, + and − to zoom, scroll to move around. 100 %
  1. Parsing: the code becomes a syntax tree (AST), which makes it possible to recognise a concatenation or a function call whatever the formatting.

  2. Pattern matching: each rule describes a dangerous construct, for example "a call to subprocess with shell=True".

  3. Flow tracking: for taint-type rules, the tool links the source to the sink through the intermediate variables.

  4. Report: each finding gives the rule, the file, the line, a severity and often a hint for the fix.

What does SAST find, and what does it miss?

SAST excels at flaws that have a recognisable shape in the code: external data that ends up in a query, an obsolete hash function, uncontrolled deserialisation. On the other hand, it is structurally blind to anything that cannot be written as a local construct: a forgotten access control in the business logic, a flaw in a dependency, a misconfiguration of the production server.

SAST sees wellSAST does not see
SQL, command, path injections, server-side XSSA missing access control in the business logic
Weak cryptography (MD5, SHA-1, ECB mode)Flaws in third-party dependencies
Deserialisation of untrusted dataThe real configuration of the deployed server
Dangerous system calls (shell=True, eval)What depends on execution (session, timing)

SAST, SCA, DAST: who sees what?

These three families of controls complement each other because they look at three different objects. SAST reads your code, SCA (Software Composition Analysis) inventories the libraries you import and checks them against published vulnerabilities, DAST attacks the running application. The Equifax breach in 2017, with 147 million people exposed, illustrates this well: it went through a known flaw in Apache Struts, a dependency that had not been updated. It is a textbook case for SCA, not for SAST, which does not read the Struts code.

ControlWhat it readsWhenExample of a flaw found
SASTyour source codefrom the commitSQL injection in your route
SCAyour dependenciesfrom the commitvulnerable Struts (Equifax)
DASTthe running applicationafter a test deploymentmissing security header, exploitable flaw

What does a real SAST result look like?

Nothing replaces a real scan to understand the job. The test application fits in two Flask routes: /user builds an SQL query by concatenation, /ping passes a parameter to a system command with shell=True. The scan uses the p/python rule set from the Semgrep registry, downloaded at launch, and the official image pinned by digest. The --metrics=off option stops the sending of usage statistics to the vendor.

Terminal window
IMG="semgrep/semgrep:1.178.0@sha256:32e459968daabe7ab86968184a29109b9564aa00392401156f9788452b42786b"
docker run --rm -v "$PWD:/src" -w /src "$IMG" \
semgrep scan --config p/python --metrics=off .

The scan reports five findings for two real flaws:

LineRuleSeverity
9sql-injection-db-cursor-execute (Django rule)WARNING
11tainted-sql-string (Flask rule, taint)ERROR
19subprocess-injectionERROR
19dangerous-subprocess-useERROR
19subprocess-shell-trueERROR

This table already holds the two lessons of the job. First, several rules report the same flaw: three findings for a single line 19. Second, a rule written for Django fires on Flask code: it is right on substance, but its title is misleading. Triage therefore starts even before talking about false positives, and it justifies choosing a rule set suited to your framework rather than enabling the whole registry.

The costliest trap lies elsewhere: this scan exits with code 0, despite five findings. Without the --error option, semgrep scan fails no pipeline, and a "green" job can hide injections. With --error, the same scan exits with code 1, and only then does the control become a blocking control.

Choosing a SAST tool

The choice rests on three criteria: the languages of your repositories, the licence with regard to your usage, and how easy it is to write your own rules. In this course, Semgrep is the reference implementation, for its readable rules and its SARIF output, the standard format that GitHub and GitLab can display. The next lesson is entirely devoted to it.

ToolLanguagesLicence and usageStrength
Semgrep (CE)30+free engine, LGPLreadable rules, fast, SARIF
BanditPythonfree, Apache 2.0simple, Python-specific
BearerJS, TS, Ruby, Python, Java, Go, PHPsource available, Elastic License 2.0flow of sensitive data
CodeQLC/C++, C#, Go, Java, JS, Python, Ruby...free only for open source codepowerful queries, built into GitHub
SonarQube25+LGPL community edition, paid advanced editionsquality and security in one dashboard

The CodeQL row deserves a careful read. Its terms of use allow the tool on code published under an open source licence or for academic research, but explicitly exclude automated analysis in continuous integration of proprietary code without a commercial contract. On a private company repository, it therefore requires a GitHub Advanced Security licence.

The same static principle applies to targets other than application code: IaC scanners (Checkov, Trivy, KICS) read Terraform, Kubernetes or a Dockerfile, secret scanners (Gitleaks) read the code and the Git history. They have their own lessons in the course. Note in passing that tfsec has been merged into Trivy by its vendor, which now recommends migrating: a guide that still presents it as a tool for the future is out of date.

Where to place SAST in the chain?

SAST belongs in two places that do not play the same role. On the workstation, a pre-commit hook gives the developer immediate feedback, but it can still be bypassed with a --no-verify. In CI, the same scan cannot be bypassed and can block the merge. The workstation serves comfort, the CI serves the guarantee: requirement SOCLE-APP-AST-1 asks precisely for a SAST run on every build, with blocking critical findings.

On the workstation: the pre-commit hook

Semgrep publishes its hooks in the semgrep/pre-commit repository, versioned like the tool. The hook only scans the modified files of the commit, which keeps it fast. The --skip-unknown-extensions option prevents it from failing on a file type that no rule covers.

.pre-commit-config.yaml
repos:
- repo: https://github.com/semgrep/pre-commit
rev: v1.178.0
hooks:
- id: semgrep
args: ["--config", "p/python", "--error", "--skip-unknown-extensions"]

In CI: a job that really blocks

The old semgrep-action is deprecated by its vendor, who recommends running Semgrep directly in its official image. Both examples below follow this recommendation, with the image pinned by digest and the --error option that turns findings into a job failure.

The job runs in the Semgrep container. permissions: {} removes all default permissions, contents: read is enough to clone, and persist-credentials: false prevents the token from staying in .git/config.

.github/workflows/sast.yml
name: SAST
on:
pull_request:
permissions: {}
jobs:
semgrep:
runs-on: ubuntu-24.04
permissions:
contents: read
container:
image: semgrep/semgrep:1.178.0@sha256:32e459968daabe7ab86968184a29109b9564aa00392401156f9788452b42786b
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Semgrep, fail on finding
run: semgrep scan --config p/python --error --metrics=off .

Blocking only new flaws

Enabling a blocking SAST on an old repository makes every merge request fail because of the existing debt, and the team ends up disabling the control. The --baseline-commit option solves this problem: Semgrep compares against the given commit and only reports the findings introduced since. Measured on the test repository, the full scan reports 5 findings; with the main branch as the reference, it only reports 3, those of the new /ping route, and it does exit with code 1 with --error.

Terminal window
# Only report what was introduced since the main branch
semgrep scan --config p/python --error --baseline-commit "$(git merge-base origin/main HEAD)" .

The debt does not disappear for all that: it is handled in a dated backlog, with a scheduled full scan alongside, otherwise the baseline becomes a permanent blind spot.

Which blocking strategy?

Blocking on everything paralyses teams, blocking on nothing produces reports nobody reads. A graduated policy sets, by severity, what blocks the merge and what only opens a ticket. The reasonable starting point: block on the ERROR severities of taint-type rules, which follow real external data, and handle the rest in review.

Semgrep severityActionExample
ERRORblock the mergeHTTP data reaching an SQL query
WARNINGticket with a deadlinesuspicious construct without a proven flow
INFOinformation in reviewbest practice not followed

Handling false positives

A false positive is an alert on code that is not actually exploitable: the data is validated elsewhere, or the code is a test example. Badly handled, false positives destroy trust in the tool: a team that ignores a hundred alerts ends up ignoring the hundred and first, the real one. Three mechanisms exist, from the most targeted to the broadest, and the most targeted is almost always the right one.

Ignoring a specific finding: nosemgrep

The nosemgrep comment goes on the line where the finding is reported, not on the line that seems faulty to you. Measured on a variant of the test application, where the query is first built in a query variable then passed to cursor.execute(query): a # nosemgrep placed on the execute line removed nothing, because the findings were reported on other lines. Placed on the line that builds query, it removed that single finding, and the other rule that reported the same flaw stayed active.

# The form that names the rule only silences that rule, on this line
query = "SELECT * FROM users WHERE id = " + user_id # nosemgrep: python.flask.security.injection.tainted-sql-string.tainted-sql-string

Always name the rule and justify the exception in code review. Measured: with the exact identifier, the finding disappears; with the identifier of another rule, it stays, which protects you from an overly broad exception. A bare nosemgrep, with no identifier, on the other hand silences every rule on the line. For Bandit, the equivalent is # nosec B105, with the test identifier.

Excluding paths: .semgrepignore

The .semgrepignore file, at the root of the repository, excludes whole paths with the syntax of a .gitignore. A measured trap changes everything: without this file, Semgrep applies a default exclusion list that already leaves out tests/, and as soon as a .semgrepignore exists, it replaces that list instead of adding to it. A file that only contains qa/ therefore suddenly makes Semgrep scan tests/, and an empty file makes it scan everything. Always carry over the default exclusions you want to keep. It is the right tool for generated code or datasets, and a bad one for tests that contain real logic.

.semgrepignore
tests/
vendor/
*.min.js

Changing rules rather than piling up exceptions

When the same rule produces false positives everywhere, the problem is no longer the line but the rule set. Replace a broad registry with the one for your framework, or write a more precise custom rule: that is the subject of the Semgrep lesson. A global exclusion that grows with every sprint is a disabled control that does not say so.

Troubleshooting

Half of SAST incidents are not flaws but pipelines that lie. A green job proves nothing until you have checked that it can fail: push a deliberate flaw once on a test branch and watch the job go red.

SymptomLikely causeSolution
Green job despite findingssemgrep scan without --error exits with code 0Add --error
Error while downloading the rulesNo network access to the Semgrep registryOpen the access or version the rules in the repository
nosemgrep has no effectComment on the wrong linePut it on the line reported by the finding
Every MR fails on old codeExisting debt--baseline-commit and a dated backlog
A Django rule on Flask codeRegistry too broadChoose the framework's rule set

Key points

  1. SAST reads your code without running it and follows data from its source to its sink.
  2. It sees neither the dependencies (SCA) nor the running application (DAST): Equifax was an SCA case.
  3. A real scan produces duplicates: several rules for a single flaw.
  4. Without --error, semgrep scan exits with code 0 and blocks nothing.
  5. --baseline-commit only blocks new flaws; the debt is handled separately.
  6. nosemgrep goes on the reported line, naming the rule.
  7. CodeQL is only free for open source code.
  8. Evidence expected by SOCLE-APP-AST-1: a blocking SAST job and tickets on the critical findings.

Knowledge check

Check that you have the essentials of this lesson. The questions only cover what has just been explained here.

Knowledge check

Check what you have learned with this interactive quiz

6 questions
6 min
70% to pass

How it works

  • The timer starts when you click Start
  • Multiple choice, true/false and short answer questions
  • You can move between questions
  • Detailed results are shown at the end

Starts the quiz and the timer

Is this project useful to you?

The guides, courses, quizzes and labs stay free. If you use them regularly, you can contribute to their upkeep and development.

Become a supporting member (new tab) One-time support (new tab) Learn more

Follow the project on LinkedIn