Configures and runs OWASP ZAP baseline scanning: `zap-baseline.py` Docker-packaged spider + passive scan suitable for CI gating; supports `-t target_url` + `-r html_report` + `-c config_file` rule customization (INFO/IGNORE/FAIL warnings) and Ajax spider via `-j` for JS-heavy SPAs; `zap-full-scan.py` active companion for staging. Covers authenticated scans end to end as a reference - ZAP Context, auth methods (form/JSON/script/browser), session management, verification strategy, OAuth/bearer injection, context XML export for `-n` - plus DAST cadence planning (PR-blocking passive baseline, nightly ZAP full + nuclei active layer, baseline-finding ratchet for legacy apps). Use when the user runs OWASP ZAP for pre-prod web app DAST, needs coverage of routes behind a login wall, or is designing a team's DAST rollout cadence.
73
92%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Low
Low-risk findings worth noting
Companion reference for zap-baseline. Consult when a team adopts DAST from
scratch, restructures scan cadence, or is drowning in findings with no triage
discipline. Layers ZAP passive baseline and active scanning (ZAP full scan +
nuclei templates) across PR / nightly windows, with a baseline ratchet so
pre-existing findings do not block PRs.
| Layer | Scan type | Cadence | Target | Risk |
|---|---|---|---|---|
| 1 | Passive baseline (ZAP baseline) | Per-PR (blocking) | Staging | Safe - passive only |
| 2 | Active scan (ZAP full scan + nuclei templates) | Nightly | Staging | Active probes - pollute staging data |
The PR-blocking layer is intentionally narrow - only fail on findings that
didn't exist before. That requires the baseline ratchet (Step 2). Nuclei
(nuclei-dast skill) complements the nightly ZAP full scan with
template-driven checks; its JSONL output feeds the same aggregation layer.
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 per-rule gating 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 dedupedCross-tool dedup is handled at the aggregation layer (security-finding-triager);
this dedup is per-tool per-run.
Two workflows implement the layering:
| Workflow file | Trigger | Job |
|---|---|---|
.github/workflows/dast.yml | pull_request | ZAP baseline + dast-pr-gate.py (Step 2) |
.github/workflows/dast-nightly.yml | cron: 0 2 * * * | ZAP full scan + nuclei template scan |
# .github/workflows/dast.yml - PR-blocking baseline
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/action-baseline@v0.13.0
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 - nightly active scan
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/action-full-scan@v0.13.0
with:
target: ${{ secrets.STAGING_URL }}
- uses: actions/upload-artifact@v4
with: { name: zap-full-report, path: report_html.html }
nuclei:
name: DAST template scan (nuclei)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- run: |
nuclei -u ${{ secrets.STAGING_URL }} -jsonl -o nuclei.jsonl
- uses: actions/upload-artifact@v4
with: { name: nuclei-report, path: nuclei.jsonl }Nuclei flag details are in the nuclei-dast skill.
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 ratioIf coverage < 80% of API surface, the spider missed routes; investigate auth flows (auth.md), JS-heavy SPAs, route-discovery gaps.
Once both tools run, aggregate each tool's output:
zap-baseline.py -t $URL -J zap.json
nuclei -u $URL -jsonl -o nuclei.jsonl
# Aggregate both + emit unified verdictThe aggregation layer (the security-finding-triager agent +
multi-tool-finding-triage) handles cross-tool dedup, severity
normalization, and 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 + nuclei without dedup | Same finding shows twice | Aggregate via triager (Step 7) |
Re-review-date + reviewer (Step 5)nuclei-dast - nuclei flags + CI integration