Security 11 min read

Laravel Security Scans in GitHub Actions: A CI/CD Pipeline Guide

Automate Laravel security checks in your GitHub Actions pipeline. Set up composer audit, static analysis, StackShield post-deploy scans, and block merges when vulnerabilities are found.

Matt King
Matt King
June 4, 2026
Last updated: June 4, 2026
Laravel Security Scans in GitHub Actions: A CI/CD Pipeline Guide

Most Laravel teams have a test pipeline. They run PHPUnit on every PR, check code style, maybe run PHPStan. But security checks? Those are still mostly manual, run occasionally, or skipped entirely.

This guide walks through building a layered security pipeline in GitHub Actions that catches vulnerabilities at every stage: dependency scanning on PR, static analysis before merge, secret detection on push, and an external scan after deploy.


Why CI/CD Security Matters

The problem with one-off security reviews is that they measure a snapshot in time. Your application changes constantly. Every PR can introduce a new dependency with a known CVE.

CI/CD security catches issues at the moment they are introduced:

  • A PR that adds a vulnerable package fails the build before merge
  • A commit with an accidentally staged .env gets blocked immediately
  • A deploy to staging triggers an external scan, blocking promotion to production if issues are found

Layer 1: Dependency Scanning with composer audit

composer audit checks your composer.lock against the PHP Security Advisories Database.

name: Security
on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

jobs:
  dependency-audit:
    name: Dependency Audit
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'
          tools: composer:v2

      - run: composer install --prefer-dist --no-progress

      - name: Audit dependencies
        run: composer audit

If any package has a known advisory, the job fails and the PR cannot be merged.

Ignoring Known Advisories

      - name: Audit dependencies
        run: |
          # CVE-2024-12345: Affects XML parser only, which we do not use.
          composer audit --ignore CVE-2024-12345

Layer 2: Static Analysis

PHPStan catches type errors and security patterns before code runs:

  static-analysis:
    name: Static Analysis
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'
          tools: composer:v2

      - run: composer install --prefer-dist --no-progress

      - name: Run PHPStan
        run: ./vendor/bin/phpstan analyse --error-format=github

The --error-format=github flag outputs errors as inline annotations on the PR diff.


Layer 3: Secret Detection

TruffleHog scans git history for accidentally committed credentials:

  secret-detection:
    name: Secret Detection
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - uses: trufflesecurity/trufflehog@main
        with:
          path: ./
          base: ${{ github.event.repository.default_branch }}
          head: HEAD
          extra_args: --only-verified

The --only-verified flag reduces false positives by only reporting secrets that TruffleHog can confirm are active.


Layer 4: Post-Deploy External Scanning

After deploying to staging, trigger a StackShield scan via the API:

  post-deploy-scan:
    name: StackShield Scan
    runs-on: ubuntu-latest
    needs: [deploy-staging]
    if: github.ref == 'refs/heads/main'
    steps:
      - name: Trigger scan
        id: trigger
        run: |
          RESPONSE=$(curl -s -X POST https://stackshield.app/api/v1/scans \
            -H "Authorization: Bearer ${{ secrets.STACKSHIELD_API_KEY }}" \
            -H "Content-Type: application/json" \
            -d "{\"url\": \"${{ secrets.STAGING_URL }}\"}")
          SCAN_ID=$(echo $RESPONSE | python3 -c "import json,sys; print(json.load(sys.stdin)['id'])")
          echo "scan_id=$SCAN_ID" >> $GITHUB_OUTPUT

      - name: Wait for results
        run: |
          for i in $(seq 1 20); do
            sleep 10
            RESULT=$(curl -s \
              -H "Authorization: Bearer ${{ secrets.STACKSHIELD_API_KEY }}" \
              "https://stackshield.app/api/v1/scans/${{ steps.trigger.outputs.scan_id }}")
            STATUS=$(echo $RESULT | python3 -c "import json,sys; print(json.load(sys.stdin)['status'])")
            if [ "$STATUS" = "completed" ]; then
              CRITICAL=$(echo $RESULT | python3 -c "
          import json, sys
          d = json.load(sys.stdin)
          print(len([f for f in d.get('findings',[]) if f.get('severity')=='critical']))
          ")
              [ "$CRITICAL" -gt "0" ] && { echo "::error::$CRITICAL critical finding(s)"; exit 1; }
              echo "Scan passed."; exit 0
            fi
          done
          echo "::warning::Scan timed out"

When Each Layer Runs

Layer Trigger Blocks
Dependency audit Every PR and push to main PR merge
Static analysis Every PR and push to main PR merge
Secret detection Every push Push (on PR)
StackShield scan After deploy to staging Promotion to production

Branch Protection Rules

  1. Go to repository Settings, then Branches
  2. Edit your branch protection rule for main
  3. Check "Require status checks to pass before merging"
  4. Add job names: Dependency Audit, Static Analysis, Secret Detection
  5. Check "Do not allow bypassing the above settings"

No PR can be merged unless all security jobs pass.


Start With One Layer

If this feels like a lot, start with composer audit. It takes five minutes to set up and catches real vulnerabilities in existing dependencies immediately. Add the other layers over time.

Run a free StackShield scan to see what your application looks like from the outside before you wire it into your pipeline.

Free security check

Is your Laravel app exposed right now?

34% of Laravel apps we scan have at least one critical issue. Most teams don't find out until something breaks. Our free scan checks your live application in under 60 seconds.

18% have debug mode on
72% missing security headers
12% have exposed .env
Scan My App Free No signup required. Results in 60 seconds.

Frequently Asked Questions

How do I run composer audit in GitHub Actions?

Add a job to your workflow that runs composer install followed by composer audit. If any advisories are found, composer audit exits with a non-zero code which automatically fails the GitHub Actions job.

Can I block a PR if a security vulnerability is found?

Yes. Set up a required status check in your repository branch protection rules. Go to Settings, then Branches, then edit your protection rule. Under "Require status checks to pass before merging", add your security workflow job name.

How do I trigger a StackShield scan after deployment?

Use the StackShield API from a GitHub Actions step that runs after your deploy step. Send a POST request with your API key and the target URL, then poll the results endpoint until the scan completes. If critical findings are returned, exit with a non-zero code to fail the pipeline.

Should security scans run on every PR or only on merge?

Run dependency audits and static analysis on every PR so developers get feedback before code is merged. Reserve post-deploy external scans for after deployment to staging, since those target a running application.

How do I ignore known vulnerabilities in CI?

For composer audit, pass specific CVE IDs with --ignore CVE-XXXX-XXXXX to suppress known issues you have accepted. Document every ignored advisory in a comment or linked issue so future team members understand why it was excluded.

Stay Updated on Laravel Security

Get actionable security tips, vulnerability alerts, and best practices for Laravel apps.