Testland
Browse all skills & agents

zap-baseline

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.

Install with skills.sh (any agent)

npx skills add testland/qa --skill zap-baseline
View source

zap-baseline

Overview

Per zaproxy.org/docs/docker/baseline-scan/ (opens in new window):

"The ZAP Baseline scan is a script that is available in the ZAP Docker images. It runs the ZAP spider against the specified target for (by default) 1 minute and then waits for the passive scanning to complete before reporting the results."

The baseline scan is passive only - non-intrusive, safe for production. The companion zap-full-scan.py adds active scanning (injection probes, XSS payloads) and is NOT safe for production; reserve for staging.

When to use

  • The repo deploys a web app + needs pre-prod DAST gate.
  • A CI workflow needs a non-intrusive scan that's safe to point at a deployed staging environment.
  • The team uses OWASP ZAP (de facto OSS DAST) over commercial alternatives.
  • The app's interesting routes sit behind a login wall (references/auth.md).
  • The team is designing its DAST rollout cadence from scratch (references/cadence.md).

Step 1 - Install

ZAP baseline runs from the official Docker image - no host install needed. Per zap-base (opens in new window):

docker pull ghcr.io/zaproxy/zaproxy:stable

Image variants:

TagUse
:stableProduction-grade; pin in CI
:weeklyLatest features; for evaluation
:bareHeadless minimal; smallest

Step 2 - First baseline scan

Per zap-base (opens in new window) (verbatim):

docker run -v $(pwd):/zap/wrk/:rw -t ghcr.io/zaproxy/zaproxy:stable zap-baseline.py \
    -t https://www.example.com -r testreport.html

The -v $(pwd):/zap/wrk/:rw mount makes the report writable to the host directory. The report file lands at ./testreport.html after the scan.

Step 3 - Common flags

Per zap-base (opens in new window):

FlagUse
-t URLTarget URL (required)
-r FILEHTML report output
-w FILEMarkdown report
-x FILEXML report
-J FILEJSON report (for finding triage)
-c FILEConfig file: rule INFO/IGNORE/FAIL behavior
-P PORTSpecify ZAP listen port
-jUse Ajax spider in addition to traditional spider (JS-heavy apps)
-m MINSSpider duration in minutes (default 1)
-dShow debug messages
-IDon't return failure on warning (gate softly)
-n CONTEXT_FILEAuthenticated scan context file

Step 4 - Authenticated scans

For apps requiring login, export a ZAP context file from the GUI (Sites → right-click → Export Context). The context file encodes:

  • Auth method (form / script / HTTP / OAuth)
  • Login URL + creds (referenced env vars; never hardcode)
  • Session-management strategy
  • Logged-in / logged-out indicators

Building that context from scratch - choosing the auth method, wiring session management, calibrating verification indicators, CSRF and OAuth/bearer handling - is walked end to end in references/auth.md. Then:

docker run -v $(pwd):/zap/wrk/:rw \
  -e ZAP_AUTH_USERNAME=$USER \
  -e ZAP_AUTH_PASSWORD=$PASS \
  -t ghcr.io/zaproxy/zaproxy:stable \
  zap-baseline.py -t https://app.example.com -n /zap/wrk/context.xml -J report.json

Step 5 - Active scan companion (zap-full-scan.py)

For staging-only deeper analysis:

docker run -v $(pwd):/zap/wrk/:rw -t ghcr.io/zaproxy/zaproxy:stable \
  zap-full-scan.py -t https://staging.example.com -J report.json

zap-full-scan.py triggers active payloads (SQLi probes, XSS, SSRF). NEVER point at production - risk of data corruption + generates audit-log noise.

Step 6 - False-positive triage (MANDATORY)

Per zap-base (opens in new window), rule customization via -c config_file where each line is <rule_id>\t<INFO|WARN|IGNORE|FAIL>\t<URL_pattern>:

Example zap-config.tsv:

10049	IGNORE	*	# Cookie No HttpOnly: legacy session cookie; tracked in JIRA-1234
40012	WARN	https://app.example.com/admin/*	# XSS: legacy admin pages; not exposed to users
10063	FAIL	*	# Permissions Policy: must be set on all responses

Three suppression layers:

MechanismExampleWhen to use
Per-rule config TSV<rule_id>\tIGNORE\t*Rule disabled globally
Per-URL pattern<rule_id>\tWARN\thttps://app/admin/*Rule downgraded for known-old code path
Context exclusion<exclude_from_context> in context XMLWhole URL trees out of scope
-I flagzap-baseline.py -I ...Soft gate: warns but exits 0

Justification template (mandatory in config):

# Rule 10049 (Cookie No HttpOnly)
# Suppressed: 2026-05-15 by alice@example.com
# Reason: legacy session cookie; tracked in JIRA-1234; expires 2026-09-15
# Re-review-date: 2026-09-15
10049	IGNORE	*

Cadence: every quarter, audit the config TSV - every IGNORE entry should have an Re-review-date. Past-due entries are removed + re-evaluated.

Step 7 - Output formats for cross-tool triage

