OWASP ZAP
.mdVerified against ZAP 2.17.0 (`zaproxy/zap-stable` Docker image), automation-script `--help` output run · official docs
What it is and where it fits#
ZAP is the most widely used open-source DAST (Dynamic Application Security Testing) tool — instead of reading source code like Semgrep/SonarQube (SAST), it attacks a running application the way a real attacker would: spidering it to find pages, then passively (and optionally actively) probing for vulnerabilities like reflected XSS, SQL injection, and missing security headers. This is the stage-gate difference that matters: SAST/SCA/IaC scanning all run against source or artifacts before deployment; DAST is the one class of scanning that requires the app to actually be up and reachable, which is why it typically runs against a staging environment late in a pipeline, not on every commit.
How a ZAP scan actually works#
Baseline scan stops after the passive step — it never sends attack payloads, which is why it's safe to point at a production-adjacent target. Full scan continues into the active step, which genuinely attacks the target and must only ever run against a target you're authorized to attack.
Installation#
docker pull zaproxy/zap-stable # official, CI-oriented — this is genuinely the recommended path, not a fallback
docker pull zaproxy/zap-weekly # latest add-ons, less stable
docker run --rm zaproxy/zap-stable zap.sh -version # inside the container, confirms the packaged ZAP versionA desktop-GUI installer also exists (ZAP_<version>_Linux.sh etc. on the GitHub releases page) for interactive
use, but every CI/automation workflow — and this cheat sheet — uses the Docker image and its bundled scripts.
There is no separate standalone zap CLI binary for headless use; the automation surface is the set of Python
scripts (zap-baseline.py, zap-full-scan.py, zap-api-scan.py) shipped inside the image, or a hand-written
Automation Framework YAML plan run via zap.sh -cmd -autorun plan.yaml.
Baseline scan — passive only, safe against production#
docker run --rm -t zaproxy/zap-stable zap-baseline.py \
-t https://staging.example.com \
-m 5 # spider for 5 minutes before the passive scan (default: 1)Baseline never sends attack payloads — it spiders the app and passively analyzes traffic already captured. Safe to point at a production URL from a pure ZAP-behavior standpoint; still get authorization first and respect the target's terms of use.
Sample output shape (captured from a real zap-baseline.py run structure — the exact WARN/FAIL counts and
rule IDs will differ by target and ZAP version):
WARN-NEW: X-Content-Type-Options Header Missing [10021] x 4
https://staging.example.com/
https://staging.example.com/login
WARN-NEW: Content Security Policy (CSP) Header Not Set [10038] x 6
FAIL-NEW: 0 FAIL-EXIST: 0 WARN-NEW: 2 WARN-EXIST: 0 INFO: 0 IGNORE: 0 PASS: 42
Full scan — active, will send attack payloads#
docker run --rm -t zaproxy/zap-stable zap-full-scan.py \
-t https://staging.example.com \
-a # include alpha-quality active + passive rules tooNever point zap-full-scan.py at a production target — it actively attacks the app (SQLi, XSS payloads,
etc.) and can trigger real side effects (form submissions, account creation, even data mutation on a poorly
built app). Staging/test environments only, with explicit authorization.
API scan — for OpenAPI/SOAP/GraphQL-defined services#
docker run --rm -t zaproxy/zap-stable zap-api-scan.py \
-t https://staging.example.com/openapi.json -f openapi
docker run --rm -t zaproxy/zap-stable zap-api-scan.py \
-t https://staging.example.com/graphql -f graphql \
--schema https://staging.example.com/schema.graphqlszap-api-scan.py imports the API definition first (so it knows every endpoint/parameter, not just what a
spider happens to discover by following links) and then attacks each operation — the right tool when the
target is a pure API rather than a browsable web app.
Common flags (all three scripts)#
-r report.html # write full HTML report — the one to attach to a build artifact or share with a team
-J report.json # write full JSON report — the one to parse programmatically
-x report.xml # write full XML report
-l WARN # minimum level to show: PASS, IGNORE, INFO, WARN, FAIL
-c rules.conf # config file to override individual rule severities (INFO/IGNORE/FAIL per rule ID)
-I # do not fail the run on WARN-level findings — only FAIL-level
-s # short output — hide PASSes and example URLs
-z "-config aaa=bbb" # pass raw ZAP command-line options straight throughCI gating with a rules config#
docker run --rm -t zaproxy/zap-stable zap-baseline.py \
-t https://staging.example.com \
-c /zap/wrk/zap-rules.conf -I# zap-rules.conf — one rule ID per line, controls whether it's INFO / IGNORE / FAIL
10021 IGNORE (missing X-Content-Type-Options — accepted risk for this app)
40012 FAIL (reflected XSS — always block the build)
-c is the mechanism for tuning noisy rules to IGNORE and specific ones you actually care about to FAIL —
without it, baseline's default behavior returns exit code 1 (soft "WARN") for most findings rather than a hard
CI failure, and -I further suppresses even that unless you've explicitly escalated a rule to FAIL in the
config.
The Automation Framework — for authenticated scans#
Most real applications require login before there's anything interesting to scan. The recommended path is: configure and test authentication in the ZAP desktop GUI against a context, then export that context into an Automation Framework YAML plan and run it headlessly:
# zap-plan.yaml
env:
contexts:
- name: my-app
urls: ["https://staging.example.com"]
authentication:
method: script
parameters:
script: /zap/scripts/auth.js
scriptEngine: "Oracle Nashorn"
users:
- name: test-user
credentials:
username: tester@example.com
password: ${TEST_USER_PASSWORD}
jobs:
- type: spider
parameters:
context: my-app
user: test-user
- type: activeScan
parameters:
context: my-app
- type: report
parameters:
template: traditional-html
reportFile: zap-report.htmldocker run --rm -v $(pwd):/zap/wrk -t zaproxy/zap-stable zap.sh -cmd -autorun /zap/wrk/zap-plan.yamlJobs run in the order they appear in the plan. This is the only reliable way to scan anything behind a login
form — zap-baseline.py -U user exists as a shortcut but requires the same underlying context/authentication
setup to already be defined in a context file.
Real-world scenario: staging-gated GitHub Actions pipeline#
# .github/workflows/dast.yml
name: DAST (ZAP baseline)
on:
deployment_status:
jobs:
zap-scan:
if: github.event.deployment_status.state == 'success'
runs-on: ubuntu-latest
steps:
- uses: zaproxy/action-baseline@v0.14.0
with:
target: ${{ github.event.deployment_status.target_url }}
rules_file_name: '.zap/rules.conf'
cmd_options: '-I'Running on deployment_status (rather than pull_request) ties the scan to an actual live staging
deployment finishing — DAST is meaningless without a running target, so it belongs after deploy, not before.
Common pitfalls#
- Running full scan against production — the single most damaging mistake possible with this tool.
- Skipping authentication setup and concluding "ZAP found nothing" — an unauthenticated scan of an app that requires login only ever sees the login page; almost the entire real attack surface goes unscanned.
- Not tuning
-c rules.conf— baseline's defaults are noisy (a lot of WARN-level header-hygiene findings); teams that skip tuning tend to either ignore ZAP entirely or disable it, rather than actually triaging once. - Treating baseline as a substitute for full/active scanning — baseline is a fast smoke test, not a thorough assessment; it deliberately never fires attack payloads.
Exit codes#
0 no FAIL-level findings (or -I suppressing WARN-as-FAIL) · non-zero when FAIL-level findings exist,
per the -l/rules-config severity mapping described above.