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

Not suited for continuous scanning

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:

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