#!/usr/bin/env bash
set -euo pipefail
# whp-cpanel-importer-quarantine-sweep.sh
#
# Daily sweeper that prunes aged per-import quarantine directories under
# /var/lib/whp/cpanel-importer-quarantine/. Each directory is named by
# import_id (e.g. /var/lib/whp/cpanel-importer-quarantine/import_6a1a...).
#
# Retention age is read from /docker/whp/settings.json key
#   cpanel_import.quarantine_retention_days  (default 90)
#
# Safety rails:
#   - Each victim path is validated to live under the configured root via a
#     realpath check + prefix match before any removal.
#   - Refuses to run if the root resolves outside the expected prefix
#     (defends against operator-edited symlinks under the import path).
#   - Removals happen only at the per-import-id directory depth; deeper
#     traversal is left to the kernel via rm -rf "$victim".
#   - Idempotent: re-running with nothing aged-out exits 0 with "nothing
#     to do" line.

QUAR_ROOT_DEFAULT=/docker/whp/cpanel-importer-quarantine
QUAR_ROOT=${WHP_QUARANTINE_ROOT:-$QUAR_ROOT_DEFAULT}
# Legacy path retained as an acceptable realpath so the symlink
# /var/lib/whp/cpanel-importer-quarantine -> /docker/whp/... resolves
# without tripping the defense check below.
QUAR_ROOT_LEGACY=/var/lib/whp/cpanel-importer-quarantine
SETTINGS=/docker/whp/settings.json
LOG=/var/log/whp/cpanel-importer-quarantine-sweep.log
DEFAULT_RETENTION=90

mkdir -p "$(dirname "$LOG")"

log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" | tee -a "$LOG" >&2; }

[ -d "$QUAR_ROOT" ] || { log "quarantine root $QUAR_ROOT does not exist; nothing to do"; exit 0; }

# Defense: refuse if QUAR_ROOT realpath escaped the expected prefix
# (somebody symlinked the dir up to /). The literal default is the only
# accepted realpath. Operators with a custom mount must export
# WHP_QUARANTINE_ROOT_REALPATH to acknowledge.
RESOLVED_ROOT="$(readlink -f "$QUAR_ROOT")"
EXPECTED_ROOT_DEFAULT="${WHP_QUARANTINE_ROOT_REALPATH:-$QUAR_ROOT_DEFAULT}"
# Accept the new canonical root OR the pre-2026.06 legacy root (both
# resolve to the same on-disk place via the relocation symlink). An
# operator-set WHP_QUARANTINE_ROOT_REALPATH overrides both.
if [ "$RESOLVED_ROOT" != "$EXPECTED_ROOT_DEFAULT" ] && [ "$RESOLVED_ROOT" != "$QUAR_ROOT_LEGACY" ]; then
    log "refusing to sweep: $QUAR_ROOT resolves to $RESOLVED_ROOT (expected $EXPECTED_ROOT_DEFAULT or $QUAR_ROOT_LEGACY)"
    exit 1
fi

# Resolve retention days from settings.json. Python is already a hard dep
# of the existing credentials sweeper (same systemd posture), so we lean
# on it rather than jq (which may not be installed on minimal images).
RETENTION_DAYS="$DEFAULT_RETENTION"
if [ -r "$SETTINGS" ]; then
    RETENTION_DAYS=$(python3 - "$SETTINGS" "$DEFAULT_RETENTION" <<'PY'
import json, sys
src, fallback = sys.argv[1], int(sys.argv[2])
try:
    with open(src) as fh:
        cfg = json.load(fh)
    val = (cfg.get('cpanel_import') or {}).get('quarantine_retention_days')
    if isinstance(val, (int, float)) and val > 0:
        print(int(val))
    else:
        print(fallback)
except Exception:
    print(fallback)
PY
    )
fi

# Clamp to a sane range — even an operator pasting a garbage number
# shouldn't trigger removal of newly-created quarantine dirs.
if ! [[ "$RETENTION_DAYS" =~ ^[0-9]+$ ]] || [ "$RETENTION_DAYS" -lt 1 ]; then
    log "refusing to sweep: invalid retention days ($RETENTION_DAYS)"
    exit 1
fi
if [ "$RETENTION_DAYS" -lt 7 ]; then
    log "retention $RETENTION_DAYS days is below the 7-day safety floor; clamping to 7"
    RETENTION_DAYS=7
fi

log "sweep starting: root=$QUAR_ROOT retention=${RETENTION_DAYS}d"

shopt -s nullglob
total_seen=0
total_removed=0

# -mindepth/-maxdepth 1 = per-import-id directories only, no deep walk.
# -mtime +N matches dirs whose mtime is older than N*24h.
while IFS= read -r -d $'\0' victim; do
    total_seen=$((total_seen + 1))
    # Re-validate every victim's resolved path. This catches a malicious
    # subdirectory symlink (e.g., "quarantine/import_xxx" being a symlink
    # to /home or /etc) created between scan and removal.
    resolved="$(readlink -f "$victim" 2>/dev/null || true)"
    case "$resolved" in
        "$RESOLVED_ROOT"/*)
            ;;
        *)
            log "skipping suspicious path: $victim -> $resolved (outside $RESOLVED_ROOT)"
            continue
            ;;
    esac
    age_days=$(( ($(date +%s) - $(stat -c %Y "$victim")) / 86400 ))
    log "removing aged quarantine: $victim (age ${age_days}d)"
    rm -rf -- "$victim"
    total_removed=$((total_removed + 1))
done < <(find "$QUAR_ROOT" -mindepth 1 -maxdepth 1 -type d -mtime "+${RETENTION_DAYS}" -print0)

if [ "$total_removed" -eq 0 ]; then
    log "nothing to do (scanned ${total_seen} ageable entries)"
else
    log "sweep complete: removed ${total_removed} of ${total_seen} ageable entries"
fi
