Reproducible DAST scans with a deterministic CLI and a hardened OWASP ZAP image.
zaprun is a small Rust CLI that drives OWASP ZAP through Automation Framework plans, plus a Wolfi-based ZAP container image that bakes the CLI in. It targets a few hard requirements:
- Digest-pinned scans. The CLI's
--imageflag refuses non-digest references, and the image's Dockerfile checksum-pins its ZAP release tarball + every helper script + the bundled add-ons. - Stable artifact contract. Every run writes the same files (
plan.yaml,run.json,summary.json,coverage.json,capabilities.json,observations.json, plus ZAP's JSON/HTML/SARIF reports) so CI gates and humans can reason about results the same way. - No reliance on live add-on installs at scan time. The image bundles the add-ons it needs at build time, so a scan can run on a sealed network.
- Reasonable defaults for CI. The image uses a non-root UID, no extra capabilities, and a literal-string-equality entrypoint dispatch that does not eval its arguments.
Three options, depending on your host:
# 1. CLI from crates.io (cross-platform: Linux / macOS / Windows).
cargo install zaprun
# 2. Prebuilt container image (linux/amd64; runs via emulation on macOS arm64).
docker pull ghcr.io/kerberosmansour/zaprun:v0.3.1
# 3. From source.
git clone https://github.com/kerberosmansour/zaprun
cargo build --release -p zaprunAt runtime the CLI drives Docker, so a working Docker daemon is required on the host regardless of how the CLI was installed.
# Pull + run the image (no Rust toolchain needed; pin by digest):
docker run --rm \
-v "$PWD/output:/zap/wrk/output" \
ghcr.io/kerberosmansour/zaprun@sha256:<digest> \
zaprun scan http://host.docker.internal:4000 --active --profile spa-pr
# Or use the cargo-installed binary against a digest-pinned image:
zaprun scan http://host.docker.internal:4000 --active --profile spa-pr
# Browser-backed OWASP PTK Phase 1 lane:
zaprun ptk http://host.docker.internal:4000 --output output/zaprun-ptkThe image's entrypoint dispatches on the first argument: zaprun hands off to the baked-in CLI; anything else falls through to a compatibility scan harness that accepts --target / --output-dir / --policy flags for existing ZAP jobs.
Full subcommand reference, exit codes, the artifact-contract schemas, end-to-end examples, and a per-platform support matrix are in the CLI manual (also published as the crate's landing page on crates.io/crates/zaprun).
zaprun init bootstraps a consumer repository with target-owned DAST config:
# From this repo while iterating locally:
cargo run -p zaprun -- init --target-dir /path/to/webapp \
--deployment-target https://staging.example.test
# Later, re-check drift after threat-model or image-pin changes:
cargo run -p zaprun -- rederive --target-dir /path/to/webappThe emitted workflow calls the latest approved digest-pinned zaprun image with first arg zaprun:
docker run --rm --user 1000:1000 \
--add-host=host.docker.internal:host-gateway \
-v "$PWD:/work:ro" \
-v "$PWD/output:/zap/wrk/output:rw" \
ghcr.io/kerberosmansour/zaprun@sha256:<digest> \
zaprun scan https://staging.example.test --active --profile web-pr --output /zap/wrk/outputWhen openapi.yaml, openapi.yml, or openapi.json exists at the target repo root, init emits zaprun api /work/<spec> --target <url> --active instead. The generated workflow preserves the safety contract: SHA-pinned actions, no pull_request_target, top-level permissions: {}, no secrets, no zaproxy/action-*, and no legacy zap-api-scan.py / zap-baseline.py / zap-full-scan.py helpers.
Every published image digest is signed (cosign keyless via Sigstore Fulcio + Rekor) and carries three attestations — SLSA Build Provenance v1, an SPDX-JSON SBOM, and a CycloneDX-JSON SBOM. The signing happens in an isolated reusable workflow that holds id-token: write (the build job does not), per SLSA Build L3 guidance.
# Pull the exact digest you'll run.
docker pull ghcr.io/kerberosmansour/zaprun@sha256:<digest>
# Verify SLSA Build Provenance + SBOMs.
gh attestation verify \
oci://ghcr.io/kerberosmansour/zaprun@sha256:<digest> \
--repo kerberosmansour/zaprun
# Verify the cosign keyless signature.
cosign verify \
--certificate-identity-regexp '^https://github.com/kerberosmansour/zaprun/' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
ghcr.io/kerberosmansour/zaprun@sha256:<digest>| Tag | Source | Stability |
|---|---|---|
@sha256:<64-hex> |
every push to main; every release | immutable — pin here in production |
:<full-git-sha> |
every push to main | immutable |
:edge |
every push to main | floating — re-points to the most recent main commit |
:v0.3.1 |
release tag | convenience alias (do not use in consumer runs; pin @sha256 instead) |
:v0.3, :v0 |
release tag (skipped for pre-releases) | floating — re-points to the latest patch / minor |
:latest |
NEVER PUBLISHED | n/a |
SECURITY.md has the full tagging-convention rationale and verification snippets.
| Path | Purpose |
|---|---|
crates/zaprun/ |
The public, self-contained CLI. Subcommands: scan, api, ptk, doctor, plan, observe, calibrate, init, rederive, triage-sarif, and explain (see docs/zaprun-cli.md). |
crates/zaprun/src/tuner/ |
The schema, SARIF, baseline, path-safety, and CWE-rule mapping code used by zaprun init, rederive, and triage-sarif. zaprun does not depend on dast-spike. |
crates/dast-spike/ |
Legacy scanner-runner experiments kept in the workspace for compatibility tests; not a dependency of zaprun. |
docker/zap/Dockerfile |
The hardened image. Wolfi base, ZAP from official tarball with SHA-256 pin, add-ons bundled at build time, Trivy-scanned in CI. |
xtasks/dast-verify/ |
Standalone rule-promotion gate for generic DAST rules and target-owned custom rules. |
.github/workflows/build-zap-image.yml |
Build + scan + push (and tag :edge on main). |
.github/workflows/sign-and-attest.yml |
Reusable workflow that signs the image and attests the build provenance + both SBOMs. Holds id-token: write (the build job does not — SLSA L3 isolation). |
.github/workflows/release.yml |
Triggered on tag push (v*). Adds semver tags via crane tag so the digest (and therefore signatures + attestations) is preserved. |
.github/workflows/scheduled-image-rebuild.yml |
Weekly (Mondays 06:00 UTC). Rebuilds + re-scans the image so newly-disclosed CVEs in the bundled deps surface promptly. Also audits .trivyignore entries against their tracking-issue age. |
.github/workflows/ci.yml |
cargo fmt --check, clippy -D warnings, full workspace tests on every push and PR. |
.github/renovate.json |
Renovate config — Cargo workspace, SHA-pinned GHA actions, Dockerfile, with grouping for tokio / serde / sigstore / SunLit-security crates. |
templates/dast-workflow.yml |
Workflow skeleton emitted into a target repo by zaprun init. |
references/*.toml |
Pinned digests for upstream artefacts (Wolfi base / ZAP image / Nuclei templates / GHA action SHAs). |
schema/ |
JSON schemas for the artifact contract. |
.trivyignore |
Explicit CVE suppressions with rationale + tracking-issue references; reviewed weekly by scheduled-image-rebuild.yml. |
/slo-dast-tuner stays thin and drives the public zaprun commands:
zaprun initwrites target-owned.zaprun/config and a digest-pinned workflow.zaprun rederivechecks threat-model/image drift and rewrites at most one reviewable diff.zaprun triage-sarifreads SARIF 2.1.0, route/OpenAPI context, and.zaprun/manifest.json, then writestriage-report.json,guided-scan-map.json, andfiltered.sarif.zaprun observereplays a concrete raw HTTP request and writesobservations.jsonwith response evidence, while preserving the SSRF/IMDS guard.xtasks/dast-verifygates candidate JavaScript DAST rules. Generic candidates must pass metadata, safety lint, vulnerable/patched synthetic fixtures, and anti-overfitting checks. App-specific rules can be accepted only as target-owned output under the target repo, such as.zaprun/scripts/.
SARIF is evidence, not authority: a result becomes dast-detectable only when there is route/request evidence. Authenticated findings remain needs-human-input until auth configuration and logged-in reachability are explicit.
v0.3.1 is a patch release for the zaprun observe SSRF guard. It retains the v0.3.0 PTK Phase 1 lane that uses ZAP's Client Spider and the image-baked Client Side Integration + PTK add-ons. The legacy workspace crates dast-spike and dast-spike-rules are not publishable crates; release the public CLI with:
cargo publish -p zaprunDo not use cargo publish --workspace for this repo.
The image release flow is:
- Merge this PR to
main;.github/workflows/build-zap-image.ymlbuilds, scans, signs, attests, and pushesghcr.io/kerberosmansour/zaprun:<git-sha>. - Tag the merged commit with
v0.3.1and push the tag;.github/workflows/release.ymlretags the existing signed digest as:v0.3.1,:v0.3, and:v0without rebuilding. - Use the release workflow's digest output for downstream pin bumps and verification.
The previous v0.3.0 image and crate are signed + attested and public; v0.3.1 publishes the observe SSRF guard patch for the CLI and hardened image.
The repo follows v<major>.<minor>.<patch> tagging via release.yml. There is no :latest image tag — consumers are expected to pin by digest.
- ARCHITECTURE.md — components, data flow, and trust boundaries.
- CHANGELOG.md — release notes.
- SECURITY.md — supply-chain controls and vulnerability disclosure.
docs/zaprun-cli.md— full CLI manual.
MIT — see LICENSE.