Introduction
Static application security testing (SAST) finds bugs such as SQL injection, hardcoded credentials, unsafe deserialization, and weak crypto by reading source code rather than running it. Semgrep is a fast, open-source SAST engine that matches code by syntax-aware patterns, so rules look like the code they find. That makes custom rules easy to write, and scans of typical repositories finish in seconds.
This guide takes you from a local install to a pull-request gate: running community rules, tuning noise, writing a custom rule, and enforcing results in CI.
Prerequisites
- Python 3.9+ with
pip, or Docker - A repository containing code in a supported language (Python, JavaScript/TypeScript, Go, Java, and many more)
- GitHub Actions enabled for the repo
Step 1: Install Semgrep
# Using pipx (recommended - isolated install)
pipx install semgrep
# Or run it without installing anything
docker run --rm -v "$PWD:/src" semgrep/semgrep semgrep --versionConfirm the version:
semgrep --versionStep 2: Run Your First Scan
Use the community-maintained auto configuration, which selects rules for the languages it detects. Note that --config auto sends rule-selection metadata to semgrep.dev; for fully offline scans use explicit rule files (see Step 4).
cd /path/to/your/repo
semgrep scan --config autoFor a more focused, security-only pass use curated registry packs:
semgrep scan \
--config p/security-audit \
--config p/owasp-top-ten \
--config p/secretsEach finding shows the rule ID, file, line, and matched code.
Step 3: Triage and Reduce Noise
Start by exporting results so you can review them outside the terminal:
semgrep scan --config p/security-audit --sarif -o semgrep.sarif
semgrep scan --config p/security-audit --json -o semgrep.jsonExclude generated or vendored code with a .semgrepignore file:
node_modules/
vendor/
dist/
*.min.js
tests/fixtures/For a confirmed false positive, suppress a single line with an inline comment and a reason:
# nosemgrep: python.lang.security.audit.dangerous-subprocess-use
subprocess.run(["ls", trusted_dir]) # trusted_dir is a hardcoded constantPrefer narrow, commented suppressions over disabling a whole rule.
Step 4: Write a Custom Rule
Custom rules enforce your own conventions. Suppose developers must never call an internal legacy_query() helper with string formatting. Create .semgrep/rules.yml:
rules:
- id: no-fstring-in-legacy-query
languages: [python]
severity: ERROR
message: >-
legacy_query() must use parameterized arguments, not f-strings
or string concatenation (SQL injection risk).
pattern-either:
- pattern: legacy_query(f"...")
- pattern: legacy_query($A + $B)
- pattern: legacy_query("..." % $X)
- pattern: legacy_query("...".format(...))
metadata:
cwe: "CWE-89"
category: securityRun only your rules (works fully offline):
semgrep scan --config .semgrep/rules.yml --errorTest rules with annotated sample code in .semgrep/rules.py, using # ruleid: and # ok: comments above lines that should and should not match:
# ruleid: no-fstring-in-legacy-query
legacy_query(f"SELECT * FROM users WHERE id = {uid}")
# ok: no-fstring-in-legacy-query
legacy_query("SELECT * FROM users WHERE id = %s", (uid,))semgrep test .semgrep/Step 5: Gate Pull Requests in GitHub Actions
Create .github/workflows/semgrep.yml:
name: Semgrep
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
security-events: write
jobs:
semgrep:
runs-on: ubuntu-latest
container:
image: semgrep/semgrep:latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Run Semgrep
run: |
semgrep scan \
--config p/security-audit \
--config p/secrets \
--config .semgrep/rules.yml \
--severity ERROR \
--error \
--sarif -o semgrep.sarif
- name: Upload SARIF
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: semgrep.sarifKey flags: --error makes the job fail when findings exist, and --severity ERROR limits blocking to high-confidence issues so developers are not buried in warnings. Pin the container image to a specific version or digest for reproducible results, and make the check required in your branch protection rules.
To scan only changed code on pull requests and keep builds fast, add --baseline-commit "$(git merge-base origin/main HEAD)" to the command.
Step 6: Add a Pre-Commit Hook (Optional)
Catch issues before they are even committed:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/semgrep/pre-commit
rev: "v1.100.0"
hooks:
- id: semgrep
args: ["--config", ".semgrep/rules.yml", "--error"]pre-commit installCheck the repository's releases page and pin rev to the current tag.
Verification
-
Introduce a deliberate flaw on a throwaway branch:
import hashlib digest = hashlib.md5(password.encode()).hexdigest() legacy_query(f"SELECT * FROM users WHERE name = '{name}'") -
Run
semgrep scan --config p/security-audit --config .semgrep/rules.yml --errorand confirm a non-zero exit code (echo $?) with findings for both lines. -
Open a pull request and confirm the Semgrep check fails and findings appear under the repository's Security > Code scanning tab.
-
Fix the code, push, and confirm the check turns green.
-
Run
semgrep test .semgrep/and confirm all rule tests pass.
Troubleshooting
| Symptom | Likely Cause | Fix |
|---|---|---|
0 findings on code you know is vulnerable | Language not detected or path ignored | Run with --verbose; check .semgrepignore and file extensions |
| Scan is slow on a large monorepo | Scanning vendored or generated code | Add exclusions, use --baseline-commit, or raise --jobs |
| Too many false positives | Broad rulesets or low-confidence rules | Use --severity ERROR, trim packs, add narrow nosemgrep comments |
| Custom rule never matches | Pattern too literal or wrong language | Test in the Semgrep Playground; use metavariables like $X and ... |
CI fails with network errors fetching p/... packs | Runner is offline or blocked | Vendor rules into the repo and use file paths instead |
| SARIF upload fails with permission error | Missing security-events: write | Add the permission, and note private repos may need code scanning enabled |
| Exit code 7 or config errors | Invalid YAML in a rule file | Run semgrep scan --validate --config .semgrep/rules.yml |
Summary
Semgrep gives you readable, fast static analysis without heavyweight tooling. You installed it, ran curated security rulesets, triaged noise with ignores and scoped suppressions, wrote and tested a custom rule, and enforced results on every pull request through GitHub Actions with SARIF reporting. Start with blocking only ERROR severity findings, review warnings periodically, and grow your custom rule library as you learn which mistakes recur in your codebase. Pair it with secrets scanning and dependency auditing for broader coverage across your pipeline.