Skip to main content
A single commit. One missed vulnerability. $50,000 in damages. This isn’t a hypothetical scenario—it’s happening to development teams every week. The problem isn’t that developers are careless. It’s that security scanning is still manual, optional, and easy to skip when deadlines loom. Manual security scanning creates gaps. Automated scanning catches vulnerabilities before they reach production, protecting your users and your reputation.

Introduction

Automated security scanning transforms security from an afterthought into a seamless part of your development workflow. Instead of remembering to run scans manually (and inevitably forgetting), your CI/CD pipeline automatically checks every commit and pull request for vulnerabilities. This guide shows you two approaches:
  • Manual setup: Step-by-step instructions for hands-on configuration
  • AI-assisted setup: Copy-paste workflows with AI prompts for rapid deployment
I’d recommend skimming the manual setup first so you understand what’s happening, then using the AI-assisted setup for a quick and easy setup. Plus, you’ll learn how API keys work, why they’re secure, and how to protect them properly in your CI/CD environment. If you’re not familiar with GitHub Actions, you can learn more about them here.

Why Automated Security Scanning Matters

The Manual Scanning Problem

Manual security scanning has three critical flaws:
  1. Human Error: Developers forget to run scans, especially under pressure
  2. Inconsistent Coverage: Different team members use different tools or settings
  3. Late Detection: Vulnerabilities are found after code reaches production

The Automated Solution

Automated scanning eliminates these problems by:
  • Running on every push: No forgotten scans
  • Consistent configuration: Same rules applied every time
  • Early detection: Vulnerabilities caught before deployment
  • Build failure: Critical issues block deployment automatically

Understanding API Keys in CI/CD

What Are API Keys?

API keys are authentication tokens that allow automated tools to access services on your behalf. Think of them as digital keys that unlock specific capabilities—in this case, security scanning services like Rafter.

Why API Keys Are Secure

API keys are designed for programmatic access and include several security features:
  • Scoped permissions: Keys can only access specific services
  • Usage tracking: All API calls are logged and monitored
  • Easy rotation: Keys can be regenerated instantly if compromised
  • Environment isolation: Keys are stored separately from your code

How GitHub Secrets Protect Your Keys

GitHub Secrets provide enterprise-grade security for sensitive data:
  • Encryption at rest: Keys are encrypted when stored
  • Encryption in transit: Keys are encrypted when accessed
  • Access control: Only authorized workflows can use secrets
  • Audit logging: All secret access is logged

Manual Setup: Step-by-Step Configuration

Step 1: Get Your Rafter API Key

If you have an existing key saved to a safe place, copy it. Otherwise, generate a new key… Rafter protects your API keys by not storing them. You get access once, when you generate/refresh the key. Then, refreshing the key invalidates the previous key to keep it safe. It’s a way to help you rotate keys, which is best practice. Navigate to Account Settings
  1. Go to your Rafter account settings
  2. Look for the “API Keys” section
Generate or Retrieve Key
  1. Click “Generate New API Key” or “Refresh API Key”
  2. The key is automatically copied to your clipboard (it won’t be shown/copiable again)
API keys are sensitive credentials. Never commit them to your repository or share them in plain text.

Step 2: Add API Key to GitHub Secrets

Go to Repository Settings
  1. Navigate to your GitHub repository (go to github.com and click on your repository)
  2. Click “Settings” → “Secrets and variables” → “Actions”
Create New Secret
  1. Click “New repository secret” (big green button)
  2. Name: RAFTER_API_KEY
  3. Value: Paste your API key (the key you copied in Step 1)
  4. Click “Add secret” (big green button)

Step 3: Create GitHub Workflow

Create Workflow Directory This just creates the directory if it doesn’t exist. The correct filepath is critical. Because it starts with a dot, it’s a hidden directory, so you won’t see it in Finder or your file explorer.
mkdir -p .github/workflows
Create Security Scan Workflow Create a new file called security-scan.yml in the .github/workflows directory (final path: .github/workflows/security-scan.yml). This is the file that will contain the security scan workflow.
# Create and open the file (or just make it as you would any other file)
nano .github/workflows/security-scan.yml
Copy and paste the following into the file:
# 1. This file MUST be saved in the following directory: .github/workflows/
#    If the .github/workflows directories don't exist, you'll need to create them.
#
# 2. Add your Rafter API key as a GitHub repository secret:
#    - Go to your repository Settings -> Secrets and variables -> Actions
#    - Click "New repository secret"
#    - Name: RAFTER_API_KEY
#    - Value: your_actual_api_key_here

