From qa-dast
Build-an-X for a layered DAST baseline workflow - ZAP baseline (passive, PR-blocking) → ZAP full-scan (active, nightly on staging) → optional Burp Pro deep scan (per-release, paid-tool deep coverage); manages baseline-finding ratchet, alert deduplication across runs, and CI cadence (PR-blocking baseline + nightly deep scan + per-release deep). Use when the team adopts DAST and needs an end-to-end coverage strategy beyond running a single tool.
How this skill is triggered — by the user, by Claude, or both
Slash command
/qa-dast:dast-baseline-runnerThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
DAST tools alone don't make a coverage strategy. A team running
DAST tools alone don't make a coverage strategy. A team running ZAP baseline once per PR catches 30% of what ZAP can find; running ZAP full + Burp + NightVision together at the right cadence catches ~80%. This skill is a build-an-X workflow - the per-team DAST cadence and aggregation strategy.
Three scan types stack:
| Layer | Scan type | Cadence | Target | Risk |
|---|---|---|---|---|
| 1 | Passive baseline (ZAP baseline) | Per-PR (blocking) | Staging | Safe - passive only |
| 2 | Active full scan (ZAP full / NightVision) | Nightly | Staging | Active probes - pollute staging data |
| 3 | Deep paid scan (Burp Pro / Enterprise) | Per-release / weekly | Staging | Active + extension-driven |
PR-blocking layer is intentionally narrow - only fail on findings that didn't exist before. This requires baseline ratchet (Step 3).
The first scan against a legacy app surfaces 100s of pre-existing findings; if they all block PRs, the team disables DAST. The ratchet pattern:
baseline-findings.json# pr-gate.py
import json
def diff_findings(current, baseline):
baseline_keys = {(f['file'], f['rule_id']) for f in baseline}
new = [f for f in current if (f['file'], f['rule_id']) not in baseline_keys]
return new
with open('current.json') as f:
current = json.load(f)
with open('baseline.json') as f:
baseline = json.load(f)
new_findings = diff_findings(current, baseline)
if any(f['severity'] in ['critical', 'high'] for f in new_findings):
print(f"FAIL: {len(new_findings)} new finding(s) on PR; not in baseline")
exit(1)
ZAP baseline natively supports this via the -c config.tsv rule
file; mirror the pattern for the cross-tool aggregation layer.
Consecutive PR-runs catch the same vulnerability multiple times;
each PR comment shows duplicate noise. Dedupe by (rule_id, url, parameter) tuple:
def dedupe_findings(findings):
seen = set()
deduped = []
for f in findings:
key = (f['rule_id'], f['url'], f.get('parameter', ''))
if key not in seen:
seen.add(key)
deduped.append(f)
return deduped
For dast-finding-triager
integration, the triager handles cross-tool dedup; this skill's
dedup is per-tool per-run.
# .github/workflows/dast.yml
on:
pull_request:
branches: [main]
jobs:
zap-baseline-pr:
name: DAST baseline (PR-blocking)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: zaproxy/[email protected]
with:
target: ${{ secrets.STAGING_URL }}
rules_file_name: '.zap/rules.tsv'
- run: python ci/dast-pr-gate.py current.json .zap/baseline-findings.json
# .github/workflows/dast-nightly.yml
on:
schedule:
- cron: '0 2 * * *' # 2 AM daily
workflow_dispatch:
jobs:
zap-full-scan:
name: DAST active full scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: zaproxy/[email protected]
with:
target: ${{ secrets.STAGING_URL }}
- name: Upload report
uses: actions/upload-artifact@v4
with: { name: zap-full-report, path: report_html.html }
nightvision:
name: DAST API spec scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- run: ./ci/run-nightvision.sh
# .github/workflows/dast-release.yml
on:
release:
types: [created]
workflow_dispatch:
jobs:
burp-deep:
name: Burp deep scan (per-release)
runs-on: self-hosted # Burp Enterprise lives on internal infra
steps:
- run: ./ci/burp-enterprise-trigger.sh
When a new finding appears in a PR-blocking scan, the team has 4 options:
.zap/rules.tsv
with # Reason: ... Re-review-date: ...Each option requires reviewer + reason + Re-review-date in commit message or PR comment. No silent suppression.
Post-scan, measure coverage to detect blind spots:
# How many endpoints did the scan cover?
jq '.spider_results.urls | length' report.json
# How many endpoints did the OpenAPI spec define?
jq '.paths | length' openapi.yaml
# Coverage ratio
If coverage < 80% of API surface, the spider missed routes; investigate auth flows, JS-heavy SPAs, route-discovery gaps.
dast-finding-triagerOnce 2+ tools run, ingest each tool's JSON output into the triager:
zap-baseline.py -t $URL -J zap.json
nightvision scan results $SCAN_ID --output json > nightvision.json
# Burp Enterprise: download via API
curl ... -o burp.json
# Agent consumes all three + emits unified verdict
The agent handles cross-tool dedup, severity normalization, waiver enforcement.
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Run full active scans on every PR | Scan time blows out CI; staging data corrupted | Baseline-only on PR; full nightly (Step 4) |
| Skip baseline ratchet | Legacy findings block every PR | Baseline + diff (Step 2) |
| Ignore coverage measurement | Missing endpoints unscanned silently | Step 6 weekly check |
| One scan per app, never re-baseline | Baseline grows stale; misses regressions in old code | Quarterly re-baseline + waiver review |
| Run ZAP + Burp + NightVision without dedup | Same finding shows 3x | Aggregate via triager (Step 7) |
For each app's DAST coverage:
Re-review-date + reviewer (Step 5)zap-baseline, burp-headless,
nightvision-dast.zap-baseline,
burp-headless,
nightvision-dast - sister toolsdast-finding-triager -
cross-tool unifierProvides CDSS development patterns for drug interaction checking, dose validation, clinical scoring (NEWS2, qSOFA), and alert classification integrated into EMR workflows.
npx claudepluginhub testland/qa --plugin qa-dast