#!/usr/bin/env bash
# Apply the Coraza WAF mode declared in /docker/whp/settings.json.
#
# Reads `coraza_waf.mode`, expected to be one of:
#   off          — sidecar stopped/removed; haproxy-manager recreated without
#                  HAPROXY_CORAZA_SPOE_BACKEND env (no SPOE in haproxy.cfg)
#   detect_only  — sidecar running; local-overrides.conf written by the rule
#                  manager (regenerate-coraza-overrides.sh) contains a blanket
#                  SecRuleEngine DetectionOnly directive plus any rule-state
#                  overrides from the DB. All rules log; nothing blocks (the
#                  DetectionOnly directive wins over baked-in enforce
#                  promotions).
#   enforce      — sidecar running; local-overrides.conf written by the rule
#                  manager contains rule-state overrides from the DB (empty
#                  if none configured). Baked-in overrides.conf provides the
#                  day-one enforce list (930120 LFI, 932100-932160 RCE,
#                  933170-933200 webshells, 944100-944300 Log4Shell, 920440
#                  backup files, 930130 .env disclosure).
#
# Always recreates haproxy-manager at the end so its template renders the
# SPOE blocks (or strips them) per the mode. Brief HAProxy outage (~3-5s);
# acceptable for an admin-triggered config change.
#
# Idempotent: safe to re-run for the same mode; skips no-op work.
# Invoked from:
#   - container-manager.sh `recreate coraza-spoa`
#   - the WHP UI's update_coraza_waf save handler
#   - the versioned installer at first deploy (to materialize the default)

set -euo pipefail

SETTINGS=/docker/whp/settings.json
CORAZA_DIR=/docker/coraza
HOST_AUDIT_DIR=/var/log/coraza
CORAZA_IMAGE=repo.anhonesthost.net/cloud-hosting-platform/coraza-spoa:latest
CONTAINER_MGR=/root/whp/scripts/container-manager.sh

if ! command -v jq >/dev/null 2>&1; then
    echo "ERROR: jq is required to parse settings.json" >&2
    exit 1
fi

if [[ ! -f "$SETTINGS" ]]; then
    echo "ERROR: $SETTINGS missing; cannot determine WAF mode" >&2
    exit 1
fi

MODE=$(jq -r '.coraza_waf.mode // "off"' "$SETTINGS")

case "$MODE" in
    off|detect_only|enforce) ;;
    *)
        echo "ERROR: invalid coraza_waf.mode '$MODE' (expected: off|detect_only|enforce)" >&2
        exit 1
        ;;
esac

echo "applying coraza_waf.mode=$MODE"

# ---- ensure host audit-log dir exists with the right ownership -----------
# distroless coraza-spoa runs as UID 65532; the host dir must be writable
# by that UID. Bind-mounted into the sidecar at /var/log/coraza.
mkdir -p "$HOST_AUDIT_DIR"
if [[ "$(stat -c %u "$HOST_AUDIT_DIR")" != "65532" ]]; then
    chown 65532:65532 "$HOST_AUDIT_DIR"
fi

mkdir -p "$CORAZA_DIR"

# Materialize host paths used by always-on bind-mounts. Files must exist
# (even empty) before docker mounts them, or Docker creates them as
# directories — which would then make the SecRule Include fail at parse.
# First run initializes local-overrides.conf as empty; subsequent runs
# preserve it (the WHP rule manager owns content).
mkdir -p "$CORAZA_DIR/host-exceptions"
if [[ ! -f "$CORAZA_DIR/local-overrides.conf" ]]; then
    cat > "$CORAZA_DIR/local-overrides.conf" <<'EOF'
# AUTOGENERATED by WHP — do not hand-edit.
# Empty file = no runtime overrides active.
EOF
fi
chown -R 65532:65532 "$CORAZA_DIR/host-exceptions" "$CORAZA_DIR/local-overrides.conf"

if [[ ! -f "$CORAZA_DIR/pre-overrides.conf" ]]; then
    cat > "$CORAZA_DIR/pre-overrides.conf" <<'EOF'
