Shipping fast means shipping code that nobody has fully read line by line — and that's exactly where injectable bugs, hardcoded secrets, and insecure deserialization slip through. If your team merges pull requests on a daily cadence, manual code review alone won't catch every vulnerability pattern before it reaches production. Setting up SAST scanning GitHub Actions workflows solves this by running static analysis automatically on every push and pull request, flagging risky code before a human reviewer even opens the diff.
This guide walks through building a working SAST scanning GitHub Actions pipeline from scratch: creating the workflow file, configuring CodeQL as your primary engine, layering in a second scanner for broader language coverage, tuning out noisy false positives, and gating merges on scan results. By the end, you'll have a pipeline that runs static application security testing on every commit and blocks pull requests that introduce high-severity findings — without slowing your team down.
Step 1: Create Your SAST Scanning GitHub Actions Workflow
Every GitHub Actions pipeline starts with a YAML file under .github/workflows/. Create .github/workflows/sast.yml in your repository root:
name: SAST Scan
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
permissions:
contents: read
security-events: write
jobs:
codeql:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout code
uses: actions/checkout@v4
The security-events: write permission is required so the workflow can upload findings to GitHub's Security tab. Triggering on both push and pull_request ensures the scan runs on feature branches before merge, not just after code lands on main.
Step 2: Configure CodeQL for Your Language Matrix
GitHub's own CodeQL engine is the most common starting point for a codeql GitHub Actions setup because it's free for public repos and tightly integrated with the Security tab. Extend the job with a language matrix and the CodeQL actions:
strategy:
fail-fast: false
matrix:
language: ['javascript-typescript', 'python', 'go']
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
queries: security-extended
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{ matrix.language }}"
The security-extended query pack pulls in additional rules beyond the default set, including checks for insecure randomness, path traversal, and weak cryptography — worth the modest increase in scan time for most production codebases. List only the languages actually present in your repo; scanning languages you don't use wastes CI minutes.
Step 3: Add Build Steps for Compiled Languages
CodeQL can trace data flow through interpreted languages like JavaScript and Python without any extra work, but compiled languages (Java, C++, C#, Go) need an actual build to generate the database CodeQL analyzes. Insert a build step between init and analyze:
- name: Set up JDK
if: matrix.language == 'java-kotlin'
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Build project
if: matrix.language == 'java-kotlin'
run: ./gradlew build -x test
Skip the test suite during the build (-x test or the equivalent for your build tool) — you only need compiled artifacts, not a full test run, and this keeps the scan job fast. If your build requires environment variables or private package registry credentials, pull them from repository secrets rather than hardcoding them in the workflow.
Step 4: Integrate a Complementary SAST Engine
CodeQL is strong, but no single scanner catches everything. Most mature setups pair it with a second engine tuned for different vulnerability classes — Semgrep is a popular choice because its rule sets are lightweight and fast to run on every PR. Add a parallel job for genuine sast pipeline integration coverage:
semgrep:
runs-on: ubuntu-latest
container:
image: semgrep/semgrep
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run Semgrep
run: semgrep ci --config=auto
env:
SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
Running two engines in parallel jobs (rather than sequentially) keeps total pipeline time close to whichever scanner is slower, instead of the sum of both. This layered approach — one broad, GitHub-native scanner plus one purpose-built rules engine — is how most teams implement static application security testing CI checks without relying on a single vendor's blind spots.
Step 5: Tune Severity Thresholds and Suppress False Positives
A SAST pipeline that fails builds on every low-severity finding gets disabled by frustrated engineers within a month. Configure a baseline that only fails CI on findings your team has agreed matter. For CodeQL, filter severity at the query-pack level or post-process results with the github/codeql-action/upload-sarif step's checkout_path and a severity gate in a follow-up job. For Semgrep, exclude noisy paths directly in a config file:
# .semgrepignore
tests/
vendor/
**/*.generated.go
For genuine false positives rather than accepted risk, use inline suppressions (// nosemgrep: rule-id) sparingly and require a code comment explaining why. Suppressions without justification tend to accumulate and quietly widen your blind spot over time — treat them as technical debt that needs periodic review, not a permanent fix.
Step 6: Gate Merges on Scan Results
Detection without enforcement just produces a dashboard nobody looks at. Go to your repository's Settings → Branches → Branch protection rules, select your main branch, and enable Require status checks to pass before merging. Add both the CodeQL and Semgrep job names as required checks. Now any pull request with an unresolved high-severity finding is physically blocked from merging until a developer fixes or explicitly triages it — turning your scanner from an advisory tool into an actual control.
Troubleshooting and Verifying Your SAST Pipeline
Workflow doesn't appear in the Security tab. Confirm security-events: write is set in permissions and that the workflow ran on the default branch at least once — CodeQL's Security tab integration requires a baseline scan on main before pull request results display inline.
CodeQL build step fails with "no source code seen." This almost always means the build step didn't actually run before analyze, or it ran but produced no compiled output for a compiled language. Check that your build command exits 0 and confirm the build tool version matches your project's actual requirements.
Scan takes too long and times out. Narrow the language matrix to only what's needed, cache dependencies (actions/cache for node_modules, Gradle caches, or pip wheels), and consider running the full security-extended query pack only on a nightly schedule while pull requests use the faster default pack.
Findings look duplicated across tools. This is expected when running CodeQL and Semgrep together, since both may flag the same SQL injection pattern with different rule IDs. Deduplicate at the triage stage rather than trying to suppress overlap in CI — losing a true positive because it looked redundant is a worse outcome than seeing it twice.
Verify the pipeline actually blocks bad code. Open a test branch, introduce a deliberately vulnerable snippet (a hardcoded credential or an unsanitized eval() call), and confirm the pull request shows a failed check and cannot be merged. This one-time test is the only way to know your branch protection rule is actually wired to the right job names.
How Safeguard Helps
A single SAST scanning GitHub Actions workflow catches a lot, but it's one piece of a larger software supply chain security posture. Safeguard extends what you build in this guide by correlating SAST findings with dependency provenance, build artifact integrity, and runtime signals — so a static analysis alert about a vulnerable code path is automatically weighted against whether that path is reachable, whether the affected package has a known exploit in the wild, and whether the artifact that shipped it was built from a verified, tamper-free pipeline.
Instead of triaging CodeQL and Semgrep results in isolation, Safeguard gives security and platform teams a single view across every repository's scanning coverage, flags repos where SAST isn't enforced as a required check, and feeds findings into SOC 2–ready audit evidence automatically. If you're already running the pipeline described above, Safeguard is the layer that tells you which findings actually deserve an engineer's attention this week — and proves to auditors and customers that the control was running the whole time.