OWASP ZAP Automated Security Testing: CI/CD Integration and API Scanning

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 Secure or HttpOnly flags
  • 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 \
  --auto

The --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 \
  -I

The -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 WARN

ZAP 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.html

For 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.json

The 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.

Read more