#!/bin/bash
################################################################################
# Script Name: nginx-security.sh
# Version: 1.0
# Description: Unified nginx security toolkit for standard (non-panel) servers.
#              Consolidates bot-blocking, JS cookie challenge, and HEAD request
#              blocking into a single subcommand-based script with shared helpers.
#              For HestiaCP / VestaCP / myVesta servers, use hestia-security.sh.
#
# Author: Phil Connor
# Contact: contact@mylinux.work
# Website: https://mylinux.work
# License: MIT
#
# Subcommands:
#   bot-block      — AI scraper and SEO bot blocking via nginx map
#   js-challenge   — JavaScript cookie challenge for headless bot detection
#   block-head     — Block HTTP HEAD request crawlers/scrapers
#   crowdsec       — CrowdSec engine + nginx lua bouncer + log acquisition
#   status         — Show status of all security features
#
# Usage:
#   sudo ./nginx-security.sh bot-block [OPTIONS]
#   sudo ./nginx-security.sh js-challenge [OPTIONS]
#   sudo ./nginx-security.sh block-head [OPTIONS]
#   sudo ./nginx-security.sh crowdsec [OPTIONS]
#   sudo ./nginx-security.sh status
#
################################################################################

set -euo pipefail

# =============================================================================
# SHARED HELPERS
# =============================================================================

# --- Colors (TTY-aware) ---
if [[ -t 1 ]]; then
    RED='\033[0;31m'
    GREEN='\033[0;32m'
    YELLOW='\033[0;33m'
    CYAN='\033[0;36m'
    BOLD='\033[1m'
    NC='\033[0m'
else
    RED="" GREEN="" YELLOW="" CYAN="" BOLD="" NC=""
fi

# --- Logging (prefixed with # for Prometheus comment compatibility) ---
info()  { echo -e "# ${GREEN}[OK]${NC} $*"; }
warn()  { echo -e "# ${YELLOW}[WARN]${NC} $*"; }
step()  { echo -e "# ${CYAN}[STEP]${NC} $*"; }
err()   { echo -e "# ${RED}[ERROR]${NC} $*" >&2; }

# --- Global variables ---
CONF_DIR="/etc/nginx/conf.d"
SITES_DIR="/etc/nginx/sites-enabled"
DRY_RUN=false
TIMESTAMP=$(date +%s)

# --- Root check ---
require_root() {
    if [[ $EUID -ne 0 && "$DRY_RUN" != "true" ]]; then
        err "Must run as root (or use --dry-run)"
        exit 1
    fi
}

# --- Require nginx ---
require_nginx() {
    if ! command -v nginx &>/dev/null; then
        err "nginx not found"
        exit 1
    fi
}

# --- Backup a file with timestamp suffix ---
backup_file() {
    local file="$1"
    if [[ -f "$file" ]]; then
        cp "$file" "${file}.bak.${TIMESTAMP}"
    fi
}

# --- Validate nginx config ---
nginx_validate() {
    step "Testing nginx configuration"
    if [[ "$DRY_RUN" == "true" ]]; then
        echo "  Would run: nginx -t"
        return 0
    fi
    if nginx -t 2>&1; then
        info "nginx config valid"
        return 0
    else
        err "nginx config test failed"
        return 1
    fi
}

# --- Reload nginx ---
nginx_reload() {
    step "Reloading nginx"
    if [[ "$DRY_RUN" == "true" ]]; then
        echo "  Would run: systemctl reload nginx"
    else
        systemctl reload nginx
        info "nginx reloaded"
    fi
}

