# Semgrep Cheat Sheet

> **Tool:** Semgrep
> **Category:** Security & Compliance
> **Verified against:** Semgrep 1.175.0, flags verified via `semgrep scan --help` run locally; rule syntax and
> registry behavior cross-checked against the official docs (semgrep.dev/docs/writing-rules/rule-syntax,
> semgrep.dev/docs/writing-rules/pattern-examples, docs.semgrep.dev/ignoring-files-folders-code), 2026-08-29
> **Official docs:** https://semgrep.dev/docs/

## What Semgrep is and where it fits

Semgrep is a static application security testing (SAST) engine that matches source code against
pattern-based rules — closer to "grep that understands syntax" than a traditional AST-walking analyzer, which
is exactly why custom rules are approachable: a rule pattern looks almost like the vulnerable code itself.
It runs entirely locally by default (nothing leaves the machine unless you explicitly pull rules from the
Semgrep Registry or opt into the paid platform), which makes it a natural first SAST tool to adopt — no
account, no server, no license negotiation before a team gets real findings.

In the shift-left pipeline, Semgrep sits at the **code** stage: it runs against source before a build even
exists, ideally as a pre-commit hook and again as a required CI check on every PR. It complements, rather than
replaces, a dependency scanner (Snyk/Trivy in this series) and a secrets scanner (Gitleaks/TruffleHog) — Semgrep
finds vulnerabilities *you wrote*, not vulnerable *dependencies you pulled in* or *secrets you leaked*.

## Installation

```bash
pipx install semgrep              # recommended — isolated, no dependency conflicts with other Python tools
brew install semgrep               # macOS/Linux Homebrew, best-effort
uv tool install semgrep            # if you already use uv
docker pull semgrep/semgrep        # no local install at all

semgrep --version                  # confirm install
semgrep --disable-version-check --version    # skip the network call to check for a newer release
```

Upgrading follows the same path you installed with: `pipx upgrade semgrep`, `brew upgrade semgrep`, etc.
Semgrep ships new releases roughly weekly — pin a version in CI (`pipx install semgrep==1.175.0`) rather than
floating on `latest`, so a rule-engine change doesn't silently start failing (or silently stop catching) things
on an unrelated commit.

## Core concepts

- **Pattern** — a code snippet with metavariables (`$X`, `$FUNC`, `...`) that matches structurally, not
  textually. `$X == $X` matches `a == a`, `foo() == foo()`, or `1+1 == 1+1` — anywhere the same expression is
  compared to itself, regardless of formatting or variable naming.
- **Rule** — a YAML document combining one or more patterns with metadata (severity, message, languages) and,
  optionally, an `fix:` autofix template.
- **Ruleset** — a curated bundle of rules published to the Semgrep Registry, referenced by a short id like
  `p/security-audit` or `p/owasp-top-ten`.
- **Taint tracking** — a rule mode (`pattern-sources` / `pattern-sinks` / `pattern-sanitizers`) that traces
  untrusted data from where it enters the program to where it's used dangerously, catching injection bugs a
  purely syntactic pattern would miss because the tainted value passes through several functions first.

## Running a scan

```bash
semgrep --config auto .                        # auto-fetch rules tailored to this project (logs project URL to Semgrep)
semgrep --config=p/security-audit .             # a specific registry ruleset, no login required
semgrep --config=p/owasp-top-ten .              # maps findings to OWASP Top 10 categories — useful for compliance reporting
semgrep --config=p/ci .                         # curated CI-friendly ruleset (lower noise than security-audit)
semgrep --config=p/secrets .                    # community secrets-detection rules (see Snyk/Gitleaks pages for dedicated tools)
semgrep --config path/to/rules.yaml .           # local/custom rule file or directory
semgrep --config p/default --config p/security-audit --config p/owasp-top-ten .   # combine multiple rulesets in one run
semgrep -e '$X == $X' --lang python .           # one-off pattern search, no rule file needed
```

