#!/usr/bin/env bash
# crowdsec.sh - install/remove CrowdSec on Debian 13 (trixie) from Debian main
# Detection-only by default. BLOCK=1 opts into ban enforcement via the
# upstream crowdsec-firewall-bouncer binary (NOT the Debian package: trixie
# ships 0.0.25, whose daemonizer conflicts with trixie's Type=notify unit -
# the forked child dies and the unit reads inactive while nothing runs).
# The upstream binary runs in iptables(ipset) mode, which on trixie is
# iptables-nft underneath - still nftables enforcement.
# Usage: sudo ./crowdsec.sh add|remove|status   (BLOCK=1 ./crowdsec.sh add)

set -euo pipefail

BOUNCER_NAME=ndr-firewall-bouncer
BOUNCER_CONF=/etc/crowdsec/bouncers/crowdsec-firewall-bouncer.yaml
BOUNCER_VERSION=0.0.36
BOUNCER_TGZ_URL="https://github.com/crowdsecurity/cs-firewall-bouncer/releases/download/v${BOUNCER_VERSION}/crowdsec-firewall-bouncer-linux-amd64.tgz"
BOUNCER_BIN=/usr/local/bin/crowdsec-firewall-bouncer
BOUNCER_UNIT=/etc/systemd/system/crowdsec-firewall-bouncer.service

# Same IFACE convention/validation as the other tool scripts.
IFACE="${IFACE:-eth0}"
[[ $IFACE =~ ^[A-Za-z0-9._@:-]+$ ]] || { echo "invalid IFACE" >&2; exit 1; }

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

usage() {
    echo "Usage: $0 {add|remove|status}"
    exit 2
}

