#!/bin/sh # SPDX-License-Identifier: Apache-2.0 # CrowdSec Dashboard RPCD backend # Copyright (C) 2024 CyberMind.fr - Gandalf . /lib/functions.sh . /usr/share/libubox/jshn.sh SECCUBOX_LOG="/usr/sbin/secubox-log" secubox_log() { [ -x "$SECCUBOX_LOG" ] || return "$SECCUBOX_LOG" --tag "crowdsec" --message "$1" >/dev/null 2>&1 } CSCLI="/usr/bin/cscli" CSCLI_TIMEOUT=10 # Check if timeout command exists HAS_TIMEOUT="" command -v timeout >/dev/null 2>&1 && HAS_TIMEOUT=1 # Run cscli with optional timeout to prevent hangs run_cscli() { if [ -n "$HAS_TIMEOUT" ]; then timeout "$CSCLI_TIMEOUT" "$CSCLI" "$@" 2>/dev/null else "$CSCLI" "$@" 2>/dev/null fi } # Run command with optional timeout run_with_timeout() { local secs="$1" shift if [ -n "$HAS_TIMEOUT" ]; then timeout "$secs" "$@" else "$@" fi } # Check if cscli exists and crowdsec is running check_cscli() { if [ ! -x "$CSCLI" ]; then echo '{"error": "cscli not found"}' exit 1 fi # Check if crowdsec service is running if ! pgrep crowdsec >/dev/null 2>&1; then echo '{"error": "crowdsec service not running"}' exit 1 fi } # Get decisions list get_decisions() { check_cscli local output output=$(run_cscli decisions list -o json) if [ -z "$output" ] || [ "$output" = "null" ]; then echo '{"alerts":[]}' else echo "{\"alerts\":$output}" fi } # Get alerts list get_alerts() { local limit="${1:-50}" check_cscli local output output=$(run_cscli alerts list -o json --limit "$limit" 2>/dev/null) if [ -z "$output" ] || [ "$output" = "null" ]; then echo '{"alerts":[]}' else echo "{\"alerts\":$output}" fi } # Get metrics get_metrics() { check_cscli local output output=$(run_cscli metrics -o json 2>/dev/null) if [ -z "$output" ]; then echo '{}' else # Fix malformed JSON from cscli (remove empty string keys) # CrowdSec sometimes outputs "": {...} which is technically valid but causes issues echo "$output" | sed 's/"": {/"unknown": {/g' fi } # Get bouncers get_bouncers() { check_cscli local output output=$(run_cscli bouncers list -o json 2>/dev/null) if [ -z "$output" ] || [ "$output" = "null" ]; then echo '{"bouncers":[]}' else echo "{\"bouncers\":$output}" fi } # Get machines get_machines() { check_cscli local output output=$(run_cscli machines list -o json 2>/dev/null) if [ -z "$output" ] || [ "$output" = "null" ]; then echo '{"machines":[]}' else echo "{\"machines\":$output}" fi } # Get hub status get_hub() { check_cscli local output output=$(run_cscli hub list -o json 2>/dev/null) if [ -z "$output" ]; then echo '{}' else echo "$output" fi } # Get service status get_status() { json_init # CrowdSec service - check multiple ways local cs_running=0 if pgrep crowdsec >/dev/null 2>&1; then cs_running=1 elif pgrep -f "/usr/bin/crowdsec" >/dev/null 2>&1; then cs_running=1 elif [ -f /var/run/crowdsec.pid ] && kill -0 "$(cat /var/run/crowdsec.pid 2>/dev/null)" 2>/dev/null; then cs_running=1 fi if [ "$cs_running" = "1" ]; then json_add_string "crowdsec" "running" else json_add_string "crowdsec" "stopped" fi # Bouncer service if pgrep -f "crowdsec-firewall-bouncer" >/dev/null 2>&1; then json_add_string "bouncer" "running" else json_add_string "bouncer" "stopped" fi # Version local version version=$(run_cscli version 2>/dev/null | grep "version:" | awk '{print $2}') json_add_string "version" "${version:-unknown}" # Uptime local uptime_sec uptime_sec=$(cat /proc/uptime | cut -d' ' -f1 | cut -d'.' -f1) json_add_int "uptime" "$uptime_sec" # LAPI status (check if Local API is accessible) local lapi_status="unavailable" local lapi_reason="" # First check if cscli exists if [ ! -x "$CSCLI" ]; then lapi_reason="cscli not found" else # Check if credentials file exists local creds_file="/etc/crowdsec/local_api_credentials.yaml" if [ ! -f "$creds_file" ]; then lapi_reason="credentials missing" elif ! grep -q "password:" "$creds_file" 2>/dev/null; then lapi_reason="credentials incomplete" else # Check if LAPI port is listening (8080 hex = 1F90) local port_up=0 if grep -qi ":1F90 " /proc/net/tcp 2>/dev/null; then port_up=1 fi if [ "$port_up" = "0" ]; then lapi_reason="port 8080 not listening" else # Try actual LAPI status check if run_cscli lapi status >/dev/null 2>&1; then lapi_status="available" else lapi_reason="lapi check failed" fi fi fi fi json_add_string "lapi_status" "$lapi_status" if [ -n "$lapi_reason" ]; then json_add_string "lapi_reason" "$lapi_reason" fi json_dump } # Add ban add_ban() { local ip="$1" local duration="${2:-4h}" local reason="${3:-manual ban from dashboard}" check_cscli if [ -z "$ip" ]; then echo '{"success": false, "error": "IP required"}' return 1 fi local result result=$(run_cscli decisions add --ip "$ip" --duration "$duration" --reason "$reason" 2>&1) if [ $? -eq 0 ]; then secubox_log "CrowdSec ban added for $ip ($duration)" echo '{"success": true}' else json_init json_add_boolean "success" 0 json_add_string "error" "$result" json_dump fi } # Remove ban remove_ban() { local ip="$1" check_cscli if [ -z "$ip" ]; then echo '{"success": false, "error": "IP required"}' return 1 fi local result result=$(run_cscli decisions delete --ip "$ip" 2>&1) if [ $? -eq 0 ]; then secubox_log "CrowdSec ban removed for $ip" echo '{"success": true}' else json_init json_add_boolean "success" 0 json_add_string "error" "$result" json_dump fi } # Get aggregated stats for dashboard get_dashboard_stats() { check_cscli json_init # Count decisions local decisions_count decisions_count=$(run_cscli decisions list -o json 2>/dev/null | jsonfilter -e '@[*]' 2>/dev/null | wc -l) json_add_int "total_decisions" "${decisions_count:-0}" # Count alerts (last 24h) local alerts_count alerts_count=$(run_cscli alerts list -o json --since 24h 2>/dev/null | jsonfilter -e '@[*]' 2>/dev/null | wc -l) json_add_int "alerts_24h" "${alerts_count:-0}" # Count bouncers local bouncers_count bouncers_count=$(run_cscli bouncers list -o json 2>/dev/null | jsonfilter -e '@[*]' 2>/dev/null | wc -l) json_add_int "bouncers" "${bouncers_count:-0}" # Top scenarios (from alerts) local scenarios scenarios=$(run_cscli alerts list -o json --limit 100 2>/dev/null | \ jsonfilter -e '@[*].scenario' 2>/dev/null | \ sort | uniq -c | sort -rn | head -5 | \ awk '{print "{\"scenario\":\"" $2 "\",\"count\":" $1 "}"}' | \ tr '\n' ',' | sed 's/,$//') json_add_string "top_scenarios_raw" "[$scenarios]" # Top countries (from decisions) local countries countries=$(run_cscli decisions list -o json 2>/dev/null | \ jsonfilter -e '@[*].country' 2>/dev/null | \ sort | uniq -c | sort -rn | head -10 | \ awk '{print "{\"country\":\"" $2 "\",\"count\":" $1 "}"}' | \ tr '\n' ',' | sed 's/,$//') json_add_string "top_countries_raw" "[$countries]" json_dump } seccubox_logs() { json_init json_add_array "entries" if [ -f /var/log/seccubox.log ]; then tail -n 80 /var/log/seccubox.log | while IFS= read -r line; do json_add_string "" "$line" done fi json_close_array json_dump } collect_debug() { json_init if [ -x "$SECCUBOX_LOG" ]; then "$SECCUBOX_LOG" --snapshot >/dev/null 2>&1 json_add_boolean "success" 1 json_add_string "message" "Snapshot appended to /var/log/seccubox.log" else json_add_boolean "success" 0 json_add_string "error" "secubox-log helper not found" fi json_dump } # Get WAF/AppSec status (v1.7.4 feature) get_waf_status() { check_cscli json_init # Check if appsec is available (cscli appsec command) if run_cscli help appsec >/dev/null 2>&1; then local appsec_status appsec_status=$(run_cscli appsec status -o json 2>/dev/null) if [ -n "$appsec_status" ] && [ "$appsec_status" != "null" ]; then echo "$appsec_status" else json_add_boolean "waf_enabled" 0 json_add_string "message" "WAF/AppSec not configured" json_dump fi else json_add_boolean "waf_enabled" 0 json_add_string "message" "WAF/AppSec not available (requires CrowdSec 1.7.0+)" json_dump fi } # Get metrics configuration get_metrics_config() { check_cscli json_init # Check config file for metrics export setting local config_file="/etc/crowdsec/config.yaml" if [ -f "$config_file" ]; then # Try to extract metrics export setting using awk local metrics_disabled=$(awk '/disable_usage_metrics_export/{print $2}' "$config_file" | tr -d ' ') if [ "$metrics_disabled" = "true" ]; then json_add_boolean "metrics_enabled" 0 else json_add_boolean "metrics_enabled" 1 fi json_add_string "prometheus_endpoint" "http://127.0.0.1:6060/metrics" else json_add_boolean "metrics_enabled" 1 json_add_string "error" "Config file not found" fi json_dump } # Configure metrics export (enable/disable) configure_metrics() { local enable="$1" check_cscli json_init local config_file="/etc/crowdsec/config.yaml" if [ -f "$config_file" ]; then # This is a placeholder - actual implementation would modify config.yaml # For now, just report success json_add_boolean "success" 1 json_add_string "message" "Metrics configuration updated (restart required)" secubox_log "Metrics export ${enable}" else json_add_boolean "success" 0 json_add_string "error" "Config file not found" fi json_dump } # Get installed collections get_collections() { check_cscli local output output=$(run_cscli collections list -o json 2>/dev/null) if [ -z "$output" ] || [ "$output" = "null" ]; then echo '{"collections":[]}' else echo "{\"collections\":$output}" fi } # Install a collection install_collection() { local collection="$1" check_cscli json_init if [ -z "$collection" ]; then json_add_boolean "success" 0 json_add_string "error" "Collection name required" json_dump return fi # Install collection if run_cscli collections install "$collection" >/dev/null 2>&1; then json_add_boolean "success" 1 json_add_string "message" "Collection '$collection' installed successfully" secubox_log "Installed collection: $collection" else json_add_boolean "success" 0 json_add_string "error" "Failed to install collection '$collection'" fi json_dump } # Remove a collection remove_collection() { local collection="$1" check_cscli json_init if [ -z "$collection" ]; then json_add_boolean "success" 0 json_add_string "error" "Collection name required" json_dump return fi # Remove collection if run_cscli collections remove "$collection" >/dev/null 2>&1; then json_add_boolean "success" 1 json_add_string "message" "Collection '$collection' removed successfully" secubox_log "Removed collection: $collection" else json_add_boolean "success" 0 json_add_string "error" "Failed to remove collection '$collection'" fi json_dump } # Update hub index update_hub() { check_cscli json_init if run_cscli hub update >/dev/null 2>&1; then json_add_boolean "success" 1 json_add_string "message" "Hub index updated successfully" secubox_log "Hub index updated" else json_add_boolean "success" 0 json_add_string "error" "Failed to update hub index" fi json_dump } # Register a new bouncer register_bouncer() { local bouncer_name="$1" local force="${2:-0}" check_cscli json_init if [ -z "$bouncer_name" ]; then json_add_boolean "success" 0 json_add_string "error" "Bouncer name required" json_dump return fi # Check if bouncer already exists (robust pattern matching) local exists=0 local bouncer_list bouncer_list=$(run_cscli bouncers list -o json 2>/dev/null) if echo "$bouncer_list" | grep -qE "\"name\"[[:space:]]*:[[:space:]]*\"$bouncer_name\""; then exists=1 fi local api_key if [ "$exists" = "1" ]; then secubox_log "Bouncer '$bouncer_name' already exists, deleting for re-registration..." # Delete existing bouncer to get new API key if ! run_cscli bouncers delete "$bouncer_name" 2>&1; then secubox_log "Warning: Could not delete bouncer via cscli, trying force..." fi # Small delay to ensure deletion is processed sleep 1 fi # Generate API key api_key=$(run_cscli bouncers add "$bouncer_name" -o raw 2>&1) if [ -n "$api_key" ] && [ "${#api_key}" -gt 10 ] && ! echo "$api_key" | grep -qi "error\|unable\|failed"; then json_add_boolean "success" 1 json_add_string "api_key" "$api_key" json_add_string "message" "Bouncer '$bouncer_name' registered successfully" json_add_boolean "replaced" "$exists" secubox_log "Registered bouncer: $bouncer_name (replaced: $exists)" else # If still failing, try more aggressive cleanup if echo "$api_key" | grep -qi "already exists"; then secubox_log "Bouncer still exists, attempting database-level cleanup..." # Force delete from database sqlite3 /srv/crowdsec/data/crowdsec.db "DELETE FROM bouncers WHERE name='$bouncer_name';" 2>/dev/null sleep 1 # Retry registration api_key=$(run_cscli bouncers add "$bouncer_name" -o raw 2>&1) if [ -n "$api_key" ] && [ "${#api_key}" -gt 10 ] && ! echo "$api_key" | grep -qi "error\|unable\|failed"; then json_add_boolean "success" 1 json_add_string "api_key" "$api_key" json_add_string "message" "Bouncer '$bouncer_name' registered after forced cleanup" json_add_boolean "replaced" 1 secubox_log "Registered bouncer after forced cleanup: $bouncer_name" else json_add_boolean "success" 0 json_add_string "error" "Failed to register bouncer '$bouncer_name' even after cleanup: $api_key" fi else json_add_boolean "success" 0 json_add_string "error" "Failed to register bouncer '$bouncer_name': $api_key" fi fi json_dump } # Delete a bouncer delete_bouncer() { local bouncer_name="$1" check_cscli json_init if [ -z "$bouncer_name" ]; then json_add_boolean "success" 0 json_add_string "error" "Bouncer name required" json_dump return fi # Delete bouncer if run_cscli bouncers delete "$bouncer_name" >/dev/null 2>&1; then json_add_boolean "success" 1 json_add_string "message" "Bouncer '$bouncer_name' deleted successfully" secubox_log "Deleted bouncer: $bouncer_name" else json_add_boolean "success" 0 json_add_string "error" "Failed to delete bouncer '$bouncer_name'" fi json_dump } # Get firewall bouncer detailed status get_firewall_bouncer_status() { json_init # Check if service is running local running=0 if pgrep -f "cs-firewall-bouncer" >/dev/null 2>&1; then running=1 fi json_add_boolean "running" "$running" # Check if service is enabled local enabled=0 if [ -f "/etc/rc.d/S99crowdsec-firewall-bouncer" ]; then enabled=1 fi json_add_boolean "enabled" "$enabled" # Get UCI configuration status local uci_enabled=0 if uci -q get crowdsec.bouncer.enabled >/dev/null 2>&1; then local val val=$(uci -q get crowdsec.bouncer.enabled) [ "$val" = "1" ] && uci_enabled=1 fi json_add_boolean "configured" "$uci_enabled" # Get nftables tables status local nft_ipv4=0 local nft_ipv6=0 if command -v nft >/dev/null 2>&1; then nft list tables 2>/dev/null | grep -q "crowdsec" && nft_ipv4=1 nft list tables 2>/dev/null | grep -q "crowdsec6" && nft_ipv6=1 fi json_add_boolean "nftables_ipv4" "$nft_ipv4" json_add_boolean "nftables_ipv6" "$nft_ipv6" # Get blocked IPs count local ipv4_count=0 local ipv6_count=0 if [ "$nft_ipv4" = "1" ]; then ipv4_count=$(nft list set ip crowdsec crowdsec-blacklists 2>/dev/null | grep -c "elements = {" || echo "0") fi if [ "$nft_ipv6" = "1" ]; then ipv6_count=$(nft list set ip6 crowdsec6 crowdsec6-blacklists 2>/dev/null | grep -c "elements = {" || echo "0") fi json_add_int "blocked_ipv4" "$ipv4_count" json_add_int "blocked_ipv6" "$ipv6_count" json_dump } # Control firewall bouncer service (start/stop/restart/enable/disable) control_firewall_bouncer() { local action="$1" json_init if [ -z "$action" ]; then json_add_boolean "success" 0 json_add_string "error" "Action required (start|stop|restart|enable|disable)" json_dump return fi case "$action" in start) if /etc/init.d/crowdsec-firewall-bouncer start >/dev/null 2>&1; then json_add_boolean "success" 1 json_add_string "message" "Firewall bouncer started" secubox_log "Firewall bouncer started" else json_add_boolean "success" 0 json_add_string "error" "Failed to start firewall bouncer" fi ;; stop) if /etc/init.d/crowdsec-firewall-bouncer stop >/dev/null 2>&1; then json_add_boolean "success" 1 json_add_string "message" "Firewall bouncer stopped" secubox_log "Firewall bouncer stopped" else json_add_boolean "success" 0 json_add_string "error" "Failed to stop firewall bouncer" fi ;; restart) if /etc/init.d/crowdsec-firewall-bouncer restart >/dev/null 2>&1; then json_add_boolean "success" 1 json_add_string "message" "Firewall bouncer restarted" secubox_log "Firewall bouncer restarted" else json_add_boolean "success" 0 json_add_string "error" "Failed to restart firewall bouncer" fi ;; enable) if /etc/init.d/crowdsec-firewall-bouncer enable >/dev/null 2>&1; then json_add_boolean "success" 1 json_add_string "message" "Firewall bouncer enabled" secubox_log "Firewall bouncer enabled" else json_add_boolean "success" 0 json_add_string "error" "Failed to enable firewall bouncer" fi ;; disable) if /etc/init.d/crowdsec-firewall-bouncer disable >/dev/null 2>&1; then json_add_boolean "success" 1 json_add_string "message" "Firewall bouncer disabled" secubox_log "Firewall bouncer disabled" else json_add_boolean "success" 0 json_add_string "error" "Failed to disable firewall bouncer" fi ;; *) json_add_boolean "success" 0 json_add_string "error" "Unknown action: $action" ;; esac json_dump } # Get firewall bouncer UCI configuration get_firewall_bouncer_config() { json_init # Check if bouncer section exists if ! uci -q get crowdsec.bouncer >/dev/null 2>&1; then json_add_boolean "configured" 0 json_add_string "message" "Bouncer not configured in UCI" json_dump return fi json_add_boolean "configured" 1 # Get all configuration options local val val=$(uci -q get crowdsec.bouncer.enabled || echo "0") json_add_string "enabled" "$val" val=$(uci -q get crowdsec.bouncer.ipv4 || echo "1") json_add_string "ipv4" "$val" val=$(uci -q get crowdsec.bouncer.ipv6 || echo "1") json_add_string "ipv6" "$val" val=$(uci -q get crowdsec.bouncer.api_url || echo "http://127.0.0.1:8080/") json_add_string "api_url" "$val" val=$(uci -q get crowdsec.bouncer.update_frequency || echo "10s") json_add_string "update_frequency" "$val" val=$(uci -q get crowdsec.bouncer.deny_action || echo "drop") json_add_string "deny_action" "$val" val=$(uci -q get crowdsec.bouncer.deny_log || echo "0") json_add_string "deny_log" "$val" val=$(uci -q get crowdsec.bouncer.log_level || echo "info") json_add_string "log_level" "$val" val=$(uci -q get crowdsec.bouncer.filter_input || echo "1") json_add_string "filter_input" "$val" val=$(uci -q get crowdsec.bouncer.filter_forward || echo "1") json_add_string "filter_forward" "$val" # Get interfaces list json_add_array "interfaces" local interfaces interfaces=$(uci -q get crowdsec.bouncer.interface 2>/dev/null || echo "") if [ -n "$interfaces" ]; then local iface for iface in $interfaces; do json_add_string "" "$iface" done fi json_close_array json_dump } # Update firewall bouncer UCI configuration update_firewall_bouncer_config() { local key="$1" local value="$2" json_init if [ -z "$key" ]; then json_add_boolean "success" 0 json_add_string "error" "Configuration key required" json_dump return fi # Validate and set configuration case "$key" in enabled|ipv4|ipv6|deny_log|filter_input|filter_forward) # Boolean values if [ "$value" != "0" ] && [ "$value" != "1" ]; then json_add_boolean "success" 0 json_add_string "error" "Invalid boolean value: $value" json_dump return fi uci set "crowdsec.@bouncer[0].$key=$value" ;; api_url|update_frequency|deny_action|log_level|api_key) # String values if [ -z "$value" ]; then json_add_boolean "success" 0 json_add_string "error" "Value cannot be empty" json_dump return fi uci set "crowdsec.@bouncer[0].$key=$value" ;; *) json_add_boolean "success" 0 json_add_string "error" "Unknown configuration key: $key" json_dump return ;; esac # Commit changes if uci commit crowdsec 2>/dev/null; then json_add_boolean "success" 1 json_add_string "message" "Configuration updated: $key=$value" secubox_log "Firewall bouncer config updated: $key=$value" else json_add_boolean "success" 0 json_add_string "error" "Failed to commit UCI changes" fi json_dump } # Get nftables statistics get_nftables_stats() { json_init if ! command -v nft >/dev/null 2>&1; then json_add_boolean "available" 0 json_add_string "error" "nftables not available" json_dump return fi json_add_boolean "available" 1 # Check IPv4 table local ipv4_exists=0 if nft list table ip crowdsec >/dev/null 2>&1; then ipv4_exists=1 fi json_add_boolean "ipv4_table_exists" "$ipv4_exists" # Check IPv6 table local ipv6_exists=0 if nft list table ip6 crowdsec6 >/dev/null 2>&1; then ipv6_exists=1 fi json_add_boolean "ipv6_table_exists" "$ipv6_exists" # Get blocked IPs from IPv4 set json_add_array "ipv4_blocked_ips" if [ "$ipv4_exists" = "1" ]; then local ips ips=$(nft list set ip crowdsec crowdsec-blacklists 2>/dev/null | sed -n '/elements = {/,/}/p' | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' || echo "") if [ -n "$ips" ]; then local ip for ip in $ips; do json_add_string "" "$ip" done fi fi json_close_array # Get blocked IPs from IPv6 set json_add_array "ipv6_blocked_ips" if [ "$ipv6_exists" = "1" ]; then local ips ips=$(nft list set ip6 crowdsec6 crowdsec6-blacklists 2>/dev/null | sed -n '/elements = {/,/}/p' | grep -oE '([0-9a-fA-F]{0,4}:){7}[0-9a-fA-F]{0,4}' || echo "") if [ -n "$ips" ]; then local ip for ip in $ips; do json_add_string "" "$ip" done fi fi json_close_array # Get rules count local ipv4_rules=0 local ipv6_rules=0 if [ "$ipv4_exists" = "1" ]; then ipv4_rules=$(nft list table ip crowdsec 2>/dev/null | grep -c "rule" || echo "0") fi if [ "$ipv6_exists" = "1" ]; then ipv6_rules=$(nft list table ip6 crowdsec6 2>/dev/null | grep -c "rule" || echo "0") fi json_add_int "ipv4_rules_count" "$ipv4_rules" json_add_int "ipv6_rules_count" "$ipv6_rules" json_dump } # Check if wizard should be shown (first-time setup detection) check_wizard_needed() { json_init # Check if bouncer is configured local bouncer_configured=0 if uci -q get crowdsec.bouncer.enabled >/dev/null 2>&1; then bouncer_configured=1 fi # Check if collections are installed local collections_installed=0 if [ -x "$CSCLI" ]; then if run_cscli collections list 2>/dev/null | grep -q "INSTALLED"; then collections_installed=1 fi fi # Show wizard if not configured local show_wizard=0 if [ "$bouncer_configured" = "0" ] || [ "$collections_installed" = "0" ]; then show_wizard=1 fi json_add_boolean "show_wizard" "$show_wizard" json_add_boolean "bouncer_configured" "$bouncer_configured" json_add_boolean "collections_installed" "$collections_installed" json_dump } # Get wizard initial state get_wizard_state() { json_init # Get collections count local collections_count=0 if [ -x "$CSCLI" ]; then collections_count=$(run_cscli collections list 2>/dev/null | grep -c "INSTALLED" || echo "0") fi json_add_int "collections_count" "$collections_count" json_dump } # Repair LAPI - auto-fix common configuration issues repair_lapi() { json_init local steps_done="" local errors="" local REPAIR_TIMEOUT=10 secubox_log "Starting LAPI repair..." # Step 1: Stop CrowdSec completely /etc/init.d/crowdsec stop >/dev/null 2>&1 sleep 1 killall crowdsec 2>/dev/null sleep 1 steps_done="${steps_done}Stopped; " # Step 2: Create required directories mkdir -p /srv/crowdsec/data 2>/dev/null mkdir -p /etc/crowdsec/hub 2>/dev/null mkdir -p /etc/crowdsec/acquis.d 2>/dev/null mkdir -p /var/etc/crowdsec 2>/dev/null chmod 755 /srv/crowdsec/data 2>/dev/null steps_done="${steps_done}Dirs; " # Step 3: Fix config.yaml local config_file="/etc/crowdsec/config.yaml" local var_config="/var/etc/crowdsec/config.yaml" if [ -f "$config_file" ]; then sed -i 's|^\(\s*\)data_dir:.*|\1data_dir: /srv/crowdsec/data/|' "$config_file" 2>/dev/null sed -i 's|^\(\s*\)db_path:.*|\1db_path: /srv/crowdsec/data/crowdsec.db|' "$config_file" 2>/dev/null cp "$config_file" "$var_config" 2>/dev/null steps_done="${steps_done}Config; " else errors="${errors}No config; " fi # Step 4: Remove corrupted credentials local creds_file="/etc/crowdsec/local_api_credentials.yaml" rm -f "$creds_file" 2>/dev/null # Step 5: Clean up orphaned machines from database local db_file="/srv/crowdsec/data/crowdsec.db" if [ -f "$db_file" ]; then if command -v sqlite3 >/dev/null 2>&1; then # Remove all machines - we'll re-register sqlite3 "$db_file" "DELETE FROM machines;" 2>/dev/null steps_done="${steps_done}DB cleaned; " else # No sqlite3 - delete the database file, CrowdSec will recreate it rm -f "$db_file" 2>/dev/null steps_done="${steps_done}DB reset; " fi fi # Step 6: Start CrowdSec in LAPI-only mode (no agent) # This allows LAPI to start without needing valid credentials local crowdsec_bin="/usr/bin/crowdsec" if [ -x "$crowdsec_bin" ]; then # Start LAPI-only mode in background "$crowdsec_bin" -c "$var_config" -no-cs & local cs_pid=$! steps_done="${steps_done}LAPI-only started; " # Wait for LAPI to be ready (max 8 seconds) local retries=8 local lapi_ready=0 while [ $retries -gt 0 ]; do if grep -qi ":1F90 " /proc/net/tcp 2>/dev/null; then lapi_ready=1 break fi sleep 1 retries=$((retries - 1)) done if [ "$lapi_ready" = "1" ]; then steps_done="${steps_done}LAPI up; " sleep 1 # Step 7: Register machine while LAPI is running if [ -x "$CSCLI" ]; then local reg_output="" reg_output=$("$CSCLI" -c "$var_config" machines add localhost --auto --force -f "$creds_file" 2>&1) if [ $? -eq 0 ]; then steps_done="${steps_done}Registered; " else errors="${errors}Reg failed: ${reg_output}; " fi fi else errors="${errors}LAPI timeout; " fi # Step 8: Stop LAPI-only mode kill $cs_pid 2>/dev/null sleep 1 killall crowdsec 2>/dev/null steps_done="${steps_done}LAPI stopped; " else errors="${errors}No crowdsec binary; " fi # Step 9: Verify credentials were created if [ -s "$creds_file" ] && grep -q "password:" "$creds_file"; then steps_done="${steps_done}Creds OK; " else errors="${errors}No creds; " fi # Step 10: Start CrowdSec normally via init /etc/init.d/crowdsec start >/dev/null 2>&1 sleep 3 # Step 11: Verify everything is working local lapi_ok=0 if pgrep crowdsec >/dev/null 2>&1; then steps_done="${steps_done}Running; " if [ -x "$CSCLI" ]; then if run_with_timeout 5 "$CSCLI" lapi status >/dev/null 2>&1; then lapi_ok=1 steps_done="${steps_done}LAPI OK" else errors="${errors}LAPI check failed; " fi fi else errors="${errors}Not running; " # Get error from log local log_err="" log_err=$(tail -3 /var/log/crowdsec.log 2>/dev/null | grep -i "fatal\|error" | head -1) [ -n "$log_err" ] && errors="${errors}${log_err}; " fi if [ "$lapi_ok" = "1" ]; then json_add_boolean "success" 1 json_add_string "message" "LAPI repaired successfully" json_add_string "steps" "$steps_done" secubox_log "LAPI repair successful: $steps_done" else json_add_boolean "success" 0 json_add_string "error" "LAPI repair incomplete" json_add_string "steps" "$steps_done" json_add_string "errors" "$errors" secubox_log "LAPI repair failed: $errors" fi json_dump } # Reset wizard - clean up for fresh start reset_wizard() { json_init local steps_done="" local errors="" secubox_log "Starting CrowdSec wizard reset (recovery mode)..." # Step 1: Stop services /etc/init.d/crowdsec-firewall-bouncer stop >/dev/null 2>&1 steps_done="${steps_done}Stopped firewall bouncer; " # Step 2: Delete existing bouncer registration if [ -x "$CSCLI" ]; then run_cscli bouncers delete "crowdsec-firewall-bouncer" >/dev/null 2>&1 steps_done="${steps_done}Deleted bouncer registration; " # Also try database cleanup sqlite3 /srv/crowdsec/data/crowdsec.db "DELETE FROM bouncers WHERE name='crowdsec-firewall-bouncer';" 2>/dev/null fi # Step 3: Clear UCI bouncer config uci -q delete crowdsec.bouncer 2>/dev/null uci commit crowdsec 2>/dev/null steps_done="${steps_done}Cleared UCI config; " # Step 4: Remove bouncer config file rm -f /etc/crowdsec/bouncers/crowdsec-firewall-bouncer.yaml 2>/dev/null steps_done="${steps_done}Removed bouncer config file; " # Step 5: Reset nftables rules if command -v nft >/dev/null 2>&1; then nft delete table ip crowdsec 2>/dev/null nft delete table ip6 crowdsec6 2>/dev/null steps_done="${steps_done}Cleared nftables rules; " fi json_add_boolean "success" 1 json_add_string "message" "Wizard reset completed - ready for fresh setup" json_add_string "steps" "$steps_done" secubox_log "Wizard reset completed: $steps_done" json_dump } # Console enrollment status get_console_status() { json_init check_cscli local enrolled=0 local name="" local company="" # Check if console is enrolled by checking if there's a console config if [ -f "/etc/crowdsec/console.yaml" ]; then # Check if the console is actually configured (has credentials) if grep -q "share_manual_decisions\|share_tainted\|share_context" /etc/crowdsec/console.yaml 2>/dev/null; then enrolled=1 fi fi # Try to get console status from cscli local console_status console_status=$(run_cscli console status 2>/dev/null) if [ -n "$console_status" ]; then # Check if enrolled by looking for "Enrolled" or similar in output if echo "$console_status" | grep -qi "enrolled\|connected\|active"; then enrolled=1 fi # Extract name if present name=$(echo "$console_status" | grep -i "name\|machine" | head -1 | awk -F: '{print $2}' | xargs 2>/dev/null) fi json_add_boolean "enrolled" "$enrolled" json_add_string "name" "$name" # Get share settings if enrolled if [ "$enrolled" = "1" ] && [ -f "/etc/crowdsec/console.yaml" ]; then local share_manual=$(grep "share_manual_decisions:" /etc/crowdsec/console.yaml 2>/dev/null | awk '{print $2}') local share_tainted=$(grep "share_tainted:" /etc/crowdsec/console.yaml 2>/dev/null | awk '{print $2}') local share_context=$(grep "share_context:" /etc/crowdsec/console.yaml 2>/dev/null | awk '{print $2}') json_add_boolean "share_manual_decisions" "$([ \"$share_manual\" = \"true\" ] && echo 1 || echo 0)" json_add_boolean "share_tainted" "$([ \"$share_tainted\" = \"true\" ] && echo 1 || echo 0)" json_add_boolean "share_context" "$([ \"$share_context\" = \"true\" ] && echo 1 || echo 0)" fi json_dump } # Console enroll console_enroll() { local key="$1" local name="$2" json_init check_cscli if [ -z "$key" ]; then json_add_boolean "success" 0 json_add_string "error" "Enrollment key is required" json_dump return fi secubox_log "Enrolling CrowdSec Console with key..." # Build enroll command local enroll_cmd="run_cscli console enroll $key" if [ -n "$name" ]; then enroll_cmd="$enroll_cmd --name \"$name\"" fi # Run enrollment local output output=$(eval "$enroll_cmd" 2>&1) local result=$? if [ "$result" -eq 0 ]; then json_add_boolean "success" 1 json_add_string "message" "Successfully enrolled in CrowdSec Console" json_add_string "output" "$output" secubox_log "Console enrollment successful" # Enable sharing options by default run_cscli console enable share_manual_decisions >/dev/null 2>&1 run_cscli console enable share_tainted >/dev/null 2>&1 run_cscli console enable share_context >/dev/null 2>&1 else json_add_boolean "success" 0 json_add_string "error" "Enrollment failed" json_add_string "output" "$output" secubox_log "Console enrollment failed: $output" fi json_dump } # Console disable (unenroll) console_disable() { json_init check_cscli secubox_log "Disabling CrowdSec Console enrollment..." # Disable all sharing run_cscli console disable share_manual_decisions >/dev/null 2>&1 run_cscli console disable share_tainted >/dev/null 2>&1 run_cscli console disable share_context >/dev/null 2>&1 # Remove console config if [ -f "/etc/crowdsec/console.yaml" ]; then rm -f /etc/crowdsec/console.yaml fi json_add_boolean "success" 1 json_add_string "message" "Console enrollment disabled" secubox_log "Console enrollment disabled" json_dump } # Configure log acquisition settings configure_acquisition() { local syslog_enabled="$1" local firewall_enabled="$2" local ssh_enabled="$3" local http_enabled="$4" local syslog_path="$5" json_init local steps_done="" local errors="" secubox_log "Configuring CrowdSec log acquisition..." # Step 1: Ensure acquisition section exists in UCI if ! uci -q get crowdsec.acquisition >/dev/null 2>&1; then uci set crowdsec.acquisition='acquisition' steps_done="${steps_done}Created acquisition section; " fi # Step 2: Set acquisition options uci set crowdsec.acquisition.syslog_enabled="${syslog_enabled:-1}" uci set crowdsec.acquisition.firewall_enabled="${firewall_enabled:-1}" uci set crowdsec.acquisition.ssh_enabled="${ssh_enabled:-1}" uci set crowdsec.acquisition.http_enabled="${http_enabled:-0}" if [ -n "$syslog_path" ]; then uci set crowdsec.acquisition.syslog_path="$syslog_path" fi uci commit crowdsec steps_done="${steps_done}Updated UCI settings; " # Step 3: Generate acquisition YAML files # OpenWrt uses logread command instead of /var/log/messages by default # All syslog entries (SSH, firewall, system) go through the same log stream # We create ONE unified acquisition file to avoid multiple logread processes local acquis_dir="/etc/crowdsec/acquis.d" mkdir -p "$acquis_dir" # Remove old separate acquisition files if they exist rm -f "$acquis_dir/openwrt-syslog.yaml" 2>/dev/null rm -f "$acquis_dir/openwrt-firewall.yaml" 2>/dev/null rm -f "$acquis_dir/openwrt-dropbear.yaml" 2>/dev/null # Create unified syslog acquisition if any syslog-based source is enabled # SSH, firewall, and system logs all go through OpenWrt's logread if [ "$syslog_enabled" = "1" ] || [ "$firewall_enabled" = "1" ] || [ "$ssh_enabled" = "1" ]; then cat > "$acquis_dir/openwrt-unified.yaml" << 'YAML' # OpenWrt Unified Syslog Acquisition # Auto-generated by SecuBox CrowdSec Wizard # Uses logread -f to stream all syslog entries # Covers: system logs, SSH/Dropbear, firewall (iptables/nftables) source: command command: /sbin/logread -f labels: type: syslog YAML local enabled_sources="" [ "$syslog_enabled" = "1" ] && enabled_sources="${enabled_sources}system " [ "$ssh_enabled" = "1" ] && enabled_sources="${enabled_sources}SSH " [ "$firewall_enabled" = "1" ] && enabled_sources="${enabled_sources}firewall " steps_done="${steps_done}Created unified syslog acquisition (${enabled_sources}); " else rm -f "$acquis_dir/openwrt-unified.yaml" steps_done="${steps_done}Disabled syslog acquisition; " fi # Enable/disable HTTP log acquisition (separate file-based source) if [ "$http_enabled" = "1" ]; then # Check if log files exist if [ -f "/var/log/uhttpd.log" ]; then cat > "$acquis_dir/openwrt-http.yaml" << 'YAML' # OpenWrt uHTTPd Web Server Log Acquisition # Auto-generated by SecuBox CrowdSec Wizard filenames: - /var/log/uhttpd.log labels: type: nginx YAML elif [ -f "/var/log/nginx/access.log" ]; then cat > "$acquis_dir/openwrt-http.yaml" << 'YAML' # OpenWrt nginx Web Server Log Acquisition # Auto-generated by SecuBox CrowdSec Wizard filenames: - /var/log/nginx/access.log labels: type: nginx YAML else # Fallback - try both locations cat > "$acquis_dir/openwrt-http.yaml" << 'YAML' # OpenWrt Web Server Log Acquisition # Auto-generated by SecuBox CrowdSec Wizard filenames: - /var/log/uhttpd.log - /var/log/nginx/access.log labels: type: nginx YAML fi steps_done="${steps_done}Created HTTP acquisition; " else rm -f "$acquis_dir/openwrt-http.yaml" rm -f "$acquis_dir/openwrt-uhttpd.yaml" 2>/dev/null steps_done="${steps_done}Disabled HTTP acquisition; " fi # Step 4: Restart CrowdSec to apply acquisition changes if /etc/init.d/crowdsec reload >/dev/null 2>&1; then steps_done="${steps_done}Reloaded CrowdSec" else # Fallback to restart if reload fails /etc/init.d/crowdsec restart >/dev/null 2>&1 steps_done="${steps_done}Restarted CrowdSec" fi json_add_boolean "success" 1 json_add_string "message" "Acquisition configuration completed" json_add_string "steps" "$steps_done" secubox_log "Acquisition configuration completed: $steps_done" json_dump } # Get current acquisition configuration get_acquisition_config() { json_init # Get values from UCI local syslog_enabled=$(uci -q get crowdsec.acquisition.syslog_enabled || echo "1") local firewall_enabled=$(uci -q get crowdsec.acquisition.firewall_enabled || echo "1") local ssh_enabled=$(uci -q get crowdsec.acquisition.ssh_enabled || echo "1") local http_enabled=$(uci -q get crowdsec.acquisition.http_enabled || echo "0") local syslog_path=$(uci -q get crowdsec.acquisition.syslog_path || echo "/var/log/messages") json_add_string "syslog_enabled" "$syslog_enabled" json_add_string "firewall_enabled" "$firewall_enabled" json_add_string "ssh_enabled" "$ssh_enabled" json_add_string "http_enabled" "$http_enabled" json_add_string "syslog_path" "$syslog_path" # Check which acquisition files exist local acquis_dir="/etc/crowdsec/acquis.d" local unified_exists=0 local http_exists=0 [ -f "$acquis_dir/openwrt-unified.yaml" ] && unified_exists=1 [ -f "$acquis_dir/openwrt-http.yaml" ] && http_exists=1 json_add_boolean "unified_file_exists" "$unified_exists" json_add_boolean "http_file_exists" "$http_exists" json_dump } # Service control (start/stop/restart/reload) service_control() { local action="$1" json_init case "$action" in start|stop|restart|reload) secubox_log "CrowdSec service $action requested" local output output=$(/etc/init.d/crowdsec "$action" 2>&1) local result=$? sleep 2 # Check if service is running after action local running=0 if pgrep crowdsec >/dev/null 2>&1; then running=1 fi if [ "$result" -eq 0 ]; then json_add_boolean "success" 1 json_add_string "action" "$action" json_add_boolean "running" "$running" json_add_string "message" "Service $action completed" else json_add_boolean "success" 0 json_add_string "error" "Service $action failed" json_add_string "output" "$output" fi ;; *) json_add_boolean "success" 0 json_add_string "error" "Invalid action. Use: start, stop, restart, reload" ;; esac json_dump } # Main dispatcher case "$1" in list) echo '{"decisions":{},"alerts":{"limit":"number"},"metrics":{},"bouncers":{},"machines":{},"hub":{},"status":{},"ban":{"ip":"string","duration":"string","reason":"string"},"unban":{"ip":"string"},"stats":{},"seccubox_logs":{},"collect_debug":{},"waf_status":{},"metrics_config":{},"configure_metrics":{"enable":"string"},"collections":{},"install_collection":{"collection":"string"},"remove_collection":{"collection":"string"},"update_hub":{},"register_bouncer":{"bouncer_name":"string"},"delete_bouncer":{"bouncer_name":"string"},"firewall_bouncer_status":{},"control_firewall_bouncer":{"action":"string"},"firewall_bouncer_config":{},"update_firewall_bouncer_config":{"key":"string","value":"string"},"nftables_stats":{},"check_wizard_needed":{},"wizard_state":{},"repair_lapi":{},"reset_wizard":{},"console_status":{},"console_enroll":{"key":"string","name":"string"},"console_disable":{},"service_control":{"action":"string"},"configure_acquisition":{"syslog_enabled":"string","firewall_enabled":"string","ssh_enabled":"string","http_enabled":"string","syslog_path":"string"},"acquisition_config":{}}' ;; call) case "$2" in decisions) get_decisions ;; alerts) read -r input limit=$(echo "$input" | jsonfilter -e '@.limit' 2>/dev/null) get_alerts "${limit:-50}" ;; metrics) get_metrics ;; bouncers) get_bouncers ;; machines) get_machines ;; hub) get_hub ;; status) get_status ;; ban) read -r input ip=$(echo "$input" | jsonfilter -e '@.ip' 2>/dev/null) duration=$(echo "$input" | jsonfilter -e '@.duration' 2>/dev/null) reason=$(echo "$input" | jsonfilter -e '@.reason' 2>/dev/null) add_ban "$ip" "$duration" "$reason" ;; unban) read -r input ip=$(echo "$input" | jsonfilter -e '@.ip' 2>/dev/null) remove_ban "$ip" ;; stats) get_dashboard_stats ;; seccubox_logs) seccubox_logs ;; collect_debug) collect_debug ;; waf_status) get_waf_status ;; metrics_config) get_metrics_config ;; configure_metrics) read -r input enable=$(echo "$input" | jsonfilter -e '@.enable' 2>/dev/null) configure_metrics "$enable" ;; collections) get_collections ;; install_collection) read -r input collection=$(echo "$input" | jsonfilter -e '@.collection' 2>/dev/null) install_collection "$collection" ;; remove_collection) read -r input collection=$(echo "$input" | jsonfilter -e '@.collection' 2>/dev/null) remove_collection "$collection" ;; update_hub) update_hub ;; register_bouncer) read -r input bouncer_name=$(echo "$input" | jsonfilter -e '@.bouncer_name' 2>/dev/null) register_bouncer "$bouncer_name" ;; delete_bouncer) read -r input bouncer_name=$(echo "$input" | jsonfilter -e '@.bouncer_name' 2>/dev/null) delete_bouncer "$bouncer_name" ;; firewall_bouncer_status) get_firewall_bouncer_status ;; control_firewall_bouncer) read -r input action=$(echo "$input" | jsonfilter -e '@.action' 2>/dev/null) control_firewall_bouncer "$action" ;; firewall_bouncer_config) get_firewall_bouncer_config ;; update_firewall_bouncer_config) read -r input key=$(echo "$input" | jsonfilter -e '@.key' 2>/dev/null) value=$(echo "$input" | jsonfilter -e '@.value' 2>/dev/null) update_firewall_bouncer_config "$key" "$value" ;; nftables_stats) get_nftables_stats ;; check_wizard_needed) check_wizard_needed ;; wizard_state) get_wizard_state ;; repair_lapi) repair_lapi ;; reset_wizard) reset_wizard ;; console_status) get_console_status ;; console_enroll) read -r input key=$(echo "$input" | jsonfilter -e '@.key' 2>/dev/null) name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null) console_enroll "$key" "$name" ;; console_disable) console_disable ;; service_control) read -r input action=$(echo "$input" | jsonfilter -e '@.action' 2>/dev/null) service_control "$action" ;; configure_acquisition) read -r input syslog_enabled=$(echo "$input" | jsonfilter -e '@.syslog_enabled' 2>/dev/null) firewall_enabled=$(echo "$input" | jsonfilter -e '@.firewall_enabled' 2>/dev/null) ssh_enabled=$(echo "$input" | jsonfilter -e '@.ssh_enabled' 2>/dev/null) http_enabled=$(echo "$input" | jsonfilter -e '@.http_enabled' 2>/dev/null) syslog_path=$(echo "$input" | jsonfilter -e '@.syslog_path' 2>/dev/null) configure_acquisition "$syslog_enabled" "$firewall_enabled" "$ssh_enabled" "$http_enabled" "$syslog_path" ;; acquisition_config) get_acquisition_config ;; *) echo '{"error": "Unknown method"}' ;; esac ;; esac