docker run -v $(pwd):/zap/wrk/:rw -t ghcr.io/zaproxy/zaproxy:stable \
  zap-baseline.py -t https://app.example.com -J zap-report.json

The JSON report feeds cross-tool aggregation. For SARIF output (GitHub Code Scanning), use a converter (zap-sarif container action).

Step 8 - CI integration

GitHub Actions:

jobs:
  zap-baseline:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: zaproxy/action-baseline@v0.13.0
        with:
          target: 'https://staging.example.com'
          rules_file_name: '.zap/rules.tsv'
          cmd_options: '-J zap-report.json'
      - uses: actions/upload-artifact@v4
        if: always()
        with: { name: zap-report, path: zap-report.json }

The official zaproxy/action-baseline action wraps the Docker invocation. Auto-creates a GitHub Issue on failure.

Designing the full team cadence around this job - PR-blocking passive baseline, nightly ZAP full + nuclei active layer, the baseline-finding ratchet for legacy apps, dedup, and coverage measurement - is in references/cadence.md.

Anti-patterns

Anti-patternWhy it failsFix
Run zap-full-scan.py against productionActive payloads pollute / damage dataStaging-only (Step 5)
Skip -J JSON outputCan't feed finding triage; report is HTML-onlyAlways pass -J (Step 7)
Ignore Ajax spider for SPAsMissing routes; coverage gapAdd -j flag (Step 3)
Hardcode credentials in context fileSecrets leak in repoReference env vars (Step 4)
Suppress without # Re-review-datePermanent debtRequired template (Step 6)

Limitations

  • Baseline is passive only; full-scan needed for active probes but is not safe in production.
  • Spider duration default 1 min may miss deep-routes apps; increase via -m (Step 3).
  • ZAP scans single-target only; for multi-app fleet, run per-app
    • aggregate via cross-tool triage.
  • Authentication setup via context file is fragile - apps with complex login flows often need custom ZAP scripts (references/auth.md).

References

  • zap-base (opens in new window) - official baseline scan documentation
  • zaproxy.org/docs/docker - full Docker docs
  • github.com/zaproxy/action-baseline - official GHA action
  • Authenticated-scan setup (context, auth methods, session management, verification): references/auth.md
  • Layered DAST cadence (PR baseline → nightly active, ratchet, dedup): references/cadence.md
  • nuclei-dast - sister DAST tool (template-driven scanning)

Authenticated scans - full auth-context setup

View source (opens in new window)

Authenticated scans - full auth-context setup

Companion reference for zap-baseline. Unauthenticated DAST scans cover only the public attack surface - for most apps, 70-90% of routes sit behind a login wall. This build-an-X workflow walks the full authenticated session setup: ZAP Context creation, choosing the right Authentication Method, wiring Session Management, calibrating logged-in/out indicators, handling CSRF tokens, injecting OAuth/bearer headers, and exporting the context file that zap-baseline.py -n context.xml (and zap-full-scan.py) consume in CI.

How to use

  1. Create a ZAP Context that includes the app URLs and excludes /logout, so the scanner does not log itself out mid-scan (Step 1).
  2. Choose the Authentication Method that matches the app's login mechanism - Form, JSON, HTTP/NTLM, Script, or Browser-Based (Steps 2-6).
  3. Set the Session Management method (cookie, HTTP header, or script) to match how the app tracks sessions (Step 7).
  4. Calibrate the Verification Strategy with logged-in and logged-out indicators so ZAP detects session expiry and re-authenticates (Step 9).
  5. Add one or more Users, injecting credentials via -config or env vars rather than hardcoding them (Step 10).
  6. Confirm auth in the Authentication Tester, then export the Context to .zap/context.xml (Step 11).
  7. Run the scan in CI with -n context.xml, or replay the same session in Burp for manual testing (Steps 11-12).

Step 1 - Create a ZAP Context

Per zaproxy.org/docs/desktop/start/features/authentication/ (opens in new window), authentication in ZAP is always scoped to a Context - a named set of URLs. Create one before touching any auth setting:

  1. Open ZAP desktop. In the Sessions dialog: Session Properties > Contexts > Add.
  2. Set the Context Name (e.g., myapp-auth).
  3. Set the Include pattern to cover all app URLs: https://app.example.com/.*
  4. Add exclude patterns for logout URLs to avoid the spider logging itself out during a scan: https://app.example.com/logout.*

All auth settings attach to this Context. CLI scans reference it via -n context.xml (Step 11).

Step 2 - Choose the Authentication Method

Per zaproxy.org/docs/desktop/start/features/authmethods/ (opens in new window), ZAP supports five built-in methods. Choose by app login mechanism:

App login typeMethod to use
HTML form POST with username + password fieldsForm-Based
JSON POST {"username":"...","password":"..."}JSON-Based
HTTP Basic / Digest / NTLM challengeHTTP/NTLM
Custom flow (OTP, magic link, multi-step)Script-Based
Modern browser-rendered SSO / OAuth redirectBrowser-Based (auth-helper addon)

Step 3 - Configure Form-Based Authentication

