#!/usr/bin/env bash
# crowdsec-ntopng.sh - mirror CrowdSec decisions into ntopng's Active Monitor list
# Visibility only: enforcement stays with the ndr-firewall-bouncer (nftables).
# Installs a oneshot sync unit + a 5-minute timer; the sync script maps active
# decisions to ntopng REST calls.
# Usage: sudo ./crowdsec-ntopng.sh enable|disable|status

set -euo pipefail

NTOPNG_CONF=/etc/ntopng/ntopng.conf
NTOPNG_PORT_DEFAULT=3000
CFG=/etc/crowdsec-ntopng.conf
SYNC_BIN=/usr/local/bin/crowdsec-ntopng-sync
LOG=/var/log/crowdsec-ntopng.log
SVC=/etc/systemd/system/crowdsec-ntopng.service
TIMER=/etc/systemd/system/crowdsec-ntopng.timer
LOCK=/run/crowdsec-ntopng.lock

# Re-exec with sudo if not root
if [[ ${EUID} -ne 0 ]]; then
    exec sudo -E "$0" "$@"
fi

usage() {
    echo "Usage: $0 {enable|disable|status}"
    exit 2
}

ntopng_port() {
    local p
    p=$(grep -oE '(^|[[:space:]])-w=[0-9]+' "${NTOPNG_CONF}" 2>/dev/null | grep -oE '[0-9]+' | tail -n1 || true)
    echo "${p:-${NTOPNG_PORT_DEFAULT}}"
}

write_sync_script() {
    local port=$1
    install -m 755 /dev/null "${SYNC_BIN}"
    cat > "${SYNC_BIN}" <<SYNC
#!/usr/bin/env bash
# crowdsec-ntopng-sync - poll CrowdSec decisions and import them as
# ntopng Active Monitor hosts. Generated by integrations/crowdsec-ntopng.sh.
set -uo pipefail

PORT=${port}
CFG=${CFG}
LOG=${LOG}

summary() {
    echo "\$1" >> "\$LOG"
}

# Read the ntopng admin password; abort with clear instructions if missing.
if [[ ! -r \$CFG ]]; then
    summary "[!] \$CFG not readable: set the ntopng admin password, then write NTOPNG_PASS=<password> to \$CFG (mode 600)"
    exit 1
fi
# shellcheck disable=SC1090
source "\$CFG"
if [[ -z \${NTOPNG_PASS:-} ]]; then
    summary "[!] NTOPNG_PASS not set in \$CFG: set the ntopng admin password, then write NTOPNG_PASS=<password> to \$CFG (mode 600)"
    exit 1
fi

# Gather active decisions as JSON (empty list is fine)
if ! cscli decisions list -o json > /tmp/crowdsec-decisions.json 2>>"\$LOG"; then
    summary "[!] cscli decisions list failed"
    exit 0   # API hiccups are never a timer failure
fi

python3 - <<'PY' >> "\$LOG" 2>&1
import json, sys, urllib.request

cfg = {}
with open("/etc/crowdsec-ntopng.conf") as f:
    for line in f:
        line = line.strip()
        if line.startswith("NTOPNG_PASS="):
            cfg["pass"] = line.split("=", 1)[1].strip().strip('"').strip("'")

try:
    with open("/tmp/crowdsec-decisions.json") as f:
        data = json.load(f)
except Exception as e:
    print(f"[!] failed to parse decisions JSON: {e}")
    sys.exit(0)

if isinstance(data, dict):
    decisions = data.get("decisions", data.get("Data", []))
else:
    decisions = data

if not decisions:
    print("0 active decisions")
    sys.exit(0)

imported = 0
skipped = 0
for d in decisions:
    ip = d.get("value") or (d.get("Value") if isinstance(d, dict) else None)
    reason = d.get("reason") or (d.get("Reason") if isinstance(d, dict) else "")
    duration = d.get("duration") or (d.get("Duration") if isinstance(d, dict) else "")
    if not ip:
        skipped += 1
        continue
    body = json.dumps({
        "host": {"ip": ip},
        "scope": "host",
        "info": f"CrowdSec: {reason}",
        "duration": str(duration),
    }).encode()
    req = urllib.request.Request(
        f"http://127.0.0.1:{PORT}/lua/rest/v2/set/active_monitor/host.json",
        data=body, method="POST",
        headers={"Authorization": f"Bearer {cfg.get('pass','')}", "Content-Type": "application/json"},
    )
    try:
        resp = urllib.request.urlopen(req, timeout=10)
        code = resp.getcode()
        resp.read()
    except urllib.error.HTTPError as e:
        code = e.code
        if code in (401, 403):
            print(f"[!] ntopng API auth failure (HTTP {code}): set the ntopng admin password, then write NTOPNG_PASS=<password> to /etc/crowdsec-ntopng.conf")
            sys.exit(1)
        print(f"[!] ntopng API HTTP {code} for {ip}; skipping")
        skipped += 1
        continue
    except Exception as e:
        print(f"[!] ntopng API error for {ip}: {e}; skipping")
        skipped += 1
        continue
    if code == 200:
        imported += 1
    else:
        skipped += 1
        print(f"[!] unexpected response code {code} for {ip}; skipping")

print(f"imported {imported} decisions, {skipped} skipped")
PY
exit_code=\$?
rm -f /tmp/crowdsec-decisions.json
exit \$exit_code
SYNC
    chmod 755 "${SYNC_BIN}"
}