`p/security-audit` and `p/owasp-top-ten` differ in intent, not just size: `security-audit` is broader,
moderate-confidence coverage meant to surface more candidates for manual review; `owasp-top-ten` is
explicitly mapped to the ten OWASP categories, which is what a compliance-driven team cites when asked "how do
you cover the OWASP Top 10." Most teams start with `p/ci` (deliberately tuned for low noise) and graduate to
`security-audit` once the initial backlog of findings is triaged.

## Rule syntax — writing a custom rule

A minimal rule needs exactly one of `pattern`, `patterns`, `pattern-either`, or `pattern-regex` at the top
level, plus `id`, `message`, `severity`, and `languages`:

```yaml
# rules/hardcoded-secret.yaml
rules:
  - id: hardcoded-db-password
    message: >
      Database password appears to be hardcoded. Load it from an environment
      variable or a secrets manager instead.
    severity: ERROR
    languages: [python]
    pattern: connect(host=..., password="...")
```

Combining patterns with boolean logic (`patterns`, `pattern-either`, `pattern-not`) is what most real rules
need — a single `pattern` is rarely precise enough on its own:

```yaml
rules:
  - id: flask-debug-true
    message: Flask app running with debug=True — remote code execution risk in production.
    severity: ERROR
    languages: [python]
    patterns:
      - pattern: app.run(..., debug=True, ...)
      - pattern-not-inside: |
          if __name__ == "__main__":
              ...
```

Taint tracking for injection-class bugs (the pattern a pure syntactic match can't express, because the
dangerous value passes through intermediate variables/functions):

```yaml
rules:
  - id: sql-injection-via-request-param
    message: Untrusted request data flows into a raw SQL query.
    severity: ERROR
    languages: [python]
    mode: taint
    pattern-sources:
      - pattern: request.args.get(...)
    pattern-sinks:
      - pattern: cursor.execute(...)
    pattern-sanitizers:
      - pattern: escape_sql(...)
```

Autofix — a `fix:` template gets applied with `--autofix`:

```yaml
rules:
  - id: use-secrets-compare-digest
    message: Use secrets.compare_digest for constant-time comparison of secrets.
    severity: WARNING
    languages: [python]
    pattern: $A == $B
    fix: secrets.compare_digest($A, $B)
```

## Validating and testing custom rules

```bash
semgrep --validate --config myrule.yaml         # lint a rule file before using it — catches YAML/schema errors early
semgrep --test --config myrule.yaml             # run the rule's test fixtures
```

Test fixtures live alongside the rule as `<rule-id>.<lang>` files using inline annotations:

```python
# hardcoded-db-password.py
connect(host="db", password="hunter2")  # ruleid: hardcoded-db-password
connect(host="db", password=os.environ["DB_PASSWORD"])  # ok: hardcoded-db-password
```

## Ignoring files and folders

Semgrep excludes untracked files already covered by `.gitignore`, plus a small built-in default-ignore list
(binaries, `.git/`, `node_modules/`, etc.). A `.semgrepignore` file at the repo root adds project-specific
exclusions using the same glob syntax as `.gitignore` — and **takes precedence** over `.gitignore` when the
two disagree (something `.gitignore` includes but `.semgrepignore` excludes will not be scanned):

```gitignore
# .semgrepignore
tests/fixtures/
vendor/
*.generated.ts
```

```bash
semgrep --config=p/security-audit . --exclude '*.min.js' --exclude tests/    # CLI-level exclusion, same run
semgrep --config=p/security-audit . --include 'src/**/*.py'                   # scan only a subset instead
```

## CI-oriented flags and gating

```bash
semgrep --config=p/security-audit --error .    # exit 1 on any finding — for pipeline gating
semgrep ci                                      # CI-aware mode: diff-aware, reads SEMGREP_APP_TOKEN if set
semgrep --baseline-commit=main .                # only report findings not present in this commit — adopt on a legacy codebase without a wall of pre-existing noise
semgrep --config=p/security-audit --severity ERROR .     # ERROR, WARNING, or INFO
```