name: Security Scan with Rafter

on:
  push:
    branches: [ main ]

jobs:
  security-scan-three:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Run security scan 🛡️
        env:
          # Use secrets for your API key
          RAFTER_API_KEY: ${{ secrets.RAFTER_API_KEY }}
        run: |
            set -euo pipefail

            # 1. Trigger scan
            SCAN_ID=$(curl -fsS -X POST \
              -H "Content-Type: application/json" \
              -H "x-api-key: ${{ secrets.RAFTER_API_KEY }}" \
              -d '{
                "repository_name": "${{ github.repository }}",
                "branch_name": "${{ github.ref_name }}"
              }' \
              https://rafter.so/api/static/scan | jq -r '.scan_id')

            echo "SCAN_ID: $SCAN_ID"

            # 2. Wait for completion (polling), for a maximum of 10 minutes (60 loops × 10s = 600s = 10m)
            MAX_POLLS=60
            POLL_COUNT=0
            while [ $POLL_COUNT -lt $MAX_POLLS ]; do
              STATUS=$(curl -fsS -H "x-api-key: ${{ secrets.RAFTER_API_KEY }}" \
                "https://rafter.so/api/static/scan?scan_id=$SCAN_ID" | jq -r '.status')
              
              if [ "$STATUS" = "completed" ] || [ "$STATUS" = "failed" ]; then
                break
              fi
              
              echo "Scan status: $STATUS (poll $((POLL_COUNT+1)) of $MAX_POLLS)"
              sleep 10
              POLL_COUNT=$((POLL_COUNT+1))
            done

            if [ "$STATUS" = "failed" ]; then
              echo "❌ Scan failed. Please try again."
              exit 1
            elif [ "$STATUS" != "completed" ]; then
              echo "❌ Scan did not complete within 10 minutes."
              exit 1
            fi

            # 3. Get results
            curl -fsS -H "x-api-key: ${{ secrets.RAFTER_API_KEY }}" \
              "https://rafter.so/api/static/scan?scan_id=$SCAN_ID" > scan-results.json

            curl -fsS -H "x-api-key: ${{ secrets.RAFTER_API_KEY }}" \
              "https://rafter.so/api/static/scan?scan_id=$SCAN_ID&format=md" > scan-results.md


      - name: Check for critical vulnerabilities 🧐
        run: |
          # Install jq if needed (usually present on ubuntu-latest)
          # sudo apt-get update && sudo apt-get install -y jq

          if [ ! -s scan-results.json ]; then
            echo "⚠️ Scan results file is empty or not found."
            exit 0 # Or exit 1 if an empty file is an error
          fi

          CRITICAL_COUNT=$(jq '.vulnerabilities | map(select(.level=="error")) | length' scan-results.json)

          if [ "$CRITICAL_COUNT" -gt 0 ]; then
            echo "❌ Found $CRITICAL_COUNT critical vulnerabilities!"
            echo "Please review the scan results and fix critical issues before merging."
            exit 1
          else
            echo "✅ No critical vulnerabilities found"
          fi

      - name: Upload scan results 📄
        uses: actions/upload-artifact@v4
        if: always() # Upload results even if vulnerabilities were found
        with:
          name: security-scan-results
          path: | 
            scan-results.json
            scan-results.md
          retention-days: 30
Save and close the file. You’re done with the manual setup!

Step 4: Test Your Setup

Make a Test Commit
git add .github/workflows/security-scan.yml
git commit -m "Add automated security scanning"
git push
Check GitHub Actions
  1. Go to your repository’s “Actions” tab
  2. Look for the “Security Scan with Rafter” workflow
  3. Verify it runs successfully

AI-Assisted Setup: Copy-Paste Deployment

The AI Assistant Approach

Modern development teams increasingly use AI assistants for rapid setup. Here’s how to leverage Rafter for security scanning configuration:

Step 1: Use This AI Prompt

Copy this prompt to help AI assistants understand the setup process. Any AI assistant will do: vibe coding platforms like Bolt, Lovable, Replit, Emergent, etc all work. Or use your favorite IDE’s built-in AI assistant like Cursor, Claude, ChatGPT, etc.
You are setting up automated security scanning for a software project. Here's what you need to do:

5. **Setup Steps**:
   - Copy this workflow to .github/workflows/security-scan.yml
   - Add your Rafter API key as a GitHub repository secret named RAFTER_API_KEY

This automated approach catches security issues before they reach production, protecting your users and maintaining code quality. Make sure it's set up correctly and securely.


## Workflow

# 1. This file MUST be saved in the following directory: .github/workflows/
#    If the .github/workflows directories don't exist, you'll need to create them.
#
# 2. Add your Rafter API key as a GitHub repository secret:
#    - Go to your repository Settings -> Secrets and variables -> Actions
#    - Click "New repository secret"
#    - Name: RAFTER_API_KEY
#    - Value: your_actual_api_key_here

name: Security Scan with Rafter

on:
  push:
    branches: [ main ]

jobs:
  security-scan-three:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Run security scan 🛡️
        env:
          # Use secrets for your API key
          RAFTER_API_KEY: ${{ secrets.RAFTER_API_KEY }}
        run: |
            set -euo pipefail

            # 1. Trigger scan
            SCAN_ID=$(curl -fsS -X POST \
              -H "Content-Type: application/json" \
              -H "x-api-key: ${{ secrets.RAFTER_API_KEY }}" \
              -d '{
                "repository_name": "${{ github.repository }}",
                "branch_name": "${{ github.ref_name }}"
              }' \
              https://rafter.so/api/static/scan | jq -r '.scan_id')

            echo "SCAN_ID: $SCAN_ID"

            # 2. Wait for completion (polling), for a maximum of 10 minutes (60 loops × 10s = 600s = 10m)
            MAX_POLLS=60
            POLL_COUNT=0
            while [ $POLL_COUNT -lt $MAX_POLLS ]; do
              STATUS=$(curl -fsS -H "x-api-key: ${{ secrets.RAFTER_API_KEY }}" \
                "https://rafter.so/api/static/scan?scan_id=$SCAN_ID" | jq -r '.status')
              
              if [ "$STATUS" = "completed" ] || [ "$STATUS" = "failed" ]; then
                break
              fi
              
              echo "Scan status: $STATUS (poll $((POLL_COUNT+1)) of $MAX_POLLS)"
              sleep 10
              POLL_COUNT=$((POLL_COUNT+1))
            done

            if [ "$STATUS" = "failed" ]; then
              echo "❌ Scan failed. Please try again."
              exit 1
            elif [ "$STATUS" != "completed" ]; then
              echo "❌ Scan did not complete within 10 minutes."
              exit 1
            fi

            # 3. Get results
            curl -fsS -H "x-api-key: ${{ secrets.RAFTER_API_KEY }}" \
              "https://rafter.so/api/static/scan?scan_id=$SCAN_ID" > scan-results.json

            curl -fsS -H "x-api-key: ${{ secrets.RAFTER_API_KEY }}" \
              "https://rafter.so/api/static/scan?scan_id=$SCAN_ID&format=md" > scan-results.md


      - name: Check for critical vulnerabilities 🧐
        run: |
          # Install jq if needed (usually present on ubuntu-latest)
          # sudo apt-get update && sudo apt-get install -y jq

          if [ ! -s scan-results.json ]; then
            echo "⚠️ Scan results file is empty or not found."
            exit 0 # Or exit 1 if an empty file is an error
          fi

          CRITICAL_COUNT=$(jq '.vulnerabilities | map(select(.level=="error")) | length' scan-results.json)

          if [ "$CRITICAL_COUNT" -gt 0 ]; then
            echo "❌ Found $CRITICAL_COUNT critical vulnerabilities!"
            echo "Please review the scan results and fix critical issues before merging."
            exit 1
          else
            echo "✅ No critical vulnerabilities found"
          fi

      - name: Upload scan results 📄
        uses: actions/upload-artifact@v4
        if: always() # Upload results even if vulnerabilities were found
        with:
          name: security-scan-results
          path: | 
            scan-results.json
            scan-results.md
          retention-days: 30

Step 3: Follow AI Instructions

  1. Paste the prompt into your AI assistant
  2. Follow the generated instructions step by step
  3. Verify the setup by checking GitHub Actions

Understanding the Workflow Components

Trigger Configuration

on:
  push:
    branches: [ main ]
