Catch Risky Code Before It Merges: Add Semgrep SAST to Your GitHub Actions Pipeline
Wire Semgrep into GitHub Actions to scan every pull request against OWASP Top 10 rules and a custom rule you write yourself — so a CI failure blocks the merge before risky code reaches main.
What You'll Build
You'll wire Semgrep into a GitHub Actions workflow that scans every pull request against OWASP Top 10 rules and a custom rule you write yourself — so a CI failure blocks the merge before risky code ever reaches main.
Prerequisites
- A GitHub repository with Actions enabled
- Python 3.9+ locally (for testing rules before pushing)
- Semgrep CLI installed locally:
pip install semgrep(version ≥ 1.0) - GitHub Advanced Security enabled or a public repository (required for the SARIF upload to the Security tab)
- Familiarity with GitHub Actions YAML and basic Python
1. Create the Workflow File
Create .github/workflows/semgrep.yaml:
name: Semgrep SAST
on:
pull_request:
branches: [main]
push:
branches: [main]
permissions:
contents: read
security-events: write # required for SARIF upload
jobs:
semgrep:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install Semgrep
run: pip install semgrep
- name: Run Semgrep (OWASP + custom rules)
run: |
semgrep scan \
--config p/owasp-top-ten \
--config .semgrep/rules/ \
--error-on-finding \
--sarif \
--output semgrep.sarif
- name: Upload results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: semgrep.sarif
Key flags explained:
| Flag | Purpose |
|---|---|
--config p/owasp-top-ten |
Pulls the OWASP Top 10 ruleset from the Semgrep Registry |
--config .semgrep/rules/ |
Also runs every .yaml file in your local rules directory |
--error-on-finding |
Exits with code 1 on any finding, failing the workflow step |
--sarif --output semgrep.sarif |
Writes SARIF output for the GitHub Security tab |
The if: always() on the upload step ensures findings reach the Security tab even when --error-on-finding causes the scan step to fail first.
2. Require the Check on Pull Requests
A failing workflow won't block a merge by itself. Go to Settings → Branches → Add rule, target main, enable Require status checks to pass before merging, and add semgrep as a required check.
3. Write a Custom Rule
Create the rules directory:
mkdir -p .semgrep/rules
Create .semgrep/rules/no-hardcoded-secret.yaml:
rules:
- id: no-hardcoded-secret
patterns:
- pattern: $KEY = "..."
- metavariable-regex:
metavariable: $KEY
regex: '(?i)(password|secret|api_key|token|auth)'
message: >
Hardcoded credential detected in variable '$KEY'.
Store secrets in environment variables or a secrets manager —
never embed them in source code.
languages: [python]
severity: ERROR
metadata:
cwe: "CWE-798: Use of Hard-coded Credentials"
category: security
How the rule works:
pattern: $KEY = "..."matches any assignment of a string literal to a named metavariable.metavariable-regexconstrains$KEYso only names matching the regex (case-insensitive:password,secret,api_key,token,auth) produce a finding.- All entries under
patterns:are ANDed — both conditions must hold simultaneously. This is what keeps the rule precise rather than flagging every string assignment.
4. Test the Rule Locally
Create a scratch file:
# test_bad_code.py
password = "hunter2" # should trigger
api_key = "sk-proj-abc123" # should trigger
db_name = "production_db" # should NOT trigger
Run Semgrep against it:
semgrep scan --config .semgrep/rules/ test_bad_code.py
To suppress a known-safe finding (e.g., a test fixture), use an inline comment:
test_password = "fixture_value" # nosemgrep: no-hardcoded-secret
Document the justification next to every suppression so it survives code review scrutiny.
Verify It Works
Expected local output — two findings, exit code 1:
Findings:
test_bad_code.py
❯❯❱ no-hardcoded-secret
Hardcoded credential detected in variable 'password'. ...
1┆ password = "hunter2"
test_bad_code.py
❯❯❱ no-hardcoded-secret
Hardcoded credential detected in variable 'api_key'. ...
2┆ api_key = "sk-proj-abc123"
Ran 1 rule on 1 file: 2 findings.
In CI: Open a pull request that includes test_bad_code.py. The semgrep status check turns red and blocks the merge. Navigate to Security → Code scanning alerts to see findings annotated with the CWE metadata you added.
Troubleshooting
No rules were run when pointing to .semgrep/rules/
Semgrep requires at least one .yaml file in the target directory. Verify the file exists: ls -la .semgrep/rules/. Validate the YAML syntax before pushing:
semgrep validate --config .semgrep/rules/
SARIF upload step fails with HTTP 403
Confirm security-events: write appears in your workflow's permissions: block. For private repositories you also need GitHub Advanced Security enabled under Settings → Security & analysis.
p/owasp-top-ten returns rate-limit errors
Anonymous Registry pulls are rate-limited. Create a free account at semgrep.dev, generate a token, store it as a repository secret named SEMGREP_APP_TOKEN, and expose it in your workflow step:
env:
SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
Too many false positives from the OWASP ruleset
Swap p/owasp-top-ten for a language-scoped pack such as p/python or p/javascript to reduce noise. Audit the rule IDs in any remaining noisy findings and suppress selectively with # nosemgrep: <rule-id> rather than silencing entire rule files.
Next Steps
- Broader secret detection: Add
--config p/secretsto catch API keys and tokens across all file types using pattern-based and entropy-based rules. - Taint analysis: Semgrep Pro (free tier available) supports inter-procedural taint mode — track user-controlled data flowing into SQL queries or shell commands across function boundaries.
- Inline PR comments: Connect your repository to Semgrep Cloud Platform to surface findings as pull request review comments instead of (or alongside) status checks.
- Interactive rule authoring: The Semgrep Playground lets you iterate on patterns against live code samples before committing rules to your repo — invaluable for tuning
metavariable-regexconstraints.
Ji-ho covers the increasingly tangled overlap between cloud architecture and security, drawing on a background as a penetration tester to keep his reporting grounded in real-world attack paths. He never lets a vendor claim go unquestioned and insists that every buzzword come with a proof of concept.
Discussion 0
No comments yet
Be the first to weigh in.