Skip to content

Manual Test Runbook — I4: Dynamic Application Security Testing (DAST)

Owner: Sagar  |  Time: ~3 min (Part A offline) · +5 min (Part B dry) · +15 min (Part C live)  |  Cloud: none · $0

Promotes I4 (.github/workflows/dast-scan.yml) from 🟦 Code Complete → 🟩 Shipped. Reusable workflow_call DAST gate (OWASP ZAP). The dynamic layer of the vuln suite: it attacks a running app over HTTP, whereas I1/I2/I3 inspect artifacts and source at rest. Part A is offline (YAML lint + rules-file check). Part B is a wiring dry-run. Part C is a live scan against a deliberately-vulnerable target.


Prerequisites

  • Local tooling: python3 (YAML lint), awk/python3 (tsv check)
  • (Part C only) gh authenticated; the workflow present on the default branch
  • (Part C only, optional) docker to stand up a local juice-shop target
  • Working directory: repo root

Steps

Part A — workflow lint + rules-file check (offline, ~3 min)

  1. Parse the workflow YAML:
python3 -c "import yaml; yaml.safe_load(open('.github/workflows/dast-scan.yml')); print('dast-scan.yml parses OK')"
# Optional, if installed:
actionlint .github/workflows/dast-scan.yml

Expected: dast-scan.yml parses OK. Confirm workflow_call inputs (target_url required, scan_type default baseline, fail_on default High, allow_url_failure, rules_file_path default .zap/rules.tsv) and that permissions: is contents: read only (no issues: write, because allow_issue_writing: false).

  1. Confirm the rules file is present and well-formed (tab-separated, 3 fields/row):
python3 - <<'PY'
rows = [l.rstrip("\n") for l in open(".zap/rules.tsv") if l.strip()]
bad = [(i+1, r) for i, r in enumerate(rows) if len(r.split("\t")) != 3]
print(f"{len(rows)} rules; malformed rows: {bad or 'none'}")
PY

Expected: all rows have 3 fields; malformed rows: none.

  1. How DAST (I4) differs from I1 / I3:
What it inspects When Tool
I1 image-scan container image CVEs (at rest) build / re-scan Trivy
I2 dependency-review dependency advisories (at rest) PR dep-review
I3 codeql first-party source (SAST, at rest) PR / push CodeQL
I4 dast-scan a running, deployed app (over HTTP) after deploy OWASP ZAP

I4 is the only gate that needs a live URL — it spiders the app and sends real requests, catching runtime issues (reflected XSS, injection, missing security headers) that static analysis cannot see. It must therefore run after a deploy to a reachable environment, not at code-time.

Part B — caller wiring dry-run (~5 min, $0)

  1. A downstream repo wires the gate after its staging deploy. The caller snippet:
# .github/workflows/deploy-and-dast.yml in a downstream repo
jobs:
  deploy-staging:
    # ... deploys the PR build, outputs the staging URL ...
    outputs:
      url: ${{ steps.deploy.outputs.url }}

  dast:
    needs: deploy-staging
    uses: snowops/snowops-automation/.github/workflows/dast-scan.yml@main
    with:
      target_url: ${{ needs.deploy-staging.outputs.url }}
      scan_type: baseline      # "full" for the intrusive active scan
      fail_on: High
      # allow_url_failure: true  # set while the env is still flaky

Walk through it: scan_type: baseline selects the baseline job (the full job is gated off via its if:); ZAP reads .zap/rules.tsv; on a High finding the gate fails; either way the zap-baseline-report artifact is uploaded.

Part C — live scan against a vulnerable target (~15 min, $0)

  1. Stand up (or point at) a deliberately-vulnerable target. OWASP Juice Shop is the canonical choice:
# Local option (runner must reach it — for a hosted runner use a public URL):
docker run --rm -d -p 3000:3000 bkimminich/juice-shop

For the GitHub-hosted runner, use a publicly reachable vulnerable demo URL (e.g. a deployed juice-shop instance or http://testphp.vulnweb.com) since the runner cannot reach your laptop's localhost.

  1. Dispatch the scan against the vulnerable target:
gh workflow run dast-scan.yml -f target_url=http://testphp.vulnweb.com -f scan_type=baseline

Confirm: - the zap (baseline) job runs and fails on High-risk findings (a FAIL-mapped rule such as SQL injection / XSS fired); - the zap-baseline-report artifact uploaded (download it; open report_html.html).

  1. Re-run against a clean/benign URL (e.g. https://example.com) and confirm the gate is green (no FAIL rules fire), proving the cutoff wiring.

  2. (Optional) Try scan_type: full against the throwaway target to confirm the full job (active attack scanner) selects instead of baseline. Never point the full scan at production.


Sign-Off

Field Value
Part A (lint + rules) ☐ PASS
Part B (wiring dry-run) ☐ PASS
Part C (live scan) ☐ PASS / ☐ skipped
Tester
Date
Result ☐ PASS