Skip to content
Security Intermediate Tutorial

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.

Ji-ho Choi
Ji-ho Choi
Security & Cloud Editor · Jun 22, 2026 · 6 min read
Catch Risky Code Before It Merges: Add Semgrep SAST to Your GitHub Actions Pipeline

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-regex constrains $KEY so 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/secrets to 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-regex constraints.
Ji-ho Choi
Written by
Ji-ho Choi · Security & Cloud Editor

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

Join the discussion

Sign in or create an account to comment and vote.

No comments yet

Be the first to weigh in.

Related Reading