Tools Used in DevSecOps · Semgrep

Semgrep

Semgrep is an open-source static analysis engine whose whole pitch is that a rule should look almost exactly like the code it's looking for. Instead of a regex that breaks the moment someone reformats a file, or a heavyweight commercial engine that needs its own query language and a compiled model of your entire codebase before it can answer anything, Semgrep parses your source into a real abstract syntax tree and matches a rule — itself parsed the same way — directly against that structure. That single design choice is why it's become the default first SAST gate on most modern pipelines: fast enough to run on every pull request, and readable enough that an application team, not just a security specialist, can write and own its own rules.

☺ Explain it like I'm 10

Imagine looking for a specific kind of sentence in a book — "someone said something, then someone else disagreed" — no matter what the exact words were. A find-and-replace search can only look for exact words, so it's useless here. Semgrep is like giving your search a little fill-in-the-blank template instead: "___ said ___, but ___ disagreed" — and it understands sentence structure well enough to find every match, even if one is "Maya said the sky was green, but Priya disagreed" and another is spread across three lines with different names entirely. It's not reading the whole book's plot to understand every character's motive (that's a much slower, much deeper kind of reading) — it's just very good at spotting the shape of a sentence.

🐢Your host for this topic: Timmy the Turtle — Semgrep is the first scan Timmy actually runs, on every single pull request, because it's the one fast enough not to make anyone wait.

What Semgrep is, and the problem it solves

☺ Like you're 10: It's the middle option between "dumb text search" and "an engine so deep it needs a PhD and an hour to run" — fast like the first, structure-aware like the second.

Semgrep (short for semantic grep) is an open-source, Apache-2.0-licensed static analysis CLI originally built by r2c, now developed by Semgrep Inc. It ships as a single pip-, brew-, or Docker-installable binary that needs no server, no database, and no network access to run: point it at a directory, give it a ruleset, and it parses every matching file, checks each rule's pattern against that file's structure, and prints findings. The matching engine itself — semgrep-core — is written in OCaml for speed; the CLI wrapper around it has historically been Python, with the project steadily moving more of that wrapper into the compiled core over time, so treat exact implementation-language claims as something to verify against the project's own repository rather than take as fixed.

The gap Semgrep fills is specifically the one between two bad options. A plain grep or regex-based linter has no idea what a comment, a string literal, or an argument boundary is, so it either misses reformatted code entirely or drowns real findings in false positives from a hit inside a code comment. At the other extreme, engines like CodeQL compile your entire codebase into a queryable relational database to support genuine interprocedural taint tracking — powerful, but slow enough that it doesn't belong on every commit, and its query language has a real learning curve. Semgrep sits deliberately in between: real parsers, real syntax trees, a rule syntax that reads like the target language itself, and a matching pass that's fast enough to gate a pull request without anyone noticing the wait. See SAST, DAST & SCA if you haven't already — it places static analysis as one of three pipeline scan categories; this page goes one tool deep.

◆ Key idea

Semgrep's core trade is speed and approachability for depth. It parses and matches one file at a time, independently — there's no whole-program compilation step by default. That's exactly what makes it fast enough for every commit, and exactly why it can miss a vulnerability that only exists as a chain across several files. Keep that trade in mind for the whole rest of this page.

Rule syntax: matching an AST, not compiling a whole-program database

☺ Like you're 10: A rule is a fill-in-the-blank version of the code you're worried about — Semgrep parses your real code the same way it parses the blank, then looks for a shape match.

A Semgrep rule is a YAML file with a pattern field, and that field is written in the target language itself, not in a separate query DSL. Write $CURSOR.execute($QUERY) as a Python pattern and Semgrep parses that string with Python's own grammar, producing a small pattern AST. It then walks the AST of every Python file in scope looking for a subtree with the same shape — same node types, same structure — ignoring whitespace, comments, and variable naming. A dollar-prefixed identifier like $QUERY is a metavariable: a wildcard that matches any single expression and binds to whatever it matched, so the same metavariable name reused later in the pattern ($X == $X) requires the same code to appear both times, not just any two expressions — that's the mechanism a rule uses to catch a self-comparison tautology, for instance. An ellipsis (...) matches zero or more of anything — arguments, statements, list elements — the difference between "this exact call" and "this call, with whatever else around it I don't care about."

OperatorMatchesTypical use
patternA single structural shapeThe base case — one thing you're looking for
pattern-eitherAny one of a list of patterns (logical OR)Several call shapes that are all equally dangerous
pattern-notExcludes a shape from an outer match (logical AND NOT)Carving out a known-safe variant, e.g. a literal string argument
pattern-inside / pattern-not-insideRequires (or forbids) the match to be nested inside another patternScoping a rule to one function, one class, one try block
$METAVARAny single expression; binds a name for reuse and messagesCapturing the exact tainted value for the finding message
... (ellipsis)Zero or more of anything — args, statements, elements"This call, regardless of its other arguments"

Rules can go one step deeper than pure structural matching with taint mode (mode: taint), which adds a lightweight source-to-sink dataflow pass within a single file or function: you declare pattern-sources (where untrusted data enters), pattern-sinks (where it becomes dangerous), and optionally pattern-sanitizers (what neutralizes it in between), and Semgrep traces whether a source's value can reach a sink through local variable assignment without passing through a sanitizer — the same source/sink/sanitizer model covered generally in static analysis & secrets detection. That's real, and useful, but it's still a per-file (increasingly per-project with Semgrep's paid cross-file/cross-function analysis) pass, not the kind of true whole-program interprocedural taint tracking CodeQL gets by compiling your entire codebase into one relational database first. Keep that distinction precise: Semgrep's dataflow is an addition bolted onto AST pattern matching, not a different foundation underneath it.

