How to Automate OWASP ZAP Scans in GitHub Actions (2026 Guide)

Published: April 8, 2026 Reading time: 10 minutes

📢 Affiliate Disclosure: This site contains affiliate links to Amazon. We earn a commission when you purchase through our links at no additional cost to you.

OWASP ZAP has official GitHub Actions that run security scans on every pull request — for free. A baseline scan takes 2 minutes. A full scan takes 10–30 minutes. Either way, you get automated vulnerability detection in your CI/CD pipeline without paying for Burp Suite Enterprise or any other commercial tool.

This guide walks through every scan type ZAP offers in GitHub Actions, with complete workflow files you can copy into your repository today. We'll cover baseline scans, full active scans, API scans, custom scan policies, authenticated scanning, and how to handle results without drowning in false positives.

1. The Three ZAP Scan Types

ZAP's GitHub Actions come in three flavours, each designed for a different use case:

Action What It Does Duration Best For
zaproxy/action-baseline Spider + passive scan only 1–3 minutes Every PR — fast feedback
zaproxy/action-full-scan Spider + passive + active scan 10–60 minutes Nightly or weekly — deep testing
zaproxy/action-api-scan API-specific scan from OpenAPI/GraphQL spec 5–20 minutes API-first applications

Start with the baseline scan on every PR. Add the full scan as a scheduled nightly job. Add the API scan if you have an OpenAPI or GraphQL schema. This layered approach gives you fast feedback on PRs and thorough testing overnight.

2. Baseline Scan: Your First 5 Minutes

The baseline scan spiders your application and runs ZAP's passive scanner. It doesn't send any attack payloads — it only analyses the responses your application already returns. This makes it safe to run on every PR, even against production-like environments.

Create .github/workflows/zap-baseline.yml:

name: ZAP Baseline Scan

on:
  pull_request:
    branches: [main]

jobs:
  zap-baseline:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Start application
        run: |
          docker compose up -d
          sleep 10  # Wait for app to be ready

      - name: ZAP Baseline Scan
        uses: zaproxy/action-baseline@v0.12.0
        with:
          target: 'http://localhost:3000'
          rules_file_name: '.zap/rules.tsv'
          cmd_options: '-a'

      - name: Upload Report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: zap-baseline-report
          path: report_html.html

That's a working security scan in 20 lines. When a developer opens a PR, ZAP will:

  1. Spider the application starting from http://localhost:3000
  2. Passively analyse every response for security issues (missing headers, cookie flags, information disclosure, etc.)
  3. Generate an HTML report uploaded as a build artifact
  4. Create a GitHub issue summarising any new findings (enabled by default)

The -a flag in cmd_options includes the alpha-quality passive scan rules, which catch more issues but may produce more false positives. Remove it if you want only stable rules.

3. Full Scan: Active Vulnerability Testing

The full scan adds active testing — ZAP sends attack payloads (SQL injection, XSS, path traversal, etc.) to every parameter it discovers. This finds real vulnerabilities but takes longer and should only run against test environments.

Create .github/workflows/zap-full-scan.yml:

name: ZAP Full Scan

on:
  schedule:
    - cron: '0 2 * * *'  # Nightly at 2 AM UTC
  workflow_dispatch:       # Manual trigger

jobs:
  zap-full-scan:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Start application
        run: |
          docker compose up -d
          sleep 15

      - name: ZAP Full Scan
        uses: zaproxy/action-full-scan@v0.10.0
        with:
          target: 'http://localhost:3000'
          rules_file_name: '.zap/rules.tsv'
          cmd_options: '-a -j'

      - name: Upload Report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: zap-full-scan-report
          path: report_html.html

The -j flag enables the AJAX Spider, which uses a headless browser to crawl JavaScript-heavy applications. If your app is a SPA (React, Vue, Angular), you need this flag — the traditional spider won't discover client-side routes.

Important: Never run the full scan against production. Active scanning sends malicious payloads that could trigger WAF blocks, corrupt data, or cause unexpected behaviour. Always target a staging or test environment.

4. API Scan: OpenAPI and GraphQL

If your application exposes an API with an OpenAPI (Swagger) or GraphQL schema, the API scan is more effective than the baseline or full scan. It reads the schema to understand every endpoint, parameter, and data type — then tests each one systematically.

name: ZAP API Scan

on:
  pull_request:
    paths:
      - 'openapi.yaml'
      - 'src/api/**'

