#!/usr/bin/env bash
# Install logrotate for the Coraza WAF logs (/var/log/coraza/audit.log + spoa.log).
#
# Stable, never-trimmed location: this is invoked by BOTH
#   - setup.sh (every fresh install / rebuild), and
#   - scripts/updates/staging/setup-coraza-logrotate.sh (backfill on update for
#     already-deployed servers)
# so EVERY WHP server that runs Coraza gets log rotation regardless of install
# vintage. (Host-level installers must live here + be called from setup.sh, NOT
# only in a versioned update dir — release-trimming drops those after 10 versions
# and fresh/rebuilt servers silently lose the fix. Same lesson as the backup
# I/O throttle installer.)
#
# Why this exists: there was NO logrotate for /var/log/coraza. The audit log
# grew unbounded (5.3 GB observed on whp01, 2026-06-08), and
# coraza-audit-rollup.php (cron */5) re-reads the ENTIRE file from the top every
# run — so it hit its 240s timeout, never finished, never shrank the log, and
# spiked CPU box-wide every 5 minutes. Capping the log at 200 MB makes the
# rollup cheap again.
#
# Idempotent: safe to re-run. Only rewrites the logrotate config when it drifts,
# and only force-rotates when the live log is actually oversized.
# Non-fatal by design: no-ops cleanly on servers that don't run Coraza (no
# /var/log/coraza directory) — logrotate `missingok` covers a not-yet-created log.

set -euo pipefail

CORAZA_LOG_DIR="/var/log/coraza"
LOGROTATE_CONF="/etc/logrotate.d/coraza"
MAX_SIZE_MB=200
# logrotate `maxsize` literal used in the config (kept in sync with MAX_SIZE_MB).
MAX_SIZE="200M"

# The Coraza container writes these logs as uid:gid 65532:65532 (distroless
# nonroot). That uid has NO host passwd entry, so a logrotate `su 65532 65532`
# directive fails with `unknown user '65532'` and logrotate SKIPS the whole
# stanza. Do NOT add `su` here — logrotate runs as root, and copytruncate
# truncates the file in place preserving its original ownership, so the
# container keeps writing without a reopen.
desired_conf() {
    cat <<EOF
${CORAZA_LOG_DIR}/audit.log ${CORAZA_LOG_DIR}/spoa.log {
    daily
    rotate 7
    maxsize ${MAX_SIZE}
    missingok
    notifempty
    compress
    delaycompress
    copytruncate
}
EOF
}

# ---- 1. Write/refresh the logrotate config only when it drifts --------------
if [[ -f "${LOGROTATE_CONF}" ]] && diff -q <(desired_conf) "${LOGROTATE_CONF}" >/dev/null 2>&1; then
    echo "  logrotate config already current: ${LOGROTATE_CONF}"
else
    desired_conf > "${LOGROTATE_CONF}"
    chmod 0644 "${LOGROTATE_CONF}"
    echo "  wrote ${LOGROTATE_CONF} (cap ${MAX_SIZE}, daily, keep 7, copytruncate)"
fi

# Validate the config; abort if logrotate rejects it (e.g. a future edit
# reintroduces an unresolvable `su`).
if command -v logrotate >/dev/null 2>&1; then
    if ! logrotate -d "${LOGROTATE_CONF}" >/dev/null 2>&1; then
        echo "  ERROR: logrotate rejected ${LOGROTATE_CONF}:" >&2
        logrotate -d "${LOGROTATE_CONF}" 2>&1 | grep -i error >&2 || true
        exit 1
    fi
else
    echo "  WARNING: logrotate binary not found; config installed but won't run until logrotate is present" >&2
fi

# ---- 2. If Coraza isn't deployed here, we're done --------------------------
if [[ ! -d "${CORAZA_LOG_DIR}" ]]; then
    echo "  ${CORAZA_LOG_DIR} absent (Coraza not deployed on this host) — config staged for when it is"
    echo "setup-coraza-logrotate.sh: OK"
    exit 0
fi

# ---- 3. Force an immediate rotation if a log is already oversized ----------
# Clears pre-existing bloat now instead of waiting for the daily logrotate run.
# copytruncate handles the live coraza-spoa writer; delaycompress keeps the
# initial rotation cheap (no inline gzip of a multi-GB file during install).
rotate_needed=0
for f in audit.log spoa.log; do
    p="${CORAZA_LOG_DIR}/${f}"
    [[ -f "${p}" ]] || continue
    size_mb=$(( $(stat -c %s "${p}" 2>/dev/null || echo 0) / 1024 / 1024 ))
    if (( size_mb > MAX_SIZE_MB )); then
        echo "  ${f} is ${size_mb} MB (> ${MAX_SIZE_MB} MB) — forcing rotation"
        rotate_needed=1
    fi
done

if (( rotate_needed == 1 )) && command -v logrotate >/dev/null 2>&1; then
    # --force ignores the daily interval; copytruncate shrinks the live file.
    logrotate --force "${LOGROTATE_CONF}" || echo "  (logrotate --force returned non-zero; daily cron will catch up)"
    # Safety net: copytruncate relies on the writer using O_APPEND. If a log is
    # STILL oversized on disk afterward (sparse-file edge case), reopen the
    # writer by restarting the Coraza container (fail-open: ~2s WAF gap).
    for f in audit.log spoa.log; do
        p="${CORAZA_LOG_DIR}/${f}"
        [[ -f "${p}" ]] || continue
        size_mb=$(( $(du -m "${p}" 2>/dev/null | cut -f1 || echo 0) ))
        if (( size_mb > MAX_SIZE_MB )) && command -v docker >/dev/null 2>&1; then
            cname=$(docker ps --format '{{.Names}}' 2>/dev/null | grep -i coraza | head -1 || true)
            if [[ -n "${cname}" ]]; then
                echo "  ${f} still ${size_mb} MB after copytruncate — restarting ${cname} to reopen logs"
                docker restart "${cname}" >/dev/null 2>&1 || true
            fi
        fi
    done
fi

echo "setup-coraza-logrotate.sh: OK"
echo "  config : ${LOGROTATE_CONF} (cap ${MAX_SIZE}, daily, keep 7)"
echo "  verify : logrotate -d ${LOGROTATE_CONF}   /   ls -lh ${CORAZA_LOG_DIR}"