# --- Collect server block configs from sites-enabled and conf.d ---
# Args: single_conf skip_file [skip_file2 ...]
collect_configs() {
    local single_conf="${1:-}"
    shift || true
    local skip_files=("$@")
    local configs=()

    if [[ -n "$single_conf" ]]; then
        if [[ ! -f "$single_conf" ]]; then
            err "Config file not found: ${single_conf}"
            exit 1
        fi
        configs+=("$single_conf")
    else
        if [[ -d "$SITES_DIR" ]]; then
            for f in "$SITES_DIR"/*; do
                [[ -f "$f" ]] && configs+=("$f")
            done
        fi
        if [[ -d "$CONF_DIR" ]]; then
            for f in "$CONF_DIR"/*.conf; do
                [[ -f "$f" ]] || continue
                local skip=false
                for sf in "${skip_files[@]}"; do
                    [[ "$f" == "$sf" ]] && skip=true && break
                done
                [[ "$skip" == "true" ]] && continue
                configs+=("$f")
            done
        fi
    fi

    for f in "${configs[@]}"; do
        grep -qP '^\s*server\s*\{' "$f" 2>/dev/null && echo "$f"
    done
}

# =============================================================================
# MAIN USAGE
# =============================================================================

usage_main() {
    cat <<EOF
Usage: sudo $(basename "$0") COMMAND [OPTIONS]

Unified nginx security toolkit for standard (non-panel) servers.
For HestiaCP / VestaCP / myVesta, use hestia-security.sh instead.

Commands:
  bot-block      AI scraper and SEO bot blocking via nginx map
  js-challenge   JavaScript cookie challenge for headless bot detection
  block-head     Block HTTP HEAD request crawlers/scrapers
  crowdsec       CrowdSec engine + nginx lua bouncer + log acquisition
  status         Show status of all security features

Run '$(basename "$0") COMMAND --help' for subcommand options.
EOF
    exit 0
}

# =============================================================================
# SUBCOMMAND: bot-block
# =============================================================================

cmd_bot_block() {
    local MAP_FILE="${CONF_DIR}/bot-block.conf"
    local SINGLE_CONF=""
    local STATUS_CODE="444"
    local REMOVE=false
    local MARKER_START="# bot-block-managed-start"
    local MARKER_END="# bot-block-managed-end"

    usage_bot_block() {
        cat <<EOF
Usage: sudo $(basename "$0") bot-block [OPTIONS]

Blocks AI scrapers, SEO bots, vulnerability scanners, and scraping frameworks
on standard nginx servers by creating an http-level map and injecting
bot-blocking rules into server blocks.

Options:
  --dry-run              Show what would be done without making changes
  --remove               Remove bot-block.conf and strip injected rules
  --conf FILE            Only modify a specific config file
  --status-code CODE     HTTP status code to return (default: 444)
                         Common alternatives: 403, 444
  -h, --help             Show this help

Examples:
  sudo $(basename "$0") bot-block
  sudo $(basename "$0") bot-block --dry-run
  sudo $(basename "$0") bot-block --conf /etc/nginx/sites-enabled/mysite.conf
  sudo $(basename "$0") bot-block --status-code 403
  sudo $(basename "$0") bot-block --remove
EOF
        exit 0
    }

    while [[ $# -gt 0 ]]; do
        case "$1" in
            --dry-run)       DRY_RUN=true; shift ;;
            --remove)        REMOVE=true; shift ;;
            --conf)          SINGLE_CONF="$2"; shift 2 ;;
            --status-code)   STATUS_CODE="$2"; shift 2 ;;
            -h|--help)       usage_bot_block ;;
            *)               echo "Unknown option: $1"; usage_bot_block ;;
        esac
    done

    require_root
    require_nginx

    # =====================================================
    # REMOVE MODE
    # =====================================================
    if [[ "$REMOVE" == "true" ]]; then
        step "Removing bot-block configuration"

        if [[ -f "$MAP_FILE" ]]; then
            if [[ "$DRY_RUN" == "true" ]]; then
                echo "  Would remove: ${MAP_FILE}"
            else
                rm -f "$MAP_FILE"
                info "Removed: ${MAP_FILE}"
            fi
        else
            warn "Map file not found: ${MAP_FILE} (already removed?)"
        fi

        step "Scanning for injected bot-block rules"
        mapfile -t configs < <(collect_configs "$SINGLE_CONF" "$MAP_FILE")

        if [[ ${#configs[@]} -eq 0 ]]; then
            warn "No server block config files found"
        else
            for conf in "${configs[@]}"; do
                if grep -q "$MARKER_START" "$conf" 2>/dev/null; then
                    if [[ "$DRY_RUN" == "true" ]]; then
                        echo "  Would clean: ${conf}"
                    else
                        cp "$conf" "${conf}.bak.${TIMESTAMP}"
                        sed -i "/${MARKER_START}/,/${MARKER_END}/d" "$conf"
                        info "Cleaned: ${conf}"
                    fi
                fi
            done
        fi

        if nginx_validate; then
            nginx_reload
        else
            warn "nginx config has errors — check other configs (js-challenge, etc.)"
        fi

        echo ""
        echo -e "${BOLD}Bot-block rules removed.${NC}"
        return 0
    fi

    # =====================================================
    # INSTALL MODE
    # =====================================================

    # Step 1: Create nginx map
    step "Creating bot-block map at ${MAP_FILE}"

    local MAP_CONTENT='# Bot-blocking map for AI scrapers, SEO bots, and vulnerability scanners
# Generated by nginx-security.sh bot-block — https://mylinux.work

map $http_user_agent $is_bad_bot {
    default 0;

    # AI scrapers
    ~*ABEvalBot              1;
    ~*GPTBot                1;
    ~*ClaudeBot             1;
    ~*anthropic-ai          1;
    ~*CCBot                 1;
    ~*Bytespider            1;
    ~*TikTokSpider          1;
    ~*cohere-ai             1;
    ~*PerplexityBot         1;
    ~*Diffbot               1;
    ~*MistralBot            1;
    ~*YandexGPTBot          1;
    ~*meta-externalagent    1;
    ~*Meta-ExternalFetcher  1;
    ~*meta-webindexer       1;
    ~*PetalBot              1;
    ~*Amazonbot             1;
    ~*Amzn-SearchBot        1;
    ~*AI2Bot                1;
    ~*Timpibot              1;
    ~*img2dataset           1;
    ~*YouBot                1;
    ~*HanaleiBot            1;
    ~*Trafilatura           1;
    ~*ShapBot               1;

    # Defunct crawlers (spoofed user agents)
    ~*Exabot                1;

    # Chinese search crawlers (no benefit for English sites)
    ~*Sogou                 1;

    # SEO scrapers
    ~*MJ12bot               1;
    ~*SemrushBot            1;
    ~*AhrefsBot             1;
    ~*DotBot                1;
    ~*DataForSeoBot         1;
    ~*SERanking             1;

    # Vulnerability scanners
    ~*Nikto                 1;
    ~*sqlmap                1;
    ~*Nmap                  1;
    ~*masscan               1;
    ~*ZmEu                  1;
    ~*Morpheus              1;

    # Lead-gen / business intelligence bots
    ~*ospa-radar            1;
    ~*HubSeedsBot           1;

    # AI scrapers / research bots
    ~*Aranet-SearchBot      1;
    ~*AzureAI-SearchBot     1;
    ~*MINERVA-DeepResearch  1;
    ~*NagetBot              1;
    ~*LAIABot               1;
    ~*pi-coding-agent       1;

    # Probe / monitoring bots
    ~*CMS-Checker           1;
    ~*NexoFaviconBot        1;
    ~*AwarioBot             1;
    ~*AwarioSmartBot        1;
    ~*CopyousBot            1;
    ~*SurdotlyBot           1;
    ~*trendictionbot        1;
    ~*wpbot                 1;
    ~*WebFetchTool          1;
    ~*YisouSpider           1;

    # Scraping frameworks
    ~*Scrapy                1;
    ~*python-requests       1;
    ~*Go-http-client        1;
    ~*Java/                 1;
    ~*libwww-perl           1;
    ~*node-fetch            1;
    ~*HeadlessChrome        1;

    # Outdated browsers (Chrome < 115 — almost certainly bots)
    ~*Chrome/([1-9][0-9]?|10[0-9]|11[0-4])\.   1;

    # Empty / missing user agent
    ""                      1;
    "-"                     1;
}'

    if [[ "$DRY_RUN" == "true" ]]; then
        echo "  Would create: ${MAP_FILE}"
    else
        if [[ -f "$MAP_FILE" ]]; then
            cp "$MAP_FILE" "${MAP_FILE}.bak.${TIMESTAMP}"
            warn "Existing map backed up"
        fi
        echo "$MAP_CONTENT" > "$MAP_FILE"
        info "Map created: ${MAP_FILE}"
    fi

    # Step 2: Inject bot-blocking rule into server blocks
    step "Scanning for server blocks to inject bot-blocking rule"

    mapfile -t configs < <(collect_configs "$SINGLE_CONF" "$MAP_FILE" "${CONF_DIR}/js-challenge.conf")

    if [[ ${#configs[@]} -eq 0 ]]; then
        warn "No server block config files found in ${SITES_DIR} or ${CONF_DIR}"
    else
        local MODIFIED=0
        for conf in "${configs[@]}"; do
            if grep -q "$MARKER_START" "$conf" 2>/dev/null; then
                warn "Already managed: ${conf} — skipping"
                continue
            fi

            if [[ "$DRY_RUN" == "true" ]]; then
                echo "  Would inject into: ${conf}"
                MODIFIED=$((MODIFIED + 1))
                continue
            fi

            cp "$conf" "${conf}.bak.${TIMESTAMP}"

            local BOT_BLOCK=""
            BOT_BLOCK+="    ${MARKER_START}\n"
            BOT_BLOCK+="    if (\$is_bad_bot) {\n"
            BOT_BLOCK+="        return ${STATUS_CODE};\n"
            BOT_BLOCK+="    }\n"
            BOT_BLOCK+="    # Block broken srcset scrapers\n"
            BOT_BLOCK+="    if (\$request_uri ~* \"%20[0-9]+w,https?://\") {\n"
            BOT_BLOCK+="        return ${STATUS_CODE};\n"
            BOT_BLOCK+="    }\n"
            BOT_BLOCK+="    # Block spoofed referers with fragment identifiers (real browsers strip these)\n"
            BOT_BLOCK+="    if (\$http_referer ~* \"#\") {\n"
            BOT_BLOCK+="        return ${STATUS_CODE};\n"
            BOT_BLOCK+="    }\n"
            BOT_BLOCK+="    # Block non-GET/HEAD methods (static sites never need POST/PUT/DELETE)\n"
            BOT_BLOCK+="    if (\$request_method !~ ^(GET|HEAD)\$ ) {\n"
            BOT_BLOCK+="        return ${STATUS_CODE};\n"
            BOT_BLOCK+="    }\n"
            BOT_BLOCK+="    ${MARKER_END}"

            awk -v block="$BOT_BLOCK" '
                /^\s*server\s*\{/ { in_server = 1; injected = 0 }
                in_server && !injected && /^\s*location\s/ {
                    printf "%s\n", block
                    print ""
                    injected = 1
                }
                /^\s*\}/ && in_server {
                    # Track brace depth to know when server block ends
                }
                { print }
            ' "$conf" > "${conf}.tmp"
            mv "${conf}.tmp" "$conf"

            info "Injected into: ${conf}"
            MODIFIED=$((MODIFIED + 1))
        done

        if [[ $MODIFIED -eq 0 ]]; then
            warn "No files modified (all already managed)"
        fi
    fi

    # Step 3: Validate and reload
    nginx_validate || exit 1
    nginx_reload

    # Summary
    echo ""
    echo -e "${BOLD}Done.${NC}"
    echo ""
    echo "  Map:          ${MAP_FILE}"
    echo "  Status code:  ${STATUS_CODE}"
    if [[ -n "$SINGLE_CONF" ]]; then
        echo "  Config:       ${SINGLE_CONF}"
    else
        echo "  Scanned:      ${SITES_DIR}/ and ${CONF_DIR}/*.conf"
    fi
    echo ""
    echo "  To remove:    sudo $(basename "$0") bot-block --remove"
    echo ""
    echo "  Verify: curl -A 'GPTBot' -o /dev/null -s -w '%{http_code}' https://yourdomain.com"
    echo "  Expected: 444 (connection dropped) or 000 (no response)"
}

# =============================================================================
# SUBCOMMAND: js-challenge
# =============================================================================

cmd_js_challenge() {
    local CHALLENGE_MAP="${CONF_DIR}/js-challenge.conf"
    local CHALLENGE_DIR="/var/www/js-challenge"
    local CHALLENGE_HTML="${CHALLENGE_DIR}/challenge.html"
    local STATE_FILE="/etc/nginx/js-challenge.env"
    local CHALLENGE_PATH="/_bc"
    local REMOVE=false
    local COOKIE_MAX_AGE=86400
    local TARPIT_COUNTRIES="${TARPIT_COUNTRIES:-CN}"
    local TARPIT_RATE="${TARPIT_RATE:-50}"
    local CHALLENGE_RATE="${CHALLENGE_RATE:-1}"
    local CHALLENGE_BURST="${CHALLENGE_BURST:-3}"
    local COOKIE_NAME=""
    local COOKIE_VALUE=""
    local USE_DBIP=false

    usage_js_challenge() {
        cat <<EOF
Usage: sudo $(basename "$0") js-challenge [OPTIONS]

Adds a JavaScript cookie challenge to nginx. Visitors that don't execute
JavaScript (headless scrapers, curl-based bots) are silently dropped.
Legitimate search engine crawlers are whitelisted by user agent.

This is designed to work alongside bot-block — run that first to block
known bad bots, then use this to catch the ones spoofing real browser
user agents.

The cookie name and token are randomized per installation. Re-running the script
rotates them, immediately invalidating any bot-cached bypass cookies.

Options:
  --dry-run              Show what would be done without making changes
  --remove               Remove challenge config, HTML, and state file
  --db-ip                Install geoip2 module + DB-IP free databases for tarpit
  --cookie-ttl SECONDS   Cookie lifetime in seconds (default: 86400 / 24h)
  --tarpit-countries CC  Space-separated GeoIP country codes to tarpit (default: CN)
  --tarpit-rate BYTES    Response rate in bytes/sec for tarpitted visitors (default: 50)
  --challenge-rate N     Sustained challenge requests per minute per IP (default: 1)
  --challenge-burst N    Initial burst of challenge requests allowed (default: 3)
  -h, --help             Show this help

Examples:
  sudo $(basename "$0") js-challenge --db-ip
  sudo $(basename "$0") js-challenge --dry-run
  sudo $(basename "$0") js-challenge --remove
EOF
        exit 0
    }

    while [[ $# -gt 0 ]]; do
        case "$1" in
            --dry-run)          DRY_RUN=true; shift ;;
            --remove)           REMOVE=true; shift ;;
            --db-ip)            USE_DBIP=true; shift ;;
            --cookie-ttl)       COOKIE_MAX_AGE="$2"; shift 2 ;;
            --tarpit-countries) TARPIT_COUNTRIES="$2"; shift 2 ;;
            --tarpit-rate)      TARPIT_RATE="$2"; shift 2 ;;
            --challenge-rate)   CHALLENGE_RATE="$2"; shift 2 ;;
            --challenge-burst)  CHALLENGE_BURST="$2"; shift 2 ;;
            -h|--help)          usage_js_challenge ;;
            *)                  echo "Unknown option: $1"; usage_js_challenge ;;
        esac
    done

    require_root

    # --- Credential generation ---
    _jsc_generate() {
        COOKIE_NAME="bc$(openssl rand -hex 1)"
        COOKIE_VALUE="$(openssl rand -hex 16)"
    }

    _jsc_save() {
        if [[ "$DRY_RUN" != "true" ]]; then
            cat > "$STATE_FILE" <<ENVEOF
# JS challenge credentials — generated $(date -Iseconds)
# Re-run the script to rotate these values
COOKIE_NAME='${COOKIE_NAME}'
COOKIE_VALUE='${COOKIE_VALUE}'
CHALLENGE_PATH='${CHALLENGE_PATH}'
ENVEOF
            chmod 600 "$STATE_FILE"
        fi
    }

    if [[ "$REMOVE" != "true" ]]; then
        _jsc_generate
        info "Generated new credentials — cookie: ${COOKIE_NAME}  token: ${COOKIE_VALUE:0:8}..."
    fi

    # =====================================================
    # Remove mode
    # =====================================================
    if [[ "$REMOVE" == "true" ]]; then
        step "Removing JS challenge configuration"

        # Remove injected blocks from server configs FIRST (before deleting
        # the map file that defines the variables they reference)
        if [[ "$DRY_RUN" == "true" ]]; then
            echo "  Would clean JS challenge blocks from server configs"
        else
            mapfile -t configs < <(collect_configs "" "")
            for conf in "${configs[@]}"; do
                if grep -q 'js-challenge-managed-start' "$conf" 2>/dev/null; then
                    cp "$conf" "${conf}.bak.${TIMESTAMP}"
                    sed -i '/js-challenge-managed-start/,/js-challenge-managed-end/d' "$conf"
                    info "Removed JS challenge block from: ${conf}"
                fi
            done
        fi

        if [[ "$DRY_RUN" == "true" ]]; then
            echo "  Would remove: ${CHALLENGE_MAP}"
            echo "  Would remove: ${CHALLENGE_DIR}"
            echo "  Would remove: ${STATE_FILE}"
            echo "  Would run: nginx -t && systemctl reload nginx"
        else
            [[ -f "$CHALLENGE_MAP" ]] && rm -f "$CHALLENGE_MAP" && info "Removed: ${CHALLENGE_MAP}"
            [[ -d "$CHALLENGE_DIR" ]] && rm -rf "$CHALLENGE_DIR" && info "Removed: ${CHALLENGE_DIR}"
            [[ -f "$STATE_FILE" ]] && rm -f "$STATE_FILE" && info "Removed: ${STATE_FILE}"
        fi

        if nginx_validate; then
            nginx_reload
        else
            warn "nginx config has errors — check other configs (bot-block, etc.)"
        fi

        echo ""
        echo -e "${BOLD}JS challenge removed.${NC}"
        return 0
    fi

    require_nginx

    # =====================================================
    # Step 1: Create the challenge HTML page
    # =====================================================
    step "Creating challenge page at ${CHALLENGE_HTML}"

    local CHALLENGE_CONTENT='<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>Verifying</title></head>
<body>
<noscript><p>JavaScript is required to access this site.</p></noscript>
<p id="msg" style="display:none;font-family:sans-serif;text-align:center;margin-top:2em;">
Cookies must be enabled to access this site.</p>
<script>
(function(){
    var p = new URLSearchParams(window.location.search);
    var r = p.get("r") || "/";
    if (r.charAt(0) !== "/") r = "/";
    var cn = "'"${COOKIE_NAME}"'";
    var cv = "'"${COOKIE_VALUE}"'";
    var secure = (location.protocol === "https:") ? ";Secure" : "";
    document.cookie = cn + "=" + cv + ";path=/;max-age='"${COOKIE_MAX_AGE}"';SameSite=Lax" + secure;
    var origRef = p.get("ref") || "direct";
    if (origRef !== "direct") {
        try { var rh = new URL(origRef).hostname; if (rh === location.hostname) origRef = "direct"; } catch(e) {}
    }
    document.cookie = "_bc_ref=" + encodeURIComponent(origRef) + ";path=/;max-age=120;SameSite=Lax" + secure;
    if (document.cookie.split(/;\s*/).every(function(c){ return c.indexOf(cn + "=") !== 0; })) {
        document.getElementById("msg").style.display = "block";
        return;
    }
    window.location.replace(r);
})();
</script>
</body>
</html>'

    if [[ "$DRY_RUN" == "true" ]]; then
        echo "  Would create: ${CHALLENGE_DIR}/"
        echo "  Would create: ${CHALLENGE_HTML}"
    else
        mkdir -p "$CHALLENGE_DIR"
        echo "$CHALLENGE_CONTENT" > "$CHALLENGE_HTML"
        info "Challenge page created: ${CHALLENGE_HTML}"
    fi

    # Save credentials
    _jsc_save

    # =====================================================
    # Step 2: Create nginx map config
    # =====================================================
    step "Creating JS challenge map at ${CHALLENGE_MAP}"

    local NGINX_COOKIE_VAR="\$cookie_${COOKIE_NAME}"

    # Install geoip2 module + DB-IP databases if requested
    if [[ "$USE_DBIP" == "true" ]]; then
        step "Installing geoip2 module and DB-IP databases"
        if [[ "$DRY_RUN" == "true" ]]; then
            echo "  Would install geoip2 module and download DB-IP databases"
        else
            # Install geoip2 nginx module
            if ! nginx -V 2>&1 | grep -q 'http_geoip2' && \
               ! ls /etc/nginx/modules-enabled/*geoip2* 2>/dev/null | grep -q . && \
               ! ls /usr/lib/nginx/modules/*geoip2* 2>/dev/null | grep -q . ; then
                if command -v apt &>/dev/null; then
                    apt install -y libnginx-mod-http-geoip2 2>/dev/null || \
                        warn "Could not install libnginx-mod-http-geoip2 — try installing manually"
                elif command -v dnf &>/dev/null; then
                    dnf install -y nginx-mod-http-geoip2 2>/dev/null || \
                        warn "Could not install nginx-mod-http-geoip2"
                fi
            else
                info "geoip2 module already installed"
            fi

            # Download DB-IP free databases
            local geoip_dir="/usr/share/GeoIP"
            mkdir -p "$geoip_dir"
            local dbip_month
            dbip_month=$(date +%Y-%m)
            local tmpdir="/tmp/dbip-download-$$"
            mkdir -p "$tmpdir"

            if [[ ! -f "${geoip_dir}/GeoLite2-City.mmdb" ]] || [[ "$USE_DBIP" == "true" ]]; then
                step "Downloading DB-IP Country database"
                if curl -fsSL "https://download.db-ip.com/free/dbip-country-lite-${dbip_month}.mmdb.gz" \
                     -o "${tmpdir}/country.mmdb.gz" 2>/dev/null; then
                    gunzip -f "${tmpdir}/country.mmdb.gz"
                    mv "${tmpdir}/country.mmdb" "${geoip_dir}/GeoLite2-City.mmdb"
                    info "DB-IP Country → ${geoip_dir}/GeoLite2-City.mmdb"
                else
                    warn "Failed to download DB-IP Country database"
                fi

                step "Downloading DB-IP ASN database"
                if curl -fsSL "https://download.db-ip.com/free/dbip-asn-lite-${dbip_month}.mmdb.gz" \
                     -o "${tmpdir}/asn.mmdb.gz" 2>/dev/null; then
                    gunzip -f "${tmpdir}/asn.mmdb.gz"
                    mv "${tmpdir}/asn.mmdb" "${geoip_dir}/GeoLite2-ASN.mmdb"
                    info "DB-IP ASN → ${geoip_dir}/GeoLite2-ASN.mmdb"
                else
                    warn "Failed to download DB-IP ASN database"
                fi
            fi
            rm -rf "$tmpdir"

            # Create monthly cron job to update DB-IP databases
            local cron_file="/etc/cron.monthly/dbip-update"
            if [[ ! -f "$cron_file" ]]; then
                step "Creating monthly DB-IP update cron job"
                cat > "$cron_file" <<'CRONEOF'
#!/bin/sh
# Monthly DB-IP database update — generated by nginx-security.sh
MONTH=$(date +%Y-%m)
TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT
if curl -fsSL "https://download.db-ip.com/free/dbip-country-lite-${MONTH}.mmdb.gz" -o "${TMPDIR}/country.mmdb.gz" && \
   curl -fsSL "https://download.db-ip.com/free/dbip-asn-lite-${MONTH}.mmdb.gz" -o "${TMPDIR}/asn.mmdb.gz" && \
   gunzip "${TMPDIR}/country.mmdb.gz" && \
   gunzip "${TMPDIR}/asn.mmdb.gz"; then
  mv "${TMPDIR}/country.mmdb" "/usr/share/GeoIP/GeoLite2-City.mmdb"
  mv "${TMPDIR}/asn.mmdb" "/usr/share/GeoIP/GeoLite2-ASN.mmdb"
  logger -t dbip-update "DB-IP databases updated"
else
  logger -t dbip-update -p user.err "DB-IP download failed — kept existing databases"
fi
CRONEOF
                chmod 755 "$cron_file"
                info "Created monthly update: ${cron_file}"
            else
                info "Monthly cron job already exists: ${cron_file}"
            fi
        fi
    fi

    # Check if geoip2 module is available and the database exists
    local GEOIP2_BLOCK=""
    local GEOIP2_AVAILABLE=false
    local TARPIT_MAP_BLOCK=""

    # Check if the module is loadable
    if nginx -V 2>&1 | grep -q 'http_geoip2' || \
       ls /etc/nginx/modules-enabled/*geoip2* 2>/dev/null | grep -q . || \
       ls /usr/lib/nginx/modules/*geoip2* 2>/dev/null | grep -q . ; then
        if [[ -f /usr/share/GeoIP/GeoLite2-City.mmdb ]] || \
           [[ -f /usr/share/GeoIP/GeoLite2-Country.mmdb ]]; then
            GEOIP2_AVAILABLE=true
        else
            warn "geoip2 module found but no GeoIP database in /usr/share/GeoIP/ — tarpit disabled"
        fi
    else
        warn "geoip2 nginx module not installed — tarpit feature disabled"
        warn "Install with: apt install libnginx-mod-http-geoip2 (Ubuntu/Debian)"
    fi

    if [[ "$GEOIP2_AVAILABLE" == "true" ]]; then
        # Check if geoip2 country_code is already defined elsewhere
        if ! grep -r 'geoip2_country_code' /etc/nginx/ \
             --include='*.conf' --exclude='js-challenge.conf' --exclude='*.bak.*' \
             -q 2>/dev/null; then
            local mmdb="/usr/share/GeoIP/GeoLite2-City.mmdb"
            [[ ! -f "$mmdb" ]] && mmdb="/usr/share/GeoIP/GeoLite2-Country.mmdb"
            GEOIP2_BLOCK='
# ── GeoIP2: country lookup for tarpit decisions ──────────────────────
# Uses the City database (superset of Country). Adjust path if needed.
geoip2 '"${mmdb}"' {
    $geoip2_country_code country iso_code;
}
'
            step "No existing geoip2 country_code config found — adding to map config"
        fi

        # Build tarpit maps
        local tarpit_entries=""
        for cc in $TARPIT_COUNTRIES; do tarpit_entries="${tarpit_entries}    \"${cc}\"  1;\n"; done

        TARPIT_MAP_BLOCK="${GEOIP2_BLOCK}
# ── Tarpit: headless Chrome bots from suspect regions ─────────────────
# Visitors from tarpit countries with no external referrer (passed through
# the challenge redirect as the _bc_ref cookie) are served at a crawl.
# This drains headless Chrome resources (~200-500 MB RAM per instance)
# without giving the bot a clear \"blocked\" signal to adapt to.
#
# The _bc_ref cookie is set by the challenge page JS from the &ref= param.
# It contains the original HTTP Referer before the 302 redirect destroyed it.
# \"direct\" = no external referrer (typed URL or bot). Cookie expires in 120s.

# Check if visitor is from a tarpit country (requires geoip2 module)
map \$geoip2_country_code \$is_tarpit_country {
    default 0;
$(for cc in $TARPIT_COUNTRIES; do echo "    \"${cc}\"  1;"; done)
}

# Tarpit only if: tarpit country + no external referrer + passed JS challenge
map \"\$is_tarpit_country:\$cookie__bc_ref\" \$tarpit_client {
    default             0;
    \"1:direct\"          1;
    \"1:\"                1;
}"
    else
        TARPIT_MAP_BLOCK='
# ── Tarpit: DISABLED (geoip2 module not available) ──────────────────
# Install libnginx-mod-http-geoip2 and re-run to enable tarpit.
# Without geoip2, the JS challenge still works — bots without JS are blocked.
# The tarpit is an optional enhancement that slows headless browser bots.
map $uri $tarpit_client {
    default 0;
}'
    fi

    # Collect server_name values from nginx configs to build same-site referer map
    local REFERER_ENTRIES=""
    local _jsc_domain_seen=()
    for _conf in /etc/nginx/conf.d/*.conf /etc/nginx/sites-enabled/*; do
        [[ -f "$_conf" ]] || continue
        while read -r _sn; do
            for _d in $_sn; do
                [[ "$_d" == "server_name" || "$_d" == ";" || "$_d" == "_" || "$_d" =~ ^[0-9] ]] && continue
                _d="${_d%;}"
                [[ " ${_jsc_domain_seen[*]:-} " == *" $_d "* ]] && continue
                _jsc_domain_seen+=("$_d")
                local _d_escaped="${_d//./\\.}"
                REFERER_ENTRIES+="    ~^1:https?://${_d_escaped}  1;\n"
            done
        done < <(grep -oP '^\s*server_name\s+\K[^;]+;?' "$_conf" 2>/dev/null)
    done

    if [[ -z "$REFERER_ENTRIES" ]]; then
        warn "No server_name values found — same-site image bypass will not work"
        warn "Images behind the challenge may cause redirect loops for browsers"
    fi

    local MAP_CONTENT='# JS cookie challenge — allowed bots and cookie check
# Generated by nginx-security.sh js-challenge — https://mylinux.work
# Cookie: '"${COOKIE_NAME}"'  Token: '"${COOKIE_VALUE:0:8}"'...
# Generated: '"$(date -Iseconds)"'

# ── Rate limit: challenge endpoint ───────────────────────────────────
# Real users hit the challenge once and keep the cookie. Headless bot farms
# spawn fresh browsers per request, hitting the challenge every time.
# Rate: '"${CHALLENGE_RATE}"'r/m with burst of '"${CHALLENGE_BURST}"' — excess gets 444.
limit_req_zone $binary_remote_addr zone=jschallenge:10m rate='"${CHALLENGE_RATE}"'r/m;

# Bots that legitimately identify themselves and should bypass the JS check
map $http_user_agent $is_allowed_bot {
    default 0;

    # Search engines
    ~*Googlebot             1;
    ~*bingbot               1;
    ~*Slurp                 1;
    ~*DuckDuckBot           1;
    ~*DuckAssistBot         1;
    ~*Baiduspider           1;
    ~*YandexBot             1;
    ~*YandexFavicons        1;
    ~*Applebot              1;
    ~*Qwantbot              1;
    ~*Qwantify              1;
    ~*Bravebot              1;
    ~*kagi-fetcher          1;
    ~*Kagibot               1;
    ~*Yahoo!                1;
    ~*Yeti                  1;

    # Social media / link previews
    ~*facebookexternalhit   1;
    ~*Facebot               1;
    ~*Twitterbot            1;
    ~*LinkedInBot           1;
    ~*Slackbot              1;
    ~*Slack-ImgProxy        1;
    ~*Discordbot            1;
    ~*TelegramBot           1;
    ~*WhatsApp              1;
    ~*redditbot             1;
    ~*ArenaUnfurlBot        1;

    # Feed readers
    ~*Feedly                1;
    ~*Miniflux              1;
    ~*FreshRSS              1;
    ~*NewsBlur              1;
    ~*Tiny\ Tiny\ RSS       1;
    ~*Inoreader             1;
    ~*NetNewsWire           1;

    # Monitoring / uptime
    ~*UptimeRobot           1;
    ~*Pingdom               1;
    ~*StatusCake            1;
    ~*Blackbox-Exporter      1;

    # AI answer bots (user-facing, not training crawlers)
    ~*OAI-SearchBot         1;
    ~*ChatGPT-User          1;
    ~*Claude-Web            1;
    ~*Claude-User           1;
    ~*MistralAI-User        1;

    # Archive / research
    ~*archive\.org_bot      1;

    # Apple Safari prefetch
    ~*safarifetcherd        1;

    # Link checkers / validators
    ~*W3C_Validator         1;
    ~*W3C-checklink         1;
    ~*LinkChecker           1;
    ~*link-check            1;

    # Decentralized search
    ~*yacybot               1;

    # Add your own allowed bots below
}

# Validate the challenge cookie — exact token match
map '"${NGINX_COOKIE_VAR}"' $js_cookie_valid {
    default                                     0;
    "'"${COOKIE_VALUE}"'"   1;
}

# Detect requests to the challenge page, downloads, and static assets (prevent redirect loops)
map $uri $is_challenge_uri {
    default 0;
    "'"${CHALLENGE_PATH}"'" 1;
    ~^/downloads/   1;
    ~*\.(css|js|woff2?)$  1;
    ~*favicon  1;
    ~*apple-touch-icon  1;
}

# Detect image sub-resource requests with same-site referer (browser <img> loads)
# These bypass the challenge because: (a) images cannot execute JS challenges,
# and (b) the same-site referer proves the browser loaded a page from this domain.
# Direct image requests from scrapers (no referer or external referer) still get challenged.
map $uri $is_image_request {
    default 0;
    ~*\.(png|jpe?g|gif|svg|webp|ico|avif)$  1;
}
map "$is_image_request:$http_referer" $is_samesite_image {
    default 0;
'"${REFERER_ENTRIES}"'}

# Combined check: need challenge if not allowed bot, no valid cookie, and not the challenge page
map "$is_allowed_bot:$js_cookie_valid:$is_challenge_uri:$is_samesite_image" $needs_js_challenge {
    default         1;
    "1:0:0:0"       0;
    "1:0:0:1"       0;
    "1:0:1:0"       0;
    "1:0:1:1"       0;
    "1:1:0:0"       0;
    "1:1:0:1"       0;
    "1:1:1:0"       0;
    "1:1:1:1"       0;
    "0:1:0:0"       0;
    "0:1:0:1"       0;
    "0:1:1:0"       0;
    "0:1:1:1"       0;
    "0:0:1:0"       0;
    "0:0:1:1"       0;
    "0:0:0:1"       0;
}
'"${TARPIT_MAP_BLOCK}"'

# Serve the challenge page
server {
    listen 127.0.0.1:18444;
    server_name _;
    root /var/www/js-challenge;

    location / {
        add_header Cache-Control "no-store, no-cache, must-revalidate" always;
        add_header Pragma "no-cache" always;
        try_files /challenge.html =404;
    }
}'

    if [[ "$DRY_RUN" == "true" ]]; then
        echo "  Would create: ${CHALLENGE_MAP}"
    else
        if [[ -f "$CHALLENGE_MAP" ]]; then
            cp "$CHALLENGE_MAP" "${CHALLENGE_MAP}.bak.${TIMESTAMP}"
            warn "Existing config backed up"
        fi
        echo "$MAP_CONTENT" > "$CHALLENGE_MAP"
        info "Map config created: ${CHALLENGE_MAP}"
    fi

    # =====================================================
    # Step 3: Inject JS challenge into server blocks
    # =====================================================
    step "Injecting JS challenge into server blocks"

    local JSC_MARKER_START="# js-challenge-managed-start"
    local JSC_MARKER_END="# js-challenge-managed-end"

    local JSC_BLOCK=""
    JSC_BLOCK+="    ${JSC_MARKER_START}\n"
    JSC_BLOCK+="    location = ${CHALLENGE_PATH} {\n"
    JSC_BLOCK+="        limit_req zone=jschallenge burst=${CHALLENGE_BURST} nodelay;\n"
    JSC_BLOCK+="        limit_req_status 444;\n"
    JSC_BLOCK+="        proxy_pass http://127.0.0.1:18444/;\n"
    JSC_BLOCK+="    }\n"
    JSC_BLOCK+="\n"
    JSC_BLOCK+="    # JS cookie challenge — redirect non-JS visitors\n"
    JSC_BLOCK+="    if (\$needs_js_challenge) {\n"
    JSC_BLOCK+="        return 302 ${CHALLENGE_PATH}?r=\$request_uri&ref=\$http_referer;\n"
    JSC_BLOCK+="    }\n"
    if [[ "$GEOIP2_AVAILABLE" == "true" ]]; then
        JSC_BLOCK+="\n"
        JSC_BLOCK+="    # Tarpit headless Chrome bots from suspect GeoIP regions\n"
        JSC_BLOCK+="    if (\$tarpit_client) {\n"
        JSC_BLOCK+="        set \$limit_rate ${TARPIT_RATE};\n"
        JSC_BLOCK+="    }\n"
    fi
    JSC_BLOCK+="    ${JSC_MARKER_END}"

    mapfile -t configs < <(collect_configs "" "$CHALLENGE_MAP" "${CONF_DIR}/bot-block.conf")

    if [[ ${#configs[@]} -eq 0 ]]; then
        warn "No server block config files found"
    else
        local MODIFIED=0
        for conf in "${configs[@]}"; do
            if grep -q "$JSC_MARKER_START" "$conf" 2>/dev/null; then
                warn "Already managed: ${conf} — skipping"
                continue
            fi

            if [[ "$DRY_RUN" == "true" ]]; then
                echo "  Would inject into: ${conf}"
                MODIFIED=$((MODIFIED + 1))
                continue
            fi

            cp "$conf" "${conf}.bak.${TIMESTAMP}"

            awk -v block="$JSC_BLOCK" '
                /^\s*server\s*\{/ { in_server = 1; injected = 0 }
                in_server && !injected && /^\s*location\s/ {
                    printf "%s\n", block
                    print ""
                    injected = 1
                }
                { print }
            ' "$conf" > "${conf}.tmp"
            mv "${conf}.tmp" "$conf"

            info "Injected into: ${conf}"
            MODIFIED=$((MODIFIED + 1))
        done

        if [[ $MODIFIED -eq 0 ]]; then
            warn "No files modified (all already managed)"
        fi
    fi

    # Step 4: Validate and reload
    nginx_validate || exit 1
    nginx_reload

    # Summary
    echo ""
    echo -e "${BOLD}Done.${NC}"
    echo ""
    echo "  Challenge map:  ${CHALLENGE_MAP}"
    echo "  Challenge page: ${CHALLENGE_HTML}"
    echo "  State file:     ${STATE_FILE}"
    echo "  Cookie name:    ${COOKIE_NAME}"
    echo "  Cookie token:   ${COOKIE_VALUE:0:8}... (32 hex chars)"
    echo "  Cookie TTL:     ${COOKIE_MAX_AGE}s"
    echo "  Tarpit countries: ${TARPIT_COUNTRIES}"
    echo "  Tarpit rate:    ${TARPIT_RATE} bytes/sec"
    echo "  Challenge rate: ${CHALLENGE_RATE}r/m (burst: ${CHALLENGE_BURST})"
    echo ""
    echo "  To rotate credentials (invalidate bot-cached cookies):"
    echo "    sudo $(basename "$0") js-challenge"
    echo ""
    echo "  To remove:      sudo $(basename "$0") js-challenge --remove"
    echo ""
    echo "  Test (bot without cookie gets redirected to challenge):"
    echo "    curl -o /dev/null -s -w '%{http_code}' https://yourdomain.com"
    echo "    Expected: 302"
    echo ""
    echo "  Test (browser completes challenge — 302 → 200):"
    echo "    Open https://yourdomain.com in a browser"
    echo "    Expected: brief redirect then page loads normally"
    echo ""
    echo "  Test (old static bypass no longer works):"
    echo "    curl -b '_bc=verified' -o /dev/null -s -w '%{http_code}' https://yourdomain.com"
    echo "    Expected: 302 (not 200 — old cookie is invalid)"
    echo ""
    echo "  Test (rate limit on challenge endpoint):"
    echo "    for i in 1 2 3 4 5; do curl -o /dev/null -s -w \"\$i: %{http_code}\n\" https://yourdomain.com${CHALLENGE_PATH}; done"
    echo "    Expected: first 3 return 200, then 444 (rate limited)"
    echo ""
    echo "  Test (allowed bot bypasses challenge):"
    echo "    curl -A 'Googlebot' -o /dev/null -s -w '%{http_code}' https://yourdomain.com"
    echo "    Expected: 200"
}

# =============================================================================
# SUBCOMMAND: block-head
# =============================================================================

cmd_block_head() {
    local REMOVE=false
    local SNIPPET_NAME="nginx.conf_block_head"
    local SNIPPET_CONTENT='# Block HEAD request crawlers/scrapers
# Added by nginx-security.sh block-head
# Returns 444 (drop connection) — no response sent to bot
if ($request_method = HEAD) {
    return 444;
}'

    usage_block_head() {
        cat <<EOF
Usage: sudo $(basename "$0") block-head [OPTIONS]

Block HTTP HEAD requests in Nginx by adding per-domain snippets that return
444 (drop connection) for HEAD method requests.

Works with HestiaCP — adds custom nginx config snippets to each domain's
server block via the conf/web include pattern.

On non-HestiaCP servers, displays the snippet for manual addition.

Options:
  --dry-run       Show what would be done without making changes
  --remove        Remove the block-head snippets from all domains
  -h, --help      Show this help
EOF
        exit 0
    }

    while [[ $# -gt 0 ]]; do
        case "$1" in
            --dry-run)  DRY_RUN=true; shift ;;
            --remove)   REMOVE=true; shift ;;
            -h|--help)  usage_block_head ;;
            *)          echo "Unknown option: $1"; usage_block_head ;;
        esac
    done

    require_root
    require_nginx

    # Check for HestiaCP
    if ! command -v v-list-users &>/dev/null; then
        echo ""
        echo "  HestiaCP not detected. Add the following inside each server block:"
        echo ""
        echo -e "${CYAN}${SNIPPET_CONTENT}${NC}"
        echo ""
        echo "  This blocks HEAD request crawlers/scrapers by dropping the connection."
        echo "  444 returns nothing — no headers, no body. Bots waste their connection."
        return 0
    fi

    # --- Find all HestiaCP domains ---
    _get_all_domain_dirs() {
        local users
        users=$(v-list-users plain 2>/dev/null | cut -f1)

        for user in $users; do
            local user_conf="/home/${user}/conf/web"
            [[ -d "$user_conf" ]] || continue
            for nginx_conf in "${user_conf}"/*/nginx.conf; do
                [[ -f "$nginx_conf" ]] || continue
                dirname "$nginx_conf"
            done
        done
    }

    # =====================================================
    # Remove mode
    # =====================================================
    if [[ "$REMOVE" == "true" ]]; then
        local removed=0

        while IFS= read -r domain_dir; do
            local snippet="${domain_dir}/${SNIPPET_NAME}"
            local ssl_snippet="${domain_dir}/nginx.ssl.conf_block_head"

            for f in "$snippet" "$ssl_snippet"; do
                if [[ -f "$f" ]]; then
                    if [[ "$DRY_RUN" == "true" ]]; then
                        echo "  Would remove: ${f}"
                    else
                        rm -f "$f"
                        info "Removed ${f}"
                    fi
                    ((removed++)) || true
                fi
            done
        done < <(_get_all_domain_dirs)

        if [[ $removed -eq 0 ]]; then
            info "No block-head snippets found — nothing to remove"
            return 0
        fi

        if [[ "$DRY_RUN" == "true" ]]; then
            echo "  Would test and reload nginx"
            return 0
        fi

        if nginx -t 2>/dev/null; then
            systemctl reload nginx
            info "nginx reloaded — HEAD requests are now allowed"
        else
            err "nginx config test failed after removal — check your config"
            exit 1
        fi
        return 0
    fi

    # =====================================================
    # Install mode
    # =====================================================
    local domain_dirs=()
    while IFS= read -r dir; do
        domain_dirs+=("$dir")
    done < <(_get_all_domain_dirs)

    if [[ ${#domain_dirs[@]} -eq 0 ]]; then
        err "No HestiaCP web domains found"
        exit 1
    fi

    info "Found ${#domain_dirs[@]} domain config(s)"
    echo ""

    local created=0
    local skipped=0
    local created_files=()

    for domain_dir in "${domain_dirs[@]}"; do
        for conf_type in "" ".ssl"; do
            local snippet
            if [[ -n "$conf_type" ]]; then
                snippet="${domain_dir}/nginx${conf_type}.conf_block_head"
            else
                snippet="${domain_dir}/${SNIPPET_NAME}"
            fi

            local main_conf="${domain_dir}/nginx${conf_type}.conf"
            [[ -f "$main_conf" ]] || continue

            if [[ -f "$snippet" ]]; then
                info "Already exists: ${snippet}"
                ((skipped++)) || true
                continue
            fi

            if [[ "$DRY_RUN" == "true" ]]; then
                echo "  Would create: ${snippet}"
                ((created++)) || true
            else
                echo "$SNIPPET_CONTENT" > "$snippet"
                created_files+=("$snippet")
                info "Created ${snippet}"
                ((created++)) || true
            fi
        done
    done

    echo ""

    if [[ $created -eq 0 && $skipped -gt 0 ]]; then
        info "HEAD requests are already blocked on all domains"
        return 0
    fi

    if [[ "$DRY_RUN" == "true" ]]; then
        echo ""
        echo "$SNIPPET_CONTENT"
        echo ""
        info "Would create ${created} snippet(s) (${skipped} already exist)"
        info "Would test nginx config and reload"
        return 0
    fi

    # Test nginx config
    info "Testing nginx configuration..."
    if nginx -t 2>&1; then
        echo ""
        info "Config test passed"
        systemctl reload nginx
        info "nginx reloaded — HEAD requests blocked on ${#domain_dirs[@]} domain(s) (444 drop)"
    else
        echo ""
        err "Config test FAILED — rolling back all changes"
        for f in "${created_files[@]}"; do
            rm -f "$f"
            err "Removed ${f}"
        done
        err "nginx was NOT reloaded — your site is unaffected"
        exit 1
    fi

    echo ""
    echo "  Verify with: curl -I https://your-site.com"
    echo "  Expected: curl returns empty reply (connection dropped)"
    echo "  To undo:  sudo $(basename "$0") block-head --remove"
}

# =============================================================================
# SUBCOMMAND: crowdsec
# =============================================================================

cmd_crowdsec() {
    local ENROLLMENT_KEY=""
    local SKIP_ENGINE=false
    local SKIP_BOUNCER=false
    local SKIP_APPSEC=false
    local REMOVE=false
    local ACQUIS_FILE="/etc/crowdsec/acquis.d/nginx.yaml"

    usage_crowdsec() {
        cat <<EOF
Usage: sudo $(basename "$0") crowdsec [OPTIONS]

Installs and configures CrowdSec engine, nginx lua bouncer, and standard nginx
log acquisition. Provides automated bot/attack detection and mitigation.

Get your enrollment key from https://app.crowdsec.net → Settings → Engines → Enroll.

Options:
  --enrollment-key KEY   CrowdSec Console enrollment key (optional)
  --skip-engine          Skip CrowdSec engine installation
  --skip-bouncer         Skip nginx lua bouncer installation
  --skip-appsec          Skip AppSec WAF component installation
  --dry-run              Show what would be done without making changes
  --remove               Remove CrowdSec bouncer and acquisition config
  -h, --help             Show this help

Examples:
  sudo $(basename "$0") crowdsec
  sudo $(basename "$0") crowdsec --enrollment-key clk1234abcd
  sudo $(basename "$0") crowdsec --skip-appsec
  sudo $(basename "$0") crowdsec --dry-run
  sudo $(basename "$0") crowdsec --remove
EOF
        exit 0
    }

    while [[ $# -gt 0 ]]; do
        case "$1" in
            --enrollment-key)  ENROLLMENT_KEY="$2"; shift 2 ;;
            --skip-engine)     SKIP_ENGINE=true; shift ;;
            --skip-bouncer)    SKIP_BOUNCER=true; shift ;;
            --skip-appsec)     SKIP_APPSEC=true; shift ;;
            --dry-run)         DRY_RUN=true; shift ;;
            --remove)          REMOVE=true; shift ;;
            -h|--help)         usage_crowdsec ;;
            *)                 echo "Unknown option: $1"; usage_crowdsec ;;
        esac
    done

    require_root
    require_nginx

    # =====================================================
    # REMOVE MODE
    # =====================================================
    if [[ "$REMOVE" == "true" ]]; then
        step "Removing CrowdSec components"

        if [[ "$DRY_RUN" == "true" ]]; then
            echo "  Would run: apt remove crowdsec-nginx-bouncer"
            echo "  Would remove: ${ACQUIS_FILE}"
            echo "  Would reload nginx"
            echo ""
            echo "  Note: CrowdSec engine is NOT removed by default."
            echo "  To fully remove: apt remove crowdsec"
        else
            if dpkg -l crowdsec-nginx-bouncer &>/dev/null 2>&1; then
                step "Removing nginx bouncer"
                apt-get remove -y crowdsec-nginx-bouncer
                info "Removed: crowdsec-nginx-bouncer"
            else
                warn "crowdsec-nginx-bouncer not installed"
            fi

            if [[ -f "$ACQUIS_FILE" ]]; then
                rm -f "$ACQUIS_FILE"
                info "Removed: ${ACQUIS_FILE}"
            else
                warn "Acquisition file not found: ${ACQUIS_FILE}"
            fi

            if nginx -t 2>&1; then
                systemctl reload nginx
                info "nginx reloaded"
            else
                err "nginx config test failed after removal"
                exit 1
            fi

            if systemctl is-active --quiet crowdsec 2>/dev/null; then
                systemctl restart crowdsec
                info "CrowdSec restarted"
            fi
        fi

        echo ""
        echo -e "${BOLD}CrowdSec bouncer and acquisition removed.${NC}"
        echo ""
        echo "  The CrowdSec engine is still installed."
        echo "  To fully remove: sudo apt remove crowdsec"
        return 0
    fi

    # =====================================================
    # Step 1: Install CrowdSec engine
    # =====================================================
    if [[ "$SKIP_ENGINE" == "true" ]]; then
        warn "Skipping CrowdSec engine installation (--skip-engine)"
    else
        step "Installing CrowdSec engine"

        if [[ "$DRY_RUN" == "true" ]]; then
            echo "  Would run: curl -s https://install.crowdsec.net | bash"
            echo "  Would run: apt install -y crowdsec"
        else
            if ! command -v cscli &>/dev/null; then
                curl -s https://install.crowdsec.net | bash
                apt-get install -y crowdsec
                info "CrowdSec engine installed"
            else
                info "CrowdSec engine already installed"
            fi
        fi

        # Install collections
        step "Installing CrowdSec collections"

        local collections=(
            "crowdsecurity/nginx"
            "crowdsecurity/http-cve"
            "crowdsecurity/base-http-scenarios"
            "crowdsecurity/sshd"
            "crowdsecurity/linux"
        )

        for collection in "${collections[@]}"; do
            if [[ "$DRY_RUN" == "true" ]]; then
                echo "  Would run: cscli collections install ${collection}"
            else
                cscli collections install "$collection" --force 2>/dev/null || true
                info "Installed collection: ${collection}"
            fi
        done
    fi

    # =====================================================
    # Step 2: Configure nginx log acquisition
    # =====================================================
    step "Configuring nginx log acquisition at ${ACQUIS_FILE}"

    local ACQUIS_CONTENT
    read -r -d '' ACQUIS_CONTENT <<'ACQUISEOF' || true