This configuration ensures scanning runs:
  • On every push to the main branch
  • Automatically, without manual intervention

API-Based Scanning

The workflow uses Rafter’s API directly instead of installing CLI tools:
- name: Run security scan 🛡️
  env:
    RAFTER_API_KEY: ${{ secrets.RAFTER_API_KEY }}
  run: |
    set -euo pipefail
    
    # 1. Trigger scan
    SCAN_ID=$(curl -fsS -X POST \
      -H "Content-Type: application/json" \
      -H "x-api-key: ${{ secrets.RAFTER_API_KEY }}" \
      -d '{
        "repository_name": "${{ github.repository }}",
        "branch_name": "${{ github.ref_name }}"
      }' \
      https://rafter.so/api/static/scan | jq -r '.scan_id')
This approach:
  • Eliminates CLI installation: No need to install tools on the runner
  • Uses secure API authentication: API key passed via headers

Polling and Status Management

# 2. Wait for completion (polling), for a maximum of 10 minutes
MAX_POLLS=60
POLL_COUNT=0
while [ $POLL_COUNT -lt $MAX_POLLS ]; do
  STATUS=$(curl -fsS -H "x-api-key: ${{ secrets.RAFTER_API_KEY }}" \
    "https://rafter.so/api/static/scan?scan_id=$SCAN_ID" | jq -r '.status')
  
  if [ "$STATUS" = "completed" ] || [ "$STATUS" = "failed" ]; then
    break
  fi
  
  echo "Scan status: $STATUS (poll $((POLL_COUNT+1)) of $MAX_POLLS)"
  sleep 10
  POLL_COUNT=$((POLL_COUNT+1))
done
The polling mechanism:
  • Checks every 10 seconds: Balances responsiveness with API load
  • Handles both success and failure: Exits on completed or failed status
  • Prevents infinite loops: Maximum 60 polls (10 minutes total)
  • Provides progress feedback: Shows current status and poll count

Error Handling and Status Management

if [ "$STATUS" = "failed" ]; then
  echo "❌ Scan failed. Please try again."
  exit 1
elif [ "$STATUS" != "completed" ]; then
  echo "❌ Scan did not complete within 10 minutes."
  exit 1
fi
This robust error handling:
  • Distinguishes failure types: Separate messages for scan failures vs timeouts
  • Fails builds appropriately: Exits with code 1 to fail the GitHub Action
  • Provides clear feedback: Specific error messages for different scenarios

Result Retrieval

# 3. Get results
curl -fsS -H "x-api-key: ${{ secrets.RAFTER_API_KEY }}" \
  "https://rafter.so/api/static/scan?scan_id=$SCAN_ID" > scan-results.json

curl -fsS -H "x-api-key: ${{ secrets.RAFTER_API_KEY }}" \
  "https://rafter.so/api/static/scan?scan_id=$SCAN_ID&format=md" > scan-results.md
The workflow retrieves results in multiple formats:
  • JSON format: For programmatic processing and vulnerability counting
  • Markdown format: For human-readable reports and documentation
  • File output: Saves results to files for artifact upload
Of course, your report will also be immediately available in your dashboard.

Vulnerability Checking

- name: Check for critical vulnerabilities
  run: |
    CRITICAL_COUNT=$(cat scan-results.json | jq '.vulnerabilities | map(select(.level=="error")) | length')
    
    if [ $CRITICAL_COUNT -gt 0 ]; then
      echo "❌ Found $CRITICAL_COUNT critical vulnerabilities!"
      exit 1
    else
      echo "✅ No critical vulnerabilities found"
    fi
This critical step:
  • Parses scan results using jq
  • Counts error-level vulnerabilities
  • Fails the build if critical issues are found
  • Provides clear feedback to developers

Advanced Configuration Options

While we’ve outlined some powerful use cases below, see our documentation for more advanced configuration options: Rafter CI/CD Documentation.

Customizing Scan Triggers

You can customize when scans run:
# Scan on all branches
on: [push, pull_request]

# Scan only on specific file changes
on:
  push:
    paths:
      - 'src/**'
      - 'package.json'

# Schedule regular scans
on:
  schedule:
    - cron: '0 2 * * 1'  # Every Monday at 2 AM

Adjusting Failure Thresholds

Modify the vulnerability checking logic:
# Fail on warnings and errors
WARNING_COUNT=$(cat scan-results.json | jq '.vulnerabilities | map(select(.level=="warning" or .level=="error")) | length')

