DAST
How to add a defensible DAST stage to CI: ZAP baseline scan, allowlist auth, treating findings like compiler errors. Authorised use only.
OWASP — DAST in CI
EXAMPLE
# SCOPE: authorised DAST against owned staging environments only.
# Do not scan systems you do not own or have written permission to test.
# ===== Where DAST sits =====
# SAST (source) -> SCA (deps) -> Build -> DAST (running app on staging)
# DAST exercises the running app: auth, sessions, headers, common injections.
# ===== ZAP baseline scan (read-only, safe defaults) =====
# .github/workflows/dast.yml
name: dast
on:
workflow_dispatch:
schedule:
- cron: '0 4 * * 1' # weekly, 04:00 Monday
jobs:
zap:
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
steps:
- uses: actions/checkout@v4
- name: ZAP baseline
uses: zaproxy/action-baseline@v0.13.0
with:
target: ${{ secrets.STAGING_BASE_URL }}
rules_file_name: '.zap/rules.tsv'
cmd_options: '-a -j -m 5 -T 10'
# .zap/rules.tsv (false-positive allowlist; one rule per line)
# Format: PLUGIN_ID\tTHRESHOLD\tNEW_RISK
10038\tIGNORE\t\t# CSP missing on /healthz (not user-facing)
10063\tIGNORE\t\t# Permissions-Policy intentionally unset on docs
# ===== Authenticated scan (after baseline is green) =====
# ZAP automation framework, scanned with a session token
# .zap/automation.yaml
env:
contexts:
- name: app
urls: [${{ secrets.STAGING_BASE_URL }}]
authentication:
method: script
parameters:
scriptName: login.zst
scriptEngine: ECMAScript
loggedInIndicator: 'Sign out'
users:
- name: scan-bot
credentials:
username: ${{ secrets.SCAN_USER }}
password: ${{ secrets.SCAN_PASS }}
jobs:
- type: spider
parameters: { context: app, user: scan-bot, maxDuration: 5 }
- type: activeScan
parameters: { context: app, user: scan-bot, policy: 'Default Policy' }
- type: report
parameters: { template: 'sarif-json', reportDir: reports/ }
# ===== Make findings part of the loop =====
# 1. Upload SARIF to GitHub code scanning
# 2. Auto-open issues for HIGH findings, labelled 'security/dast'
# 3. Allowlist with a comment + an expiry date
# 4. Track median-time-to-fix as a metric
# ===== Verification: hardened response headers =====
# These are usually the first wins:
# Content-Security-Policy: default-src 'self'; ...
# X-Content-Type-Options: nosniff
# X-Frame-Options: DENY (or CSP frame-ancestors)
# Referrer-Policy: strict-origin-when-cross-origin
# Permissions-Policy: camera=(), microphone=(), geolocation=()
# Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
# ===== Patterns to internalise =====
# - DAST runs against staging that mirrors prod; do not point at prod
# - Use a dedicated scan-user with limited data and audit tags
# - Baseline first (read-only) before active scans
# - Manage false positives with timed allowlists, not 'IGNORE forever'
# - Treat HIGH findings like a failing test: fix or expire
# ===== Pitfalls =====
# - Pointing the scanner at prod and tipping over user-facing endpoints
# - No auth -> only the public surface is exercised; deep bugs hide
# - Allowlists with no owner or expiry -> findings rot
# - Scan-user with prod-like permissions -> blast radius if compromised
# - Treating DAST as a one-off audit instead of weekly hygiene
Why it matters
DAST is hygiene, not a stunt. Weekly baseline on staging, authenticated scan once that is green, SARIF into the same dashboard as SAST. Allowlist with expiry. The unfixable findings shrink each quarter, and the fixable ones land like bug fixes.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// Dynamic scan against staging: ZAP baseline scan, Burp Suite Pro. // Run weekly; treat findings as tickets.Try it Yourself »
Discussion
Loading…