Your rule's pattern (YAML) db.run($Q, ...) parsed with the target language's own parser Target file: checkout.py db.run(base + city, timeout=5) parsed the same way, independently Pattern AST fragment Full AST of the file Structural match, modulo formatting $Q binds to "base + city" — same shape, ignores style Finding: file, line, message, metavariable bindings

The practical upshot: each file is a closed world. Semgrep never has to know what checkout.py imports from a module three directories away in order to match a rule against it, which is exactly what keeps a scan measured in seconds rather than minutes. It's also exactly the ceiling this page keeps returning to — the comparison against CodeQL and SonarQube later on is really a comparison of what each tool gave up to get its speed, or gave up its speed to get.

The community ruleset versus a rule you write yourself

☺ Like you're 10: Off-the-shelf rules catch things every codebase might have wrong; a rule you write yourself catches the one mistake that's specific to how your team's own code works.

Most Semgrep runs start with the free Registry at semgrep.dev/r — thousands of community- and vendor-contributed rules, organized into named packs you reference with a p/ prefix. semgrep scan --config=auto is the common default: it detects which languages and frameworks are present in the repository and pulls the relevant curated packs automatically, and if you're logged in via semgrep login, it also pulls whatever custom rules and policies your organization has configured centrally. Narrower, hand-picked packs are the more reproducible choice for a merge-blocking gate:

$ semgrep scan --config=p/ci .                 # curated, low-noise pack made specifically for CI gating
$ semgrep scan --config=p/owasp-top-ten .       # rules mapped to the current OWASP Top Ten categories
$ semgrep scan --config=p/security-audit .      # broader, higher-recall — expect more findings to triage
$ semgrep scan --config=p/python --config=p/flask .   # stack-specific packs, composable

The community rulesets are broad by necessity — written to be roughly right across every codebase that might install them — which means they can't see the one anti-pattern that only exists inside your own organization's own code. This course has a running example of exactly that gap: Secure Coding Patterns follows Benny building queries through an internal db.run() wrapper instead of the raw database driver, and Timmy's off-the-shelf ruleset never flags it, because it was trained to recognize the driver's own execute() call as a sink — it has no idea a wrapper three layers down is the same thing. That's not a bug in the ruleset; it's the exact shape of thing a generic pack structurally cannot know. The fix is a custom rule, and it's worth writing the fuller taint-mode version this time, so it also catches the unsafe value flowing through an extra layer of local variables before it ever reaches db.run():

