#!/usr/bin/env bash
# zap-scan.sh — offline OWASP ZAP DAST wrapper for the DevSecOps pipeline.
#
# Runs a ZAP scan against an already-deployed, AUTHORIZED staging URL and writes
# HTML/JSON/XML/Markdown reports to a directory. Designed to run air-gapped:
# add-on auto-update is disabled and the container/network is constrained.
#
# Usage:
#   zap-scan.sh <mode> <target-url> <report-dir> [runtime]
#     mode        baseline | full | api
#     target-url  http(s) URL of your staging app  (api mode: URL of the
#                 OpenAPI/SOAP/GraphQL definition, reachable or a mounted file)
#     report-dir  host directory that receives the reports
#     runtime     docker | podman   (default: autodetect, docker preferred)
#
#   baseline  spider + passive scan          (safe, non-intrusive; CI default)
#   full      spider + AJAX spider + ACTIVE   (intrusive — authorized targets only)
#   api       import an API definition + active scan of the described endpoints
#
# Exit codes mirror ZAP: 0 clean, 1 at least one FAIL rule, 2 at least one WARN,
# 3 ZAP error. The pipeline treats >0 as a gate failure (see config/*.conf to
# tune rule actions). Never append '|| true' to a release gate.
set -euo pipefail

MODE="${1:?mode required: baseline|full|api}"
TARGET="${2:?target url required}"
REPORT_DIR="${3:?report directory required}"
RUNTIME="${4:-}"

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
source "$ROOT/config/versions.env"

if [[ -z "$RUNTIME" ]]; then
  if command -v docker >/dev/null; then RUNTIME=docker; else RUNTIME=podman; fi
fi

case "$MODE" in
  baseline) ZAP_SCRIPT=zap-baseline.py ;;
  full)     ZAP_SCRIPT=zap-full-scan.py ;;
  api)      ZAP_SCRIPT=zap-api-scan.py ;;
  *) echo "unknown mode: $MODE" >&2; exit 3 ;;
esac

mkdir -p "$REPORT_DIR"
REPORT_DIR="$(cd "$REPORT_DIR" && pwd)"
stamp="$(date -u +%Y%m%dT%H%M%SZ)"

# Rule-action policy: which passive/active rules WARN vs FAIL vs IGNORE.
# Tune config/zap-baseline.conf rather than editing this script.
conf_arg=()
conf_file="$ROOT/config/zap-${MODE}.conf"
[[ -f "$conf_file" ]] || conf_file="$ROOT/config/zap-baseline.conf"
if [[ -f "$conf_file" ]]; then
  cp "$conf_file" "$REPORT_DIR/rules.conf"
  conf_arg=(-c rules.conf)
fi

# Force fully-offline behaviour: no add-on update, no telemetry / call-home.
OFFLINE_OPTS='-silent -config autoupdate.checkOnStart=false -config autoupdate.checkAddonUpdates=false -config autoupdate.downloadNewRelease=false -config telemetry.enabled=false'

# Rootless Podman needs the reports dir writable by the in-container zap user.
userns=()
if [[ "$RUNTIME" == podman ]]; then
  userns=(--userns=keep-id --user "$(id -u):$(id -g)")
fi

# --network here is the container's ONLY reachability. Baseline/full need to reach
# the staging target; they do NOT need general internet. On an isolated CI network
# the default bridge is appropriate. If staging is on the host loopback, add
# --network=host (Podman) or run the app in the same compose network.
NET="${ZAP_NETWORK:-}"          # e.g. export ZAP_NETWORK=host  or a named network
net_arg=(); [[ -n "$NET" ]] && net_arg=(--network "$NET")

status=0
set -x
"$RUNTIME" run --rm --pull=never "${net_arg[@]}" "${userns[@]}" \
  -v "$REPORT_DIR:/zap/wrk:Z" \
  "$ZAP_IMAGE_LOCAL" \
  "$ZAP_SCRIPT" -t "$TARGET" \
    -r "zap-${MODE}-${stamp}.html" \
    -J "zap-${MODE}-${stamp}.json" \
    -x "zap-${MODE}-${stamp}.xml" \
    -w "zap-${MODE}-${stamp}.md" \
    "${conf_arg[@]}" \
    -z "$OFFLINE_OPTS" || status=$?
set +x

echo "ZAP $MODE finished with exit=$status; reports in $REPORT_DIR"
exit "$status"
