Integrate SAST into GitHub Actions
Add SAST to GitHub Actions for automated security scanning. This lesson explains the concept, provides a hands-on workflow, and covers common pitfalls.
Focus: integrate sast into github actions
You’ve just pushed a seemingly innocent pull request that merges a new feature. But hours later, your production logs reveal a SQL injection that slipped through code review. Sound familiar? That’s the reality when security is a manual afterthought. Integrating Static Application Security Testing (SAST) directly into GitHub Actions means every commit, every pull request, gets scanned for vulnerabilities automatically — before they ever reach production. In this lesson, you’ll stop treating security as a checkpoint and start treating it as part of your CI/CD pipeline.
The problem this lesson solves
Modern development moves fast. Code is merged multiple times a day, and relying on developers to manually run security tools is a recipe for disaster. The pain is real: vulnerabilities like injection flaws, insecure deserialization, or hardcoded secrets often hide in code until they’re exploited. By the time a security review happens, the damage is done.
The legacy approach — a separate security audit at the end of a sprint — is slow, reactive, and often misses issues that were introduced weeks ago. It also creates a bottleneck: security teams become the gatekeepers, converting every small change into a big review cycle.
Integrating SAST into GitHub Actions solves this by shifting security left. Instead of waiting, you run static analysis on every push. The scanner parses your source code (without executing it) and flags known vulnerability patterns. This means you catch issues in the same pull request that introduced them, when fixing them is cheapest — right after you write the code, not months later.
For security-conscious teams, CI-embedded SAST also provides an auditable trail: every scan run, its result, and the resolution are recorded. That’s valuable for compliance, and it builds a culture where security is a shared responsibility, not a separate silo.
Core concept / mental model
Think of SAST as a spell checker for security. When you write a document, a spell checker scans each word against a dictionary; it doesn’t understand the story, but it catches typos instantly. Similarly, a SAST tool scans your code’s syntax and data flow, matching patterns of known vulnerabilities — like a str.format with user input or a raw SQL query built by concatenation.
In your CI pipeline, GitHub Actions is the orchestrator. You define a workflow file — usually .github/workflows/security.yml — that triggers on events like push or pull_request. Within that workflow, a job checks out your code, runs a SAST tool (for example, GitHub’s own CodeQL, or a third-party like Semgrep), and then uploads the results as artifacts or annotations directly on the pull request.
A key distinction: SAST is white-box testing. It sees your source code, unlike DAST (Dynamic Application Security Testing) which probes a running application from the outside. SAST is fast, runs early, and covers all code paths — even ones that aren’t easily reachable by an attacker. But it can produce false positives because it doesn’t execute the code; it’s a heuristic, not a proof.
The mental model is simple: push → scan → feedback loop. The scanner reports issues, the developer fixes them, and the loop repeats. Over time, your codebase becomes more secure by design, not by accident.
How it works step by step
The process of integrating SAST into GitHub Actions follows a predictable pattern. Let’s break it down into steps you can apply to any project.
- Choose a SAST tool — Decide between GitHub’s built-in CodeQL (free for public repositories, integrated with GitHub UI) or a third-party tool like Semgrep, Snyk Code, or Bandit for Python. Each has different strengths (see the comparison table in the next section).
- Create a workflow file — Inside your repository, create
.github/workflows/sast.yml. This YAML file defines when the scan runs and what it does. - Define triggers — You’ll typically run SAST on every pull request to check new code, and on pushes to the main branch for a full scan. For speed, you can limit the scan to changed files.
- Configure the job — The job runs on a specific runner (e.g.,
ubuntu-latest). It checks out the code, installs dependencies if needed, and runs the scanner. - Run the scanner — The SAST tool analyzes your code and produces a report (often in SARIF format, Security Analysis Results Interchange Format).
- Upload results — Use the
github/codeql-action/upload-sarifaction or a similar step to upload the report. GitHub then displays findings as annotations on the pull request, and you can block merging if critical issues are found. - Review and remediate — Developers see the findings right in the PR conversation. They fix the code, and the next push triggers a new scan.
Cause and effect: each push triggers the workflow, the scanner runs, and the PR gets a green or red checkmark. This is your security feedback loop.
Hands-on walkthrough
Let’s put the theory into practice. We’ll set up a simple GitHub Actions workflow that runs CodeQL on a Python project. This example is complete and runnable — place it in .github/workflows/sast.yml in your repository.
Example 1: Basic CodeQL workflow
name: "SAST Scan"
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
analyze:
name: CodeQL Analyze
runs-on: ubuntu-latest
permissions:
security-events: write
actions: read
strategy:
fail-fast: false
matrix:
language: [ 'python' ]
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
- name: Build (if needed)
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
Expected output: When you push this workflow, you’ll see a new check in your PR. Clicking on it shows the scanning steps. If vulnerabilities are found, security alerts appear in the Security tab of your repo.
Example 2: Using Semgrep with custom rules
For more control, you might prefer Semgrep. This workflow runs Semgrep with a custom rule to detect eval() usage — a common source of code injection.
First, create a Semgrep rule file, say semgrep-rules/no-eval.yaml:
rules:
- id: no-eval
patterns:
- pattern: eval($ARG)
message: Avoid using eval() — risk of code injection
languages: [python]
severity: WARNING
Then, add this to your workflow after the checkout step:
- name: Run Semgrep
uses: returntocorp/semgrep-action@v1
with:
config: semgrep-rules/no-eval.yaml
generateSarif: 1
- name: Upload SARIF file
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: semgrep.sarif
Expected output: The action runs Semgrep, generates a SARIF file, and uploads it. Any eval calls in your code will show up as warnings in the PR’s “Checks” tab.
Example 3: Blocking merge on critical findings
You can enforce a security gate by setting the job to fail when critical issues are found. This snippet uses the sarif-validator action to fail the job if any error-level findings exist.
- name: Validate SARIF and fail on critical
uses: prefecthq/sarif-validator@v1
with:
sarif_file: semgrep.sarif
fail_on: error
Expected output: If the validator finds an error-level issue, the job fails, and the PR cannot be merged until it’s fixed.
Pro tip: Use
pull_requesttriggers to scan only changed files by specifyingpaths:in the workflow. This cuts scan time dramatically and keeps PR feedback fast.
Compare options / when to choose what
There’s no one-size-fits-all SAST tool. Here’s a comparison to help you decide.
| Tool | Language Support | Ease of Setup | False Positives | Cost | Best For |
|---|---|---|---|---|---|
| CodeQL | C/C++, C#, Java, JavaScript, Python, Go | Very easy (official GitHub action) | Moderate | Free for public repos, paid for private | Teams already on GitHub, want deep analysis |
| Semgrep | 20+ languages | Easy (Docker or action) | Low (especially with custom rules) | Free OSS, paid tiers | Teams needing custom rules and speed |
| Bandit | Python only | Easy (pip install) | Low | Free | Python-only projects, simple CI |
| Snyk Code | Many languages | Easy (official action) | Low | Free tier, paid for advanced | Teams wanting both SAST and dependency scanning |
| SonarQube | Many languages | Moderate (requires server) | Low | Free Community Edition | Larger enterprises with centralized quality gates |
When to choose what: If you’re on GitHub and want zero-config, start with CodeQL. If you need custom rules or support for many languages quickly, Semgrep is excellent. For Python-only projects, Bandit is lightweight. Snyk provides a nice integrated experience across both code and supply-chain. SonarQube scales to org-wide quality standards.
Troubleshooting & edge cases
-
The workflow runs but logs show “No source code found.” This usually happens when the checkout action isn’t placed before the scan step. Ensure
actions/checkout@v3is the first step. -
CodeQL fails on build step — If your project needs a specific build step, CodeQL’s
initaction may not detect it automatically for compiled languages. Fix: setbuild-mode: manualand provide your own build commands. -
Permissions error:
security-events: writemissing — CodeQL requires this permission to upload results. If you get a 403 when uploading SARIF, add thepermissions:block as shown in the example. -
False positives overwhelming the PR — Tune your scanner. For CodeQL, you can add
queries: - security-extendedto focus on high-precision queries. For Semgrep, narrow your rules to critical vulnerabilities. -
Scanning slow on large repos — Optimize by running SAST only on pull requests, not every push. Use path filters to scan only changed directories, and limit the matrix to relevant languages.
-
Duplicate findings across tools — If you run multiple SAST tools, you might see the same issue reported several times. Use GitHub’s
workflow_dispatchto trigger a consolidated scan once a week, or configure tools to ignore each other’s reports. -
Ignoring pre-existing issues — Right after integrating SAST, you may inherit dozens of old findings. Set a baseline by running a scan on your main branch before the feature work, and file issues for those. Then enforce new findings only on changed code.
Pro tip: Always test your workflow on a dummy branch first. Push a small change that introduces a known vulnerability (like
eval(input())) and verify the scan catches it — this confirms your setup works before relying on it.
What you learned & what's next
You’ve learned why security scanning must be automated in CI, how SAST tools work as your security spell checker, and how to integrate them into GitHub Actions step by step. You’ve seen three working examples: a basic CodeQL workflow, a custom Semgrep scan, and a way to block merges on critical findings. You also know how to choose between tools and fix common problems like permissions, false positives, and slow scans.
You can now explain the core idea behind integrating SAST into GitHub Actions and complete a practical exercise for it. You understand that SAST is a white-box approach, that GitHub Actions provides the event-driven infrastructure, and that SARIF format enables seamless display of results in PRs.
In the next lesson, you’ll build on this foundation by learning how to integrate DAST (Dynamic Application Security Testing) into a CI pipeline — testing your running application for vulnerabilities that static analysis can’t catch. You’ll also explore how to combine SAST results with dependency scanning for a full-spectrum security posture.
For now, if you haven’t already, go to one of your repositories and add the CodeQL workflow from Example 1. Push a commit and watch the scan run. That hands-on experience is the real teacher.
Practice recap
Now apply what you learned: go to a real Python repository on GitHub (or create a new one) and add the CodeQL workflow from Example 1. Push a commit that introduces a simple eval(input()) call and watch the scan flag it. Then, install Semgrep locally and run it with a custom rule to see how it differs. Finally, merge a fix and confirm the PR goes green. This hands-on loop will cement your understanding of SAST in CI.
Common mistakes
- Forgetting to add checkout as the first step — without it, the scanner sees an empty directory and reports 'no source code', which can break the workflow.
- Omitting
security-events: writein job permissions — CodeQL and other uploaders will fail with a 403 when trying to attach the SARIF file to the PR. - Running SAST on every push to main, which can slow down CI significantly; use
pull_requestevents and path filters to scan only what changed. - Ignoring pre-existing vulnerabilities after integration — you should baseline the main branch first, then enforce new findings on changed code only.
Variations
- Use Semgrep for custom rules: unlike CodeQL’s preset query packs, Semgrep allows you to write one-off pattern rules that match your codebase’s specific risks (e.g., blocking
subprocess.callwith user input). - Set up multiple SAST tools in the same workflow — for example, run Bandit for Python and CodeQL for JavaScript — to cover different language ecosystems in one pipeline.
- Schedule a weekly cron job (
on: schedule: - cron: '0 2 * * 1') to run a full SAST scan on the main branch, complementing the fast PR-only scans.
Real-world use cases
- A fintech startup adds CodeQL to their GitHub Actions pipeline and automatically blocks merging PRs that introduce SQL injection patterns, reducing their security review time by 70%.
- An open-source library maintainer uses Semgrep with a custom rule to ensure no contributor ever commits code that calls
pickle.loadson untrusted input, preventing remote code execution vulnerabilities. - A healthcare software vendor runs Snyk Code alongside their existing dependency scanning, giving them a unified view of code-level flaws and known library vulnerabilities in daily builds for compliance audits.
Key takeaways
- SAST in CI shifts security left by catching vulnerabilities in the same pull request that introduces them, when fixes are cheapest.
- GitHub Actions provides event-driven triggers and a rich ecosystem of actions to integrate SAST tools with minimal code.
- The SARIF format lets any SAST tool upload findings that appear directly as annotations on pull requests, making security review seamless.
- Choose tools based on language support, false positive rate, and integration effort — CodeQL is the simplest for GitHub users, Semgrep is flexible, Bandit is lightweight for Python.
- Troubleshoot common issues like permissions, missing checkout, and slow scans by testing on a dummy branch and using path filters.
- Baseline existing vulnerabilities before enforcing strict gates to avoid blocking development on pre-existing issues.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.