jobs:
  zap-api-scan:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Start application
        run: |
          docker compose up -d
          sleep 10

      - name: ZAP API Scan
        uses: zaproxy/action-api-scan@v0.7.0
        with:
          target: 'http://localhost:3000/openapi.yaml'
          format: openapi
          rules_file_name: '.zap/rules.tsv'

      - name: Upload Report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: zap-api-report
          path: report_html.html

The format parameter accepts openapi, soap, or graphql. For GraphQL, point the target at your GraphQL endpoint and ZAP will introspect the schema automatically.

Trigger this scan only when API-related files change (paths filter) to avoid running it on frontend-only PRs.

5. Custom Scan Rules and False Positive Management

Out of the box, ZAP will flag everything it finds. Some of those findings won't apply to your application — maybe you intentionally don't set X-Frame-Options because your app needs to be embedded, or your API returns stack traces in development mode by design.

Create a rules file at .zap/rules.tsv to control alert behaviour:

# .zap/rules.tsv
# Format: rule_id	action	name
10010	IGNORE	Cookie No HttpOnly Flag
10011	IGNORE	Cookie Without Secure Flag
10015	WARN	Incomplete or No Cache-control Header Set
10020	FAIL	X-Frame-Options Header Not Set
10021	FAIL	X-Content-Type-Options Header Missing
10035	FAIL	Strict-Transport-Security Header Not Set
10038	WARN	Content Security Policy (CSP) Header Not Set
10096	IGNORE	Timestamp Disclosure - Unix
40012	FAIL	Cross Site Scripting (Reflected)
40014	FAIL	Cross Site Scripting (Persistent)
40018	FAIL	SQL Injection
90033	FAIL	Loosely Scoped Cookie

Actions:

  • IGNORE — Suppress the alert entirely. Use for known false positives or intentional configurations.
  • WARN — Report the alert but don't fail the build. Use for informational findings you want to track.
  • FAIL — Fail the build if this alert is raised. Use for findings that must be fixed before merge.

Commit this file to your repository. Every developer can see which rules are enforced and why. When someone asks "why did ZAP fail my PR?", the answer is in the rules file, not in a vendor dashboard behind a login.

To find rule IDs for alerts you want to tune, check the ZAP Alert Reference — every alert has a numeric ID and documentation explaining what it detects.

6. Authenticated Scanning

By default, ZAP scans as an unauthenticated user. To test pages behind login, you need to provide authentication context. There are two approaches in GitHub Actions:

Option A: Hook Script (Simpler)

Create a hook script that runs before the scan and sets authentication tokens:

# .zap/auth-hook.py
import requests

def zap_started(zap, target):
    # Login and get a session token
    resp = requests.post(
        f'{target}/api/auth/login',
        json={'email': 'test@example.com', 'password': 'test-password'}
    )
    token = resp.json()['token']

    # Add the token as a header for all ZAP requests
    zap.replacer.add_rule(
        description='Auth Token',
        enabled=True,
        matchtype='REQ_HEADER',
        matchregex=False,
        matchstring='Authorization',
        replacement=f'Bearer {token}'
    )

Reference it in your workflow:

      - name: ZAP Baseline Scan
        uses: zaproxy/action-baseline@v0.12.0
        with:
          target: 'http://localhost:3000'
          rules_file_name: '.zap/rules.tsv'
          hook_file_name: '.zap/auth-hook.py'

Option B: ZAP Context File (More Control)

For complex authentication flows (form-based login, CSRF tokens, multi-step auth), create a ZAP context file that defines the authentication method, login URL, and session management:

<!-- .zap/context.xml -->
<configuration>
  <context>
    <name>Authenticated Context</name>
    <incregexes>http://localhost:3000.*</incregexes>
    <authentication>
      <type>2</type> <!-- Form-based -->
      <loginindicator>\QDashboard\E</loginindicator>
      <logoutindicator>\QSign In\E</logoutindicator>
    </authentication>
  </context>
</configuration>

The hook script approach is simpler for token-based APIs. The context file approach is better for traditional web applications with form-based login. Either way, store credentials in GitHub Secrets — never hardcode them in workflow files.

7. Handling Results: Artifacts, Issues, and Build Gates

ZAP's GitHub Actions provide three output mechanisms:

HTML Report (Default)

Every scan generates report_html.html in the workspace. Upload it as a build artifact so developers can download and review the full findings. This is the most detailed output.