add() {
    echo "[*] Installing crowdsec (Debian main package)"
    export DEBIAN_FRONTEND=noninteractive
    apt-get update -y || true
    apt-get install -y crowdsec

    echo "[*] cscli hub update"
    cscli hub update

    echo "[*] Ensuring collections: crowdsecurity/linux, crowdsecurity/sshd"
    for c in crowdsecurity/linux crowdsecurity/sshd; do
        if cscli collections list -a -o raw 2>/dev/null | grep -q "^${c},"; then
            echo "    $c already present"
        else
            cscli collections install "$c"
        fi
    done

    echo "[*] Enabling crowdsec.service"
    systemctl enable --now crowdsec.service

    if [[ ${BLOCK:-0} = 1 ]]; then
        echo "[*] Installing ipset (the bouncer's iptables mode needs it)"
        apt-get install -y ipset

        echo "[*] Installing the upstream bouncer binary v${BOUNCER_VERSION} -> ${BOUNCER_BIN}"
        # The Debian trixie package (0.0.25) is unusable: its daemonizer
        # conflicts with the packaged Type=notify unit (the forked child
        # dies; the unit reads inactive while nothing runs).
        local tmp_tgz tmp_dir
        tmp_tgz=$(mktemp /tmp/fw-bouncer-XXXXXX.tgz)
        curl -fSL -o "${tmp_tgz}" "${BOUNCER_TGZ_URL}"
        tmp_dir=$(mktemp -d /tmp/fw-bouncer-XXXXXX)
        tar -xzf "${tmp_tgz}" -C "${tmp_dir}"
        install -m 0755 "$(find "${tmp_dir}" -name crowdsec-firewall-bouncer -type f | head -n1)" "${BOUNCER_BIN}"
        rm -rf "${tmp_dir}" "${tmp_tgz}"

        echo "[*] Writing ${BOUNCER_UNIT}"
        tee "${BOUNCER_UNIT}" >/dev/null <<EOF
[Unit]
Description=CrowdSec firewall bouncer (upstream binary)
After=network-online.target crowdsec.service
Wants=network-online.target crowdsec.service

[Service]
Type=simple
ExecStart=${BOUNCER_BIN} -c ${BOUNCER_CONF}
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF
        systemctl daemon-reload

        if [[ ! -f ${BOUNCER_CONF} ]]; then
            install -d -m 0750 "$(dirname "${BOUNCER_CONF}")"
            cat > "${BOUNCER_CONF}" <<EOF
mode: iptables
api_url: http://127.0.0.1:8080/
log_level: info
EOF
        else
            # The Debian package's nftables default breaks the upstream
            # binary on trixie; pin the verified iptables mode.
            sed -i -e "s|^mode: .*|mode: iptables|" -e "/^daemonize:/d" "${BOUNCER_CONF}"
        fi

        # cscli talks to the local API, which only exists once crowdsec.service
        # is up; wait briefly for it instead of racing the service start.
        local lapi_ok=0 i
        for i in $(seq 1 30); do
            if cscli bouncers list -o raw >/dev/null 2>&1; then
                lapi_ok=1
                break
            fi
            sleep 1
        done
        if [[ ${lapi_ok} -ne 1 ]]; then
            echo "[!] crowdsec local API did not come up; check 'journalctl -u crowdsec.service'" >&2
            exit 1
        fi

        if grep -qE '^api_key: .+' "${BOUNCER_CONF}"; then
            echo "[i] bouncer already configured in ${BOUNCER_CONF}; keeping existing API key"
        else
            # cscli shows the API key only once, so a leftover registration
            # without a usable key must be dropped before re-adding.
            cscli bouncers delete "${BOUNCER_NAME}" 2>/dev/null || true
            local bkey
            bkey=$(cscli bouncers add "${BOUNCER_NAME}" -o raw)
            if [[ ${#bkey} -lt 20 ]]; then
                echo "[!] bouncer registration failed (key length ${#bkey}); output was: ${bkey}" >&2
                exit 1
            fi
            sed -i -e "s|^mode: .*|mode: iptables|" \
                   -e "s|^api_url: .*|api_url: http://127.0.0.1:8080/|" \
                   -e "s|^api_key: .*|api_key: ${bkey}|" "${BOUNCER_CONF}"
            if ! grep -qE '^api_key: .+' "${BOUNCER_CONF}"; then
                echo "[!] failed to set api_key in ${BOUNCER_CONF}" >&2
                exit 1
            fi
        fi

        echo "[*] Enabling crowdsec-firewall-bouncer.service"
        systemctl enable --now crowdsec-firewall-bouncer.service
        if ! systemctl is-active --quiet crowdsec-firewall-bouncer.service; then
            echo "[!] crowdsec-firewall-bouncer failed to start; check 'journalctl -u crowdsec-firewall-bouncer.service'" >&2
            exit 1
        fi

        # The decisive check: the LAPI pull timestamp must ADVANCE while the
        # service runs (is-active alone has been misleading here).
        echo "[*] Verifying the bouncer pulls from the LAPI"
        local pull1 pull2
        pull1=$(cscli bouncers list -o raw | tail -n1 | cut -d, -f4)
        sleep 12
        pull2=$(cscli bouncers list -o raw | tail -n1 | cut -d, -f4)
        if [[ -z ${pull1} || ${pull1} = "${pull2}" ]]; then
            echo "[!] bouncer registered but the LAPI pull timestamp is not advancing (first: ${pull1}, second: ${pull2})" >&2
            exit 1
        fi
        echo "[+] bouncer alive and pulling (last pull: ${pull2})"
    else
        echo "[i] Detection-only mode; run with BLOCK=1 to install the firewall bouncer"
    fi

    echo "[+] CrowdSec installed (detection-only unless BLOCK=1; check 'cscli metrics' and 'cscli alerts list')"
}

remove() {
    export DEBIAN_FRONTEND=noninteractive

    echo "[*] Tearing down crowdsec-firewall-bouncer"
    systemctl stop crowdsec-firewall-bouncer.service 2>/dev/null || true
    systemctl disable crowdsec-firewall-bouncer.service 2>/dev/null || true
    rm -f "${BOUNCER_UNIT}"
    systemctl daemon-reload 2>/dev/null || true
    rm -f "${BOUNCER_BIN}"
    apt-get purge -y crowdsec-firewall-bouncer 2>/dev/null || true
    if command -v cscli >/dev/null 2>&1; then
        cscli bouncers delete "${BOUNCER_NAME}" 2>/dev/null || true
    fi

    echo "[*] Stopping and disabling crowdsec.service"
    systemctl stop crowdsec.service 2>/dev/null || true
    systemctl disable crowdsec.service 2>/dev/null || true

    echo "[*] Purging crowdsec"
    apt-get purge -y crowdsec 2>/dev/null || true

    echo "[*] Removing crowdsec config/state/logs"
    rm -rf /etc/crowdsec /var/lib/crowdsec /var/log/crowdsec

    echo "[*] apt-get autoremove --purge"
    apt-get autoremove --purge -y

    echo "[+] CrowdSec removed"
}

status() {
    echo "[*] crowdsec service:"
    systemctl is-active crowdsec.service 2>/dev/null && systemctl is-enabled crowdsec.service 2>/dev/null || echo "inactive/not installed"
    echo "[*] crowdsec-firewall-bouncer service:"
    systemctl is-active crowdsec-firewall-bouncer.service 2>/dev/null && systemctl is-enabled crowdsec-firewall-bouncer.service 2>/dev/null || echo "inactive/not installed"
    echo "[*] cscli metrics:"
    if command -v cscli >/dev/null 2>&1; then
        cscli bouncers list 2>/dev/null || true
        cscli metrics 2>/dev/null | head -12 || true
        cscli collections list 2>/dev/null | head -6 || true
    else
        echo "cscli not installed"
    fi
}

case "${1:-}" in
    add)    add ;;
    remove) remove ;;
    status) status ;;
    *)      usage ;;
esac
