Integrating SAST with GitHub Actions: Complete CI/CD Security Guide

Learn how to automate Static Application Security Testing (SAST) in your GitHub Actions workflows, catch vulnerabilities before they reach production, and build security into your development process.

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
💡 Pro Tip:

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:

GitHub Secrets Configuration
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:

.github/workflows/security-scan.yml
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:

cloudbuild.yaml
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.

.github/workflows/semgrep.yml
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
⚠️ Warning:

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:

.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:

Diff-aware scanning
- 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:

  1. Go to Settings → Branches → Branch protection rules
  2. Add rule for main branch
  3. Enable "Require status checks to pass before merging"
  4. Select your security workflow (e.g., "sast-scan")
  5. 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.yml to exclude paths: node_modules/, test/, vendor/
  • Add # nosemgrep comments for legitimate exceptions
  • Contribute improvements to Semgrep rules via GitHub

SARIF Upload Fails

  • Ensure security-events: write permission 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:

💡 Need Help?

ElevatedIQ offers managed security scanning with custom rule development, false positive tuning, and 24/7 support. Get in touch to learn more.