GitHub Issue (Default)

By default, ZAP creates or updates a GitHub issue titled "ZAP Scan Baseline Report" with a summary of findings. Disable this with issue_title: '' if you don't want automated issues.

Build Failure

The scan fails the GitHub Actions job if any alert matches a FAIL rule in your rules file. This is your security gate — PRs with critical findings can't be merged until the findings are resolved or the rule is adjusted.

For teams that want more control, ZAP also outputs JSON (-J report_json.json in cmd_options) that you can parse in subsequent workflow steps:

      - name: Check for critical findings
        if: always()
        run: |
          if [ -f report_json.json ]; then
            CRITICAL=$(jq '[.site[].alerts[] | select(.riskcode == "3")] | length' report_json.json)
            HIGH=$(jq '[.site[].alerts[] | select(.riskcode == "2")] | length' report_json.json)
            echo "Critical: $CRITICAL, High: $HIGH"
            if [ "$CRITICAL" -gt 0 ]; then
              echo "::error::$CRITICAL critical vulnerabilities found"
              exit 1
            fi
          fi

8. Advanced Patterns

Scan Against a Deployed Preview Environment

If your CI deploys a preview environment (Vercel, Netlify, Render), scan that instead of a local Docker container:

      - name: Deploy Preview
        id: deploy
        run: echo "url=https://pr-${{ github.event.number }}.preview.example.com" >> $GITHUB_OUTPUT

      - name: Wait for Preview
        run: |
          for i in $(seq 1 30); do
            if curl -s -o /dev/null -w "%{http_code}" ${{ steps.deploy.outputs.url }} | grep -q "200"; then
              break
            fi
            sleep 10
          done

      - name: ZAP Baseline Scan
        uses: zaproxy/action-baseline@v0.12.0
        with:
          target: ${{ steps.deploy.outputs.url }}

Matrix Scanning Multiple Targets

If you have multiple services, use a matrix strategy to scan them in parallel:

jobs:
  zap-scan:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        target:
          - { name: 'frontend', url: 'http://localhost:3000' }
          - { name: 'api', url: 'http://localhost:8080' }
          - { name: 'admin', url: 'http://localhost:9090' }
    steps:
      - name: ZAP Baseline Scan - ${{ matrix.target.name }}
        uses: zaproxy/action-baseline@v0.12.0
        with:
          target: ${{ matrix.target.url }}

Caching ZAP Add-ons

ZAP downloads add-ons on every run. Cache them to speed up subsequent scans:

      - name: Cache ZAP Add-ons
        uses: actions/cache@v4
        with:
          path: ~/.ZAP/plugin
          key: zap-addons-${{ hashFiles('.zap/rules.tsv') }}

9. Troubleshooting Common Issues

Problem Cause Fix
Scan finds 0 URLs App not ready when scan starts Add a health check wait loop before the scan step
SPA routes not discovered Traditional spider can't execute JavaScript Add -j to cmd_options to enable AJAX Spider
Too many false positives Default rules flag everything Create a rules.tsv file and IGNORE known false positives
Scan takes too long Full scan on large app Use baseline for PRs, full scan on schedule only. Add -m 10 to limit spider time to 10 minutes.
Permission denied errors ZAP Docker container runs as zap user Ensure report output paths are writable. The action handles this by default.
Can't connect to localhost ZAP runs in Docker, app runs on host Use docker.cmd_options: '--network host' or run app in Docker on the same network

For detailed debugging, add -d to cmd_options to enable ZAP debug logging. The logs will appear in the GitHub Actions output.

The Bottom Line

Adding ZAP to GitHub Actions takes less time than reading this article. Start with the baseline scan on PRs — it's fast, safe, and catches the low-hanging fruit that every application should fix (missing security headers, cookie misconfigurations, information disclosure).

Once the baseline is running, add the full scan as a nightly job for deeper testing. Add the API scan if you have an OpenAPI spec. Layer in custom rules to manage false positives and enforce your team's security standards.

The result is a security scanning program that runs on every code change, costs nothing, and catches vulnerabilities before they reach production. That's a better security posture than most organisations achieve with $50K/yr in commercial tooling.

For a comparison of ZAP against Burp Suite (including Burp's Enterprise CI/CD offering), see our ZAP vs Burp Suite comparison. For a broader look at vulnerability scanning tools, check our Nuclei vs traditional scanners analysis.

Advertisement