# AUTOGENERATED by WHP — do not hand-edit.
# Empty file = no per-host exemptions active.
EOF
fi
chown 65532:65532 "$CORAZA_DIR/pre-overrides.conf"

# ---- sidecar lifecycle ---------------------------------------------------
sidecar_running() {
    docker ps --filter name=coraza-spoa --format '{{.Names}}' | grep -q '^coraza-spoa$'
}
sidecar_exists() {
    docker ps -a --filter name=coraza-spoa --format '{{.Names}}' | grep -q '^coraza-spoa$'
}
stop_remove_sidecar() {
    if sidecar_exists; then
        docker stop coraza-spoa >/dev/null 2>&1 || true
        docker rm coraza-spoa >/dev/null 2>&1 || true
    fi
}
start_sidecar() {
    # Always bind-mount the runtime-managed overrides + host-exceptions dir,
    # regardless of mode. Empty file = no overrides active (baked-in
    # overrides.conf still applies). The detect_only-specific bind-mount on
    # /etc/coraza/overrides.conf is REMOVED — detect_only is now expressed
    # via local-overrides.conf writing 'SecRuleEngine DetectionOnly' (see the
    # detect_only case below).
    docker pull "$CORAZA_IMAGE" >/dev/null
    docker run -d \
        --name coraza-spoa \
        --network client-net \
        --restart unless-stopped \
        --label whp.tier=edge \
        --memory=512m --memory-swap=1g \
        -v "$HOST_AUDIT_DIR:/var/log/coraza" \
        -v "$CORAZA_DIR/pre-overrides.conf:/etc/coraza/pre-overrides.conf:ro" \
        -v "$CORAZA_DIR/local-overrides.conf:/etc/coraza/local-overrides.conf:ro" \
        -v "$CORAZA_DIR/host-exceptions:/etc/coraza/host-exceptions:ro" \
        "$CORAZA_IMAGE" >/dev/null
}

case "$MODE" in
    off)
        if sidecar_exists; then
            echo "  stopping + removing coraza-spoa"
            stop_remove_sidecar
        else
            echo "  coraza-spoa already absent"
        fi
        ;;

    detect_only)
        # detect_only: regenerate-coraza-overrides.sh handles the file
        # render — it reads coraza_waf.mode from settings.json and emits
        # SecRuleEngine DetectionOnly plus any rule overrides from the DB.
        stop_remove_sidecar
        echo "  starting coraza-spoa (detect-only)"
        start_sidecar
        ;;

    enforce)
        # regenerate-coraza-overrides.sh will rewrite local-overrides.conf
        # from DB state (omitting the SecRuleEngine DetectionOnly directive
        # since mode != detect_only). Baked-in overrides.conf provides the
        # day-one enforce list.
        stop_remove_sidecar
        echo "  starting coraza-spoa with baked-in enforce list"
        start_sidecar
        ;;
esac

# Make the bundled CRS metadata reachable from the host (PHP UI reads it).
# Idempotent: re-copies on every apply. Only meaningful when sidecar is up.
if sidecar_running; then
    docker cp coraza-spoa:/etc/coraza/rules-catalog.json /docker/coraza/rules-catalog.json 2>/dev/null || true
    chmod 644 /docker/coraza/rules-catalog.json 2>/dev/null || true
fi

# Re-render runtime overrides from DB. Idempotent; skips silently if the
# sidecar isn't up (off mode) or if the regen script isn't present yet
# (fresh deploys before first WHP scripts sync).
if [[ "$MODE" != "off" ]] && [[ -x /root/whp/scripts/regenerate-coraza-overrides.sh ]]; then
    /root/whp/scripts/regenerate-coraza-overrides.sh || \
        echo "  WARN: regenerate-coraza-overrides.sh failed; live config may be stale (DB state intact)"
fi

# ---- recreate haproxy-manager so haproxy.cfg matches the mode ------------
# recreate_haproxy() in container-manager.sh reads settings.json and adds
# the HAPROXY_CORAZA_SPOE_BACKEND env var when mode != off.
echo "  recreating haproxy-manager to apply config..."
"$CONTAINER_MGR" recreate haproxy-manager

echo "apply-coraza-waf-mode.sh: OK (mode=$MODE)"