# Fail only on specific vulnerability types (using hashed rule IDs)
CRITICAL_COUNT=$(cat scan-results.json | jq '.vulnerabilities | map(select(.ruleId | startswith("R-"))) | length')

# Fail on specific vulnerability patterns
SQL_INJECTION_COUNT=$(cat scan-results.json | jq '.vulnerabilities | map(select(.message | contains("SQL injection"))) | length')

Customizing Polling Behavior

Adjust the polling mechanism for different scan durations:
# For faster scans (reduce polling interval)
sleep 5  # Check every 5 seconds instead of 10

# For longer scans (increase timeout)
MAX_POLLS=120  # 15 minutes instead of 10 (max scan length currently supported by Rafter)

# For more verbose output
echo "Scan status: $STATUS (poll $((POLL_COUNT+1)) of $MAX_POLLS) - $(date)"

Adding Notifications

Integrate with Slack, Discord, or email:
- name: Notify on failure
  if: failure()
  uses: 8398a7/action-slack@v3
  with:
    status: failure
    text: "Security scan failed: ${{ github.event.head_commit.message }}"

Troubleshooting Common Issues

API Key Not Found

Error: Error: RAFTER_API_KEY environment variable not set Solution:
  1. Verify the secret exists in GitHub repository settings
  2. Check the secret name matches exactly: RAFTER_API_KEY
  3. Ensure the workflow uses ${{ secrets.RAFTER_API_KEY }}

Scan Request Fails

Error: curl: (22) The requested URL returned error: 401 or curl: (22) The requested URL returned error: 400 Solution:
  • 401 Unauthorized: Check your API key is valid and not expired
  • 400 Bad Request: Verify repository name format (should be owner/repo)
  • Network issues: The -fsS flags will cause curl to fail silently on HTTP errors
  • Branch name issues: Ensure ${{ github.ref_name }} returns the correct branch name

Scan Status Issues

Error: Scan stuck in “processing” or “pending” status Solution:
  • Timeout handling: The workflow automatically fails after 10 minutes
  • Status checking: Verify the API returns valid status values (completed, failed, processing, pending)
  • Polling frequency: Adjust sleep 10 if scans typically take longer
  • API rate limits: Check if you’re hitting API rate limits

Scan Results Not Found

Error: scan-results.json: No such file or directory Solution:
  • Check scan completion: Ensure the scan reached “completed” status
  • Verify file output: The workflow saves results to scan-results.json and scan-results.md
  • API response format: Confirm the API returns results in the expected JSON format
  • Error handling: The set -euo pipefail ensures the script fails if curl commands fail

Security Best Practices

API Key Management

  • Rotate keys regularly: Generate new keys every 90 days
  • Monitor usage: Check API key usage logs for anomalies
  • Use least privilege: Only grant necessary permissions
  • Never log keys: Ensure keys don’t appear in logs or outputs

Workflow Security

  • Pin action versions: Use specific commit SHAs instead of tags
  • Review permissions: Limit workflow permissions to minimum required
  • Audit regularly: Review workflow changes and access patterns
  • Use trusted sources: Only use official GitHub Actions

Repository Security

  • Enable branch protection: Require status checks before merging
  • Use required reviewers: Require security team review for workflow changes
  • Monitor secrets: Regularly audit repository secrets
  • Enable security alerts: Use GitHub’s security features

Measuring Success

Key Metrics to Track

  • Scan Coverage: Percentage of commits scanned
  • Detection Rate: Vulnerabilities found per scan
  • Fix Time: Average time from detection to resolution
  • False Positive Rate: Incorrect vulnerability reports

Success Indicators

  • Zero critical vulnerabilities in production deployments
  • Consistent scan execution across all branches
  • Rapid vulnerability resolution (under 24 hours)
  • Developer adoption of security-first practices

Conclusion

Automated security scanning transforms security from a manual, error-prone process into a seamless part of your development workflow. By catching vulnerabilities before they reach production, you protect your users, your reputation, and your bottom line. The setup process takes just 5 minutes, but the protection lasts for the lifetime of your project. Whether you prefer manual configuration or AI-assisted setup, the result is the same: comprehensive security coverage that runs automatically on every commit. Start with the basic workflow, customize it for your needs, and watch your security posture improve with every deployment.