Skip to content
$cd ..

// post

DAST with OWASP ZAP in CI/CD Pipelines

DAST (Dynamic Application Security Testing) is the security testing discipline that gets skipped most often in DevSecOps programs. SAST is easy to add to a pipeline — point a tool at your source code and get results. DAST requires a running application, an authenticated session, a crawl strategy, and a way to interpret results without drowning in false positives.

That’s why teams skip it. But DAST finds a class of vulnerabilities that SAST misses: runtime behavior, authentication flaws, server configuration issues, and injection vulnerabilities that only manifest at runtime.

OWASP ZAP (Zed Attack Proxy) is the most widely used open-source DAST tool. It’s mature, actively maintained, and has CI/CD integration support that makes automation practical.

Two Modes: Baseline vs Full Active Scan

ZAP offers several scan modes. For CI/CD, two are most relevant:

Baseline Scan — Passive only. ZAP spiders the application and flags issues it can observe without sending attack payloads. Fast (minutes), low false-positive rate, safe to run against any environment. Good for PR pipelines.

Full Scan — Active scan. ZAP sends attack payloads to identify injection, XSS, CSRF, and other active vulnerabilities. Slower (minutes to hours), higher false-positive rate, should run against a dedicated test environment — never production.

Setting Up ZAP in Docker

ZAP’s Docker images make CI integration straightforward without managing a local ZAP installation.

# Baseline scan against a running app
docker run --rm \
  -v $(pwd):/zap/wrk:rw \
  ghcr.io/zaproxy/zaproxy:stable zap-baseline.py \
  -t https://staging.example.com \
  -r zap-report.html \
  -J zap-report.json \
  -I  # don't fail on warnings, only on errors
# Full active scan
docker run --rm \
  -v $(pwd):/zap/wrk:rw \
  ghcr.io/zaproxy/zaproxy:stable zap-full-scan.py \
  -t https://staging.example.com \
  -r zap-full-report.html \
  -z "-config scanner.attackStrength=LOW"

GitHub Actions Integration

name: DAST — Baseline Scan

on:
  pull_request:
  schedule:
    - cron: '0 2 * * 1'  # weekly full scan on Mondays

jobs:
  zap-baseline:
    runs-on: ubuntu-latest
    steps:
      - name: Start application (example — replace with your setup)
        run: docker compose up -d app

      - name: Wait for app to be ready
        run: |
          timeout 60 bash -c 'until curl -sf http://localhost:8080/health; do sleep 2; done'

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

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

Handling Authentication

The hardest part of DAST automation is authentication. ZAP needs to log in as a user to test authenticated endpoints.

Script-based authentication (form login):

# zap-auth.py — ZAP automation script
import requests

# ZAP API
zap_url = "http://localhost:8080"

# Set up form-based auth
requests.post(f"{zap_url}/JSON/authentication/action/setAuthenticationMethod/", params={
    "contextId": "1",
    "authMethodName": "formBasedAuthentication",
    "authMethodConfigParams": (
        "loginUrl=https://app.example.com/login"
        "&loginRequestData=username={username}&password={password}"
    )
})

JWT/Bearer token via automation framework:

ZAP’s newer Automation Framework (YAML-based) handles this more cleanly:

# zap-automation.yaml
env:
  contexts:
    - name: app-context
      urls:
        - https://staging.example.com
      authentication:
        method: browser
        parameters:
          loginPageUrl: https://staging.example.com/login
          loginPageWait: 5
        verification:
          method: response
          loggedInRegex: '"authenticated":true'

jobs:
  - type: spider
    parameters:
      maxDuration: 5
  - type: activeScan
    parameters:
      maxScanDurationInMins: 30
  - type: report
    parameters:
      reportFile: zap-report.html
      reportTitle: DAST Report

Tuning to Reduce False Positives

A raw ZAP scan against most applications produces noise. The rules configuration file (.zap/rules.tsv) lets you tune which alerts fail the build:

# .zap/rules.tsv — format: rule_id\tOK/WARN/FAIL\toptional_reason
10202	WARN	Absence of Anti-CSRF Tokens (handled at framework level)
10038	IGNORE	Content Security Policy Header Not Set (handled by CDN)
40012	FAIL	Cross Site Scripting (Reflected)
40014	FAIL	Cross Site Scripting (Persistent)
90022	FAIL	Application Error Disclosure

The principle: fail on vulnerabilities that represent real, exploitable risk in your context. Warn or ignore on issues that are mitigated elsewhere or don’t apply to your architecture.

What ZAP Finds That SAST Misses

DAST is complementary to SAST, not a replacement. Things ZAP reliably finds:

These are runtime behaviors that no amount of static code analysis will reliably surface.

Practical Recommendations

  1. Start with baseline scans on every PR — no authentication needed, fast, low noise
  2. Run full scans weekly against a staging environment — with authentication, dedicated environment
  3. Define your rules file from day one — set severity thresholds appropriate to your context
  4. Treat ZAP findings like any other bug — create tickets, track remediation, don’t just re-scan
  5. Use the Automation Framework for complex setups — it’s more maintainable than shell scripts
Previous
Introducing OWASP ThreatAtlas: Collaborative Threat Modeling at Scale
Next
EPSS: A Smarter Way to Prioritize CVEs