OWASP ZAP Automated Security Testing: CI/CD Integration and API Scanning
OWASP ZAP is the most widely used open-source web application security scanner. This guide covers integrating ZAP's Docker-based full scan and API scanning into GitHub Actions CI/CD pipelines, configuring authentication for authenticated scans, and handling alert triage to keep pipelines actionable.
Key Takeaways
Run ZAP as Docker in CI — no installation needed. The ghcr.io/zaproxy/zaproxy:stable image gives you a reproducible scanner in any CI environment without managing a local ZAP installation.
Passive scanning finds issues without sending attacks. Passive mode observes traffic and flags misconfigurations, missing headers, and information leakage — safe to run against production-like environments.
Active scanning sends attack payloads — gate it carefully. Active scans probe for SQLi, XSS, and SSRF vulnerabilities; restrict them to staging environments with rate limiting enabled.
OpenAPI/Swagger specs dramatically improve API coverage. Importing your spec into ZAP replaces manual spider crawling and ensures every endpoint and parameter gets tested.
False positives require a triage policy, not removal. Use ZAP's context files and alert filters to suppress known false positives systematically, keeping your failure threshold meaningful.
OWASP ZAP (Zed Attack Proxy) has been the go-to open-source DAST (Dynamic Application Security Testing) tool for over a decade. Its combination of passive observation, active scanning, and extensible scripting makes it practical for both manual penetration testing and automated CI/CD security gates.
This guide focuses on the automation story: running ZAP headlessly via Docker, scanning REST APIs from an OpenAPI specification, authenticating against protected endpoints, and integrating everything into a GitHub Actions workflow that gives you actionable security feedback on every pull request.
Understanding ZAP Scanning Modes
Before writing any pipeline configuration, it is worth understanding what ZAP's two fundamental scanning modes actually do.
Passive Scanning
Passive scanning observes HTTP traffic that passes through ZAP's proxy. It never sends additional requests. Instead it analyses requests and responses for:
- Missing security headers (
Content-Security-Policy,X-Frame-Options,Strict-Transport-Security) - Information leakage in response bodies or headers
- Cookies set without
SecureorHttpOnlyflags - Mixed content on HTTPS pages
Because passive scanning is read-only, it is safe to run against production-like environments and will not cause side effects.
Active Scanning
Active scanning sends crafted attack payloads to every discovered endpoint and parameter. It tests for SQL injection, reflected XSS, path traversal, SSRF, command injection, and dozens of other vulnerability classes.
Active scanning is intrusive — it will create noise in application logs, may trigger rate limiters, and in theory could cause data corruption on poorly isolated environments. Always restrict active scans to isolated staging environments with a dedicated database.
Running ZAP with Docker
The official ZAP Docker images expose three scan scripts suited for different use cases:
| Script | Use case |
|---|---|
zap-baseline.py |
Passive scan only — safe for production-like targets |
zap-full-scan.py |
Passive + active scan — staging only |
zap-api-scan.py |
API-specific scan using OpenAPI/SOAP/GraphQL spec |
Basic Baseline Scan
docker run --rm \
ghcr.io/zaproxy/zaproxy:stable \
zap-baseline.py \
-t https://staging.example.com \
-r report.html \
-J report.json \
--autoThe --auto flag enables the automation framework mode introduced in ZAP 2.12, which gives finer control over scan policies and output formats.
Full Scan with Report Output
docker run --rm \
-v $(<span class="hljs-built_in">pwd)/zap-reports:/zap/wrk/:rw \
ghcr.io/zaproxy/zaproxy:stable \
zap-full-scan.py \
-t https://staging.example.com \
-r report.html \
-J report.json \
-x report.xml \
-l WARN \
-IThe -I flag tells ZAP to ignore warnings when computing the exit code — only actual failures (high-confidence, high-severity findings) cause a non-zero exit. The -l WARN sets the minimum alert level to include in the report.
API Scanning with OpenAPI Specifications
REST APIs require a different approach than web applications. The traditional spider struggles to discover API endpoints because there is no HTML to crawl. Providing an OpenAPI (formerly Swagger) specification solves this completely.
Running zap-api-scan Against a Local Spec
docker run --rm \
-v $(<span class="hljs-built_in">pwd):/zap/wrk/:rw \
ghcr.io/zaproxy/zaproxy:stable \
zap-api-scan.py \
-t /zap/wrk/openapi.yaml \
-f openapi \
-r api-report.html \
-J api-report.json \
-l WARNZAP will import the spec, generate requests for every path and method combination, fuzz each parameter, and report findings grouped by endpoint.
Handling Authentication in API Scans
Most APIs require authentication. ZAP supports several authentication mechanisms through its context system. For Bearer token authentication, create a context.yaml automation framework configuration:
env:
contexts:
- name: "API Context"
urls:
- "https://staging.example.com/api"
authentication:
method: "header"
parameters:
name: "Authorization"
value: "Bearer ${API_TOKEN}"
sessionManagement:
method: "headers"Pass the token via environment variable rather than hardcoding it:
docker run --rm \
-e API_TOKEN=<span class="hljs-string">"${STAGING_API_TOKEN}" \
-v $(<span class="hljs-built_in">pwd):/zap/wrk/:rw \
ghcr.io/zaproxy/zaproxy:stable \
zap-api-scan.py \
-t /zap/wrk/openapi.yaml \
-f openapi \
-z <span class="hljs-string">"-config globalenv.API_TOKEN=${STAGING_API_TOKEN}" \
-r api-report.htmlFor session-based authentication (login form), ZAP's automation framework supports script-based login that submits credentials and captures the session cookie.
GitHub Actions Integration
Here is a complete GitHub Actions workflow that runs a ZAP API scan on every pull request targeting main:
name: Security Scan
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
zap-api-scan:
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write # needed for SARIF upload
pull-requests: write # needed for PR comments
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Start application
run: |
docker compose -f docker-compose.test.yml up -d
# Wait for health check
timeout 60 bash -c 'until curl -sf http://localhost:8080/health; do sleep 2; done'
- name: ZAP API Scan
uses: zaproxy/action-api-scan@v0.7.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
docker_name: "ghcr.io/zaproxy/zaproxy:stable"
target: "http://localhost:8080"
format: openapi
api_scan_rules_file_name: ".zap/rules.tsv"
issue_title: "ZAP Security Findings"
fail_action: true
allow_issue_writing: true
artifact_name: zap-report
- name: Upload SARIF report
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: report_json.jsonThe zaproxy/action-api-scan action handles the Docker invocation, network bridging (so ZAP can reach your localhost:8080 service), and report upload as a PR comment.
Configuring Alert Rules to Reduce Noise
Create a .zap/rules.tsv file in your repository to customize which alerts cause failures:
# Rule ID Action Parameter Evidence
10020 WARN x-frame-options
10021 IGNORE
10035 FAIL
10038 WARN
10040 FAIL
90001 IGNORE The columns are: alert rule ID, action (FAIL/WARN/IGNORE), optional parameter filter, optional evidence filter. Use IGNORE for alerts that are confirmed false positives in your specific environment.
Find rule IDs in the ZAP alert documentation or from your JSON report's pluginid field.
Interpreting ZAP Alerts
ZAP classifies alerts by two dimensions: Risk (High/Medium/Low/Informational) and Confidence (High/Medium/Low/False Positive).
A High Risk / High Confidence alert like "SQL Injection" demands immediate attention. A Medium Risk / Low Confidence alert like "Possible XSS" may warrant investigation but is more likely a false positive. Use this matrix when setting your pipeline failure threshold.
Common Findings and What They Mean
Missing Content-Security-Policy header — The application does not instruct browsers which sources to trust for scripts, styles, and frames. Implement a CSP header in your reverse proxy or application middleware.
Cookie without SameSite attribute — Session cookies accessible in cross-origin requests increase CSRF risk. Set SameSite=Strict or SameSite=Lax depending on your cross-origin requirements.
X-Content-Type-Options header not set — Without nosniff, browsers may MIME-sniff responses and execute unexpected content as scripts. Add X-Content-Type-Options: nosniff globally.
Server leaks version information — HTTP response headers like Server: nginx/1.18.0 or X-Powered-By: Express 4.18 help attackers target known CVEs. Strip or genericise these headers.
Active vs Passive: Choosing the Right Scan for Each Environment
A practical approach for most teams is a tiered scanning strategy:
Pull request pipeline — Run zap-baseline.py (passive only) against a fresh ephemeral environment spun up in the CI job. Fast (2-5 minutes), safe, catches configuration regressions.
Nightly / pre-release pipeline — Run zap-full-scan.py or zap-api-scan.py in active mode against a persistent staging environment. Slower (10-30 minutes), finds deeper vulnerabilities.
Manual penetration testing — Use ZAP's desktop UI with manual exploration, custom scripts, and fuzzing for comprehensive coverage before major releases.
This tiered approach keeps PR pipelines fast while ensuring deep scanning happens on a regular cadence.
Handling False Positives Systematically
False positives are inevitable. The key is handling them with a documented, auditable process rather than simply disabling alerts.
The .zap/rules.tsv file described above should be reviewed during security stand-ups. Each IGNORE entry should have a comment explaining the rationale. Consider pairing it with a IGNORE-JUSTIFICATION.md documenting why each suppression is valid.
For alert filters that depend on context (e.g., a path that intentionally reflects user input because it is a search preview endpoint), use ZAP's context-aware alert filters in the automation framework YAML rather than global rule suppression. This keeps the suppression scoped to the specific URL pattern.
Summary
OWASP ZAP provides a mature, free, and highly configurable DAST solution for teams building security into their CI/CD pipelines. The combination of passive scanning in PRs and active scanning in nightly builds gives broad coverage without slowing down developer feedback loops. Providing an OpenAPI spec dramatically improves API coverage compared to web spidering, and the automation framework's context-aware authentication support means authenticated scanning is practical even for complex session management schemes.