Continuous Security Testing: Integrating DAST into CI/CD Pipelines
The shift-left security narrative is well established: find vulnerabilities earlier in the development cycle, where they're cheaper and faster to fix. SAST and SCA fit naturally into this model — they run on source code before anything is deployed. DAST is different. It requires a running application. That makes CI/CD integration more complex, but also more valuable: you're testing the actual deployed artifact, not a static representation of it.
This guide covers how to integrate DAST into CI/CD pipelines without destroying developer velocity, which vulnerability classes continuous DAST is suited for (and which it isn't), and how to manage the false-positive problem that kills most automated security programmes.
Where DAST Fits in the Pipeline
The key decision is which environment to scan. You have three realistic options:
| Environment | Pros | Cons |
|---|---|---|
| Ephemeral review/preview | Tests every PR; catches issues before merge | Short-lived; may not have full data; auth complexity |
| Staging (persistent) | Full feature set; stable auth; realistic data shapes | Shared environment; scan may interfere with other tests |
| Production | Real configuration; catches post-deploy drift | Risk of disruption; requires careful scope limiting |
The pragmatic answer for most teams: run a targeted, fast scan on staging after every deployment to staging, and run a full scan on a weekly schedule. Skip per-PR scanning unless you have ephemeral environments with predictable authentication — the complexity overhead is rarely justified for DAST specifically (SAST is better suited for per-PR gating).
The Authenticated Scan Requirement
The single most important DAST configuration decision is authentication. An unauthenticated scan tests the login page and the marketing site. An authenticated scan tests the actual application. The authenticated attack surface is typically 5–10x larger.
Authentication strategies for CI/CD DAST:
Cookie/session token injection
The simplest approach: create a dedicated DAST test account, log in programmatically before the scan, and pass the session cookie to the scanner.
# Example: Get session token via API login, then run Nuclei
#!/bin/bash
# Authenticate and capture session token
SESSION=$(curl -s -X POST https://staging.example.com/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"dast-test@example.com","password":"'$DAST_TEST_PASSWORD'"}' \
| jq -r '.token')
# Run Nuclei with the token
nuclei -u https://staging.example.com \
-H "Authorization: Bearer $SESSION" \
-t http/vulnerabilities/ \
-t http/misconfiguration/ \
-severity medium,high,critical \
-o nuclei-results.json \
-json
Form-based authentication with OWASP ZAP
# ZAP headless scan with form authentication
docker run --rm \
-v $(pwd):/zap/wrk \
ghcr.io/zaproxy/zaproxy:stable \
zap-full-scan.py \
-t https://staging.example.com \
-U "dast-test@example.com" \
-P "$DAST_TEST_PASSWORD" \
--hook=/zap/wrk/zap-auth-hook.py \
-r zap-report.html \
-J zap-report.json
API key authentication
# For API-first applications, pass API key directly
nuclei -u https://api.staging.example.com \
-H "X-API-Key: $DAST_API_KEY" \
-t http/vulnerabilities/generic/ \
-t http/misconfiguration/ \
-severity medium,high,critical
Create a dedicated DAST account: Never use a real user account for automated scanning. Create a dedicated test account with realistic permissions (the role your most privileged typical user would have), and store its credentials in CI/CD secrets — not in code. The account should have enough data to be realistic but should be clearly labelled as a test account so you can identify and exclude its activity in analytics and audit logs.
GitHub Actions Integration
Here's a practical GitHub Actions workflow that runs Nuclei on staging after every deployment:
name: DAST Scan
on:
deployment_status:
# Only run when staging deployment is successful
states: [success]
jobs:
dast:
name: Dynamic Security Scan
runs-on: ubuntu-latest
if: github.event.deployment.environment == 'staging'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Nuclei
run: |
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
nuclei -update-templates
- name: Authenticate and get session token
id: auth
run: |
TOKEN=$(curl -s -X POST ${{ vars.STAGING_URL }}/api/auth/login \
-H "Content-Type: application/json" \
-d "{\"email\":\"${{ secrets.DAST_EMAIL }}\",\"password\":\"${{ secrets.DAST_PASSWORD }}\"}" \
| jq -r '.token')
echo "session_token=$TOKEN" >> $GITHUB_OUTPUT
- name: Run DAST scan
run: |
nuclei \
-u ${{ vars.STAGING_URL }} \
-H "Authorization: Bearer ${{ steps.auth.outputs.session_token }}" \
-t http/vulnerabilities/ \
-t http/misconfiguration/ \
-t ssl/ \
-severity medium,high,critical \
-o nuclei-results.json \
-json
continue-on-error: true # Don't fail the job yet — evaluate results first
- name: Evaluate results
run: |
# Count critical and high findings
CRITICAL=$(cat nuclei-results.json | jq -r 'select(.info.severity=="critical")' | wc -l)
HIGH=$(cat nuclei-results.json | jq -r 'select(.info.severity=="high")' | wc -l)
echo "Critical findings: $CRITICAL"
echo "High findings: $HIGH"
# Fail pipeline on critical findings
if [ "$CRITICAL" -gt "0" ]; then
echo "::error::$CRITICAL critical security finding(s) detected"
exit 1
fi
- name: Upload results
uses: actions/upload-artifact@v4
if: always()
with:
name: dast-results
path: nuclei-results.json
GitLab CI Integration
dast-scan:
stage: security
image: ubuntu:22.04
needs:
- deploy-staging
only:
- main
environment:
name: staging
before_script:
- apt-get update -qq && apt-get install -y curl jq golang-go
- go install github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
- ~/go/bin/nuclei -update-templates
script:
- |
TOKEN=$(curl -s -X POST $STAGING_URL/api/auth/login \
-H "Content-Type: application/json" \
-d "{\"email\":\"$DAST_EMAIL\",\"password\":\"$DAST_PASSWORD\"}" \
| jq -r '.token')
~/go/bin/nuclei \
-u $STAGING_URL \
-H "Authorization: Bearer $TOKEN" \
-t http/vulnerabilities/ \
-t http/misconfiguration/ \
-severity medium,high,critical \
-o nuclei-results.json \
-json || true
# Fail on critical findings
CRITICAL=$(cat nuclei-results.json | jq -r 'select(.info.severity=="critical")' | wc -l 2>/dev/null || echo 0)
if [ "$CRITICAL" -gt "0" ]; then
echo "CRITICAL findings detected: $CRITICAL"
exit 1
fi
artifacts:
paths:
- nuclei-results.json
when: always
expire_in: 30 days
Managing the False-Positive Problem
The number one reason security tools get disabled is false positives. A scanner that cries wolf on every deployment destroys developer trust, and a disabled scanner provides zero protection.
Severity thresholds are your friend
Don't gate on medium findings — the false-positive rate is too high for automated blocking. Gate on high and critical only. Pipe medium findings to a triage queue where the security team can review them weekly.
Template allowlisting
Review your Nuclei template set and exclude templates that generate false positives in your specific stack. If your application intentionally serves a /.git/config redirect (returning a 301) but a template flags it as exposed, suppress that template.
# Exclude specific templates by ID
nuclei -u $TARGET \
-exclude-id CVE-2021-XXXX,tech-detect-info \
-severity high,critical
# Or use a .nuclei-ignore file in your repo
# Format: one template ID per line
echo "tech-detect-generic" >> .nuclei-ignore
nuclei -u $TARGET -no-interactsh
Baseline comparison
Rather than failing on every finding, compare the current scan against a stored baseline. Only new findings since the last accepted baseline trigger a pipeline failure. This prevents known, accepted findings from blocking every deployment.
#!/bin/bash
# Compare current results with baseline
CURRENT_FINDINGS=$(cat nuclei-results.json | jq -r '.template-id + ":" + .matched-at' | sort)
BASELINE_FINDINGS=$(cat nuclei-baseline.json | jq -r '.template-id + ":" + .matched-at' | sort 2>/dev/null || echo "")
NEW_FINDINGS=$(comm -23 <(echo "$CURRENT_FINDINGS") <(echo "$BASELINE_FINDINGS"))
if [ -n "$NEW_FINDINGS" ]; then
echo "New security findings detected:"
echo "$NEW_FINDINGS"
exit 1
else
echo "No new findings compared to baseline."
fi
What Continuous DAST Is Good For (and What It Isn't)
Well suited for continuous scanning
- Regression detection — catches when a previously fixed vulnerability is reintroduced
- Configuration drift — security headers removed, TLS configuration weakened, admin interfaces exposed after infrastructure changes
- New CVEs against known software — when a new Nuclei template targets a CVE in software you run, the next scan catches it
- Dependency-level exposure — exposed debug endpoints, version disclosure, default credentials on new services
Not suited for continuous scanning
- Business logic vulnerabilities — a DAST tool cannot understand that a user should not be able to apply duplicate discounts. This requires human testing.
- Complex multi-step attack chains — an attacker might need to create an account, modify a profile, trigger a background job, and then exploit a race condition. Automated scanners don't model this.
- Novel vulnerability classes — a scanner can only find what its templates cover. Zero-day patterns, creative chaining, and logic-level issues require human creativity.
Continuous DAST complements penetration testing; it doesn't replace it. Think of it as a regression guard: it ensures that the findings from your last penetration test don't quietly reappear, and it catches the automatable configuration issues that would otherwise be found only in the next engagement — six months later. The quarterly penetration test remains the mechanism for finding what automation can't.
Metrics to Track for Continuous DAST
Measure these to demonstrate the value of continuous security testing to your organisation:
- Mean time to detect (MTTD) — how quickly does DAST catch a newly introduced vulnerability? Target: within one deployment cycle.
- Mean time to remediate (MTTR) — how long does it take to fix a finding from detection to verified resolution?
- False positive rate — what percentage of findings require no action? High false positive rates indicate tuning is needed.
- Finding recurrence rate — are previously fixed findings reappearing? High recurrence indicates remediation isn't addressing root causes.
- Coverage over time — is the number of endpoints being scanned increasing as the application grows? Stagnant coverage means new features aren't being tested.
A Practical Maturity Model for Continuous DAST
Level 1: Scheduled scanning
Run a full Nuclei scan against staging weekly. No pipeline integration yet. Results emailed to the security team. Takes a day to set up. This is better than nothing and generates quick wins.
Level 2: Deployment-triggered scanning
Integrate DAST into the staging deployment pipeline. Run on every successful staging deployment. Gate on critical findings only. Accept false positives at medium and below. This is the target for most teams within the first 90 days.
Level 3: Baseline comparison and triage workflow
Implement baseline comparison so only new findings fail the pipeline. Build a triage workflow for medium findings — weekly review by security engineer, tracked in the issue tracker. Reduce false positive rate through template tuning.
Level 4: Full authenticated coverage with business-context configuration
Multiple authenticated roles tested per scan (regular user, admin, API consumer). Scope configured to match actual release features. Integration with penetration test findings to create targeted regression templates. DAST findings automatically open issues in the team's issue tracker.
Continuous DAST Without the Pipeline Complexity
Ironimo runs Kali Linux-grade web application security scans on demand — no GitHub Actions YAML to write, no Nuclei installation to maintain, no false-positive tuning to manage. Schedule scans against your staging environment, configure authenticated access once, and get structured finding reports with every run.
Use Ironimo as your continuous DAST layer, and let your engineers ship with confidence that the security baseline is being maintained between penetration tests.
Start free scan