Why Automate SAST in CI/CD?
Security vulnerabilities discovered in production are exponentially more expensive to fix than those caught during development. By integrating SAST into your GitHub Actions pipeline, you can:
- Shift left on security - Catch issues before code review
- Block vulnerable code - Prevent merges that introduce security risks
- Automate compliance - Generate audit-friendly security reports
- Developer education - Provide immediate feedback on secure coding practices
Start with warnings only in your initial integration. This prevents blocking legitimate work while your team learns the tooling. Graduate to required checks after 2-3 weeks.
Prerequisites
Before we begin, ensure you have:
- A GitHub repository with code to scan
- GitHub Actions enabled (free for public repos)
- Basic familiarity with YAML and GitHub Actions syntax
- ElevatedIQ GCP project configured (or use Semgrep CLI directly)
Method 1: Using ElevatedIQ Cloud Functions
ElevatedIQ provides serverless SAST scanning through Google Cloud Functions. This method offers the most comprehensive scanning with SAST, DAST, and dependency analysis.
Step 1: Set Up GitHub Secrets
Navigate to your repository's Settings → Secrets and variables → Actions and add:
GCP_PROJECT_ID=your-elevatediq-project
GCP_SERVICE_ACCOUNT_KEY={"type":"service_account",...}
GCS_BUCKET=your-reports-bucket
Step 2: Create the Workflow File
Create .github/workflows/security-scan.yml in your repository:
name: Security Scan
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
schedule:
# Run every Monday at 9 AM UTC
- cron: '0 9 * * 1'
jobs:
sast-scan:
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write # For uploading SARIF results
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history for better analysis
- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@v1
with:
service_account_key: ${{ secrets.GCP_SERVICE_ACCOUNT_KEY }}
project_id: ${{ secrets.GCP_PROJECT_ID }}
export_default_credentials: true
- name: Trigger ElevatedIQ Scan
id: scan
run: |
# Zip the repository
zip -r source.zip . -x "*.git*" "node_modules/*" "*.zip"
# Upload to GCS
gsutil cp source.zip gs://${{ secrets.GCS_BUCKET }}/scans/${{ github.sha }}/
# Trigger Cloud Build
BUILD_ID=$(gcloud builds submit \
--no-source \
--config=cloudbuild.yaml \
--substitutions=_SOURCE_ZIP=gs://${{ secrets.GCS_BUCKET }}/scans/${{ github.sha }}/source.zip,_SCAN_ID=${{ github.sha }} \
--format="value(id)")
echo "build_id=$BUILD_ID" >> $GITHUB_OUTPUT
# Wait for scan to complete
gcloud builds wait $BUILD_ID --project=${{ secrets.GCP_PROJECT_ID }}
- name: Download Scan Results
run: |
mkdir -p reports
gsutil cp gs://${{ secrets.GCS_BUCKET }}/reports/${{ github.sha }}/semgrep-results.sarif reports/
gsutil cp gs://${{ secrets.GCS_BUCKET }}/reports/${{ github.sha }}/SECURITY_SUMMARY.md reports/
- name: Upload SARIF to GitHub Security
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: reports/semgrep-results.sarif
category: sast-elevatediq
- name: Comment PR with Summary
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const summary = fs.readFileSync('reports/SECURITY_SUMMARY.md', 'utf8');
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `## 🛡️ Security Scan Results\n\n${summary}`
});
- name: Upload Reports as Artifacts
uses: actions/upload-artifact@v3
if: always()
with:
name: security-reports
path: reports/
retention-days: 30
- name: Check for Critical Vulnerabilities
run: |
# Fail build if critical vulnerabilities found
CRITICAL_COUNT=$(grep -c '"level": "error"' reports/semgrep-results.sarif || echo "0")
if [ "$CRITICAL_COUNT" -gt "0" ]; then
echo "❌ Found $CRITICAL_COUNT critical vulnerabilities!"
echo "Review the Security tab for details."
exit 1
else
echo "✅ No critical vulnerabilities found."
fi
Step 3: Configure Cloud Build
Create cloudbuild.yaml in your repository root:
steps:
# Download and extract source
- name: 'gcr.io/cloud-builders/gsutil'
args: ['cp', '${_SOURCE_ZIP}', '/workspace/source.zip']
- name: 'ubuntu'
args: ['unzip', '/workspace/source.zip', '-d', '/workspace/code']
# Run Semgrep SAST
- name: 'returntocorp/semgrep'
args:
- 'scan'
- '--config=auto'
- '--sarif'
- '--output=/workspace/semgrep-results.sarif'
- '/workspace/code'
# Upload results
- name: 'gcr.io/cloud-builders/gsutil'
args:
- 'cp'
- '/workspace/semgrep-results.sarif'
- 'gs://${_GCS_BUCKET}/reports/${_SCAN_ID}/'
timeout: 1200s # 20 minutes
Method 2: Using Semgrep Directly (Faster, Free)
For a simpler setup without external dependencies, run Semgrep directly in GitHub Actions. This is perfect for open-source projects or teams just getting started.
name: Semgrep SAST
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
semgrep:
runs-on: ubuntu-latest
container:
image: returntocorp/semgrep
steps:
- uses: actions/checkout@v4
- name: Run Semgrep
run: |
semgrep scan \
--config=auto \
--sarif \
--output=semgrep-results.sarif \
--error \
--verbose \
.
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v2
if: always()
with:
sarif_file: semgrep-results.sarif
The --error flag will fail the build on any findings. For initial integration, remove this flag or use --severity=ERROR to only fail on critical issues.
Advanced Configuration
Custom Semgrep Rules
Create custom rules specific to your codebase in .semgrep/rules.yaml:
rules:
- id: hardcoded-api-key
pattern: |
const API_KEY = "..."
message: "Hardcoded API key detected. Use environment variables."
severity: ERROR
languages: [javascript, typescript]
- id: sql-injection-risk
pattern: |
db.query($QUERY + $USER_INPUT)
message: "Potential SQL injection. Use parameterized queries."
severity: ERROR
languages: [javascript]
Conditional Scanning
Only scan files that changed in a PR to speed up checks:
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v40
with:
files: |
**/*.js
**/*.ts
**/*.py
- name: Run Semgrep on changed files
if: steps.changed-files.outputs.any_changed == 'true'
run: |
echo "${{ steps.changed-files.outputs.all_changed_files }}" | \
xargs semgrep scan --config=auto --sarif
Branch Protection Rules
Enforce security checks before merge:
- Go to Settings → Branches → Branch protection rules
- Add rule for
mainbranch - Enable "Require status checks to pass before merging"
- Select your security workflow (e.g., "sast-scan")
- Enable "Require branches to be up to date before merging"
Viewing Results
GitHub Security Tab
SARIF results automatically appear in the Security → Code scanning alerts tab. You can filter by severity, dismiss false positives, and track remediation.
Pull Request Comments
The workflow above automatically comments on PRs with a security summary. Developers see findings without leaving the PR interface.
Artifacts
Full reports are saved as workflow artifacts for 30 days. Download them from the Actions tab for detailed analysis.
Best Practices
🎯 Start Small
Begin with warnings only. Let developers learn the tool before enforcing blocks.
🔄 Iterate Rules
Tune rules based on false positives. Disable noisy checks, add custom patterns.
📊 Track Metrics
Monitor time-to-fix, vulnerability trends, and false positive rates.
🎓 Educate Team
Use findings as teaching moments. Share secure coding resources.
Troubleshooting
Build Times Too Long
- Use diff-aware scanning (only changed files)
- Cache dependencies with
actions/cache@v3 - Run scans on schedule instead of every commit
- Use matrix strategy to parallelize language-specific scans
Too Many False Positives
- Start with
--config=p/security-audit(high confidence rules only) - Use
.semgrep.ymlto exclude paths:node_modules/, test/, vendor/ - Add
# nosemgrepcomments for legitimate exceptions - Contribute improvements to Semgrep rules via GitHub
SARIF Upload Fails
- Ensure
security-events: writepermission is granted - Check SARIF file is valid JSON (use
jq . semgrep-results.sarif) - Verify GitHub Advanced Security is enabled (required for private repos)
Integration with Other Tools
Dependency Scanning
Combine SAST with dependency analysis:
- name: Run Trivy for dependencies
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
format: 'sarif'
output: 'trivy-results.sarif'
Secret Scanning
Add GitGuardian or TruffleHog for hardcoded secrets:
- name: TruffleHog Secret Scan
uses: trufflesecurity/trufflehog@main
with:
path: ./
base: ${{ github.event.repository.default_branch }}
Next Steps
You've now automated SAST in your GitHub Actions pipeline! To continue improving your security posture:
- Write custom Semgrep rules for your codebase
- Add DAST scanning for runtime vulnerabilities
- Try our interactive demo to see SAST in action
- Contact us for enterprise support and custom rules
ElevatedIQ offers managed security scanning with custom rule development, false positive tuning, and 24/7 support. Get in touch to learn more.