`--error` is what turns Semgrep from "prints findings, exits 0" into "fails the build" — without it, a scan
with real findings still exits 0 and a CI step silently passes.

## Output formats

```bash
semgrep --config=p/security-audit --json -o results.json .
semgrep --config=p/security-audit --sarif -o results.sarif .   # for GitHub code scanning upload
semgrep --config=p/security-audit --gitlab-sast .               # GitLab SAST report format
semgrep --config=p/security-audit --junit-xml -o results.xml .
semgrep --config=p/security-audit --text .                      # human-readable, default when writing to a TTY
```

## Real-world scenario: gating a PR on new findings only

A team adopting Semgrep on a five-year-old codebase can't realistically fix every existing finding before
merging their first PR. Baseline against the target branch instead of the whole tree:

```bash
git fetch origin main
semgrep --config=p/security-audit --baseline-commit=origin/main --error .
```

This reports (and fails the build on) only findings introduced by the current branch — pre-existing issues on
`main` are silently excluded from the gate, so the team can fix the backlog on its own timeline while still
blocking *new* problems immediately.

## Real-world scenario: GitHub Actions CI gate

```yaml
# .github/workflows/semgrep.yml
name: Semgrep
on: [pull_request]
jobs:
  semgrep:
    runs-on: ubuntu-latest
    container:
      image: semgrep/semgrep
    steps:
      - uses: actions/checkout@v4
      - run: semgrep ci --config=p/security-audit --config=p/owasp-top-ten
```

`semgrep ci` (rather than `semgrep scan`) is purpose-built for this: it's diff-aware against the PR's base
branch automatically, and if `SEMGREP_APP_TOKEN` is set it also posts results back to the Semgrep platform for
trend tracking across the repo.

## Secrets scanning (Semgrep Secrets, needs Pro/registry access)

```bash
semgrep --config=p/secrets .              # community secrets rules, no login required
semgrep --secrets .                       # full Secrets product with live validation — requires a paid plan
semgrep --historical-secrets .            # scan git history, not just the working tree, for secrets rules
```

This overlaps with Gitleaks/TruffleHog (see their dedicated cheat sheets) — the community `p/secrets` ruleset
is a reasonable free baseline, but a dedicated secrets scanner with live verification is generally more
precise for that specific job.

## Common pitfalls

- **Forgetting `--error`** — the most common "why didn't this fail CI" report. Semgrep's default exit code
  ignores findings entirely.
- **Treating `--config auto` as fully offline** — it logs the project's remote URL to Semgrep's servers to
  select tailored rules; use a pinned `p/<ruleset>` id instead if that's a concern.
- **`.semgrepignore` silently winning over `.gitignore`** — if a path is scanned when you expected it to be
  ignored (or vice versa), check `.semgrepignore` first; it overrides `.gitignore` on conflict.
- **5-second per-rule-per-file timeout** — `--timeout` defaults to 5.0s; a very large generated file can
  silently get skipped rather than scanned. Raise it (`--timeout 30`) rather than assuming a clean scan of
  such a file actually happened.

## Exit codes

`0` OK (clean, or findings exist but `--error` not set) · `1` findings found (with `--error`) · `2` fatal
error · `3` invalid target code · `4` invalid pattern · `5` unparseable YAML rule file · `7` missing
configuration · `8` invalid language · `13` invalid API key.

## When to reach for something else

Semgrep covers first-party source code (SAST). For dependency vulnerabilities, reach for Snyk or Trivy's
filesystem scan; for container images, Trivy or Grype; for IaC misconfigurations, Checkov or tfsec (or
`trivy config`); for leaked credentials, Gitleaks or TruffleHog. All are documented as their own cheat sheets
in this Security & Compliance section.
