Files
crowdsec-manager/crowdsec-manager.sh

1755 lines
52 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# ============================================================================
# CrowdSec Manager
# ============================================================================
# Verwaltet automatisch eine CrowdSec-Allowlist und bietet interaktive
# CrowdSec-Administration über ein Menü.
#
# Autor: Patrick Asmus
# www.patrick-asmus.de
# Lizenz: MIT
# ============================================================================
set -euo pipefail
# ----------------------------------------------------------------------------
# Globale Variablen
# ----------------------------------------------------------------------------
SCRIPT_NAME="$(basename "$0")"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SCRIPT_VERSION="0.2.1"
CONFIG_FILE="${SCRIPT_DIR}/config/crowdsec-manager.conf"
# Farben (werden ggf. deaktiviert wenn nicht im Terminal)
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
MAGENTA='\033[0;35m'
BOLD='\033[1m'
DIM='\033[2m'
NC='\033[0m'
# Statistiken
STAT_ADDED=0
STAT_REMOVED=0
STAT_UNCHANGED=0
STAT_ERRORS=0
STAT_RESOLVED_DOMAINS=0
STAT_TOTAL_IPS=0
REPORT_MESSAGES=()
# Temporäre Dateien
TEMP_DIR=""
CURRENT_IPS_FILE=""
NEW_IPS_FILE=""
# ============================================================================
# TEIL 1: HILFSFUNKTIONEN
# ============================================================================
# Farben deaktivieren wenn nicht im Terminal oder wenn als Cron ausgeführt
disable_colors_if_needed() {
if [[ ! -t 1 ]] || [[ -n "${CRON:-}" ]]; then
RED=""
GREEN=""
YELLOW=""
BLUE=""
CYAN=""
MAGENTA=""
BOLD=""
DIM=""
NC=""
fi
}
# Trennlinie zeichnen
draw_line() {
local char="${1:--}"
local width="${2:-60}"
printf '%*s\n' "$width" '' | tr ' ' "$char"
}
# Logging-Funktion
log() {
local level="$1"
shift
local message="$*"
local timestamp
timestamp="$(date '+%Y-%m-%d %H:%M:%S')"
local -A levels=([DEBUG]=0 [INFO]=1 [WARN]=2 [ERROR]=3)
local current_level="${levels[${LOG_LEVEL:-INFO}]:-1}"
local msg_level="${levels[$level]:-1}"
if [[ $msg_level -lt $current_level ]]; then
return 0
fi
local color=""
case "$level" in
DEBUG) color="$CYAN" ;;
INFO) color="$GREEN" ;;
WARN) color="$YELLOW" ;;
ERROR) color="$RED" ;;
esac
local log_line="[${timestamp}] [${level}] ${message}"
local colored_line="${color}[${timestamp}] [${BOLD}${level}${NC}${color}] ${message}${NC}"
echo -e "$colored_line"
if [[ -n "${LOG_FILE:-}" ]]; then
echo "$log_line" >> "$LOG_FILE" 2>/dev/null || true
fi
}
# Log-Rotation
rotate_logs() {
if [[ -z "${LOG_FILE:-}" ]] || [[ "${LOG_MAX_SIZE_KB:-0}" -eq 0 ]]; then
return 0
fi
if [[ ! -f "$LOG_FILE" ]]; then
return 0
fi
local size_kb
size_kb=$(du -k "$LOG_FILE" 2>/dev/null | cut -f1)
if [[ "$size_kb" -ge "$LOG_MAX_SIZE_KB" ]]; then
log "INFO" "Log-Rotation wird durchgeführt (${size_kb}KB >= ${LOG_MAX_SIZE_KB}KB)"
for ((i = LOG_ROTATE_COUNT - 1; i >= 1; i--)); do
local prev=$((i - 1))
if [[ -f "${LOG_FILE}.${prev}" ]]; then
mv "${LOG_FILE}.${prev}" "${LOG_FILE}.${i}"
fi
done
if [[ -f "$LOG_FILE" ]]; then
mv "$LOG_FILE" "${LOG_FILE}.0"
fi
touch "$LOG_FILE"
fi
}
# Temporäre Dateien
setup_temp() {
TEMP_DIR="$(mktemp -d /tmp/crowdsec-manager-XXXXXX)"
CURRENT_IPS_FILE="${TEMP_DIR}/current_ips.txt"
NEW_IPS_FILE="${TEMP_DIR}/new_ips.txt"
touch "$CURRENT_IPS_FILE" "$NEW_IPS_FILE"
}
cleanup_temp() {
if [[ -n "${TEMP_DIR:-}" ]] && [[ -d "$TEMP_DIR" ]]; then
rm -rf "$TEMP_DIR"
fi
}
# Lock-File Management
acquire_lock() {
if [[ "${LOCK_ENABLED:-false}" != "true" ]]; then
return 0
fi
local lock_file="${LOCK_FILE:-/tmp/crowdsec-manager.lock}"
local timeout="${LOCK_TIMEOUT:-300}"
local waited=0
while [[ -f "$lock_file" ]]; do
local lock_pid
lock_pid=$(cat "$lock_file" 2>/dev/null || echo "")
if [[ -n "$lock_pid" ]] && ! kill -0 "$lock_pid" 2>/dev/null; then
log "WARN" "Verwaistes Lock-File gefunden (PID: $lock_pid). Wird entfernt."
rm -f "$lock_file"
break
fi
if [[ "$timeout" -eq 0 ]] || [[ "$waited" -ge "$timeout" ]]; then
log "ERROR" "Lock-File existiert bereits (PID: $lock_pid). Abbruch."
return 1
fi
log "DEBUG" "Warte auf Lock-Freigabe... (${waited}s/${timeout}s)"
sleep 5
waited=$((waited + 5))
done
echo $$ > "$lock_file"
log "DEBUG" "Lock erworben (PID: $$)"
}
release_lock() {
if [[ "${LOCK_ENABLED:-false}" != "true" ]]; then
return 0
fi
local lock_file="${LOCK_FILE:-/tmp/crowdsec-manager.lock}"
if [[ -f "$lock_file" ]]; then
local lock_pid
lock_pid=$(cat "$lock_file" 2>/dev/null || echo "")
if [[ "$lock_pid" == "$$" ]]; then
rm -f "$lock_file"
log "DEBUG" "Lock freigegeben (PID: $$)"
fi
fi
}
# ============================================================================
# TEIL 2: VALIDIERUNG
# ============================================================================
is_valid_ipv4() {
local ip="$1"
local regex='^([0-9]{1,3}\.){3}[0-9]{1,3}$'
if [[ "$ip" =~ $regex ]]; then
local IFS='.'
read -ra octets <<< "$ip"
for octet in "${octets[@]}"; do
if [[ "$octet" -gt 255 ]]; then
return 1
fi
done
return 0
fi
return 1
}
is_valid_ipv6() {
local ip="$1"
if [[ "$ip" =~ ^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$ ]] || \
[[ "$ip" =~ ^([0-9a-fA-F]{0,4}:){1,7}:$ ]] || \
[[ "$ip" =~ ^::([0-9a-fA-F]{0,4}:){0,6}[0-9a-fA-F]{0,4}$ ]] || \
[[ "$ip" =~ ^[0-9a-fA-F]{1,4}(:[0-9a-fA-F]{1,4}){7}$ ]]; then
return 0
fi
return 1
}
is_cidr() {
local entry="$1"
[[ "$entry" =~ / ]]
}
is_ip_or_cidr() {
local entry="$1"
if is_cidr "$entry"; then
local ip="${entry%/*}"
local mask="${entry#*/}"
if is_valid_ipv4 "$ip"; then
[[ "$mask" -ge 0 && "$mask" -le 32 ]] 2>/dev/null && return 0
elif is_valid_ipv6 "$ip"; then
[[ "$mask" -ge 0 && "$mask" -le 128 ]] 2>/dev/null && return 0
fi
return 1
fi
is_valid_ipv4 "$entry" || is_valid_ipv6 "$entry"
}
is_domain() {
local entry="$1"
if ! is_valid_ipv4 "$entry" && ! is_valid_ipv6 "$entry" && ! is_cidr "$entry"; then
if [[ "$entry" =~ ^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*\.[a-zA-Z]{2,}$ ]]; then
return 0
fi
fi
return 1
}
# ============================================================================
# TEIL 3: DNS-AUFLÖSUNG
# ============================================================================
resolve_domain() {
local domain="$1"
local resolved_ips=()
log "DEBUG" "Löse Domain auf: $domain"
local dig_opts="+short +timeout=${DNS_TIMEOUT:-5} +retry=${DNS_RETRIES:-3}"
if [[ -n "${DNS_SERVER:-}" ]]; then
dig_opts+=" @${DNS_SERVER}"
fi
# IPv4 (A Records)
local ipv4_results
ipv4_results=$(dig $dig_opts A "$domain" 2>/dev/null | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' || true)
if [[ -n "$ipv4_results" ]]; then
while IFS= read -r ip; do
if is_valid_ipv4 "$ip"; then
resolved_ips+=("$ip")
log "DEBUG" " IPv4 aufgelöst: $domain -> $ip"
fi
done <<< "$ipv4_results"
fi
# IPv6 (AAAA Records)
if [[ "${RESOLVE_IPV6:-true}" == "true" ]]; then
local ipv6_results
ipv6_results=$(dig $dig_opts AAAA "$domain" 2>/dev/null | grep -E '^[0-9a-fA-F:]+$' | grep ':' || true)
if [[ -n "$ipv6_results" ]]; then
while IFS= read -r ip; do
if is_valid_ipv6 "$ip"; then
resolved_ips+=("$ip")
log "DEBUG" " IPv6 aufgelöst: $domain -> $ip"
fi
done <<< "$ipv6_results"
fi
fi
if [[ ${#resolved_ips[@]} -eq 0 ]]; then
log "WARN" "Keine IPs für Domain '$domain' aufgelöst"
STAT_ERRORS=$((STAT_ERRORS + 1))
REPORT_MESSAGES+=("WARN: Keine Auflösung für $domain")
return 1
fi
STAT_RESOLVED_DOMAINS=$((STAT_RESOLVED_DOMAINS + 1))
printf '%s\n' "${resolved_ips[@]}"
}
resolve_all_entries() {
log "INFO" "Starte DNS-Auflösung für ${#ALLOWLIST_ENTRIES[@]} Einträge..."
for entry in "${ALLOWLIST_ENTRIES[@]}"; do
entry=$(echo "$entry" | sed 's/#.*$//' | xargs)
[[ -z "$entry" ]] && continue
if is_ip_or_cidr "$entry"; then
log "DEBUG" "Direkter Eintrag: $entry"
echo "$entry" >> "$NEW_IPS_FILE"
STAT_TOTAL_IPS=$((STAT_TOTAL_IPS + 1))
elif is_domain "$entry"; then
local resolved
if resolved=$(resolve_domain "$entry"); then
while IFS= read -r ip; do
echo "$ip" >> "$NEW_IPS_FILE"
STAT_TOTAL_IPS=$((STAT_TOTAL_IPS + 1))
done <<< "$resolved"
fi
else
log "WARN" "Unbekannter Eintragstyp: '$entry' - wird übersprungen"
STAT_ERRORS=$((STAT_ERRORS + 1))
fi
done
if [[ -f "$NEW_IPS_FILE" ]]; then
sort -u "$NEW_IPS_FILE" -o "$NEW_IPS_FILE"
STAT_TOTAL_IPS=$(wc -l < "$NEW_IPS_FILE")
fi
log "INFO" "${STAT_TOTAL_IPS} eindeutige IPs/CIDRs aus ${STAT_RESOLVED_DOMAINS} aufgelösten Domains ermittelt"
}
# ============================================================================
# TEIL 4: CROWDSEC INTERAKTION (Kern-Funktionen)
# ============================================================================
# CrowdSec CLI Wrapper
cscli_exec() {
eval "$CSCLI_CMD $*"
}
crowdsec_health_check() {
if [[ "${HEALTH_CHECK:-true}" != "true" ]]; then
return 0
fi
log "INFO" "Prüfe CrowdSec-Erreichbarkeit..."
if ! cscli_exec "version" &>/dev/null; then
log "ERROR" "CrowdSec ist nicht erreichbar! Befehl: '$CSCLI_CMD version'"
REPORT_MESSAGES+=("ERROR: CrowdSec nicht erreichbar")
return 1
fi
log "INFO" "CrowdSec ist erreichbar"
return 0
}
ensure_allowlist_exists() {
# Prüfe ob die Allowlist bereits existiert
if cscli_exec "allowlists inspect ${ALLOWLIST_NAME}" &>/dev/null; then
log "DEBUG" "Allowlist '${ALLOWLIST_NAME}' existiert bereits"
return 0
fi
# Allowlist erstellen
log "INFO" "Erstelle Allowlist '${ALLOWLIST_NAME}'..."
if cscli_exec "allowlists create ${ALLOWLIST_NAME} -d '${ALLOWLIST_DESCRIPTION}'" 2>&1; then
log "INFO" "Allowlist '${ALLOWLIST_NAME}' erfolgreich erstellt"
return 0
else
log "ERROR" "Fehler beim Erstellen der Allowlist '${ALLOWLIST_NAME}'"
return 1
fi
}
get_current_allowlist() {
log "INFO" "Rufe aktuelle Allowlist '${ALLOWLIST_NAME}' ab..."
if ! ensure_allowlist_exists; then
return 1
fi
# Aktuelle IPs aus der Allowlist auslesen
local inspect_output
inspect_output=$(cscli_exec "allowlists inspect ${ALLOWLIST_NAME} -o json" 2>/dev/null || echo "null")
if [[ "$inspect_output" == "null" ]] || [[ -z "$inspect_output" ]]; then
log "DEBUG" "Keine bestehenden Einträge in der Allowlist gefunden"
return 0
fi
# IPs/CIDRs aus dem JSON extrahieren (Feld "items" -> "value")
echo "$inspect_output" | grep -oP '"value"\s*:\s*"\K[^"]+' | sort -u > "$CURRENT_IPS_FILE" 2>/dev/null || true
local count
count=$(wc -l < "$CURRENT_IPS_FILE" 2>/dev/null || echo "0")
log "INFO" "${count} bestehende Einträge in der Allowlist gefunden"
}
add_to_allowlist() {
local ip="$1"
log "DEBUG" "Füge hinzu: $ip"
if [[ "${DRY_RUN:-false}" == "true" ]]; then
log "INFO" "[DRY-RUN] Würde hinzufügen: $ip"
return 0
fi
local result
if result=$(cscli_exec "allowlists add ${ALLOWLIST_NAME} $ip -d '${ALLOWLIST_REASON}'" 2>&1); then
log "DEBUG" "Erfolgreich hinzugefügt: $ip"
STAT_ADDED=$((STAT_ADDED + 1))
return 0
else
log "ERROR" "Fehler beim Hinzufügen von $ip: $result"
STAT_ERRORS=$((STAT_ERRORS + 1))
REPORT_MESSAGES+=("ERROR: Konnte $ip nicht hinzufügen: $result")
return 1
fi
}
remove_from_allowlist() {
local ip="$1"
log "DEBUG" "Entferne: $ip"
if [[ "${DRY_RUN:-false}" == "true" ]]; then
log "INFO" "[DRY-RUN] Würde entfernen: $ip"
return 0
fi
local result
if result=$(cscli_exec "allowlists remove ${ALLOWLIST_NAME} $ip" 2>&1); then
log "DEBUG" "Erfolgreich entfernt: $ip"
STAT_REMOVED=$((STAT_REMOVED + 1))
return 0
else
log "ERROR" "Fehler beim Entfernen von $ip: $result"
STAT_ERRORS=$((STAT_ERRORS + 1))
REPORT_MESSAGES+=("ERROR: Konnte $ip nicht entfernen: $result")
return 1
fi
}
sync_allowlist() {
log "INFO" "Synchronisiere Allowlist..."
local to_add
to_add=$(comm -23 "$NEW_IPS_FILE" "$CURRENT_IPS_FILE" 2>/dev/null || true)
local to_remove=""
if [[ "${AUTO_CLEANUP:-true}" == "true" ]]; then
to_remove=$(comm -13 "$NEW_IPS_FILE" "$CURRENT_IPS_FILE" 2>/dev/null || true)
fi
local unchanged
unchanged=$(comm -12 "$NEW_IPS_FILE" "$CURRENT_IPS_FILE" 2>/dev/null || true)
if [[ -n "$unchanged" ]]; then
STAT_UNCHANGED=$(echo "$unchanged" | wc -l)
fi
# Keine Änderungen erkannt → frühzeitig zurückkehren
if [[ -z "$to_add" ]] && [[ -z "$to_remove" ]]; then
log "INFO" "Keine Änderungen erkannt - Allowlist ist bereits aktuell"
return 1
fi
if [[ -n "$to_add" ]]; then
log "INFO" "Neue IPs zum Hinzufügen gefunden:"
while IFS= read -r ip; do
[[ -z "$ip" ]] && continue
log "INFO" " + $ip"
add_to_allowlist "$ip"
done <<< "$to_add"
fi
if [[ -n "$to_remove" ]]; then
log "INFO" "Veraltete IPs zum Entfernen gefunden:"
while IFS= read -r ip; do
[[ -z "$ip" ]] && continue
log "INFO" " - $ip"
remove_from_allowlist "$ip"
done <<< "$to_remove"
fi
return 0
}
# ============================================================================
# TEIL 5: BACKUP
# ============================================================================
create_backup() {
if [[ "${BACKUP_ENABLED:-false}" != "true" ]]; then
return 0
fi
log "INFO" "Erstelle Backup..."
local backup_dir="${BACKUP_DIR:-/var/backup/crowdsec-manager}"
mkdir -p "$backup_dir"
local backup_file="${backup_dir}/allowlist_$(date '+%Y%m%d_%H%M%S').bak"
if [[ -f "$CURRENT_IPS_FILE" ]]; then
cp "$CURRENT_IPS_FILE" "$backup_file"
log "INFO" "Backup erstellt: $backup_file"
fi
local retain=${BACKUP_RETAIN_COUNT:-7}
local backups
backups=$(find "$backup_dir" -name "allowlist_*.bak" -type f 2>/dev/null | sort -r)
local count=0
while IFS= read -r file; do
count=$((count + 1))
if [[ $count -gt $retain ]]; then
rm -f "$file"
log "DEBUG" "Altes Backup gelöscht: $file"
fi
done <<< "$backups"
}
# ============================================================================
# TEIL 6: BENACHRICHTIGUNGEN (Ntfy, Gotify, E-Mail, Desktop)
# ============================================================================
build_notification_message() {
local status="OK"
if [[ $STAT_ERRORS -gt 0 ]]; then
status="FEHLER"
fi
local msg=""
msg+="=== CrowdSec Manager Report ===\n"
msg+="Status: $status\n"
msg+="Zeitpunkt: $(date '+%Y-%m-%d %H:%M:%S')\n"
msg+="---\n"
msg+="Domains aufgelöst: $STAT_RESOLVED_DOMAINS\n"
msg+="IPs gesamt: $STAT_TOTAL_IPS\n"
msg+="Hinzugefügt: $STAT_ADDED\n"
msg+="Entfernt: $STAT_REMOVED\n"
msg+="Unverändert: $STAT_UNCHANGED\n"
msg+="Fehler: $STAT_ERRORS\n"
if [[ ${#REPORT_MESSAGES[@]} -gt 0 ]]; then
msg+="---\n"
msg+="Details:\n"
for m in "${REPORT_MESSAGES[@]}"; do
msg+=" $m\n"
done
fi
if [[ "${DRY_RUN:-false}" == "true" ]]; then
msg+="---\n"
msg+="HINWEIS: Dry-Run Modus - Keine Änderungen durchgeführt\n"
fi
echo -e "$msg"
}
should_notify() {
local notify_on="${NOTIFY_ON:-changes}"
case "$notify_on" in
always) return 0 ;;
changes)
if [[ $STAT_ADDED -gt 0 ]] || [[ $STAT_REMOVED -gt 0 ]] || [[ $STAT_ERRORS -gt 0 ]]; then
return 0
fi
return 1
;;
errors)
[[ $STAT_ERRORS -gt 0 ]] && return 0
return 1
;;
esac
return 1
}
send_notify_desktop() {
if [[ "${NOTIFY_DESKTOP_ENABLED:-false}" != "true" ]]; then
return 0
fi
if ! command -v notify-send &>/dev/null; then
log "WARN" "notify-send nicht gefunden. Desktop-Benachrichtigung übersprungen."
return 1
fi
local urgency="normal"
[[ $STAT_ERRORS -gt 0 ]] && urgency="critical"
local summary="CrowdSec Allowlist: +${STAT_ADDED} -${STAT_REMOVED} (${STAT_ERRORS} Fehler)"
notify-send -u "$urgency" "CrowdSec Manager" "$summary"
log "DEBUG" "Desktop-Benachrichtigung gesendet"
}
send_notify_ntfy() {
if [[ "${NOTIFY_NTFY_ENABLED:-false}" != "true" ]]; then
return 0
fi
if ! command -v curl &>/dev/null; then
log "WARN" "curl nicht gefunden. Ntfy-Benachrichtigung übersprungen."
return 1
fi
local message
message=$(build_notification_message)
local title="CrowdSec Manager"
local priority="${NOTIFY_NTFY_PRIORITY:-default}"
local tags="${NOTIFY_NTFY_TAGS:-shield}"
if [[ $STAT_ERRORS -gt 0 ]]; then
priority="high"
tags="warning,shield"
fi
local curl_opts=(
-s
-H "Title: $title"
-H "Priority: $priority"
-H "Tags: $tags"
)
if [[ -n "${NOTIFY_NTFY_TOKEN:-}" ]]; then
curl_opts+=(-H "Authorization: Bearer ${NOTIFY_NTFY_TOKEN}")
fi
local url="${NOTIFY_NTFY_URL:-https://ntfy.sh}/${NOTIFY_NTFY_TOPIC:-crowdsec-manager}"
if curl "${curl_opts[@]}" -d "$message" "$url" &>/dev/null; then
log "DEBUG" "Ntfy-Benachrichtigung gesendet"
else
log "WARN" "Ntfy-Benachrichtigung fehlgeschlagen"
fi
}
send_notify_gotify() {
if [[ "${NOTIFY_GOTIFY_ENABLED:-false}" != "true" ]]; then
return 0
fi
if ! command -v curl &>/dev/null; then
log "WARN" "curl nicht gefunden. Gotify-Benachrichtigung übersprungen."
return 1
fi
local gotify_url="${NOTIFY_GOTIFY_URL:-}"
local gotify_token="${NOTIFY_GOTIFY_TOKEN:-}"
if [[ -z "$gotify_url" ]] || [[ -z "$gotify_token" ]]; then
log "WARN" "Gotify URL oder Token nicht konfiguriert."
return 1
fi
local message
message=$(build_notification_message)
local title="CrowdSec Manager"
local priority="${NOTIFY_GOTIFY_PRIORITY:-5}"
# Bei Fehlern höhere Priorität
if [[ $STAT_ERRORS -gt 0 ]]; then
priority=8
fi
local api_url="${gotify_url}/message?token=${gotify_token}"
# JSON-Payload erstellen Nachricht für JSON escapen
local escaped_message
escaped_message=$(echo "$message" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))" 2>/dev/null || \
echo "$message" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g' | sed ':a;N;$!ba;s/\n/\\n/g' | sed 's/^/"/;s/$/"/')
local escaped_title
escaped_title=$(echo "$title" | sed 's/"/\\"/g')
if curl -s -X POST "$api_url" \
-H "Content-Type: application/json" \
-d "{\"title\":\"${escaped_title}\",\"message\":${escaped_message},\"priority\":${priority}}" \
&>/dev/null; then
log "DEBUG" "Gotify-Benachrichtigung gesendet an ${gotify_url}"
else
log "WARN" "Gotify-Benachrichtigung fehlgeschlagen"
fi
}
send_notify_email() {
if [[ "${NOTIFY_EMAIL_ENABLED:-false}" != "true" ]]; then
return 0
fi
local message
message=$(build_notification_message)
local to="${NOTIFY_EMAIL_TO:-}"
local from="${NOTIFY_EMAIL_FROM:-crowdsec@$(hostname)}"
local subject="${NOTIFY_EMAIL_SUBJECT:-CrowdSec Manager Report}"
if command -v curl &>/dev/null && [[ -n "${NOTIFY_EMAIL_SMTP_SERVER:-}" ]]; then
local smtp_url="smtp://${NOTIFY_EMAIL_SMTP_SERVER}:${NOTIFY_EMAIL_SMTP_PORT:-587}"
if [[ "${NOTIFY_EMAIL_SMTP_TLS:-true}" == "true" ]]; then
smtp_url="smtps://${NOTIFY_EMAIL_SMTP_SERVER}:${NOTIFY_EMAIL_SMTP_PORT:-465}"
fi
local email_body
email_body="From: ${from}\r\nTo: ${to}\r\nSubject: ${subject}\r\n\r\n${message}"
local curl_opts=(
-s
--url "$smtp_url"
--mail-from "$from"
--mail-rcpt "$to"
)
if [[ -n "${NOTIFY_EMAIL_SMTP_USER:-}" ]]; then
curl_opts+=(--user "${NOTIFY_EMAIL_SMTP_USER}:${NOTIFY_EMAIL_SMTP_PASS:-}")
fi
if echo -e "$email_body" | curl "${curl_opts[@]}" -T - 2>/dev/null; then
log "DEBUG" "E-Mail-Benachrichtigung gesendet an $to"
else
log "WARN" "E-Mail-Benachrichtigung fehlgeschlagen"
fi
elif command -v mail &>/dev/null; then
echo -e "$message" | mail -s "$subject" -r "$from" "$to"
log "DEBUG" "E-Mail-Benachrichtigung gesendet an $to (via mail)"
else
log "WARN" "Weder curl noch mail verfügbar. E-Mail-Benachrichtigung übersprungen."
fi
}
send_notifications() {
if [[ "${NOTIFY_ENABLED:-false}" != "true" ]]; then
return 0
fi
if ! should_notify; then
log "DEBUG" "Keine Benachrichtigung erforderlich (Bedingung: ${NOTIFY_ON:-changes})"
return 0
fi
log "INFO" "Sende Benachrichtigungen..."
send_notify_desktop
send_notify_ntfy
send_notify_gotify
send_notify_email
}
# ============================================================================
# TEIL 7: CROWDSEC ADMIN-FUNKTIONEN (Interaktiv)
# ============================================================================
# ---[ Metriken anzeigen ]---
admin_show_metrics() {
echo ""
echo -e "${BOLD}${BLUE}=== CrowdSec Metriken ===${NC}"
draw_line "=" 60
echo ""
cscli_exec "metrics" 2>/dev/null || {
echo -e "${RED}Fehler beim Abrufen der Metriken.${NC}"
}
echo ""
}
# ---[ Alle Decisions anzeigen ]---
admin_list_decisions() {
echo ""
echo -e "${BOLD}${BLUE}=== Aktuelle Decisions ===${NC}"
draw_line "=" 60
echo ""
cscli_exec "decisions list" 2>/dev/null || {
echo -e "${YELLOW}Keine Decisions vorhanden oder Fehler beim Abrufen.${NC}"
}
echo ""
}
# ---[ Decision für eine IP suchen ]---
admin_search_decision() {
echo ""
echo -e "${BOLD}${BLUE}=== Decision suchen ===${NC}"
echo -n -e "${CYAN}IP-Adresse eingeben: ${NC}"
read -r search_ip
if [[ -z "$search_ip" ]]; then
echo -e "${YELLOW}Keine IP eingegeben.${NC}"
return
fi
echo ""
echo -e "${DIM}Suche nach Decisions für: $search_ip${NC}"
draw_line "-" 60
cscli_exec "decisions list -i '$search_ip'" 2>/dev/null || {
echo -e "${YELLOW}Keine Decisions für $search_ip gefunden.${NC}"
}
echo ""
}
# ---[ Decision manuell hinzufügen ]---
admin_add_decision() {
echo ""
echo -e "${BOLD}${BLUE}=== Decision manuell hinzufügen ===${NC}"
echo ""
# IP eingeben
echo -n -e "${CYAN}IP-Adresse oder CIDR: ${NC}"
read -r add_ip
if [[ -z "$add_ip" ]]; then
echo -e "${YELLOW}Keine IP eingegeben. Abbruch.${NC}"
return
fi
# Typ wählen
echo ""
echo -e "${BOLD}Typ auswählen:${NC}"
echo " 1) ban - IP sperren"
echo " 2) allow - IP erlauben (Allowlist)"
echo " 3) captcha - Captcha anzeigen"
echo " 4) throttle - Drosselung"
echo -n -e "${CYAN}Auswahl [1-4]: ${NC}"
read -r type_choice
local decision_type
case "$type_choice" in
1) decision_type="ban" ;;
2) decision_type="allow" ;;
3) decision_type="captcha" ;;
4) decision_type="throttle" ;;
*)
echo -e "${YELLOW}Ungültige Auswahl. Verwende 'ban'.${NC}"
decision_type="ban"
;;
esac
# Dauer eingeben
echo -n -e "${CYAN}Dauer (z.B. 10m, 1h, 24h, 7d) [Standard: 4h]: ${NC}"
read -r duration
duration="${duration:-4h}"
# Grund eingeben
echo -n -e "${CYAN}Grund (optional) [Standard: manual via script]: ${NC}"
read -r reason
reason="${reason:-manual via script}"
# Scope bestimmen
local scope="ip"
if is_cidr "$add_ip"; then
scope="range"
fi
# Zusammenfassung
echo ""
draw_line "-" 60
echo -e "${BOLD}Zusammenfassung:${NC}"
echo -e " IP/Range: ${CYAN}$add_ip${NC}"
echo -e " Typ: ${CYAN}$decision_type${NC}"
echo -e " Scope: ${CYAN}$scope${NC}"
echo -e " Dauer: ${CYAN}$duration${NC}"
echo -e " Grund: ${CYAN}$reason${NC}"
draw_line "-" 60
echo ""
echo -n -e "${YELLOW}Decision hinzufügen? [j/N]: ${NC}"
read -r confirm
if [[ "$confirm" =~ ^[jJyY]$ ]]; then
echo ""
if cscli_exec "decisions add --type '$decision_type' --scope '$scope' --value '$add_ip' --duration '$duration' --reason '$reason'" 2>&1; then
echo -e "${GREEN}Decision erfolgreich hinzugefügt!${NC}"
else
echo -e "${RED}Fehler beim Hinzufügen der Decision.${NC}"
fi
else
echo -e "${YELLOW}Abgebrochen.${NC}"
fi
echo ""
}
# ---[ Decision entfernen ]---
admin_remove_decision() {
echo ""
echo -e "${BOLD}${BLUE}=== Decision entfernen ===${NC}"
echo ""
echo -e "${BOLD}Entfernen nach:${NC}"
echo " 1) IP-Adresse"
echo " 2) Decision-ID"
echo -n -e "${CYAN}Auswahl [1-2]: ${NC}"
read -r remove_choice
case "$remove_choice" in
1)
echo -n -e "${CYAN}IP-Adresse: ${NC}"
read -r remove_ip
if [[ -z "$remove_ip" ]]; then
echo -e "${YELLOW}Keine IP eingegeben.${NC}"
return
fi
echo -n -e "${YELLOW}Decision für $remove_ip wirklich entfernen? [j/N]: ${NC}"
read -r confirm
if [[ "$confirm" =~ ^[jJyY]$ ]]; then
if cscli_exec "decisions delete --ip '$remove_ip'" 2>&1; then
echo -e "${GREEN}Decision entfernt!${NC}"
else
echo -e "${RED}Fehler beim Entfernen.${NC}"
fi
else
echo -e "${YELLOW}Abgebrochen.${NC}"
fi
;;
2)
echo -n -e "${CYAN}Decision-ID: ${NC}"
read -r remove_id
if [[ -z "$remove_id" ]]; then
echo -e "${YELLOW}Keine ID eingegeben.${NC}"
return
fi
if ! [[ "$remove_id" =~ ^[0-9]+$ ]]; then
echo -e "${RED}Ungültige Decision-ID (nur Zahlen erlaubt).${NC}"
return
fi
echo -n -e "${YELLOW}Decision #$remove_id wirklich entfernen? [j/N]: ${NC}"
read -r confirm
if [[ "$confirm" =~ ^[jJyY]$ ]]; then
if cscli_exec "decisions delete --id '$remove_id'" 2>&1; then
echo -e "${GREEN}Decision #$remove_id entfernt!${NC}"
else
echo -e "${RED}Fehler beim Entfernen.${NC}"
fi
else
echo -e "${YELLOW}Abgebrochen.${NC}"
fi
;;
*)
echo -e "${YELLOW}Ungültige Auswahl.${NC}"
;;
esac
echo ""
}
# ---[ Alerts anzeigen ]---
admin_list_alerts() {
echo ""
echo -e "${BOLD}${BLUE}=== Aktuelle Alerts ===${NC}"
draw_line "=" 60
echo ""
cscli_exec "alerts list" 2>/dev/null || {
echo -e "${YELLOW}Keine Alerts vorhanden oder Fehler beim Abrufen.${NC}"
}
echo ""
}
# ---[ Alert Detail-Inspektion ]---
admin_inspect_alert() {
echo ""
echo -e "${BOLD}${BLUE}=== Alert inspizieren ===${NC}"
echo -n -e "${CYAN}Alert-ID eingeben: ${NC}"
read -r alert_id
if [[ -z "$alert_id" ]]; then
echo -e "${YELLOW}Keine Alert-ID eingegeben.${NC}"
return
fi
if ! [[ "$alert_id" =~ ^[0-9]+$ ]]; then
echo -e "${RED}Ungültige Alert-ID (nur Zahlen erlaubt).${NC}"
return
fi
echo ""
echo -e "${DIM}Inspiziere Alert #$alert_id...${NC}"
draw_line "-" 60
cscli_exec "alerts inspect '$alert_id' -d" 2>/dev/null || {
echo -e "${RED}Fehler beim Inspizieren von Alert #$alert_id.${NC}"
}
echo ""
}
# ---[ Alert löschen ]---
admin_delete_alert() {
echo ""
echo -e "${BOLD}${BLUE}=== Alert löschen ===${NC}"
echo ""
echo -e "${BOLD}Löschen nach:${NC}"
echo " 1) Einzelne Alert-ID"
echo " 2) Alle Alerts für eine IP"
echo " 3) Alle Alerts für einen Zeitraum"
echo -n -e "${CYAN}Auswahl [1-3]: ${NC}"
read -r delete_choice
case "$delete_choice" in
1)
echo -n -e "${CYAN}Alert-ID: ${NC}"
read -r del_id
if [[ -z "$del_id" ]] || ! [[ "$del_id" =~ ^[0-9]+$ ]]; then
echo -e "${RED}Ungültige Alert-ID.${NC}"
return
fi
echo -n -e "${YELLOW}Alert #$del_id wirklich löschen? [j/N]: ${NC}"
read -r confirm
if [[ "$confirm" =~ ^[jJyY]$ ]]; then
cscli_exec "alerts delete --id '$del_id'" 2>&1
else
echo -e "${YELLOW}Abgebrochen.${NC}"
fi
;;
2)
echo -n -e "${CYAN}IP-Adresse: ${NC}"
read -r del_ip
if [[ -z "$del_ip" ]]; then
echo -e "${YELLOW}Keine IP eingegeben.${NC}"
return
fi
echo -n -e "${YELLOW}Alle Alerts für $del_ip löschen? [j/N]: ${NC}"
read -r confirm
if [[ "$confirm" =~ ^[jJyY]$ ]]; then
cscli_exec "alerts delete --ip '$del_ip'" 2>&1
else
echo -e "${YELLOW}Abgebrochen.${NC}"
fi
;;
3)
echo -n -e "${CYAN}Zeitraum (z.B. 24h, 7d): ${NC}"
read -r del_range
if [[ -z "$del_range" ]]; then
echo -e "${YELLOW}Kein Zeitraum eingegeben.${NC}"
return
fi
echo -n -e "${YELLOW}Alle Alerts älter als $del_range löschen? [j/N]: ${NC}"
read -r confirm
if [[ "$confirm" =~ ^[jJyY]$ ]]; then
cscli_exec "alerts delete --range '$del_range'" 2>&1
else
echo -e "${YELLOW}Abgebrochen.${NC}"
fi
;;
*)
echo -e "${YELLOW}Ungültige Auswahl.${NC}"
;;
esac
echo ""
}
# ---[ Bouncers verwalten ]---
admin_list_bouncers() {
echo ""
echo -e "${BOLD}${BLUE}=== Registrierte Bouncers ===${NC}"
draw_line "=" 60
echo ""
cscli_exec "bouncers list" 2>/dev/null || {
echo -e "${RED}Fehler beim Abrufen der Bouncers.${NC}"
}
echo ""
}
# ---[ Machines verwalten ]---
admin_list_machines() {
echo ""
echo -e "${BOLD}${BLUE}=== Registrierte Machines ===${NC}"
draw_line "=" 60
echo ""
cscli_exec "machines list" 2>/dev/null || {
echo -e "${RED}Fehler beim Abrufen der Machines.${NC}"
}
echo ""
}
# ---[ Parsers anzeigen ]---
admin_list_parsers() {
echo ""
echo -e "${BOLD}${BLUE}=== Installierte Parsers ===${NC}"
draw_line "=" 60
echo ""
cscli_exec "parsers list" 2>/dev/null || {
echo -e "${RED}Fehler beim Abrufen der Parsers.${NC}"
}
echo ""
}
# ---[ Scenarios anzeigen ]---
admin_list_scenarios() {
echo ""
echo -e "${BOLD}${BLUE}=== Installierte Scenarios ===${NC}"
draw_line "=" 60
echo ""
cscli_exec "scenarios list" 2>/dev/null || {
echo -e "${RED}Fehler beim Abrufen der Scenarios.${NC}"
}
echo ""
}
# ---[ Collections anzeigen ]---
admin_list_collections() {
echo ""
echo -e "${BOLD}${BLUE}=== Installierte Collections ===${NC}"
draw_line "=" 60
echo ""
cscli_exec "collections list" 2>/dev/null || {
echo -e "${RED}Fehler beim Abrufen der Collections.${NC}"
}
echo ""
}
# ---[ CrowdSec Version & Hub Status ]---
admin_show_status() {
echo ""
echo -e "${BOLD}${BLUE}=== CrowdSec Status ===${NC}"
draw_line "=" 60
echo ""
echo -e "${BOLD}Version:${NC}"
cscli_exec "version" 2>/dev/null || echo -e "${RED}Nicht erreichbar${NC}"
echo ""
echo -e "${BOLD}Hub Status:${NC}"
cscli_exec "hub list" 2>/dev/null || echo -e "${RED}Fehler${NC}"
echo ""
}
# ---[ Eigenen cscli-Befehl ausführen ]---
admin_custom_command() {
echo ""
echo -e "${BOLD}${BLUE}=== Eigenen cscli-Befehl ausführen ===${NC}"
echo -e "${DIM}Basis: ${CSCLI_CMD}${NC}"
echo -e "${DIM}Gib nur die Argumente nach 'cscli' ein.${NC}"
echo -e "${DIM}Beispiel: decisions list -o json${NC}"
echo ""
echo -n -e "${CYAN}cscli> ${NC}"
read -r custom_args
if [[ -z "$custom_args" ]]; then
echo -e "${YELLOW}Kein Befehl eingegeben.${NC}"
return
fi
echo ""
draw_line "-" 60
cscli_exec "$custom_args" 2>&1 || true
draw_line "-" 60
echo ""
}
# ---[ IP-Info: Alle Infos zu einer IP sammeln ]---
admin_ip_info() {
echo ""
echo -e "${BOLD}${BLUE}=== IP-Informationen ===${NC}"
echo -n -e "${CYAN}IP-Adresse eingeben: ${NC}"
read -r info_ip
if [[ -z "$info_ip" ]]; then
echo -e "${YELLOW}Keine IP eingegeben.${NC}"
return
fi
echo ""
draw_line "=" 60
echo -e "${BOLD}Informationen für: ${CYAN}$info_ip${NC}"
draw_line "=" 60
# DNS Reverse-Lookup
echo ""
echo -e "${BOLD}[1/4] Reverse-DNS:${NC}"
local rdns
rdns=$(dig +short -x "$info_ip" 2>/dev/null || true)
if [[ -n "$rdns" ]]; then
echo -e " ${GREEN}$rdns${NC}"
else
echo -e " ${DIM}Kein PTR-Record gefunden${NC}"
fi
# Decisions für diese IP
echo ""
echo -e "${BOLD}[2/4] Aktive Decisions:${NC}"
cscli_exec "decisions list -i '$info_ip'" 2>/dev/null || {
echo -e " ${DIM}Keine Decisions gefunden${NC}"
}
# Alerts für diese IP
echo ""
echo -e "${BOLD}[3/4] Alerts:${NC}"
local alerts_output
alerts_output=$(cscli_exec "alerts list -i '$info_ip'" 2>/dev/null || true)
if [[ -n "$alerts_output" ]]; then
echo "$alerts_output"
# Alert-IDs extrahieren für Detail-Inspektion
echo ""
echo -e "${BOLD}[4/4] Alert-Details:${NC}"
local alert_ids
alert_ids=$(echo "$alerts_output" | grep -oP '^\s*\K[0-9]+' | head -5)
if [[ -n "$alert_ids" ]]; then
echo -e "${DIM}Zeige Details für die letzten Alerts (max. 5):${NC}"
while IFS= read -r aid; do
[[ -z "$aid" ]] && continue
echo ""
echo -e "${MAGENTA}--- Alert #$aid ---${NC}"
cscli_exec "alerts inspect '$aid' -d" 2>/dev/null || true
done <<< "$alert_ids"
fi
else
echo -e " ${DIM}Keine Alerts gefunden${NC}"
echo ""
echo -e "${BOLD}[4/4] Alert-Details:${NC}"
echo -e " ${DIM}Übersprungen (keine Alerts)${NC}"
fi
draw_line "=" 60
echo ""
}
# ============================================================================
# TEIL 8: INTERAKTIVES MENÜ
# ============================================================================
show_menu_banner() {
clear 2>/dev/null || true
echo ""
echo -e "${BOLD}${BLUE}"
echo " ╔═══════════════════════════════════════════════════════╗"
echo " ║ ║"
echo " ║ CrowdSec Manager v${SCRIPT_VERSION}"
echo " ║ ║"
echo " ║ Patrick Asmus ║"
echo " ║ www.patrick-asmus.de ║"
echo " ║ ║"
echo " ╚═══════════════════════════════════════════════════════╝"
echo -e "${NC}"
echo -e " ${DIM}Basisbefehl: ${CSCLI_CMD}${NC}"
echo ""
}
show_main_menu() {
echo -e "${BOLD}${GREEN} ── ALLOWLIST MANAGEMENT ──────────────────────────────${NC}"
echo -e " ${CYAN} 1)${NC} Allowlist synchronisieren (DNS auflösen & updaten)"
echo -e " ${CYAN} 2)${NC} Allowlist synchronisieren (Dry-Run)"
echo -e " ${CYAN} 3)${NC} Aktuelle Allowlist anzeigen"
echo -e " ${CYAN} 4)${NC} Konfiguration testen"
echo -e " ${CYAN} 5)${NC} Alle verwalteten Einträge entfernen (Flush)"
echo ""
echo -e "${BOLD}${GREEN} ── DECISIONS ─────────────────────────────────────────${NC}"
echo -e " ${CYAN}10)${NC} Alle Decisions anzeigen"
echo -e " ${CYAN}11)${NC} Decision suchen (nach IP)"
echo -e " ${CYAN}12)${NC} Decision manuell hinzufügen"
echo -e " ${CYAN}13)${NC} Decision entfernen"
echo ""
echo -e "${BOLD}${GREEN} ── ALERTS ────────────────────────────────────────────${NC}"
echo -e " ${CYAN}20)${NC} Alle Alerts anzeigen"
echo -e " ${CYAN}21)${NC} Alert inspizieren (Detail-Ansicht)"
echo -e " ${CYAN}22)${NC} Alerts löschen"
echo ""
echo -e "${BOLD}${GREEN} ── INFORMATIONEN ─────────────────────────────────────${NC}"
echo -e " ${CYAN}30)${NC} IP-Informationen (Komplett-Check)"
echo -e " ${CYAN}31)${NC} CrowdSec Metriken"
echo -e " ${CYAN}32)${NC} CrowdSec Status & Version"
echo -e " ${CYAN}33)${NC} Bouncers anzeigen"
echo -e " ${CYAN}34)${NC} Machines anzeigen"
echo -e " ${CYAN}35)${NC} Parsers anzeigen"
echo -e " ${CYAN}36)${NC} Scenarios anzeigen"
echo -e " ${CYAN}37)${NC} Collections anzeigen"
echo ""
echo -e "${BOLD}${GREEN} ── ERWEITERT ─────────────────────────────────────────${NC}"
echo -e " ${CYAN}40)${NC} Eigenen cscli-Befehl ausführen"
echo ""
echo -e " ${CYAN} 0)${NC} Beenden"
echo ""
draw_line "─" 60
}
interactive_menu() {
load_config
disable_colors_if_needed
while true; do
show_menu_banner
show_main_menu
echo -n -e " ${BOLD}Auswahl: ${NC}"
read -r choice
case "$choice" in
# Allowlist Management
1) run_allowlist_sync false ;;
2) run_allowlist_sync true ;;
3) show_current_list ;;
4) test_config ;;
5)
echo -n -e " ${YELLOW}Wirklich ALLE verwalteten Einträge entfernen? [j/N]: ${NC}"
read -r confirm
if [[ "$confirm" =~ ^[jJyY]$ ]]; then
flush_allowlist
else
echo -e " ${YELLOW}Abgebrochen.${NC}"
fi
;;
# Decisions
10) admin_list_decisions ;;
11) admin_search_decision ;;
12) admin_add_decision ;;
13) admin_remove_decision ;;
# Alerts
20) admin_list_alerts ;;
21) admin_inspect_alert ;;
22) admin_delete_alert ;;
# Informationen
30) admin_ip_info ;;
31) admin_show_metrics ;;
32) admin_show_status ;;
33) admin_list_bouncers ;;
34) admin_list_machines ;;
35) admin_list_parsers ;;
36) admin_list_scenarios ;;
37) admin_list_collections ;;
# Erweitert
40) admin_custom_command ;;
# Beenden
0|q|Q|exit)
echo ""
echo -e " ${GREEN}Auf Wiedersehen!${NC}"
echo ""
exit 0
;;
*)
echo -e " ${RED}Ungültige Auswahl: $choice${NC}"
;;
esac
echo ""
echo -n -e " ${DIM}Drücke Enter um fortzufahren...${NC}"
read -r
done
}
# Allowlist Sync als Funktion (wird aus Menü und CLI aufgerufen)
run_allowlist_sync() {
local dry_run_override="${1:-false}"
local orig_dry_run="${DRY_RUN:-false}"
if [[ "$dry_run_override" == "true" ]]; then
DRY_RUN=true
fi
# Statistiken zurücksetzen
STAT_ADDED=0
STAT_REMOVED=0
STAT_UNCHANGED=0
STAT_ERRORS=0
STAT_RESOLVED_DOMAINS=0
STAT_TOTAL_IPS=0
REPORT_MESSAGES=()
echo ""
log "INFO" "============================================"
log "INFO" " CrowdSec Manager v${SCRIPT_VERSION}"
log "INFO" " Patrick Asmus - www.patrick-asmus.de"
log "INFO" "============================================"
if [[ "${DRY_RUN}" == "true" ]]; then
log "WARN" "*** DRY-RUN MODUS AKTIV ***"
fi
rotate_logs
if ! check_dependencies; then
DRY_RUN="$orig_dry_run"
return 1
fi
if ! acquire_lock; then
DRY_RUN="$orig_dry_run"
return 1
fi
trap 'release_lock; cleanup_temp' EXIT INT TERM
setup_temp
if ! crowdsec_health_check; then
cleanup_temp
release_lock
DRY_RUN="$orig_dry_run"
return 1
fi
get_current_allowlist
resolve_all_entries
if sync_allowlist; then
# Änderungen erkannt → Backup erstellen & benachrichtigen
create_backup
send_notifications
fi
print_summary
cleanup_temp
release_lock
trap - EXIT INT TERM
DRY_RUN="$orig_dry_run"
if [[ $STAT_ERRORS -gt 0 ]]; then
return 2
fi
return 0
}
# ============================================================================
# TEIL 9: Report / Zusammenfassung
# ============================================================================
print_summary() {
echo ""
log "INFO" "============================================"
log "INFO" " ZUSAMMENFASSUNG"
log "INFO" "============================================"
log "INFO" "Domains aufgelöst: $STAT_RESOLVED_DOMAINS"
log "INFO" "IPs/CIDRs gesamt: $STAT_TOTAL_IPS"
log "INFO" "Hinzugefügt: $STAT_ADDED"
log "INFO" "Entfernt: $STAT_REMOVED"
log "INFO" "Unverändert: $STAT_UNCHANGED"
if [[ $STAT_ERRORS -gt 0 ]]; then
log "WARN" "Fehler: $STAT_ERRORS"
else
log "INFO" "Fehler: $STAT_ERRORS"
fi
if [[ "${DRY_RUN:-false}" == "true" ]]; then
log "WARN" "DRY-RUN MODUS - Keine Änderungen vorgenommen"
fi
log "INFO" "============================================"
echo ""
}
# ============================================================================
# TEIL 10: CLI-FUNKTIONEN (nicht-interaktiv)
# ============================================================================
show_help() {
cat << EOF
${BOLD}CrowdSec Manager v${SCRIPT_VERSION}${NC}
Verwaltet automatisch eine CrowdSec-Allowlist und bietet interaktive
CrowdSec-Administration.
${BOLD}Verwendung:${NC}
$SCRIPT_NAME [OPTIONEN]
${BOLD}Modi:${NC}
(ohne Optionen) Startet das interaktive Menü
--sync-allowlist Allowlist synchronisieren (für Cron)
${BOLD}Optionen:${NC}
-c, --config FILE Konfigurationsdatei angeben
-d, --dry-run Dry-Run: Zeigt was passieren würde
-v, --verbose Ausführliche Ausgabe (DEBUG)
-q, --quiet Minimale Ausgabe (ERROR)
-l, --list Aktuelle Allowlist anzeigen
-f, --flush Alle verwalteten Einträge entfernen
-t, --test Konfiguration testen
-i, --interactive Interaktives Menü starten
--sync-allowlist Allowlist-Sync ausführen (nicht-interaktiv)
-h, --help Diese Hilfe anzeigen
-V, --version Version anzeigen
${BOLD}Beispiele:${NC}
$SCRIPT_NAME # Interaktives Menü
$SCRIPT_NAME --sync-allowlist # Allowlist-Sync (Cron-Modus)
$SCRIPT_NAME --sync-allowlist --dry-run # Testlauf
$SCRIPT_NAME --sync-allowlist --quiet # Sync ohne Ausgabe (nur Fehler)
$SCRIPT_NAME --interactive # Interaktives Menü
$SCRIPT_NAME --list # Allowlist anzeigen
$SCRIPT_NAME --config /etc/conf.conf # Eigene Config
${BOLD}Cron-Beispiel:${NC}
*/30 * * * * ${SCRIPT_DIR}/${SCRIPT_NAME} --sync-allowlist --quiet 2>&1
${BOLD}Konfiguration:${NC}
Standard: config/crowdsec-manager.conf
EOF
}
show_version() {
echo "CrowdSec Manager v${SCRIPT_VERSION}"
}
show_current_list() {
echo ""
echo -e "${BOLD}Aktuelle CrowdSec Allowlist (${ALLOWLIST_NAME}):${NC}"
draw_line "-" 60
cscli_exec "allowlists inspect ${ALLOWLIST_NAME}" 2>/dev/null || echo "Keine Einträge oder Allowlist nicht gefunden."
draw_line "-" 60
}
flush_allowlist() {
log "WARN" "Entferne ALLE verwalteten Einträge aus der Allowlist..."
if [[ "${DRY_RUN:-false}" == "true" ]]; then
log "INFO" "[DRY-RUN] Würde Allowlist '${ALLOWLIST_NAME}' löschen und neu erstellen"
return 0
fi
# Allowlist komplett löschen und leer neu erstellen
if cscli_exec "allowlists delete ${ALLOWLIST_NAME}" 2>/dev/null; then
log "INFO" "Allowlist '${ALLOWLIST_NAME}' gelöscht."
else
log "WARN" "Allowlist '${ALLOWLIST_NAME}' konnte nicht gelöscht werden (existiert möglicherweise nicht)."
fi
# Leer neu erstellen
if cscli_exec "allowlists create ${ALLOWLIST_NAME} -d '${ALLOWLIST_DESCRIPTION}'" 2>&1; then
log "INFO" "Leere Allowlist '${ALLOWLIST_NAME}' neu erstellt."
else
log "ERROR" "Fehler beim Neuerstellen der Allowlist."
return 1
fi
}
test_config() {
echo -e "\n${BOLD}Konfigurationstest:${NC}\n"
echo -e "${GREEN}[OK]${NC} Konfigurationsdatei geladen: $CONFIG_FILE"
echo -n "CrowdSec-Erreichbarkeit... "
if cscli_exec "version" &>/dev/null; then
echo -e "${GREEN}[OK]${NC}"
else
echo -e "${RED}[FEHLER]${NC} Befehl: '$CSCLI_CMD version'"
fi
echo -n "DNS-Auflösung... "
if command -v dig &>/dev/null; then
echo -e "${GREEN}[OK]${NC} dig gefunden"
else
echo -e "${RED}[FEHLER]${NC} dig nicht gefunden (dnsutils installieren)"
fi
echo ""
echo -e "${BOLD}Konfigurierte Einträge (${#ALLOWLIST_ENTRIES[@]}):${NC}"
for entry in "${ALLOWLIST_ENTRIES[@]}"; do
entry=$(echo "$entry" | sed 's/#.*$//' | xargs)
[[ -z "$entry" ]] && continue
if is_ip_or_cidr "$entry"; then
echo -e " ${CYAN}[IP/CIDR]${NC} $entry"
elif is_domain "$entry"; then
echo -n " [Domain] $entry -> "
local ips
ips=$(resolve_domain "$entry" 2>/dev/null)
if [[ -n "$ips" ]]; then
echo -e "${GREEN}$(echo "$ips" | tr '\n' ', ' | sed 's/,$//')${NC}"
else
echo -e "${RED}Nicht auflösbar${NC}"
fi
else
echo -e " ${RED}[UNBEKANNT]${NC} $entry"
fi
done
echo ""
echo -e "${BOLD}Benachrichtigungen:${NC}"
echo " Aktiviert: ${NOTIFY_ENABLED}"
echo " Desktop: ${NOTIFY_DESKTOP_ENABLED:-false}"
echo " Ntfy: ${NOTIFY_NTFY_ENABLED:-false}"
echo " Gotify: ${NOTIFY_GOTIFY_ENABLED:-false}"
echo " E-Mail: ${NOTIFY_EMAIL_ENABLED:-false}"
echo ""
}
# ============================================================================
# TEIL 11: KONFIGURATION & ABHÄNGIGKEITEN
# ============================================================================
load_config() {
if [[ ! -f "$CONFIG_FILE" ]]; then
echo "FEHLER: Konfigurationsdatei nicht gefunden: $CONFIG_FILE" >&2
echo "Erstelle eine Konfigurationsdatei basierend auf der Vorlage:" >&2
echo " cp config/crowdsec-manager.conf.example config/crowdsec-manager.conf" >&2
exit 1
fi
# shellcheck source=/dev/null
source "$CONFIG_FILE"
if [[ -n "${LOG_FILE:-}" ]]; then
local log_dir
log_dir="$(dirname "$LOG_FILE")"
if [[ ! -d "$log_dir" ]]; then
mkdir -p "$log_dir" 2>/dev/null || true
fi
fi
}
check_dependencies() {
local missing=()
if ! command -v dig &>/dev/null; then
missing+=("dig (dnsutils/bind-utils)")
fi
if ! command -v curl &>/dev/null; then
if [[ "${NOTIFY_NTFY_ENABLED:-false}" == "true" ]] || \
[[ "${NOTIFY_GOTIFY_ENABLED:-false}" == "true" ]] || \
[[ "${NOTIFY_EMAIL_ENABLED:-false}" == "true" ]]; then
missing+=("curl (für Benachrichtigungen)")
fi
fi
if [[ ${#missing[@]} -gt 0 ]]; then
log "ERROR" "Fehlende Abhängigkeiten:"
for dep in "${missing[@]}"; do
log "ERROR" " - $dep"
done
return 1
fi
return 0
}
# ============================================================================
# TEIL 12: HAUPTPROGRAMM
# ============================================================================
main() {
local action="interactive"
# Argumente parsen
while [[ $# -gt 0 ]]; do
case "$1" in
-c|--config)
CONFIG_FILE="$2"
shift 2
;;
-d|--dry-run)
DRY_RUN=true
shift
;;
-v|--verbose)
LOG_LEVEL="DEBUG"
shift
;;
-q|--quiet)
LOG_LEVEL="ERROR"
shift
;;
-l|--list)
action="list"
shift
;;
-f|--flush)
action="flush"
shift
;;
-t|--test)
action="test"
shift
;;
-i|--interactive)
action="interactive"
shift
;;
--sync-allowlist)
action="sync-allowlist"
shift
;;
-h|--help)
disable_colors_if_needed
show_help
exit 0
;;
-V|--version)
show_version
exit 0
;;
*)
echo "Unbekannte Option: $1" >&2
echo "Verwende '$SCRIPT_NAME --help' für Hilfe" >&2
exit 1
;;
esac
done
disable_colors_if_needed
# Spezielle Aktionen
case "$action" in
list)
load_config
show_current_list
exit 0
;;
test)
load_config
test_config
exit 0
;;
flush)
load_config
flush_allowlist
exit 0
;;
interactive)
interactive_menu
exit 0
;;
sync-allowlist)
;;
esac
# === Automatischer Sync (--sync-allowlist) ===
load_config
[[ -n "${DRY_RUN:-}" ]] || DRY_RUN=false
run_allowlist_sync false
local rc=$?
exit $rc
}
# Script ausführen
main "$@"