Semgrep
Semgrep is a fast, open-source static analysis tool that finds bugs, security issues, and code quality problems by matching code patterns the way a developer thinks about code — not as plain text or AST dumps, but as structured code with semantic understanding. It supports 30+ languages and requires no compilation.
Official docs: https://semgrep.dev/docs/
Installation
# macOS
brew install semgrep
# pip (any platform)
python3 -m pip install semgrep
# confirm
semgrep --version
Core concepts
Semgrep works by matching patterns against an abstract syntax tree (AST), not raw text. This means foo(1, 2) and foo( 1, 2 ) are equivalent. Patterns can include:
- Metavariables (
$X,$VAR) — match any expression and bind it for reuse - Ellipsis (
...) — match any sequence of arguments, statements, or characters - Typed metavariables (
$TYPE $VAR) — language-specific type constraints
Running scans
Quick start (recommended)
# Scan current directory with auto-selected community rules
semgrep scan --config auto
# Scan a specific path
semgrep scan --config auto src/
# Use a registry ruleset
semgrep scan --config p/python src/
semgrep scan --config p/security-audit .
semgrep scan --config p/owasp-top-ten .
semgrep scan --config p/javascript .
Common scan flags
| Flag | Description |
|---|---|
-c / --config |
Rules source: auto, p/<ruleset>, path to YAML, or URL |
-e / --pattern |
One-shot pattern (ephemeral rule); requires --lang |
-l / --lang |
Language for --pattern scans (e.g. python, js, go) |
--json |
Machine-readable JSON output |
--sarif |
SARIF format (used by GitHub code scanning) |
-o / --output |
Write results to a file instead of stdout |
--severity |
Filter: ERROR, WARNING, INFO |
--exclude |
Glob patterns to skip (e.g. --exclude='*.min.js') |
--include |
Restrict to matching paths |
--no-git-ignore |
Scan gitignored files too |
--timeout |
Per-file timeout in seconds (default 5) |
--metrics |
auto / on / off — telemetry control |
-j / --jobs |
Parallelism (default: ~85% of logical cores) |
-v / --verbose |
Show which rules are running, parse errors, etc. |
--dataflow-traces |
Show how tainted data reaches findings (SARIF/text) |
--autofix |
Apply fix: patches from rules automatically |
Ephemeral (one-shot) patterns
Great for quick checks without writing a YAML file:
# Find self-comparisons (likely bugs)
semgrep scan -e '$X == $X' --lang=python .
# Find any os.system call
semgrep scan -e 'os.system(...)' --lang=python .
# Find requests.get with verify=False
semgrep scan -e 'requests.get(..., verify=False, ...)' --lang=python .
Multiple configs at once
semgrep scan --config p/python --config rules/my-custom.yaml src/
Output to file
semgrep scan --config auto --json -o results.json .
semgrep scan --config auto --sarif -o results.sarif .
Popular registry rulesets
Find rulesets at https://semgrep.dev/r
| Config | What it scans |
|---|---|
auto |
Auto-selects rules for detected languages (recommended) |
p/python |
Python best practices and bugs |
p/javascript |
JavaScript / Node.js |
p/typescript |
TypeScript |
p/go |
Go |
p/java |
Java |
p/ruby |
Ruby |
p/rust |
Rust |
p/security-audit |
Cross-language security findings |
p/owasp-top-ten |
OWASP Top 10 categories |
p/python-command-injection |
Python command injection specifically |
p/secrets |
Hardcoded secrets and credentials |
p/ci |
CI/CD configuration issues |
p/terraform |
Infrastructure-as-code |
p/docker |
Dockerfile issues |
Writing custom rules
Rules live in YAML files. See references/rule-syntax.md for full reference.
Minimal rule
rules:
- id: no-eval
languages: [python]
severity: ERROR
message: "Avoid eval() — it executes arbitrary code. Use ast.literal_eval() for safe parsing."
pattern: eval(...)
Patterns with AND logic (patterns)
rules:
- id: unverified-db-query
languages: [python]
severity: WARNING
message: "db_query called without verify=True"
patterns:
- pattern: db_query(...)
- pattern-not: db_query(..., verify=True, ...)
OR logic (pattern-either)
rules:
- id: weak-hash
languages: [python]
severity: ERROR
message: "MD5 and SHA1 are cryptographically weak. Use SHA-256 or better."
pattern-either:
- pattern: hashlib.md5(...)
- pattern: hashlib.sha1(...)
Scope restriction (pattern-inside / pattern-not-inside)
rules:
- id: exec-in-request-handler
languages: [python]
severity: ERROR
message: "Executing shell commands inside a Flask route is dangerous"
patterns:
- pattern: os.system(...)
- pattern-inside: |
@app.route(...)
def $FUNC(...):
...
Auto-fix with fix:
rules:
- id: use-subprocess-not-os-system
languages: [python]
severity: WARNING
message: "Prefer subprocess.run() over os.system()"
pattern: os.system($CMD)
fix: subprocess.run($CMD, shell=True)
Metavariables and constraints
rules:
- id: hardcoded-password-arg
languages: [python]
severity: ERROR
message: "Hardcoded password passed as argument"
patterns:
- pattern: $FUNC(..., password=$PASS, ...)
- metavariable-regex:
metavariable: $PASS
regex: '"[^"]+"' # matches a string literal
Cross-statement tracking (taint analysis)
rules:
- id: user-input-to-system
languages: [python]
severity: ERROR
message: "User input reaches a shell command"
mode: taint
pattern-sources:
- pattern: input(...)
pattern-sinks:
- pattern: os.system(...)
Rule with paths: restrictions
rules:
- id: no-debug-print
languages: [python]
severity: INFO
message: "Remove debug print statements before merging"
pattern: print(...)
paths:
exclude:
- tests/
- scripts/
Ignoring findings
Inline suppression
result = os.system(cmd) # nosem
result = os.system(cmd) # nosem: rule-id-here
.semgrepignore file
Works like .gitignore:
# .semgrepignore
tests/
vendor/
*.min.js
generated/
CI/CD integration
GitHub Actions (OSS, no account required)
name: Semgrep
on:
push:
branches: [main]
pull_request:
jobs:
semgrep:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: returntocorp/semgrep-action@v1
with:
config: auto
GitHub Actions with SARIF (code scanning)
- name: Run Semgrep
run: semgrep scan --config auto --sarif -o semgrep.sarif .
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: semgrep.sarif
semgrep ci (managed, requires SEMGREP_APP_TOKEN)
SEMGREP_APP_TOKEN=<token> semgrep ci
In CI, semgrep ci only reports findings introduced by the current PR/MR (differential scan).
Pre-commit hook
# .pre-commit-config.yaml
repos:
- repo: https://github.com/returntocorp/semgrep
rev: v1.127.0 # use latest tag from https://github.com/returntocorp/semgrep/releases
hooks:
- id: semgrep
args: ['--config', 'auto', '--error']
The --error flag makes semgrep exit non-zero when findings exist, blocking the commit.
Output formats
| Format | Flag | Use case |
|---|---|---|
| Text (default) | --text |
Human reading |
| JSON | --json |
Scripts, parsing |
| SARIF | --sarif |
GitHub/GitLab code scanning |
| JUnit XML | --junit-xml |
Jenkins, CI test reports |
| GitLab SAST | --gitlab-sast |
GitLab security dashboard |
| Emacs | --emacs |
Emacs flycheck |
| Vim | --vim |
Vim ale/syntastic |
Performance tips
- Use
--include='*.py'to restrict file types when scanning large repos - Avoid overly broad
...ellipsis in deeply-nested patterns — it can be slow --timeout(default 5s per file) prevents runaway rules- Use
-jto control parallelism; don't over-subscribe cores .semgrepignoreto skip vendored/generated directories--exclude-minified-filesto skip minified JS/CSS
Common pitfalls
- False positives with complex patterns: Use
pattern-notandpattern-not-insideto narrow scope - Missing matches due to equivalent syntax: Semgrep normalizes whitespace/parens, but not semantic aliases (e.g.
open()vsPath.open()) - Metavariable scope:
$Xin onepatternis shared with$Xin sibling patterns underpatterns: - Language mismatch: Always specify
languages:correctly — wrong language = no matches - Registry rules need internet:
--config auto/--config p/Xdownloads rules; use--metrics=offfor airgapped environments
Reference files
references/rule-syntax.md— Complete YAML rule syntax with all operators and metavariable operatorsreferences/languages.md— All supported languages, file extensions, andlanguages:key values