How to Integrate DAST Into Your CI/CD Pipeline (With OWASP ZAP Examples)
Most teams treat security testing as something that happens right before a release, if it happens at all. They run a scanner once, get a 200-page PDF report, and then ignore it because the deadline is tomorrow. This is not security. This is theater.
Dynamic Application Security Testing (DAST) is a "black box" approach that tests your running application from the outside, exactly like an attacker would. Unlike static analysis that reads your code, DAST sends real requests and analyzes real responses. It finds SQL injection, XSS, authentication flaws, and misconfigurations that only show up at runtime.
The real power of DAST comes when you automate it. Put it in your CI/CD pipeline, set quality gates, and stop vulnerabilities before they reach production. That is what this guide covers.
How DAST Actually Works
A DAST scanner follows four steps. Understanding them helps you tune scans and interpret results.
Step 1: Discovery (Crawling). The scanner explores your application to map every endpoint. It follows links, submits forms, and executes JavaScript to find all possible entry points.
Step 2: Passive Scanning. While crawling, the scanner observes responses for issues without attacking anything. It checks for missing security headers (CSP, HSTS, X-Frame-Options), exposed sensitive information like stack traces and version numbers, insecure cookies missing HttpOnly or Secure flags, and weak SSL/TLS configurations.
Step 3: Active Scanning. The scanner sends attack payloads to each endpoint:
# SQL Injection test
GET /api/users?id=1' OR '1'='1
# XSS test
GET /search?q=<script>alert('XSS')</script>
# Path Traversal test
GET /files?path=../../../../etc/passwd
It analyzes responses to detect vulnerabilities. Database errors mean SQL injection is possible. Script content reflected back means XSS. Sensitive file content means path traversal.
Step 4: Reporting. Results get categorized by severity (Critical through Informational), confidence level, CWE/CVE identifiers, and OWASP Top 10 mapping.
When to Use Each Scan Type
Not every scan belongs in every environment.
Passive scans observe traffic without attacking. They are safe for production, fast (1-5 minutes), and catch missing headers, insecure cookies, and information disclosure. Run these everywhere.
Active scans send attack payloads. They belong in staging and test environments only. They take 30 minutes to several hours and can cause real damage to databases and trigger security alerts.
API scans test REST and GraphQL endpoints using OpenAPI or Swagger specs. Medium risk, medium duration. Perfect for microservice architectures.
Setting Up OWASP ZAP
OWASP ZAP is free, open-source, and built for automation. It is the best starting point for most teams.
Installation via Docker
# Pull the stable image
docker pull ghcr.io/zaproxy/zaproxy:stable
# Baseline Scan (passive only, safe for production)
docker run -v $(pwd):/zap/wrk/:rw \
ghcr.io/zaproxy/zaproxy:stable zap-baseline.py \
-t https://example.com -r report.html
# Full Scan (active, staging only)
docker run -v $(pwd):/zap/wrk/:rw \
ghcr.io/zaproxy/zaproxy:stable zap-full-scan.py \
-t https://staging.example.com -r report.html
# API Scan (OpenAPI/GraphQL)
docker run -v $(pwd):/zap/wrk/:rw \
ghcr.io/zaproxy/zaproxy:stable zap-api-scan.py \
-t https://api.example.com/openapi.json \
-f openapi -r report.html
Configuring Authentication
Most applications require authentication to test protected endpoints. ZAP handles this with context files:
# zap-context.yaml
env:
contexts:
- name: "My App"
urls:
- "https://staging.example.com"
includePaths:
- "https://staging.example.com/.*"
excludePaths:
- "https://staging.example.com/logout"
authentication:
method: "form"
parameters:
loginUrl: "https://staging.example.com/login"
loginRequestData: "username={%username%}&password={%password%}"
verification:
method: "response"
loggedInRegex: "\\QWelcome\\E"
loggedOutRegex: "\\QLogin\\E"
users:
- name: "test_user"
credentials:
username: "[email protected]"
password: "testpass123"
Then reference it in your scan:
zap-full-scan.py \
-t https://staging.example.com \
-n zap-context.yaml \
-r report.html
Tuning False Positives
Noisy scanners get ignored. Tune yours with a rules file:
# rules-config.tsv
10021 IGNORE (X-Content-Type-Options)
10038 IGNORE (Content Security Policy)
10055 FAIL (CSP Scanner)
40012 FAIL (Cross Site Scripting)
40018 FAIL (SQL Injection)
zap-full-scan.py \
-t https://staging.example.com \
-c rules-config.tsv \
-r report.html
Where Burp Suite Fits In
Burp Suite is the industry standard for manual penetration testing. While ZAP excels at automation, Burp Suite shines when a human is driving.
Use Burp Suite for:
- Deep manual testing with the Proxy, Repeater, and Intruder tools
- Out-of-band vulnerability detection via Burp Collaborator
- Advanced attack techniques like brute forcing and parameter fuzzing
- Professional penetration test engagements
Use ZAP for:
- Automated CI/CD scans
- Baseline security checks on every PR
- API scanning with OpenAPI specs
- Any situation where you need free and repeatable scans
The best approach is to use both. ZAP handles your automated pipeline scans. Burp Suite handles periodic manual deep dives. They complement each other well.
CI/CD Integration
This is where DAST stops being a checkbox and starts preventing real vulnerabilities.
The Strategy
Run different scan types at different stages:
- Pull Requests: Quick baseline scan (5-10 minutes, passive only)
- Staging Deploy: Full active scan (30-60 minutes)
- Production: Passive monitoring only, never active scans
GitHub Actions
# .github/workflows/dast.yml
name: DAST Scan
on:
pull_request:
push:
branches: [main]
jobs:
zap_scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Start Application
run: |
docker-compose up -d
timeout 60 bash -c 'until curl -f http://localhost:3000/health; do sleep 2; done'
- name: ZAP Baseline Scan
uses: zaproxy/[email protected]
with:
target: 'http://localhost:3000'
rules_file_name: '.zap/rules.tsv'
cmd_options: '-a'
- name: Upload ZAP Report
if: always()
uses: actions/upload-artifact@v4
with:
name: zap-report
path: report_html.html
GitLab CI
dast_scan:
stage: security
image: ghcr.io/zaproxy/zaproxy:stable
script:
- zap-baseline.py -t $TARGET_URL -r gl-dast-report.html -J gl-dast-report.json
artifacts:
when: always
reports:
dast: gl-dast-report.json
paths:
- gl-dast-report.html
expire_in: 1 week
allow_failure: false
Quality Gates That Actually Work
A scan without a quality gate is just noise. Parse results and fail builds when it matters:
#!/bin/bash
# parse-zap-results.sh
REPORT="zap-report.json"
CRITICAL=$(jq '[.site[].alerts[] | select(.riskcode=="3" and .confidence=="3")] | length' $REPORT)
HIGH=$(jq '[.site[].alerts[] | select(.riskcode=="3")] | length' $REPORT)
MEDIUM=$(jq '[.site[].alerts[] | select(.riskcode=="2")] | length' $REPORT)
echo "Critical: $CRITICAL | High: $HIGH | Medium: $MEDIUM"
if [ $CRITICAL -gt 0 ]; then
echo "::error::Found $CRITICAL critical vulnerabilities"
exit 1
elif [ $HIGH -gt 5 ]; then
echo "::error::Found $HIGH high-severity vulnerabilities (threshold: 5)"
exit 1
elif [ $MEDIUM -gt 20 ]; then
echo "::warning::Found $MEDIUM medium-severity vulnerabilities (threshold: 20)"
fi
echo "Security scan passed"
For even more targeted filtering, block specific vulnerability types:
#!/bin/bash
# Block specific CWEs: SQL Injection, XSS, Path Traversal, OS Command Injection
BLOCKLIST=("89" "79" "22" "78")
for CWE in "${BLOCKLIST[@]}"; do
COUNT=$(jq "[.site[].alerts[] | select(.cweid==\"$CWE\")] | length" zap-report.json)
if [ $COUNT -gt 0 ]; then
echo "::error::Found $COUNT instances of CWE-$CWE"
exit 1
fi
done
Advanced: Full Scan With PR Comments
For staging deployments, run a full scan and post results directly to the PR:
- name: Run ZAP Full Scan
run: |
docker run -v $(pwd):/zap/wrk/:rw \
ghcr.io/zaproxy/zaproxy:stable zap-full-scan.py \
-t ${{ env.STAGING_URL }} \
-r zap-report.html \
-J zap-report.json \
-w zap-report.md \
-z "-config api.maxchildren=5"
- name: Comment PR with Results
if: always() && github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const report = fs.readFileSync('zap-report.md', 'utf8');
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `## DAST Scan Results\n\n${report}`
});
Troubleshooting Common Issues
Scan takes too long. Reduce threads and set a timeout:
zap-full-scan.py -t URL -z "-config api.maxchildren=2" -m 30
Too many false positives. Use a rules.tsv file to IGNORE irrelevant alerts. Exclude paths like /logout, /static/, and /health from your context.
Authentication not working. Enable debug mode to see what ZAP is doing:
docker run -v $(pwd):/zap/wrk/:rw \
ghcr.io/zaproxy/zaproxy:stable \
zap-full-scan.py -t URL -d
Out of memory. Increase Docker memory and reduce thread count:
docker run -m 4g ghcr.io/zaproxy/zaproxy:stable \
zap-full-scan.py -t URL -z "-config scanner.threadPerHost=1"
What DAST Does Not Catch
DAST is one layer of defense, not a silver bullet. It misses code-level flaws like buffer overflows and race conditions (use SAST for those). It struggles with complex multi-step business logic attacks. It cannot test endpoints behind authentication without proper setup. And it will not find client-side vulnerabilities in mobile or desktop apps.
The best security testing strategy combines SAST for early detection during development, DAST for runtime validation in staging, and IAST for comprehensive coverage during testing. Add dependency scanning and infrastructure security on top and you have real defense in depth.
Getting Started
If you are not running DAST today, start small:
- Add a ZAP baseline scan to one project's CI pipeline. This takes 15 minutes to set up.
- Run it for a week and review the findings. Tune false positives with a rules file.
- Add a quality gate that blocks critical vulnerabilities.
- Expand to full active scans on staging deployments.
- Track metrics over time: vulnerability density, false positive rate, and mean time to remediate.
The goal is not to catch every vulnerability on day one. The goal is to build a security feedback loop that gets better over time. Start with passive scans, tune the noise, and gradually increase coverage.
Related Security Posts
- Dependency Scanning: Finding Vulnerabilities Before Attackers Do - Catch known CVEs in your libraries before DAST even runs, so your scans focus on application-level flaws
- Secure Coding Practices Every DevOps Engineer Should Know - Fix the root causes DAST finds: input validation, output encoding, and proper error handling
- CI/CD Pipeline Hardening - Secure the pipeline that runs your DAST scans so attackers cannot tamper with results or skip security gates
For guidance on setting pass/fail thresholds in your pipeline, see our guide on security gates.
We earn commissions when you shop through the links below.
DigitalOcean
Cloud infrastructure for developers
Simple, reliable cloud computing designed for developers
DevDojo
Developer community & tools
Join a community of developers sharing knowledge and tools
Acronis
The most secure backup
Acronis: the most secure backup solution for your data

QuizAPI
Developer-first quiz platform
Build, generate, and embed quizzes with a powerful REST API. AI-powered question generation and live multiplayer.
Want to support DevOps Daily and reach thousands of developers?
Become a SponsorFound an issue?