Per zap-methods (opens in new window), Form-Based auth requires:

  • Login URL: the POST endpoint (e.g., https://app.example.com/login)
  • Login Request POST Data: encodes credentials as URL params: username={⁠%username%}&password={⁠%password%} ZAP replaces {⁠%username%} / {⁠%password%} with User credentials at scan time. Never hardcode credentials in this field.
  • Username field and Password field names (match the HTML name attributes).

Per zap-methods (opens in new window), Form-Based auth supports re-authentication - ZAP detects session expiry and re-logs in automatically mid-scan.

CSRF token handling for form login: if the login form contains an anti-CSRF token field, configure its name in Tools > Options > Anti CSRF Tokens (per zap-auth (opens in new window)). ZAP fetches the login page, extracts the token, and replays it with the POST automatically.

Step 4 - Configure JSON-Based Authentication

Per zap-methods (opens in new window), JSON-Based auth is for apps whose login endpoint accepts a JSON body rather than form-encoded params:

  • Login URL: the POST endpoint
  • Login Request POST Data: {"username":"{⁠%username%}","password":"{⁠%password%}"}

ZAP sends Content-Type: application/json automatically. Supports re-authentication. Use this for REST API login endpoints returning a session cookie or JWT response body.

Step 5 - Configure Script-Based Authentication

Per zap-methods (opens in new window), Script-Based auth handles flows that Form-Based and JSON-Based cannot: OTP-augmented logins, multi-step forms, OAuth authorization-code flows with PKCE, or apps that rotate CSRF seeds on every page load. It requires the Script Console add-on and an Authentication script that builds the login request via helper.prepareMessage().

See script-based-auth.md (opens in new window) for the Script Console setup, the Groovy authenticate() skeleton, and the OAuth authorization-code exchange pattern.

Step 6 - Configure Browser-Based Authentication (auth-helper addon)

Per zaproxy.org/docs/desktop/addons/authentication-helper/ (opens in new window), the Authentication Helper add-on provides Browser-Based Authentication for apps that use JS-rendered login pages, SSO redirects, or WebAuthn flows that headless HTTP clients cannot replay:

authentication:
  method: "browser"
  parameters:
    loginPageUrl: "https://app.example.com/login"
  verification:
    method: "autodetect"
sessionManagement:
  method: "autodetect"

ZAP launches Firefox, navigates to loginPageUrl, fills the username and password fields, and captures the resulting session token. The autodetect verification asks ZAP to find a suitable verification URL automatically.

Step 7 - Configure Session Management

Per zaproxy.org/docs/desktop/start/features/sessionmanagement/ (opens in new window), ZAP supports three session management methods. Set in Session Properties > Context > Session Management:

App session typeMethod
Session ID in a cookie (JSESSIONID, session, etc.)Cookie-Based Session Management
Authorization header (Basic, JWT Bearer)HTTP Authentication Session Management
Custom header or token rotationScript-Based Session Management

Per zap-session (opens in new window), Cookie-Based "session is being tracked through cookies" and tokens are imported from the HTTP Sessions Extension.

Per zap-session (opens in new window), Script-Based "is called whenever session management actions are performed" and requires the Scripts Console add-on.

Step 8 - Inject OAuth/Bearer Tokens via Environment Variables

Per zap-auth (opens in new window), ZAP exposes environment variables (ZAP_AUTH_HEADER_VALUE, ZAP_AUTH_HEADER, ZAP_AUTH_HEADER_SITE) for header-based injection of pre-obtained bearer tokens - OAuth client-credentials, API keys, CI-issued JWTs. Set them in the CI environment before the scan. For a full authorization-code exchange, use Script-Based auth (Step 5) instead and let ZAP manage token refresh.

See oauth-bearer-injection.md (opens in new window) for the variable table and the CI Docker example.

Step 9 - Set Authentication Verification Strategy

Per zap-verify (opens in new window), ZAP uses an Authentication Verification Strategy to know whether a request runs as an authenticated user, driven by a Logged-In Indicator and a Logged-Out Indicator regex. Four strategies exist (Check Every Response, Check Every Request, Check Every Request or Response, Poll the Specified URL); calibrate the indicators by flagging logged-in and logged-out responses in the History tab.

See verification-strategy.md (opens in new window) for the indicator examples, the strategy-selection table, and the calibration steps.

Step 10 - Add Users

Per zaproxy.org/docs/desktop/start/features/users/ (opens in new window), users are configured per-context at Session Properties > Context > Users > Add. Each user stores credentials that map to the Authentication Method's {⁠%username%} / {⁠%password%} placeholders.

Per zap-users (opens in new window): "Authentication Methods define the process; Users store the specific credentials needed for each user account." One context can hold multiple users (admin, read-only, unauthenticated) to test privilege separation in a single scan.

Never store plaintext credentials in the exported context XML committed to version control. Reference environment variables in CI (Step 8 pattern) or use ZAP's -config CLI flag to inject credentials at scan time:

zap-full-scan.py -t https://app.example.com \
  -n /zap/wrk/context.xml \
  -config context.users\(0\).name=scanner \
  -config context.users\(0\).credentials.username=$ZAP_USER \
  -config context.users\(0\).credentials.password=$ZAP_PASS

Step 11 - Export the Context XML

Once auth is confirmed working via the Authentication Tester (per zap-helper (opens in new window), under Tools > Authentication Tester or Ctrl+T), export the Context:

File > Export Context > save as context.xml

Commit context.xml to the repo at .zap/context.xml. The file encodes auth method, session management strategy, verification strategy, and include/exclude URL patterns. It does NOT contain user credentials when users are configured with the -config override pattern above.

Use in CI:

docker run --rm \
  -e ZAP_AUTH_USERNAME=$ZAP_USER \
  -e ZAP_AUTH_PASSWORD=$ZAP_PASS \
  -v $(pwd):/zap/wrk/:rw \
  ghcr.io/zaproxy/zaproxy:stable \
  zap-baseline.py -t https://app.example.com -n /zap/wrk/.zap/context.xml -J report.json

Per ../SKILL.md (opens in new window), the -n CONTEXT_FILE flag loads this file and activates authentication for the scan.

Step 12 - Replay with Burp Suite

For apps already configured in ZAP, mirror the session in Burp for manual testing by capturing a valid authenticated request via ZAP proxy, then:

  1. Export the HAR: right-click the authenticated request in ZAP History, Save as HAR.
  2. In Burp, import via Proxy > HTTP history > Import HAR.
  3. Set up a Macro (Project > Session handling rules > Macros) that replays the login POST and extracts the session token using a regex matching the cookie or JSON access_token field.
  4. Add a Session handling rule (Settings > Sessions > Session handling rules > Add) with scope covering the entire app and the macro set as the rule action.

This keeps Burp and ZAP scanning the same authenticated surface without re-configuring login from scratch in each tool.

Worked example

A team needs authenticated DAST coverage of a form-login SPA at app.example.com whose login page carries an anti-CSRF token.

  1. They create a Context myapp-auth including https://app.example.com/.* and excluding https://app.example.com/logout.* (Step 1).
  2. Login is an HTML form POST, so they pick Form-Based auth with POST data username={⁠%username%}&password={⁠%password%} and register the CSRF field name under Tools > Options > Anti CSRF Tokens (Steps 2-3).
  3. Sessions ride a JSESSIONID cookie, so they set Cookie-Based Session Management (Step 7).
  4. They flag a logged-in response containing href="/logout" and a logged-out Please log in page as indicators, using Check Every Response (Step 9, verification-strategy.md (opens in new window)).
  5. They add a scanner user and export the Context to .zap/context.xml, with credentials supplied at scan time via -config (Steps 10-11).
  6. CI runs zap-baseline.py -t https://app.example.com -n /zap/wrk/.zap/context.xml -J report.json.

Result: the baseline scan authenticates, re-logs in when the session expires, and reports vulnerabilities across the routes behind the login wall instead of only the public pages.

Anti-patterns

Anti-patternWhy it failsFix
Skip context creation, use -u user:pass flagNo re-auth; spider logs out mid-scanContext + auth method (Steps 1-3)
Hardcode credentials in context.xmlSecrets leak in version control-config injection or env vars (Step 10)
No logged-out indicatorZAP reports false coverage on expired sessionsCalibrate both indicators (Step 9)
Form-based auth on a JSON-API loginZAP sends form-encoded body; app rejects itJSON-based auth (Step 4)
Exclude /login from context scopeAuth POST never proxied; ZAP can't authenticateInclude login URL; exclude only /logout (Step 1)
Browser-based auth without auth-helper addonmethod: browser is not a built-in; scan failsInstall Authentication Helper from Marketplace (Step 6)
Set verification strategy but no indicatorsStrategy is inactive; ZAP never detects re-auth needSupply at least one logged-in regex (Step 9)

Limitations

  • ZAP auth context cannot be built or tested without the ZAP desktop or automation framework; no pure-CLI context creation exists.
  • MFA (TOTP, SMS OTP) requires Script-Based auth with a TOTP library or a pre-generated token injected via env var; ZAP has no native MFA support.
  • Browser-Based auth requires a local browser and is not available in headless Docker without a virtual display or the auth-helper's browser-in-Docker mode.
  • Per zap-methods (opens in new window), Manual Authentication "does not support re-authentication in case the webapp logs a user out"; avoid for active scans longer than the session TTL.
  • Context XML export includes URL patterns but not user credentials when using the -config flag injection pattern; anyone needing credentials must supply them separately.

Sources

DAST scan cadence - layered rollout planning

View source (opens in new window)

DAST scan cadence - layered rollout planning

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.

Step 1 - Layer the scans by intrusiveness

LayerScan typeCadenceTargetRisk
1Passive baseline (ZAP baseline)Per-PR (blocking)StagingSafe - passive only
2Active scan (ZAP full scan + nuclei templates)NightlyStagingActive 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.

Step 2 - Baseline-finding ratchet

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:

  1. Run the scan once against current main → save as baseline-findings.json
  2. Per-PR: run the scan, diff against baseline, fail only on NEW findings
  3. Periodically (weekly): re-baseline, requiring waiver entries for any persisted findings
# 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.

Step 3 - Alert deduplication across runs

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

Cross-tool dedup is handled at the aggregation layer (security-finding-triager); this dedup is per-tool per-run.

Step 4 - CI cadence

Two workflows implement the layering:

Workflow fileTriggerJob
.github/workflows/dast.ymlpull_requestZAP baseline + dast-pr-gate.py (Step 2)
.github/workflows/dast-nightly.ymlcron: 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.

Step 5 - Per-finding triage workflow

When a new finding appears in a PR-blocking scan, the team has 4 options:

  1. Fix - code change resolves the finding
  2. Suppress with justification - add to .zap/rules.tsv with # Reason: ... Re-review-date: ...
  3. Add to baseline - explicit acceptance; finding tracked in baseline file with reviewer attribution
  4. Escalate - beyond PR scope; create a tracker ticket + waive per-PR with explicit ticket reference

Each option requires reviewer + reason + Re-review-date in commit message or PR comment. No silent suppression.

Step 6 - Coverage measurement

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 (auth.md (opens in new window)), JS-heavy SPAs, route-discovery gaps.

Step 7 - Aggregate cross-tool findings

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 verdict

The aggregation layer (the security-finding-triager agent + multi-tool-finding-triage) handles cross-tool dedup, severity normalization, and waiver enforcement.

Anti-patterns specific to DAST cadence

Anti-patternWhy it failsFix
Run full active scans on every PRScan time blows out CI; staging data corruptedBaseline-only on PR; full nightly (Step 4)
Skip baseline ratchetLegacy findings block every PRBaseline + diff (Step 2)
Ignore coverage measurementMissing endpoints unscanned silentlyStep 6 weekly check
One scan per app, never re-baselineBaseline grows stale; misses regressions in old codeQuarterly re-baseline + waiver review
Run ZAP + nuclei without dedupSame finding shows twiceAggregate via triager (Step 7)

End-to-end cadence checklist

  1. ZAP baseline runs PR-blocking against staging (Step 4)
  2. ZAP full scan + nuclei run nightly against staging (Step 4)
  3. Baseline ratchet active for legacy apps (Step 2)
  4. Per-tool dedup applied (Step 3)
  5. Cross-tool dedup via triager (Step 7)
  6. Coverage measured weekly (Step 6)
  7. Suppression entries have Re-review-date + reviewer (Step 5)

Limitations

  • Active scans are inherently destructive on staging data; pair with a staging-data refresh cadence.
  • DAST coverage is bounded by spider + spec discovery; SPA-heavy apps need careful auth + route-discovery setup (auth.md (opens in new window)).
  • The cadence assumes overnight windows; high-velocity shops may need shorter cycles + smaller scan scopes.

Sources

  • OWASP WSTG - owasp.org/www-project-web-security-testing-guide
  • OWASP DSOMM (DevSecOps Maturity Model) for cadence guidance
  • nuclei-dast - nuclei flags + CI integration

ZAP OAuth / Bearer token injection

View source (opens in new window)

ZAP OAuth / Bearer token injection

Per zap-auth (opens in new window), ZAP exposes three environment variables for header-based authentication injection - useful for pre-obtained bearer tokens (OAuth client-credentials flow, API keys, CI-issued JWTs):

VariablePurpose
ZAP_AUTH_HEADER_VALUEThe token value (Bearer eyJ...)
ZAP_AUTH_HEADERHeader name (defaults to Authorization if unset)
ZAP_AUTH_HEADER_SITERestrict injection to this domain only

Set these in the CI environment before running the scan:

export ZAP_AUTH_HEADER_VALUE="Bearer $(./scripts/get-ci-token.sh)"
export ZAP_AUTH_HEADER_SITE="app.example.com"

docker run --rm \
  -e ZAP_AUTH_HEADER_VALUE \
  -e ZAP_AUTH_HEADER_SITE \
  -v $(pwd):/zap/wrk/:rw \
  ghcr.io/zaproxy/zaproxy:stable \
  zap-full-scan.py -t https://app.example.com -n /zap/wrk/context.xml -J report.json

For OAuth flows requiring a full authorization-code exchange, use Script-Based auth (references/script-based-auth.md (opens in new window)) to run the exchange inside ZAP and let ZAP manage token refresh during the scan. Environment-variable injection is the right path for client-credentials and static-API-key auth.

ZAP Script-Based Authentication

View source (opens in new window)

ZAP Script-Based Authentication

Per zap-methods (opens in new window), Script-Based auth handles flows that Form-Based and JSON-Based cannot: OTP-augmented logins, multi-step forms, OAuth authorization-code flows with PKCE, or apps that rotate CSRF seeds on every page load.

Prerequisites:

  1. Install the Script Console add-on from the ZAP Marketplace.
  2. In Tools > Scripts, create a new Authentication script (type: Authentication). ZAP ships example scripts at scripts/authentication/ inside the ZAP installation directory.
  3. The script receives helper, paramsValues, and credentials; it must call helper.prepareMessage() to build a login request and return the response.

Minimal skeleton (Groovy):

def authenticate(helper, paramsValues, credentials) {
    def loginUrl = paramsValues.get("Login URL")
    def msg = helper.prepareMessage()
    msg.setRequestHeader("POST " + loginUrl + " HTTP/1.1\r\n" +
        "Host: app.example.com\r\n" +
        "Content-Type: application/json\r\n")
    def body = '{"user":"' + credentials.getParam("Username") + '",' +
               '"pass":"' + credentials.getParam("Password") + '"}'
    msg.setRequestBody(body)
    helper.sendAndReceive(msg)
    return msg
}

Select the script in Session Properties > Context > Authentication > Script-Based Authentication, then set any script parameters.

For OAuth authorization-code flows: the script fetches the /authorize redirect, extracts the code, POSTs to /token, and stores the resulting access_token in a ZAP environment variable for header injection (see references/oauth-bearer-injection.md (opens in new window)).

ZAP Authentication Verification Strategy

View source (opens in new window)

ZAP Authentication Verification Strategy

Per zap-verify (opens in new window), ZAP uses an Authentication Verification Strategy to know whether a request is executing as an authenticated user. Configure in Session Properties > Context > Authentication > Verification:

Logged-In Indicator: a regex present in responses when the user is authenticated. Examples:

  • \QWelcome, \E (welcome banner with the username)
  • \Qhref="/logout"\E (logout link in nav)
  • \Q"role":"user"\E (JSON response field)

Logged-Out Indicator: a regex present in responses when the session has expired. Examples:

  • \QPlease log in\E
  • \Qlocation: /login\E (redirect header)
  • HTTP/1\.1 401

Per zap-verify (opens in new window), four strategies are available:

StrategyUse when
Check Every ResponseTraditional HTML apps (indicator in page body)
Check Every RequestClient-side sessions (JWT in Authorization header)
Check Every Request or ResponseMixed; SPA + API combo
Poll the Specified URLDedicated /api/me or /session/check endpoint

Calibration steps:

  1. Browse the app manually through ZAP proxy while logged in.
  2. Right-click a response in the History tab that contains the logged-in text. Choose Flag as Context > <context-name> Logged in indicator. ZAP extracts the regex automatically.
  3. Browse to a page after logging out. Right-click that response. Choose Flag as Context > <context-name> Logged out indicator.
  4. Confirm both indicators in Session Properties > Context > Authentication.

Related skills

codeql-queries

Configures and runs GitHub CodeQL - semantic-database SAST with queries written in the CodeQL declarative query language; supports `codeql database create` (per-language) + `codeql database analyze` with --format=sarif; ships query packs (`codeql/javascript-queries`, `codeql/python-queries`, `codeql/java-queries`, `codeql/go-queries`, etc.); integrates with GitHub Code Scanning via SARIF upload; suppression via inline comment + sarif-filter + Security-tab dismissal. Use when the team uses GitHub-hosted repos and needs deep semantic SAST beyond pattern matching (cross-file taint flows, dataflow analysis).

cve-exploitability-triage

Ranks known CVE findings by real-world exploitability instead of severity alone: enriches each CVE with its EPSS probability (the chance exploitation activity is observed in the next 30 days) and CISA KEV membership (confirmed exploited in the wild), applies OpenVEX status assertions to set aside vulnerabilities the product is not affected by, applies a reachability heuristic for vulnerable code that is never called, and assigns every finding to one of four buckets (Fix-Now, Fix-This-Sprint, Fix-Backlog, Accept-Risk) using documented EPSS thresholds. Treats a CISA KEV listing as non-waivable under any justification. Use when a dependency, container image, or SBOM vulnerability scan has produced more CVEs than the team can fix in the available window and someone has to decide which ones get fixed first and which can wait.

dependabot-config

Reference for `.github/dependabot.yml` - GitHub-native dependency-update orchestrator. Required keys (`version: 2`, `updates[]` array) plus per-update fields (`package-ecosystem`, `directory` / `directories`, `schedule.interval`); common optional fields (`ignore`, `groups`, `allow`, `labels`, `milestone`, `open-pull-requests-limit`, `target-branch`, `vendor`, `versioning-strategy`, `assignees`, `commit-message`); auto-rebase + grouped-PR + security-only updates. Use when authoring or reviewing Dependabot configs in GitHub-hosted repos.

gitleaks-scanning

Configures and runs gitleaks - Go-based secret scanner with `gitleaks git` (scan local git via `git log -p`), `gitleaks dir` (filesystem), `gitleaks stdin` (pipe); 100+ built-in rules + custom rules in `.gitleaks.toml` ([[rules]] with regex / entropy / keywords / tags); allowlist via [[rules.allowlists]] (commits / paths / stopwords); pre-commit hook + GitHub Action integration; plus baseline management for legacy debt - onboarding a repo with historical findings via `--baseline-path` snapshots, `.gitleaksignore`, cross-tool suppression consistency with TruffleHog, and rot-prevention cadence. Use when the team needs OSS secret scanning at commit time + CI gate, or is adopting scanning on a repo with pre-existing findings.

language-native-sast

Language-native SAST linters - the first-party "linter as SAST" family that runs inside each ecosystem's standard toolchain with no separate scanner server: Bandit (Python, 60+ B-rules, severity x confidence filtering), gosec (Go, 40+ G-rules, AST + SSA taint tracking, golangci-lint integration), eslint-plugin-security + eslint-plugin-no-unsanitized (JS/TS, 14 detect-* rules + DOM-sink XSS), and PMD's Apex security ruleset (Salesforce, ApexSOQLInjection / ApexCRUDViolation / ApexSharingViolations). Covers the shared adoption pattern - install as a dev dependency, first scan, suppression-with-justification discipline, baseline-diff adoption for legacy code, SARIF output + CI gating - with per-tool depth in references. Use when a repo needs in-toolchain security linting for Python, Go, JavaScript/TypeScript, or Apex; for cross-language or cross-file taint analysis use semgrep-rules / codeql-queries instead.

multi-tool-finding-triage

Merges two or more security scanner reports into one gate. Use when you need a single BLOCK or PASS decision from multiple scanners instead of reading N separate reports. Normalizes each report into one common finding format (a canonical `Finding`), deduplicates on a per-domain key while recording which scanners agree (`caught_by` consensus), validates a waiver (finding-suppression) file, rejecting any missing `expires:` / `approved_by:` / `reason:` or expired, enriches CVE findings with EPSS (exploit-probability) and CISA KEV (known-exploited catalog), then applies a `fail_on` severity threshold to emit BLOCK or PASS plus a bucketed pull-request comment. Works across static (SAST), dynamic (DAST), secret, dependency (SCA), container, and IaC scanners. To run a single scanner instead use semgrep-rules, codeql-queries, or one of the language-native-sast linters; this runs after them to merge output - the cross-scanner gate, not a single-scanner wrapper.

npm-pip-maven-audit

Configures and runs native package-manager audit commands across ecosystems - `npm audit --audit-level=high` (npm), `yarn npm audit` (Yarn 2+), `pnpm audit` (pnpm), `pip-audit` (Python via PyPA), `mvn dependency:check` (Maven via OWASP Dependency-Check plugin), `cargo audit` (Rust, with `.cargo/audit.toml` suppression, `--deny` semantics, SARIF, binary auditing, and the rustsec/audit-check Action as a reference), and `bundle audit` (Ruby Bundler, with `.bundler-audit.yml` waivers, Rake integration, and CI gating as a reference); fastest no-install-required SCA option. Use when the team wants fast, no-extra-tooling SCA in CI as a first line of defense, when a Rust or Ruby repo needs its ecosystem-native scanner, or pairs with snyk/osv-scanner for layered coverage.

nuclei-dast

Installs and runs ProjectDiscovery Nuclei template-based HTTP scanning: selects templates via `-t {path}` and `-tags`/`-severity` filters, controls request rate with `-rl`, emits JSONL output via `-j` for cross-tool finding aggregation, authors custom YAML matchers for app-specific checks, and gates CI on severity thresholds. Use when the team runs Nuclei alongside ZAP for template-driven DAST coverage, needs fuzzing-style probes beyond ZAP passive scan, or wants to operationalize community CVE templates in a pipeline.

osv-scanner

Configures and runs Google OSV-Scanner - open-source SCA against the OSV.dev vulnerability database; supports `osv-scanner scan -r ./` recursive scan + per-lockfile scan via `-L package-lock.json`; SBOM input (CycloneDX / SPDX) for non-standard package managers; `--format json|sarif|markdown|vertical|html` output; suppressions via `osv-scanner.toml` config. Use when the team needs OSS-native SCA without commercial-license overhead, or wants a second-opinion DB pair with Snyk's commercial DB.

reachability-analyzer

Runs dead-dependency analysis across JS, Python, and Rust projects using ecosystem-native static tools (`depcheck`/`knip` for JS, `vulture` for Python, `cargo-machete` for Rust), then cross-references the unused-dependency list against SCA findings to downrank vulns in code that is never loaded. Use when SCA output (from `osv-scanner`, `snyk-test`, or `npm-pip-maven-audit`) is too noisy to triage and the team needs to separate unreachable CVEs from exploitable ones before sprint planning; sibling cve-exploitability-triage ranks by EPSS/KEV exploitation signal, not code reachability.

renovate-config

Reference for `renovate.json` - Mend Renovate dependency-update orchestrator (multi-platform: GitHub / GitLab / Bitbucket / Azure DevOps / Gitea); top-level keys (`extends` for preset references, `schedule`, `prConcurrentLimit`, `vulnerabilityAlerts`); `packageRules[]` array with `matchPackageNames` / `matchUpdateTypes` / `automerge` matching; `ignoreDeps`, `addLabels`, `automergeSchedule`. Use when authoring or reviewing Renovate configs in any repo platform Renovate supports.

sbom-formats

Reference for the two SBOM specification families and how to choose between them - CycloneDX v1.6 (OWASP-curated, security-focused: components, services, dependencies, first-class vulnerabilities[] with embedded VEX, formulation, ML/SaaS BOMs; XML / JSON / Protobuf) as the primary format, with SPDX 2.3 + 3.0 (Linux Foundation, license-focused: packages, relationships, license expressions, Tag-Value/JSON encodings, ISO/IEC 5962:2021) covered as a reference. Includes per-language generators, schema validation, sign + attest CI wiring, and the format-choice guidance (CycloneDX for security-focused consumers; SPDX for US Federal procurement, Linux Foundation, and license-compliance contexts). Use when the user asks to write or validate an SBOM in CycloneDX or SPDX form, or the team must pick its SBOM format.

secrets-rotation-runner

Build-an-X for the secret-rotation workflow after detection - detect via gitleaks/trufflehog/kingfisher → identify provider via verifier → rotate via provider API (AWS IAM / GitHub PAT / Stripe / GCP / Azure / Twilio / Slack / etc.) → invalidate old secret → audit log via observability stack → post-mortem cross-ref. Use when a secret is detected in code (or proactively for periodic rotation) - assume git-history scrub does NOT prevent compromise.

semgrep-rules

Configures and runs Semgrep - pattern-based SAST across 30+ languages with the Semgrep Registry rulesets (`p/owasp-top-ten`, `p/default`, `auto`) plus custom YAML rules; integrates `semgrep ci` for PR-blocking gates with `--baseline-commit` diff-aware scanning, per-finding inline `nosemgrep` suppressions, `--exclude` / `--include` path filters, output formats (`--json` / `--sarif` / `--gitlab-sast` / `--junit-xml`), and severity filter (INFO/WARNING/ERROR). Use when the user runs Semgrep, asks about pattern rules, or needs a low-friction SAST gate without semantic-DB setup.

snyk-test

Configures and runs Snyk, a commercial multi-mode scanner: snyk test for SCA (dependency scanning), snyk code test for SAST (code security scanning), snyk container test for container images, snyk iac test for IaC (infrastructure-as-code), snyk monitor for continuous new-vuln alerts; policy file .snyk for ignore + patch. Use when the team has a Snyk license and needs SCA (dependency scanning) or continuous vuln monitoring; for open-source scanning without a Snyk license, prefer osv-scanner.

sonarqube-rules

Configures and runs SonarQube / SonarCloud - multi-language SAST + Quality Gate platform with built-in Sonar Way rule profiles + custom rule plugins; integrates `sonar-scanner` with `sonar-project.properties` config; supports Quality Gate definitions including new-code-period blocking, branch + PR analysis, and per-issue suppression via `// NOSONAR` comment or `@SuppressWarnings("squid:RULE_ID")` annotation. Use when the user runs SonarQube Community / Developer / Enterprise edition or SonarCloud, or needs a multi-language SAST + code-quality platform with persistent issue tracking.

syft-generation

Generates, scans, and diffs Software Bills of Materials (SBOMs) with the Anchore stack - Syft generation from container images / directories / archives across OCI / Docker / Singularity formats (output CycloneDX-JSON / SPDX-JSON / Syft-JSON / table / GitHub-JSON, cosign attestation); the paired generate + scan workflow with Grype (`grype sbom:./sbom.json`, `--fail-on high`, `--only-fixed`, `.grype.yaml` ignore rules with mandatory `expires:`, EPSS/KEV prioritization); and SBOM-to-SBOM diffing via `cyclonedx diff --component-versions` to gate CI on net-new components and detect supply-chain drift between builds. Use when the team needs SBOM artifacts for compliance (US EO 14028, EU CRA, FDA medical-device guidance), SBOM-driven vulnerability scanning, or dependency-drift detection between releases.

trivy-image

Configures and runs Trivy for container image scanning: Aqua Security's all-in-one scanner combining vuln + secret + misconfiguration + license detection in one pass; `trivy image {image}` with --severity HIGH,CRITICAL filter; --format sarif/json (incl. scan-embedded CycloneDX; for standalone SBOM generation see syft-generation + sbom-formats); .trivyignore CVE suppression file; --ignore-unfixed for actionable filter; --scanners vuln/misconfig/license/secret toggle. Use when the team wants a single tool covering container image security across multiple dimensions, not for producing a standalone CycloneDX SBOM.

trufflehog-scanning

Configures and runs TruffleHog v3 - secret scanner with **live verification** (validates discovered secrets against provider APIs to confirm actual exposure vs entropy false positive); supports per-source subcommands (`git`, `github`, `gitlab`, `filesystem`, `s3`, `docker`, `gcs`, `postman`); `--results=verified` filter for high-precision output; `--exclude-detectors=TYPE` for noise reduction; exits 183 on findings via `--fail`. Use when the team needs verified secret findings (low false-positive rate) or scans across cloud + repo + container surfaces.

vex-author

Authors and validates OpenVEX documents - produces `not_affected`, `affected`, `fixed`, and `under_investigation` statements with justification codes using `vexctl create`; attaches VEX assertions to container images; outputs `.openvex.json` files consumed on a downstream VEX-filter / vulnerability-prioritization path. Use when a scanner flags a CVE that analysis confirms is not exploitable in your deployment, and a machine-readable `not_affected` assertion is needed to suppress false positives without discarding the finding from the audit trail.