write_units() {
    cat > "${SVC}" <<EOF
[Unit]
Description=CrowdSec decisions to ntopng Active Monitor sync (oneshot)
After=network-online.target ntopng.service crowdsec.service
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/bin/flock -n ${LOCK} ${SYNC_BIN}
EOF

    cat > "${TIMER}" <<EOF
[Unit]
Description=Run crowdsec-ntopng sync every 5 minutes

[Timer]
OnCalendar=*:0/5
Persistent=true

[Install]
WantedBy=timers.target
EOF
    systemctl daemon-reload
}

enable() {
    if [[ ! -x /usr/bin/cscli ]]; then
        echo "[!] cscli not found; run ../crowdsec.sh add first" >&2
        exit 1
    fi

    local port
    port=$(ntopng_port)

    echo "[*] Checking that ntopng is listening on port ${port}"
    if ! ss -ltn "sport = :${port}" 2>/dev/null | grep -q LISTEN; then
        echo "[!] nothing listening on port ${port}; run ../ntopng.sh add first or fix -w= in ${NTOPNG_CONF}" >&2
        exit 1
    fi

    if [[ ! -r ${CFG} ]]; then
        echo "[!] ${CFG} not found: set the ntopng admin password, then write NTOPNG_PASS=<password> to ${CFG} (mode 600)" >&2
        exit 1
    fi

    echo "[*] Writing sync script to ${SYNC_BIN}"
    write_sync_script "${port}"
    echo "[*] Installing systemd units"
    write_units

    echo "[*] Enabling and starting crowdsec-ntopng.timer"
    systemctl enable --now crowdsec-ntopng.timer
    if ! systemctl is-active --quiet crowdsec-ntopng.timer; then
        echo "[!] crowdsec-ntopng.timer failed to become active; check 'systemctl status crowdsec-ntopng.timer'" >&2
        exit 1
    fi

    echo "[*] Running the sync once now"
    if systemctl start crowdsec-ntopng.service; then
        tail -n 3 "${LOG}" 2>/dev/null || true
    else
        echo "[!] first sync failed; see ${LOG}" >&2
        tail -n 10 "${LOG}" 2>/dev/null || true
        exit 1
    fi

    echo "[+] CrowdSec decisions now mirrored to ntopng Active Monitor (visibility only; enforcement is ndr-firewall-bouncer)"
}

disable() {
    echo "[*] Stopping and disabling crowdsec-ntopng.timer"
    systemctl stop crowdsec-ntopng.timer 2>/dev/null || true
    systemctl disable crowdsec-ntopng.timer 2>/dev/null || true

    echo "[*] Removing units, sync script, config and log"
    rm -f "${SVC}" "${TIMER}" "${SYNC_BIN}" "${CFG}" "${LOG}"
    systemctl daemon-reload
    echo "[+] crowdsec-ntopng integration removed"
}

status() {
    echo "[*] timer:"
    if systemctl list-unit-files crowdsec-ntopng.timer >/dev/null 2>&1 && [[ -f ${TIMER} ]]; then
        systemctl is-active crowdsec-ntopng.timer 2>/dev/null || echo "inactive"
        systemctl is-enabled crowdsec-ntopng.timer 2>/dev/null || echo "disabled"
    else
        echo "not installed"
    fi

    echo "[*] last log lines:"
    tail -n 5 "${LOG}" 2>/dev/null || echo "(no log yet)"

    echo "[*] active CrowdSec decisions:"
    if command -v cscli >/dev/null 2>&1; then
        cscli decisions list -o json 2>/dev/null | python3 -c '
import json, sys
try:
    data = json.load(sys.stdin)
    if isinstance(data, dict):
        data = data.get("decisions", data.get("Data", []))
    print(len(data))
except Exception:
    print("unknown")
' || echo "unknown"
    else
        echo "cscli not installed"
    fi
}

case "${1:-}" in
    enable)  enable ;;
    disable) disable ;;
    status)  status ;;
    *)       usage ;;
esac
