# Adding OWASP ZAP (DAST) to the DevSecOps pipeline — offline guide

This bundle adds **OWASP ZAP** as the *Dynamic Application Security Testing
(DAST)* stage of your pipeline, for an **air-gapped RHEL 9.6 / x86_64**
environment. It fits the existing toolchain:

```
Jira ─ Gitea ─ Drone ─ SonarQube (SAST) ─ deploy staging + openQA ─ ZAP (DAST) ─ Jira
       source   CI/CD    static analysis        system tests        dynamic scan   findings
```

ZAP is the piece labelled **"OWASP ZAP — Dynamic application security testing"**
in the target architecture: it drives a running instance of the application over
HTTP(S) and reports SQL injection, XSS, missing security headers, auth/session
issues, etc. It runs **after** the app is deployed to staging (SonarQube and
openQA come first), and its findings feed back into **Jira**.

Everything here works with **no internet access**: the ZAP container image, the
full add-on catalogue, and a JRE are pre-downloaded and checksum-verified, and
every ZAP invocation disables add-on auto-update and call-home.

---

## What is in this bundle

| Path | Purpose |
|---|---|
| `config/versions.env` | Pinned ZAP version, image digests, checksums. Single source of truth. |
| `config/zap-baseline.conf` | Rule policy: which alerts WARN vs FAIL vs IGNORE (the gate). |
| `scripts/00-download.sh` | **Connected builder**: download + verify image, add-ons, JRE. |
| `scripts/99-package.sh` | **Connected builder**: produce one transferable tarball + `SHA256SUMS`. |
| `scripts/10-load.sh` | **Air-gapped host**: verify bundle, load image into docker/podman. |
| `scripts/20-verify-offline.sh` | **Air-gapped host**: prove a real ZAP scan runs fully offline. |
| `scripts/zap-scan.sh` | Reusable wrapper: `baseline` / `full` / `api` scan → reports. |
| `drone/.drone.zap.yml` | Drone Docker-pipeline DAST stage to merge into your release pipeline. |
| `demo/app.py` | A deliberately-vulnerable app used only to exercise the gate. |
| `images/`, `downloads/`, `downloads/addons/` | The offline payload. |
| `metadata/` | Provenance: digests, catalogue, verification logs. |

This mirrors the online/offline `scripts/` split used by the rest of the
`/work/acb` DevSecOps work: `00`/`99` run on the connected builder, `10`/`20` on
the target.

---

## 1. On the connected builder (has internet)

```bash
cd devsecops-zap
scripts/00-download.sh      # ~3 GB: ZAP image + 117 add-ons + JRE, all verified
scripts/99-package.sh       # -> ../devsecops-zap-offline-2.17.0-linux-amd64.tar.gz
```

`00-download.sh` refuses to continue if the upstream image digest no longer
matches `config/versions.env`, and rejects any file whose checksum is wrong.
Re-running it re-downloads only what is missing or changed.

Transfer the resulting `devsecops-zap-offline-*.tar.gz` **and** its `.sha256` to
the air-gapped environment by your approved media path.

---

## 2. On the air-gapped RHEL 9.6 host

```bash
sha256sum -c devsecops-zap-offline-2.17.0-linux-amd64.tar.gz.sha256
tar xzf devsecops-zap-offline-2.17.0-linux-amd64.tar.gz
cd devsecops-zap

scripts/10-load.sh docker       # on a Drone Docker-runner host
#   or: scripts/10-load.sh podman   # for ad-hoc/manual scans

scripts/20-verify-offline.sh docker
```

`10-load.sh` re-verifies `SHA256SUMS`, loads the image, and runs
`zap.sh -version` with `--network=none` to prove it starts with no network.

`20-verify-offline.sh` is the acceptance test: it starts the vulnerable demo app
on an **`--internal` (no-gateway) container network**, runs a real
`zap-baseline.py` scan against it, and asserts ZAP produced a report with the
expected findings and the correct gate exit code — all offline. Its reports land
in `evidence/`.

> **Docker vs Podman stores are separate.** Load the image into whichever runtime
> will run the scan. On a Drone Docker runner, that is Docker.

### Optional: publish to your internal registry

If you have several runners, push once to your internal registry instead of
loading a tar on each:

```bash
skopeo copy --dest-tls-verify=true \
  docker-archive:images/zaproxy-2.17.0-amd64.tar \
  docker://registry.internal.example/devsecops/zap:2.17.0
```

Then set `ZAP_IMAGE_LOCAL` in `config/versions.env` (and the image line in
`drone/.drone.zap.yml`) to that registry path.

---

## 3. Wire it into Drone

`drone/.drone.zap.yml` is a runnable Docker pipeline containing the `zap-baseline`
step. In your real release pipeline, ZAP goes **after** deploy-to-staging and
openQA and **before** promotion:

```
source checks → build + SonarQube gate → image scan → deploy staging + openQA
            → ZAP DAST (this bundle) → sign → promote → evidence
```

Two ways to give ZAP a target:

- **A — existing staging URL:** drop the `services:` block and point `-t` at your
  deployed staging URL (e.g. `https://staging.example.internal`). Ensure the
  runner network can reach staging and *nothing else outbound is needed*.
- **B — sidecar service (shown in the file):** bring the app up as a Drone
  `service` and scan it by service name on the pipeline's private network.

The step fails the build on any nonzero ZAP exit; `publish-zap-reports` always
runs so findings are preserved even on failure. Replace its `echo` with your
evidence upload and Jira issue creation.

If your Drone uses **exec** runners rather than Docker, call the wrapper directly
from the pipeline instead of the `image:` steps:

```yaml
- name: zap-dast
  commands:
    - /opt/devsecops-zap/scripts/zap-scan.sh baseline https://staging.example.internal $DRONE_WORKSPACE/zap-reports
```

---

## 4. Running scans manually

```bash
# passive, non-intrusive — the CI default
scripts/zap-scan.sh baseline https://staging.example.internal ./reports

# active scan — INTRUSIVE, authorized disposable targets only
scripts/zap-scan.sh full https://staging.example.internal ./reports

# API scan from an OpenAPI/SOAP/GraphQL definition
scripts/zap-scan.sh api https://staging.example.internal/openapi.json ./reports
```

Set `ZAP_NETWORK=host` (Podman) if the target is on the host loopback.

### Scan modes

| Mode | ZAP script | What it does | Intrusive? |
|---|---|---|---|
| `baseline` | `zap-baseline.py` | Spider + passive scan (headers, cookies, info leaks, cached JS libs) | No — safe for shared staging |
| `full` | `zap-full-scan.py` | Baseline + AJAX spider + **active** attacks (injection, XSS…) | **Yes** — only on authorized, disposable targets |
| `api` | `zap-api-scan.py` | Import an API schema, then active-scan the described endpoints | **Yes** |

Start with `baseline` in CI. Move to `full`/`api` against a dedicated,
throwaway, authorized test environment once you have contexts, auth and rule
policy defined.

---

## 5. Tuning the gate (Jira feedback loop)

`config/zap-baseline.conf` maps each ZAP rule ID to `IGNORE`, `WARN`, or `FAIL`.
Only `FAIL` blocks the pipeline; `WARN` is reported but non-blocking.

Recommended rollout:

1. First runs: keep new/noisy rules at `WARN`, keep injection/XSS at `FAIL`.
2. Fix findings or record justified exceptions, then promote rules to `FAIL`.
3. For each `FAIL` alert, open a Jira issue (title = alert name + URL, attach the
   HTML report) — this is the "Security findings & remediation → Jira" loop in
   the architecture.
4. Only `IGNORE` a rule with a written justification in the comment column.

Never neutralise the gate with `|| true`. Exit codes: `0` clean, `1` a FAIL rule
tripped, `2` only WARN, `3` ZAP error.

---

## 6. What ZAP does and does not cover

- ZAP is **DAST** — it tests a *running* web app/API. It complements, and does
  not replace, SonarQube (SAST) or your container/dependency scanners.
- **Baseline** is passive crawling; it is not a complete active test. Real
  coverage needs authenticated contexts and active/API scans against a
  disposable environment.
- ZAP targets HTTP(S) applications. Native/embedded/CLI software is out of scope
  for ZAP; keep using the other DevSecOps scanners for those.
- Only scan systems you are **authorized** to test. Active scans send attack
  traffic; point them at staging/test, never production.

---

## 7. Maintenance / refresh

Vulnerability knowledge in ZAP lives in its **add-ons** (passive/active scan
rules). To refresh:

1. On the connected builder, bump `ZAP_VERSION` / digests in
   `config/versions.env` to an approved upstream release.
2. Re-run `scripts/00-download.sh` and `scripts/99-package.sh`.
3. Transfer, then re-run `scripts/10-load.sh` and `scripts/20-verify-offline.sh`.

Because the offline worker never contacts the ZAP marketplace, add-on freshness
is entirely a function of how often you repeat this refresh. Treat stale scan
rules the same way the rest of the stack treats stale vulnerability databases.

To add an add-on the base image lacks (e.g. `openapi`, `soap`, `graphql`,
`spiderAjax` for AJAX apps), copy the verified `.zap` file from
`downloads/addons/` into the container's `/zap/plugin/` at build/run time, or
bake a thin image `FROM localhost/devsecops/zap:2.17.0` that `COPY`s them in.

---

## Provenance

Pinned in `config/versions.env`; recorded in `metadata/download-provenance.txt`:

- Image: `ghcr.io/zaproxy/zaproxy` (ZAP 2.17.0), amd64 manifest digest
  `sha256:71db37cd5b75663b35758d10aaec05bf6fbac23f5020e3046c70e628a5f84efa`.
- 117 add-ons for the 2.17 line, each verified against the upstream
  `ZapVersions-2.17.xml` catalogue (see `metadata/addons.tsv`).
- Eclipse Temurin JRE 21.0.12.1+1 (native-install fallback only).