# .semgrep/rules/acme-db-run-taint.yaml
# Off-the-shelf rules know the driver's execute() call as a sink.
# They've never been taught that OUR db.run() wrapper is the same thing.
rules:
  - id: acme-db-run-unsanitized-query
    mode: taint
    languages: [python]
    severity: ERROR
    message: >
      Untrusted input reaches db.run() without going through
      its own bind-parameter form. See go/db-wrapper.
    metadata:
      cwe: "CWE-89: SQL Injection"
      owasp: "A03:2021 - Injection"
      confidence: HIGH
    pattern-sources:
      - patterns:
          - pattern-either:
              - pattern: request.args.get(...)
              - pattern: request.form.get(...)
              - pattern: request.json[...]
    pattern-sinks:
      - patterns:
          - pattern: db.run($QUERY, ...)
          - pattern-not: db.run("...", ...)     # a pure string literal has nothing to inject
    pattern-sanitizers:
      - pattern: db.run($QUERY, params=$PARAMS)  # our wrapper's own bind-parameter form

Notice the shape of the fix: it's not "ban db.run()," which would just get the rule disabled by an annoyed team the first time it fires on a safe call — it's "teach the scanner that our own wrapper is a sink, and that our own wrapper's parameterized form is the sanitizer that makes it safe." That's the whole value proposition of writing your own rules instead of only running the Registry: the Registry knows every public framework's dangerous calls; only your team knows your team's. See static analysis & secrets detection for how a rule like this gets wired into a required, merge-blocking check, and for the baselining and confidence-threshold tuning that keeps a growing rule set from burying real findings under noise.

Day-to-day commands

☺ Like you're 10: One command to look, one command to gate a build, and a couple of ways to say "I already know about this one" without turning the rule off for everyone else.

# local, exploratory scan — good for authoring and testing a rule
$ semgrep scan --config=p/ci .
$ semgrep scan --config=.semgrep/rules/acme-db-run-taint.yaml .   # test one rule file directly

# semgrep ci — the CI-specific subcommand, not just "scan run in CI"
# diff-aware: only NEW findings on this branch, versus the baseline, are blocking
# logged-in orgs get their configured rulesets automatically, no --config needed
$ semgrep login
$ semgrep ci

# scope a scan to only what changed since a commit, without the ci subcommand's extras
$ semgrep scan --config=p/ci --baseline-commit=origin/main .

# machine-readable output for a dashboard, DefectDojo, or GitHub code scanning
$ semgrep scan --config=p/ci --sarif -o results.sarif .
$ semgrep scan --config=p/ci --json -o results.json .

# apply a rule's own fix: suggestion, not blind autofix — review the diff before it merges
$ semgrep scan --config=.semgrep/ --autofix --dry-run .

Two escape hatches matter for keeping a gate trustworthy rather than something people route around. A .semgrepignore file — same syntax as .gitignore, and Semgrep also respects .gitignore automatically — excludes whole paths: generated code, vendored dependencies, test fixtures deliberately full of "unsafe" examples. An inline // nosemgrep: acme-db-run-unsanitized-query (or # nosemgrep: rule-id, comment syntax per language) suppresses one specific line for one specific rule — always name the rule ID rather than a bare nosemgrep, which silences every rule on that line, including ones added after the comment was written and long forgotten.

Gotchas and failure modes

☺ Like you're 10: Most of the surprises come from the same root cause — a rule matches shape, not meaning, so it can be fooled by anything that looks right but isn't, or looks wrong but is actually fine.

The failure modes cluster around the same tradeoff the rest of this page keeps naming.

⚠ Watch out

The single most common way teams sour on Semgrep is running a broad, high-recall pack (p/security-audit or an unscoped --config=auto) as a hard merge-blocking gate on day one, with no baseline. A legacy repository can surface hundreds of pre-existing findings in one run, and a team facing that wall almost always disables the gate entirely rather than triaging four hundred items before their next release. Run once against the existing codebase, accept the current findings as a baseline with --baseline-commit, and gate only on new findings a change introduces — the same principle covered generally in static analysis & secrets detection, and the difference between a gate people trust and one they click past out of habit.

Where Semgrep sits: speed versus depth against CodeQL and SonarQube

☺ Like you're 10: Three tools, three different trades between how fast they run and how deep they actually think — and the right answer is usually more than one of them, at different points in the pipeline.

All three tools are legitimate SAST engines, and the real question is rarely "which one is best" — it's which stage of the pipeline each one earns its keep at.

