# OWASP ZAP Cheat Sheet

> **Tool:** OWASP ZAP (Zed Attack Proxy)
> **Category:** Security & Compliance
> **Verified against:** ZAP 2.17.0 (`zaproxy/zap-stable` Docker image), automation-script `--help` output run
> locally via `docker run --rm zaproxy/zap-stable zap-baseline.py --help` etc.; Automation Framework YAML
> structure cross-checked against zaproxy.org/docs/desktop/addons/automation-framework/, 2026-08-29
> **Official docs:** https://www.zaproxy.org/docs/docker/

## 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

```mermaid
sequenceDiagram
    participant CI as CI Job
    participant ZAP as ZAP (headless)
    participant App as Target App (staging)

    CI->>ZAP: zap-baseline.py -t https://staging...
    ZAP->>App: Spider — crawl every reachable page/link
    App-->>ZAP: HTML, links, forms discovered
    ZAP->>ZAP: Passive scan — analyze traffic already captured, no new requests
    opt Full scan only
        ZAP->>App: Active scan — send attack payloads (SQLi, XSS, ...)
        App-->>ZAP: Responses analyzed for vulnerable behavior
    end
    ZAP-->>CI: Report (HTML/JSON/XML) + exit code
```

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

```bash
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 version
```

A 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

```bash
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

```bash
docker run --rm -t zaproxy/zap-stable zap-full-scan.py \
  -t https://staging.example.com \
  -a                                   # include alpha-quality active + passive rules too
```

**Never 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

```bash
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.graphqls
```

`zap-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)

```bash
-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 through
```

## CI gating with a rules config

```bash
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:

```yaml
# 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.html
```

```bash
docker run --rm -v $(pwd):/zap/wrk -t zaproxy/zap-stable zap.sh -cmd -autorun /zap/wrk/zap-plan.yaml
```

Jobs 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

```yaml
# .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.
