How to Automate OWASP ZAP Scans in GitHub Actions (2026 Guide)
📢 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:
- Spider the application starting from
http://localhost:3000 - Passively analyse every response for security issues (missing headers, cookie flags, information disclosure, etc.)
- Generate an HTML report uploaded as a build artifact
- 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.