# Nginx log acquisition — generated by nginx-security.sh crowdsec
# Standard nginx access log
filenames:
  - /var/log/nginx/access.log
labels:
  type: nginx
---
# Standard nginx error log
filenames:
  - /var/log/nginx/error.log
labels:
  type: nginx
---
# Site-specific logs (sites-enabled virtual hosts)
filenames:
  - /var/log/nginx/*access*.log
  - /var/log/nginx/*error*.log
labels:
  type: nginx
---
# System auth log (SSH brute-force detection)
filenames:
  - /var/log/auth.log
labels:
  type: syslog
ACQUISEOF

    if [[ "$DRY_RUN" == "true" ]]; then
        echo "  Would create: ${ACQUIS_FILE}"
    else
        mkdir -p "$(dirname "$ACQUIS_FILE")"
        echo "$ACQUIS_CONTENT" > "$ACQUIS_FILE"
        info "Created: ${ACQUIS_FILE}"
    fi

    # =====================================================
    # Step 3: Install nginx lua bouncer
    # =====================================================
    if [[ "$SKIP_BOUNCER" == "true" ]]; then
        warn "Skipping nginx lua bouncer installation (--skip-bouncer)"
    else
        step "Installing nginx lua bouncer dependencies"

        if [[ "$DRY_RUN" == "true" ]]; then
            echo "  Would run: apt install -y lua5.1 libnginx-mod-http-lua luarocks gettext-base lua-cjson"
            echo "  Would run: apt install -y crowdsec-nginx-bouncer"
        else
            apt-get install -y lua5.1 libnginx-mod-http-lua luarocks gettext-base lua-cjson 2>/dev/null || true
            info "Lua dependencies installed"

            step "Installing crowdsec-nginx-bouncer"
            apt-get install -y crowdsec-nginx-bouncer
            info "crowdsec-nginx-bouncer installed"
        fi
    fi

    # =====================================================
    # Step 4: Restart nginx
    # =====================================================
    step "Restarting nginx"
    if [[ "$DRY_RUN" == "true" ]]; then
        echo "  Would run: systemctl restart nginx"
    else
        if nginx -t 2>&1; then
            systemctl restart nginx
            info "nginx restarted"
        else
            err "nginx config test failed"
            exit 1
        fi
    fi

    # =====================================================
    # Step 5: AppSec WAF (optional)
    # =====================================================
    if [[ "$SKIP_APPSEC" == "true" ]]; then
        warn "Skipping AppSec component (--skip-appsec)"
    else
        step "Installing AppSec collections"

        local appsec_collections=(
            "crowdsecurity/appsec-virtual-patching"
            "crowdsecurity/appsec-generic-rules"
        )

        for collection in "${appsec_collections[@]}"; do
            if [[ "$DRY_RUN" == "true" ]]; then
                echo "  Would run: cscli collections install ${collection}"
            else
                cscli collections install "$collection" --force 2>/dev/null || true
                info "Installed collection: ${collection}"
            fi
        done

        step "Adding AppSec acquisition block to ${ACQUIS_FILE}"

        local APPSEC_BLOCK
        read -r -d '' APPSEC_BLOCK <<'APPSECEOF' || true
---
# AppSec Component
listen_addr: 127.0.0.1:7422
appsec_config: crowdsecurity/appsec-default
name: nginx_appsec
source: appsec
labels:
  type: appsec
APPSECEOF

        if [[ "$DRY_RUN" == "true" ]]; then
            echo "  Would append AppSec block to: ${ACQUIS_FILE}"
        else
            echo "" >> "$ACQUIS_FILE"
            echo "$APPSEC_BLOCK" >> "$ACQUIS_FILE"
            info "AppSec acquisition block added to: ${ACQUIS_FILE}"
        fi
    fi

    # =====================================================
    # Step 6: Console enrollment (optional)
    # =====================================================
    if [[ -n "$ENROLLMENT_KEY" ]]; then
        step "Enrolling in CrowdSec Console"

        if [[ "$DRY_RUN" == "true" ]]; then
            echo "  Would run: cscli console enroll ${ENROLLMENT_KEY}"
            echo "  Would run: systemctl restart crowdsec"
        else
            cscli console enroll "$ENROLLMENT_KEY"
            info "Enrolled in CrowdSec Console"
            systemctl restart crowdsec
            info "CrowdSec restarted"
        fi
    fi

    # =====================================================
    # Step 7: Restart CrowdSec to pick up acquisition
    # =====================================================
    step "Restarting CrowdSec"
    if [[ "$DRY_RUN" == "true" ]]; then
        echo "  Would run: systemctl restart crowdsec"
    else
        systemctl restart crowdsec
        info "CrowdSec restarted"
    fi

    # =====================================================
    # Step 8: Verify
    # =====================================================
    step "Verifying installation"

    if [[ "$DRY_RUN" == "true" ]]; then
        echo "  Would run: cscli bouncers list"
        echo "  Would run: nginx -t"
    else
        echo ""
        echo "  Registered bouncers:"
        cscli bouncers list 2>/dev/null || warn "Could not list bouncers"
        echo ""

        if nginx -t 2>&1; then
            info "nginx config valid"
        else
            err "nginx config test failed"
        fi
    fi

    # =====================================================
    # Summary
    # =====================================================
    echo ""
    echo -e "${BOLD}Done.${NC}"
    echo ""
    echo "  Acquisition: ${ACQUIS_FILE}"
    if [[ "$SKIP_ENGINE" != "true" ]]; then
        echo "  Engine:      CrowdSec installed"
    fi
    if [[ "$SKIP_BOUNCER" != "true" ]]; then
        echo "  Bouncer:     crowdsec-nginx-bouncer installed"
    fi
    if [[ "$SKIP_APPSEC" != "true" ]]; then
        echo "  AppSec:      WAF component configured"
    fi
    if [[ -n "$ENROLLMENT_KEY" ]]; then
        echo "  Console:     Enrolled (accept in CrowdSec Console web UI)"
    fi
    echo ""
    echo "  Get enrollment key: https://app.crowdsec.net → Settings → Engines → Enroll"
    echo ""
    echo "  Useful commands:"
    echo "    cscli decisions list      — show active decisions (bans)"
    echo "    cscli alerts list         — show recent alerts"
    echo "    cscli bouncers list       — show registered bouncers"
    echo "    cscli metrics             — show processing metrics"
    echo "    cscli hub update          — update hub index"
    echo ""
    echo "  To remove:    sudo $(basename "$0") crowdsec --remove"
}

# =============================================================================
# SUBCOMMAND: status
# =============================================================================

cmd_status() {
    while [[ $# -gt 0 ]]; do
        case "$1" in
            -h|--help)
                echo "Usage: sudo $(basename "$0") status"
                echo ""
                echo "Show status of all security features."
                exit 0
                ;;
            *)  echo "Unknown option: $1"; exit 1 ;;
        esac
    done

    echo ""
    echo -e "${BOLD}=== Nginx Security Status ===${NC}"
    echo ""

    # --- Bot-block ---
    echo -e "${CYAN}Bot-block:${NC}"
    local bot_map="/etc/nginx/conf.d/bot-block.conf"
    if [[ -f "$bot_map" ]]; then
        local bot_count
        bot_count=$(grep -c '~\*' "$bot_map" 2>/dev/null || echo "0")
        echo -e "  Status: ${GREEN}Active${NC}"
        echo "  Map: ${bot_map}"
        echo "  Bot patterns: ${bot_count}"
    else
        echo -e "  Status: ${YELLOW}Not configured${NC}"
    fi
    echo ""

    # --- JS challenge ---
    echo -e "${CYAN}JS Challenge:${NC}"
    local jsc_map="/etc/nginx/conf.d/js-challenge.conf"
    local jsc_state="/etc/nginx/js-challenge.env"
    if [[ -f "$jsc_map" ]]; then
        echo -e "  Status: ${GREEN}Active${NC}"
        echo "  Map: ${jsc_map}"
        if [[ -f "$jsc_state" ]]; then
            local _cookie_name=""
            _cookie_name=$(grep "^COOKIE_NAME=" "$jsc_state" 2>/dev/null | cut -d"'" -f2)
            if [[ -n "$_cookie_name" ]]; then
                echo "  Cookie name: ${_cookie_name}"
            fi
        fi
    else
        echo -e "  Status: ${YELLOW}Not configured${NC}"
    fi
    echo ""

    # --- Block HEAD ---
    echo -e "${CYAN}Block HEAD:${NC}"
    local head_count=0
    if command -v v-list-users &>/dev/null; then
        head_count=$(find /home/*/conf/web/ -name "nginx.conf_block_head" 2>/dev/null | wc -l || echo "0")
    fi
    if [[ $head_count -gt 0 ]]; then
        echo -e "  Status: ${GREEN}Active${NC}"
        echo "  Snippets: ${head_count} domain(s)"
    else
        echo -e "  Status: ${YELLOW}Not configured${NC}"
    fi
    echo ""

    # --- CrowdSec ---
    echo -e "${CYAN}CrowdSec:${NC}"
    if command -v cscli &>/dev/null; then
        if systemctl is-active --quiet crowdsec 2>/dev/null; then
            echo -e "  Engine: ${GREEN}Running${NC}"
        else
            echo -e "  Engine: ${RED}Stopped${NC}"
        fi

        if dpkg -l crowdsec-nginx-bouncer &>/dev/null 2>&1; then
            echo -e "  Bouncer: ${GREEN}Installed${NC}"
            local bouncer_count
            bouncer_count=$(cscli bouncers list -o raw 2>/dev/null | tail -n +2 | wc -l || echo "0")
            echo "  Registered bouncers: ${bouncer_count}"
        else
            echo -e "  Bouncer: ${YELLOW}Not installed${NC}"
        fi

        local decision_count
        decision_count=$(cscli decisions list -o raw 2>/dev/null | tail -n +2 | wc -l || echo "0")
        echo "  Active decisions: ${decision_count}"
    else
        echo -e "  Status: ${YELLOW}Not installed${NC}"
    fi
    echo ""

    # --- Nginx ---
    echo -e "${CYAN}Nginx:${NC}"
    if systemctl is-active --quiet nginx 2>/dev/null; then
        echo -e "  Status: ${GREEN}Running${NC}"
    else
        echo -e "  Status: ${RED}Stopped${NC}"
    fi

    if command -v nginx &>/dev/null; then
        if nginx -t 2>/dev/null; then
            echo -e "  Config: ${GREEN}Valid${NC}"
        else
            echo -e "  Config: ${RED}Invalid${NC}"
        fi
    fi
    echo ""
}

# =============================================================================
# MAIN DISPATCH
# =============================================================================

case "${1:-}" in
    bot-block)     shift; cmd_bot_block "$@" ;;
    js-challenge)  shift; cmd_js_challenge "$@" ;;
    block-head)    shift; cmd_block_head "$@" ;;
    crowdsec)      shift; cmd_crowdsec "$@" ;;
    status)        shift; cmd_status "$@" ;;
    -h|--help|"")  usage_main ;;
    *)             err "Unknown command: $1"; usage_main ;;
esac