ToolAnalysis basisTypical speedDepthFits best at
SemgrepPer-file AST pattern matching; lightweight taint mode within a file/projectSeconds to low minutes, even on a large repoShallow by default, deliberately — no whole-program databaseEvery commit, every pull request; the fast, first, and cheapest gate
CodeQLCompiles the codebase into a relational database; real interprocedural taint tracking via QL queriesMinutes to tens of minutes; a full build step for compiled languagesDeep — traces a tainted value across function and file boundariesScheduled or nightly scans; free on public GitHub repos via GitHub Advanced Security
SonarQubeA bundled multi-language rule engine plus "Security Hotspots" flagged for human review, wrapped in a Quality GateLow minutes; scales with a persistent server, not a stateless CLIModerate — broad coverage and code-quality metrics, shallower dataflow than CodeQLA team-facing dashboard combining security and code quality, with a human review step baked in

A mature pipeline rarely picks just one. Semgrep runs first because it's cheap enough to run on literally every commit without anyone noticing the wait — that's the whole reason it exists. CodeQL earns its much higher cost on a schedule, or on the services where an interprocedural taint chain is actually worth the wait to catch, rather than on the hot path of every PR. SonarQube's Quality Gate workflow fits teams that want security findings sitting next to code-quality metrics in one dashboard, with Security Hotspots specifically designed to be triaged by a human rather than auto-blocking — a different philosophy from Semgrep's "clean means clean, findings mean fail" model. None of the three replaces the other two; each fills the gap the others' speed-versus-depth trade leaves open. See vulnerability management & triage for how findings from more than one of these get deduplicated into a single backlog once you're running more than one.

🎬 At the Shift-Left Squad
🦫

Benny the Beaver: Fixed the db.run() thing for real this time — bound parameters, no more f-strings. I also wrote the taint rule Timmy wanted so it never sneaks back in.

🐢

Timmy the Turtle: Ran clean, and it's fast — this whole repo scans in under a minute. That's why it's the gate on every single PR, not just the important ones.

🦊

Foxy: Under a minute, every commit? How is that even possible with everything Rocky finds crawling through this code?

🐢

Timmy the Turtle: Because it never builds a model of the whole program — it just parses each file and matches the shape of your rule against it, one file at a time. CodeQL does build that whole-program model, which is exactly why it runs nightly instead of on every push.

🐘

Ellie the Elephant: Does the new rule also catch it if someone hardcodes the database password right next to the call?

🐢

Timmy the Turtle: No — wrong tool for that job on purpose. A hardcoded credential is what gitleaks exists for. I'd rather have three narrow scanners that are each right about their own lane than one scanner pretending to cover everything.

🦝

Rocky the Raccoon: Noted. I'll go looking for the next wrapper nobody's taught the rule about yet.

✓ Checkpoint

1. What does a Semgrep rule's pattern field actually get parsed into, and why does that make it different from a plain regex? 2. What's the practical difference between running --config=auto and writing a custom rule for your own internal wrapper function? 3. Name two gotchas that come specifically from Semgrep matching per-file rather than compiling a whole-program database. 4. In one sentence each, where does Semgrep sit relative to CodeQL and to SonarQube on the speed-versus-depth trade?

Check your answers
  1. It's parsed into an AST using the target language's own real parser — the same way Semgrep parses the file it's scanning. That's what lets it match structure (ignoring whitespace, formatting, and variable naming) instead of matching literal text, which is what makes it far more resilient to reformatted or renamed code than a plain regex.
  2. --config=auto pulls broad, general-purpose community rules that know common frameworks' dangerous calls (a raw SQL driver's execute(), for instance) but have no idea your team wraps that call in its own internal function three layers down. A custom rule closes exactly that gap — it teaches the scanner about a sink (or source, or sanitizer) that only exists in your own codebase.
  3. Any two of: taint tracking doesn't follow a value across function/file boundaries the way a compiled whole-program model would (and never crosses a network call into another service at all); metavariable equality is syntactic rather than semantic, so it can be fooled by two textually-identical but non-deterministic expressions; and --config=auto pulling from an unpinned, evolving Registry means a pipeline can start failing with no code change at all.
  4. Against CodeQL: Semgrep trades CodeQL's deep, compiled, interprocedural taint tracking for speed — it's the tool that runs on every commit, while CodeQL is worth its much higher cost on a schedule or for services where a cross-function taint chain is worth the wait. Against SonarQube: Semgrep is a stateless, pattern-matching CLI built for fast pass/fail gating, while SonarQube is a persistent-server, dashboard-oriented tool that pairs security findings with code-quality metrics and a human-review step (Security Hotspots) rather than Semgrep's cleaner "findings mean fail" model.