073890f062
ISSUE: The intel_tags logic at lines 2991+ uses variables multi_vector and geo_bonus to build threat intelligence tags. But these variables were declared as 'local' INSIDE the skip_scoring conditional block (lines 2855, 2885). PROBLEM: In bash, 'local' variables are function-scoped (not block-scoped like other languages). But declaring them inside a conditional block creates an expectation they're only needed inside that block. When used OUTSIDE the block (after line 2957), they may be undefined if the block wasn't executed (e.g., when skip_scoring=1). BEHAVIOR WITH BUG: 1. When skip_scoring=0 (not whitelisted): - multi_vector and geo_bonus are initialized inside the block - Used outside the block - Works (but relies on block being executed) 2. When skip_scoring=1 (whitelisted): - multi_vector and geo_bonus are NEVER initialized - Used outside the block at lines 2991, 2999+ with undefined values - Undefined variables expand to empty strings in bash - Conditions like [ "$multi_vector" -eq 1 ] silently fail - Intel tags for multi-vector and geo-based threats not generated IMPACT: - Whitelisted IPs: MULTI-VECTOR and HOSTILE tags never shown (even if they should be) - Intel_tags incomplete for whitelisted attacks with geographic/multi-vector indicators - Misleading threat summary (appears less sophisticated than actual) ROOT CAUSE: Variables needed across scopes were declared inside a conditional block instead of before the conditional. FIX: Declare multi_vector=0 and geo_bonus=0 BEFORE the skip_scoring block (line 2748). Remove the duplicate 'local' declarations inside the block. Now both variables: - Are initialized to 0 before the skip_scoring check - Can be safely used in intel_tags logic (lines 2991+) - Work correctly for both whitelisted and non-whitelisted IPs LINES CHANGED: - Added declarations at line ~2755 (before skip_scoring block) - Removed declarations from line 2861 (was in multi_vector logic) - Removed declarations from line 2891 (was in geo_bonus logic) VERIFICATION: - Syntax: ✓ Pass - Scope: ✓ Variables now accessible throughout IP processing - Logic: ✓ Same initialization semantics, better scope management Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
3911 lines
171 KiB
Bash
Executable File
3911 lines
171 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
################################################################################
|
|
# Live Network Security Monitor - ENHANCED with Intelligence
|
|
################################################################################
|
|
# Purpose: Real-time monitoring with bot intelligence and threat scoring
|
|
# Version: 2.0 - Intelligence Mode
|
|
# Features:
|
|
# - Bot classification using learned signatures
|
|
# - IP reputation DB integration
|
|
# - Real-time threat scoring (0-100)
|
|
# - Attack vector detection
|
|
# - Quick action blocking system
|
|
# - Ban tracking and history
|
|
################################################################################
|
|
|
|
# Get script directory
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
source "$SCRIPT_DIR/lib/common-functions.sh"
|
|
source "$SCRIPT_DIR/lib/system-detect.sh"
|
|
source "$SCRIPT_DIR/lib/ip-reputation.sh"
|
|
source "$SCRIPT_DIR/lib/bot-signatures.sh"
|
|
source "$SCRIPT_DIR/lib/attack-patterns.sh"
|
|
source "$SCRIPT_DIR/lib/threat-intelligence.sh"
|
|
|
|
# Enhanced attack detection (ET Open signatures)
|
|
source "$SCRIPT_DIR/lib/attack-signatures.sh" 2>/dev/null || true
|
|
source "$SCRIPT_DIR/lib/http-attack-analyzer.sh" 2>/dev/null || true
|
|
source "$SCRIPT_DIR/lib/rate-anomaly-detector.sh" 2>/dev/null || true
|
|
|
|
# Require root
|
|
if [ "$EUID" -ne 0 ]; then
|
|
print_error "This script must be run as root"
|
|
exit 1
|
|
fi
|
|
|
|
# Color definitions for threat levels
|
|
CRITICAL_COLOR='\033[1;41;97m' # White on Red background
|
|
HIGH_COLOR='\033[1;31m' # Bold Red
|
|
MEDIUM_COLOR='\033[1;33m' # Bold Yellow
|
|
LOW_COLOR='\033[0;36m' # Cyan
|
|
SAFE_COLOR='\033[0;32m' # Green
|
|
INFO_COLOR='\033[0;37m' # White
|
|
BOLD='\033[1m' # Bold text
|
|
NC='\033[0m'
|
|
|
|
# Configuration
|
|
REFRESH_INTERVAL=2 # Seconds between dashboard refreshes
|
|
MAX_DISPLAY_LINES=20
|
|
THREAT_THRESHOLD_CRITICAL=80
|
|
THREAT_THRESHOLD_HIGH=60
|
|
THREAT_THRESHOLD_MEDIUM=40
|
|
|
|
# Display mode (compact by default for small terminals)
|
|
COMPACT_MODE=1
|
|
TERMINAL_HEIGHT=$(tput lines 2>/dev/null || echo "24")
|
|
|
|
# Temporary files for tracking
|
|
TEMP_DIR="/tmp/live-monitor-$$"
|
|
SNAPSHOT_DIR="/tmp/server-toolkit-live-monitor"
|
|
mkdir -p "$TEMP_DIR" "$SNAPSHOT_DIR" 2>/dev/null
|
|
touch "$TEMP_DIR/recent_events"
|
|
touch "$TEMP_DIR/ip_data"
|
|
echo "0" > "$TEMP_DIR/event_counter"
|
|
echo "0" > "$TEMP_DIR/total_blocks"
|
|
|
|
# IPset configuration
|
|
IPSET_NAME=""
|
|
IPSET_AVAILABLE=0
|
|
IPSET_SUPPORTS_TIMEOUT=0
|
|
IPSET_INIT_ERROR="" # Store initialization error message
|
|
|
|
# Initialize IPset for fast blocking (if available)
|
|
# PRIORITY: Always use CSF's chain_DENY if available - it's the standard CSF blocking ipset
|
|
if command -v ipset &>/dev/null; then
|
|
# Check if CSF's chain_DENY IPset exists (use it regardless of timeout support)
|
|
if ipset list chain_DENY &>/dev/null 2>&1; then
|
|
# CSF ipset exists - use it for all blocking!
|
|
IPSET_NAME="chain_DENY"
|
|
IPSET_AVAILABLE=1
|
|
|
|
# Check if it supports timeouts (nice-to-have, not required)
|
|
if ipset list chain_DENY | grep -q "^Type:.*timeout"; then
|
|
IPSET_SUPPORTS_TIMEOUT=1
|
|
echo "✓ Using CSF IPset: chain_DENY (with timeout support)" >> "$TEMP_DIR/debug.log" 2>/dev/null || true
|
|
else
|
|
IPSET_SUPPORTS_TIMEOUT=0
|
|
echo "✓ Using CSF IPset: chain_DENY (without timeout - CSF manages cleanup)" >> "$TEMP_DIR/debug.log" 2>/dev/null || true
|
|
fi
|
|
else
|
|
# CSF ipset doesn't exist - only create our own as last resort
|
|
echo "→ CSF chain_DENY ipset not found - creating temporary monitor ipset" >> "$TEMP_DIR/debug.log" 2>/dev/null || true
|
|
|
|
IPSET_NAME="live_monitor_$$"
|
|
|
|
# Capture detailed error output
|
|
IPSET_CREATE_OUTPUT=$(ipset create "$IPSET_NAME" hash:ip timeout 3600 maxelem 65536 2>&1)
|
|
IPSET_CREATE_EXIT=$?
|
|
|
|
if [ "${IPSET_CREATE_EXIT:-1}" -eq 0 ]; then
|
|
IPSET_AVAILABLE=1
|
|
IPSET_SUPPORTS_TIMEOUT=1
|
|
|
|
# Add iptables rule to block IPs in the set
|
|
IPTABLES_OUTPUT=$(iptables -I INPUT -m set --match-set "$IPSET_NAME" src -j DROP 2>&1)
|
|
IPTABLES_EXIT=$?
|
|
|
|
if [ "${IPTABLES_EXIT:-1}" -ne 0 ]; then
|
|
# iptables rule failed - clean up ipset and report error
|
|
ipset destroy "$IPSET_NAME" 2>/dev/null
|
|
IPSET_AVAILABLE=0
|
|
IPSET_INIT_ERROR="iptables rule creation failed: $IPTABLES_OUTPUT"
|
|
echo "✗ IPset created but iptables rule failed: $IPTABLES_OUTPUT" >> "$TEMP_DIR/debug.log" 2>/dev/null || true
|
|
else
|
|
echo "✓ Temporary IPset initialized: $IPSET_NAME (fast blocking enabled)" >> "$TEMP_DIR/debug.log" 2>/dev/null || true
|
|
fi
|
|
else
|
|
# IPset creation failed - capture why
|
|
IPSET_INIT_ERROR="ipset creation failed: $IPSET_CREATE_OUTPUT"
|
|
echo "✗ IPset creation failed: $IPSET_CREATE_OUTPUT" >> "$TEMP_DIR/debug.log" 2>/dev/null || true
|
|
|
|
# Check for common issues and provide helpful diagnostics
|
|
if echo "$IPSET_CREATE_OUTPUT" | grep -qi "module"; then
|
|
KERNEL_MODS=$(lsmod | grep -E "ip_set|xt_set" || echo "NOT LOADED")
|
|
IPSET_INIT_ERROR="$IPSET_INIT_ERROR | Kernel modules: $KERNEL_MODS"
|
|
echo " → Kernel module issue detected. Loaded modules: $KERNEL_MODS" >> "$TEMP_DIR/debug.log" 2>/dev/null || true
|
|
fi
|
|
|
|
if echo "$IPSET_CREATE_OUTPUT" | grep -qi "permission"; then
|
|
IPSET_INIT_ERROR="$IPSET_INIT_ERROR | Permission denied (need root)"
|
|
echo " → Permission denied. Current user: $(whoami), EUID: $EUID" >> "$TEMP_DIR/debug.log" 2>/dev/null || true
|
|
fi
|
|
fi
|
|
fi
|
|
else
|
|
# ipset command not found - provide diagnostic info
|
|
IPSET_INIT_ERROR="ipset command not found in PATH"
|
|
echo "✗ IPset not available - using CSF for blocking" >> "$TEMP_DIR/debug.log" 2>/dev/null || true
|
|
|
|
# Check if ipset package is installed
|
|
if command -v rpm &>/dev/null && rpm -q ipset &>/dev/null; then
|
|
IPSET_INIT_ERROR="$IPSET_INIT_ERROR | Package installed but not in PATH"
|
|
echo " → ipset package IS installed but command not found" >> "$TEMP_DIR/debug.log" 2>/dev/null || true
|
|
elif command -v dpkg &>/dev/null && dpkg -l ipset 2>/dev/null | grep -q "^ii"; then
|
|
IPSET_INIT_ERROR="$IPSET_INIT_ERROR | Package installed but not in PATH"
|
|
echo " → ipset package IS installed but command not found" >> "$TEMP_DIR/debug.log" 2>/dev/null || true
|
|
else
|
|
IPSET_INIT_ERROR="$IPSET_INIT_ERROR | Package not installed"
|
|
echo " → ipset package NOT installed" >> "$TEMP_DIR/debug.log" 2>/dev/null || true
|
|
fi
|
|
fi
|
|
|
|
# Initialize blocked IPs cache immediately on startup
|
|
{
|
|
# Get CSF temporary blocks - extract just the IP address
|
|
if command -v csf &>/dev/null; then
|
|
csf -t 2>/dev/null | awk '/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/ {print $1}'
|
|
fi
|
|
|
|
# Get CSF permanent denies
|
|
if [ -f /etc/csf/csf.deny ]; then
|
|
awk '/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/ {print $1}' /etc/csf/csf.deny 2>/dev/null
|
|
fi
|
|
|
|
# Get iptables DROP rules
|
|
if command -v iptables &>/dev/null; then
|
|
iptables -L INPUT -n -v 2>/dev/null | awk '/DROP/ && $8 ~ /^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/ {print $8}'
|
|
fi
|
|
} | sort -u > "$TEMP_DIR/blocked_ips_cache" 2>/dev/null
|
|
|
|
# Log cache initialization for debugging
|
|
if [ -f "$TEMP_DIR/blocked_ips_cache" ]; then
|
|
CACHED_COUNT=$(wc -l < "$TEMP_DIR/blocked_ips_cache" 2>/dev/null || echo "0")
|
|
echo "Initialized blocked IPs cache with $CACHED_COUNT IPs" >> "$TEMP_DIR/debug.log"
|
|
fi
|
|
|
|
# Cleanup function
|
|
cleanup() {
|
|
echo ""
|
|
echo "Stopping monitoring processes..."
|
|
|
|
# Kill all child processes
|
|
pkill -P $$ 2>/dev/null
|
|
|
|
# Wait a moment for background jobs
|
|
sleep 1
|
|
|
|
# SAVE SNAPSHOT BEFORE DELETING TEMP FILES!
|
|
echo "Saving IP reputation snapshot..."
|
|
save_snapshot
|
|
|
|
# Also save to IP reputation database for permanent tracking
|
|
if [ ${#IP_DATA[@]} -gt 0 ]; then
|
|
for ip in "${!IP_DATA[@]}"; do
|
|
IFS='|' read -r score hits bot_type attacks ban_count rep_score <<< "${IP_DATA[$ip]}"
|
|
# Update IP reputation database if score > 0
|
|
if [ "$score" -gt 0 ] && type record_ip_data &>/dev/null; then
|
|
record_ip_data "$ip" "$score" "$hits" "$attacks" "$ban_count" 2>/dev/null &
|
|
fi
|
|
done
|
|
wait # Wait for all database updates to complete
|
|
echo "✓ Saved ${#IP_DATA[@]} IPs to reputation database"
|
|
fi
|
|
|
|
# Clean up IPset and iptables rule ONLY if we created them (not CSF's chain_DENY)
|
|
if [ "$IPSET_AVAILABLE" -eq 1 ] && [ "$IPSET_NAME" != "chain_DENY" ]; then
|
|
echo "Removing IPset firewall rules..."
|
|
iptables -D INPUT -m set --match-set "$IPSET_NAME" src -j DROP 2>/dev/null
|
|
ipset destroy "$IPSET_NAME" 2>/dev/null
|
|
echo "✓ IPset cleaned up"
|
|
fi
|
|
|
|
# Clean up temp directory (AFTER saving snapshot)
|
|
rm -rf "$TEMP_DIR" 2>/dev/null
|
|
|
|
# Restore cursor
|
|
command -v tput &>/dev/null && tput cnorm
|
|
|
|
echo "✓ Cleanup complete - snapshot saved to $SNAPSHOT_DIR"
|
|
exit 0
|
|
}
|
|
|
|
trap cleanup EXIT INT TERM
|
|
|
|
# Save current monitoring state to temp files (for persistence across sessions)
|
|
save_snapshot() {
|
|
# Save IP_DATA associative array to PERMANENT storage (survives script exit)
|
|
local snapshot_file="$SNAPSHOT_DIR/latest_snapshot.dat"
|
|
local timestamp=$(date +%Y%m%d_%H%M%S)
|
|
local timestamped_file="$SNAPSHOT_DIR/snapshot_${timestamp}.dat"
|
|
|
|
# Write IP data to both current and timestamped snapshot
|
|
{
|
|
for ip in "${!IP_DATA[@]}"; do
|
|
echo "IP_DATA[$ip]=${IP_DATA[$ip]}"
|
|
done
|
|
|
|
# Write attack type counters
|
|
for attack in "${!ATTACK_TYPE_COUNTER[@]}"; do
|
|
echo "ATTACK_TYPE_COUNTER[$attack]=${ATTACK_TYPE_COUNTER[$attack]}"
|
|
done
|
|
|
|
# Write totals
|
|
echo "TOTAL_THREATS=$TOTAL_THREATS"
|
|
echo "TOTAL_BLOCKS=$TOTAL_BLOCKS"
|
|
echo "START_TIME=$START_TIME"
|
|
} > "$snapshot_file" 2>/dev/null
|
|
|
|
# Also save timestamped copy for history
|
|
cp "$snapshot_file" "$timestamped_file" 2>/dev/null
|
|
|
|
# Keep only last 10 snapshots to prevent disk bloat
|
|
ls -t "$SNAPSHOT_DIR"/snapshot_*.dat 2>/dev/null | tail -n +11 | xargs rm -f 2>/dev/null
|
|
}
|
|
|
|
# Statistics counters
|
|
declare -A IP_DATA # Stores: IP -> score|hits|bot_type|attacks|ban_count|rep_score
|
|
declare -A IP_TIMESTAMPS # Stores: IP -> comma-separated attack timestamps (last 100)
|
|
declare -A IP_ATTACK_VECTORS # Stores: IP -> unique attack vectors (SSH,WEB,EMAIL,etc)
|
|
declare -A SUBNET_ATTACKS # Stores: subnet -> attack count
|
|
declare -A ATTACK_TYPE_COUNTER
|
|
TOTAL_THREATS=0
|
|
TOTAL_BLOCKS=0
|
|
START_TIME=$(date +%s)
|
|
MAX_TRACKED_IPS=500 # Prevent memory overflow
|
|
VELOCITY_WINDOW=3600 # 1 hour in seconds
|
|
DECAY_CHECK_INTERVAL=1800 # Check for decay every 30 minutes
|
|
LAST_DECAY_CHECK=$START_TIME
|
|
|
|
# Hide cursor for cleaner display
|
|
command -v tput &>/dev/null && tput civis
|
|
|
|
################################################################################
|
|
# Intelligence Functions
|
|
################################################################################
|
|
|
|
# Get or create IP intelligence data
|
|
# Returns: score|hits|bot_type|attacks|ban_count|rep_score
|
|
get_ip_intelligence() {
|
|
local ip="$1"
|
|
|
|
# Check if we have cached data
|
|
if [ -n "${IP_DATA[$ip]}" ]; then
|
|
echo "${IP_DATA[$ip]}"
|
|
return 0
|
|
fi
|
|
|
|
# Query IP reputation database
|
|
local rep_data=$(lookup_ip "$ip" 2>/dev/null)
|
|
|
|
if [ -n "$rep_data" ]; then
|
|
# Parse: IP|HIT_COUNT|REP_SCORE|COUNTRY|ATTACK_FLAGS|...
|
|
IFS='|' read -r _ db_hits rep_score country attack_flags _ _ _ notes ban_count _ <<< "$rep_data"
|
|
|
|
# Initialize with learned data
|
|
local score=${rep_score:-0}
|
|
local hits=${db_hits:-0}
|
|
local bot_type="unknown"
|
|
local attacks=$(decode_attack_flags "$attack_flags" 2>/dev/null | tr ',' ' ' || echo "")
|
|
ban_count=${ban_count:-0}
|
|
|
|
# Cache it
|
|
IP_DATA[$ip]="$score|$hits|$bot_type|$attacks|$ban_count|$rep_score"
|
|
echo "${IP_DATA[$ip]}"
|
|
else
|
|
# New IP - initialize
|
|
IP_DATA[$ip]="0|0|unknown||0|0"
|
|
echo "${IP_DATA[$ip]}"
|
|
fi
|
|
}
|
|
export -f get_ip_intelligence
|
|
|
|
# Write IP data directly to file (for cross-process communication)
|
|
write_ip_data_to_file() {
|
|
local ip="$1"
|
|
local data="$2"
|
|
|
|
# Use flock for thread-safe writes (with timeout to prevent deadlocks)
|
|
# CRITICAL FIX: Increased timeout from 5 to 30 seconds
|
|
# Reason: At 70+ IPs/sec with write_ip_data_to_file backgrounded,
|
|
# 5-second timeout causes 20-30% silent data loss on high-velocity attacks
|
|
# 30-second timeout ensures all IPs are tracked during sustained attacks
|
|
(
|
|
flock -w 30 200 || return 1
|
|
|
|
# Read existing data
|
|
local temp_file="$TEMP_DIR/ip_data.tmp"
|
|
cp "$TEMP_DIR/ip_data" "$temp_file" 2>/dev/null || touch "$temp_file"
|
|
|
|
# Remove old entry for this IP (if exists)
|
|
grep -v "^${ip}=" "$temp_file" > "${temp_file}.new" 2>/dev/null || true
|
|
|
|
# Add new entry
|
|
echo "${ip}=${data}" >> "${temp_file}.new"
|
|
|
|
# Atomic replacement
|
|
mv "${temp_file}.new" "$TEMP_DIR/ip_data"
|
|
rm -f "$temp_file"
|
|
|
|
) 200>"$TEMP_DIR/ip_data.lock"
|
|
}
|
|
export -f write_ip_data_to_file
|
|
|
|
# Update IP intelligence
|
|
update_ip_intelligence() {
|
|
local ip="$1"
|
|
local url="$2"
|
|
local user_agent="$3"
|
|
local method="${4:-GET}"
|
|
|
|
# Get current data
|
|
local current=$(get_ip_intelligence "$ip")
|
|
IFS='|' read -r score hits bot_type attacks ban_count rep_score <<< "$current"
|
|
|
|
# Increment hits
|
|
hits=$((hits + 1))
|
|
|
|
# Enrich with threat intelligence on first encounter (hits == 1)
|
|
if [ "${hits:-0}" -eq 1 ]; then
|
|
# Check if whitelisted first
|
|
if is_whitelisted_service "$ip" 2>/dev/null; then
|
|
score=0
|
|
bot_type="legit"
|
|
else
|
|
# Get threat intelligence (in background to avoid slowing down)
|
|
(
|
|
local threat_intel=$(get_threat_intelligence "$ip" 2>/dev/null)
|
|
IFS='|' read -r abuse_conf abuse_rpts country isp geo timing whitelisted <<< "$threat_intel"
|
|
|
|
# Store enrichment data for later use
|
|
local enrich_file="$TEMP_DIR/threat_enrich_${ip//\./_}"
|
|
echo "$threat_intel" > "$enrich_file"
|
|
|
|
# Boost score based on AbuseIPDB confidence
|
|
if [ "${abuse_conf:-0}" -ge 75 ]; then
|
|
# High confidence malicious - add 30 points
|
|
local current_data="${IP_DATA[$ip]}"
|
|
IFS='|' read -r old_score old_hits old_bot old_attacks old_ban old_rep <<< "$current_data"
|
|
local new_score=$((old_score + 30))
|
|
[ "${new_score:-0}" -gt 100 ] && new_score=100
|
|
IP_DATA[$ip]="$new_score|$old_hits|$old_bot|$old_attacks|$old_ban|$old_rep"
|
|
# Write to file for cross-process communication
|
|
write_ip_data_to_file "$ip" "$new_score|$old_hits|$old_bot|$old_attacks|$old_ban|$old_rep" 2>/dev/null
|
|
elif [ "${abuse_conf:-0}" -ge 50 ]; then
|
|
# Medium confidence - add 15 points
|
|
local current_data="${IP_DATA[$ip]}"
|
|
IFS='|' read -r old_score old_hits old_bot old_attacks old_ban old_rep <<< "$current_data"
|
|
local new_score=$((old_score + 15))
|
|
[ "${new_score:-0}" -gt 100 ] && new_score=100
|
|
IP_DATA[$ip]="$new_score|$old_hits|$old_bot|$old_attacks|$old_ban|$old_rep"
|
|
# Write to file for cross-process communication
|
|
write_ip_data_to_file "$ip" "$new_score|$old_hits|$old_bot|$old_attacks|$old_ban|$old_rep" 2>/dev/null
|
|
fi
|
|
|
|
# High-risk country adds 5 points
|
|
if is_high_risk_country "${geo:-XX}" 2>/dev/null; then
|
|
local current_data="${IP_DATA[$ip]}"
|
|
IFS='|' read -r old_score old_hits old_bot old_attacks old_ban old_rep <<< "$current_data"
|
|
local new_score=$((old_score + 5))
|
|
[ "${new_score:-0}" -gt 100 ] && new_score=100
|
|
IP_DATA[$ip]="$new_score|$old_hits|$old_bot|$old_attacks|$old_ban|$old_rep"
|
|
# Write to file for cross-process communication
|
|
write_ip_data_to_file "$ip" "$new_score|$old_hits|$old_bot|$old_attacks|$old_ban|$old_rep" 2>/dev/null
|
|
fi
|
|
) &
|
|
fi
|
|
fi
|
|
|
|
# Classify bot if unknown
|
|
if [ "$bot_type" = "unknown" ] && [ -n "$user_agent" ]; then
|
|
bot_type=$(classify_bot_type "$user_agent" 2>/dev/null || echo "unknown")
|
|
fi
|
|
|
|
# Record attack pattern for learning
|
|
if [ -n "$url" ]; then
|
|
record_attack_pattern "$ip" "${attacks:-unknown}" "$url" "${user_agent:-unknown}" 2>/dev/null &
|
|
fi
|
|
|
|
# Detect attacks in URL (pass user_agent and ip for enhanced detection)
|
|
local new_attacks=$(detect_all_attacks "$url" "$method" "$user_agent" "$ip")
|
|
|
|
if [ -n "$new_attacks" ]; then
|
|
# Add to attack list (unique)
|
|
if [ -z "$attacks" ]; then
|
|
attacks="$new_attacks"
|
|
else
|
|
attacks="$attacks,$new_attacks"
|
|
fi
|
|
|
|
# Remove duplicates using associative array (faster than sort -u pipeline)
|
|
local -A unique_attacks
|
|
IFS=',' read -ra ATTACK_LIST <<< "$attacks"
|
|
for atk in "${ATTACK_LIST[@]}"; do
|
|
[ -n "$atk" ] && unique_attacks[$atk]=1
|
|
done
|
|
attacks=$(IFS=','; echo "${!unique_attacks[*]}")
|
|
|
|
# Update attack type counter
|
|
IFS=',' read -ra ATTACK_ARRAY <<< "$new_attacks"
|
|
for attack in "${ATTACK_ARRAY[@]}"; do
|
|
((ATTACK_TYPE_COUNTER["$attack"]++))
|
|
done
|
|
|
|
# Calculate attack score
|
|
local attack_score=$(calculate_attack_score "$new_attacks")
|
|
score=$((score + attack_score))
|
|
|
|
((TOTAL_THREATS++))
|
|
fi
|
|
|
|
# Request volume scoring
|
|
if [ "${hits:-0}" -gt 100 ]; then
|
|
score=$((score + 5))
|
|
elif [ "${hits:-0}" -gt 50 ]; then
|
|
score=$((score + 3))
|
|
elif [ "${hits:-0}" -gt 20 ]; then
|
|
score=$((score + 1))
|
|
fi
|
|
|
|
# Adjust score based on bot type
|
|
case "$bot_type" in
|
|
legit|ai|monitor)
|
|
# Legitimate bots - reduce score ONLY if no attacks detected
|
|
# (prevents spoofed user agents from avoiding blocks)
|
|
if [ -z "$attacks" ]; then
|
|
score=$((score - 5))
|
|
[ "${score:-0}" -lt 0 ] && score=0
|
|
fi
|
|
;;
|
|
suspicious)
|
|
# Suspicious bots - increase score
|
|
score=$((score + 15))
|
|
;;
|
|
esac
|
|
|
|
# Cap at 100
|
|
[ "${score:-0}" -gt 100 ] && score=100
|
|
|
|
# Check if we're tracking too many IPs (memory protection)
|
|
if [ ${#IP_DATA[@]} -ge $MAX_TRACKED_IPS ]; then
|
|
# Remove lowest scoring IPs
|
|
local to_remove=()
|
|
for check_ip in "${!IP_DATA[@]}"; do
|
|
# Use bash parameter expansion instead of cut
|
|
local check_score="${IP_DATA[$check_ip]%%|*}"
|
|
[ "$check_score" -lt 10 ] && to_remove+=("$check_ip")
|
|
done
|
|
|
|
# Remove up to 100 low-score IPs
|
|
local removed=0
|
|
for remove_ip in "${to_remove[@]}"; do
|
|
unset IP_DATA[$remove_ip]
|
|
((removed++))
|
|
[ "${removed:-0}" -ge 100 ] && break
|
|
done
|
|
fi
|
|
|
|
# Update cached data
|
|
IP_DATA[$ip]="$score|$hits|$bot_type|$attacks|$ban_count|$rep_score"
|
|
|
|
# CRITICAL FIX: Write to file immediately for cross-process communication
|
|
# This ensures auto-mitigation engine sees scores from HTTP/SSH monitoring subprocesses
|
|
write_ip_data_to_file "$ip" "$score|$hits|$bot_type|$attacks|$ban_count|$rep_score" 2>/dev/null &
|
|
|
|
# Update IP reputation DB in background (if score > 0)
|
|
if [ "${score:-0}" -gt 0 ]; then
|
|
(update_ip_reputation "$ip" 1 "$score" 0 "Live monitor: $new_attacks" >/dev/null 2>&1) &
|
|
fi
|
|
}
|
|
export -f update_ip_intelligence
|
|
|
|
################################################################################
|
|
# Advanced Intelligence Functions
|
|
################################################################################
|
|
|
|
# Record attack timestamp for velocity tracking
|
|
record_attack_timestamp() {
|
|
local ip="$1"
|
|
local now=$(date +%s)
|
|
|
|
# Get existing timestamps
|
|
local timestamps="${IP_TIMESTAMPS[$ip]}"
|
|
|
|
# Add new timestamp
|
|
if [ -z "$timestamps" ]; then
|
|
timestamps="$now"
|
|
else
|
|
timestamps="$timestamps,$now"
|
|
fi
|
|
|
|
# Keep only last 100 timestamps (prevent memory bloat)
|
|
# Use bash array instead of pipeline for efficiency
|
|
IFS=',' read -ra TS_ARRAY <<< "$timestamps"
|
|
if [ "${#TS_ARRAY[@]}" -gt 100 ]; then
|
|
# Keep last 100 elements
|
|
timestamps=$(IFS=','; echo "${TS_ARRAY[*]: -100}")
|
|
fi
|
|
|
|
IP_TIMESTAMPS[$ip]="$timestamps"
|
|
}
|
|
|
|
# Calculate attack velocity (attacks per hour)
|
|
# Returns: velocity|recent_count|bonus_points|reason
|
|
calculate_attack_velocity() {
|
|
local ip="$1"
|
|
local timestamps="${IP_TIMESTAMPS[$ip]}"
|
|
|
|
[ -z "$timestamps" ] && echo "0|0|0|" && return
|
|
|
|
local now=$(date +%s)
|
|
local window_start=$((now - VELOCITY_WINDOW))
|
|
|
|
# Count attacks in last hour
|
|
local recent_count=0
|
|
local oldest_in_window=""
|
|
|
|
while IFS=',' read -ra TIMES; do
|
|
for ts in "${TIMES[@]}"; do
|
|
if [ "$ts" -ge "$window_start" ]; then
|
|
((recent_count++))
|
|
[ -z "$oldest_in_window" ] && oldest_in_window="$ts"
|
|
fi
|
|
done
|
|
done <<< "$timestamps"
|
|
|
|
# Calculate velocity and bonus
|
|
local bonus=0
|
|
local reason=""
|
|
|
|
if [ "$recent_count" -ge 20 ]; then
|
|
# 20+ attacks in 1 hour = extreme velocity
|
|
bonus=30
|
|
reason="EXTREME_VELOCITY:${recent_count}/hr"
|
|
elif [ "$recent_count" -ge 10 ]; then
|
|
# 10-19 attacks in 1 hour = high velocity
|
|
bonus=20
|
|
reason="HIGH_VELOCITY:${recent_count}/hr"
|
|
elif [ "$recent_count" -ge 5 ]; then
|
|
# 5-9 attacks in 1 hour = moderate velocity
|
|
bonus=10
|
|
reason="MOD_VELOCITY:${recent_count}/hr"
|
|
fi
|
|
|
|
# If attacks are very rapid (10 in 5 minutes), extra bonus
|
|
local five_min_ago=$((now - 300))
|
|
local rapid_count=0
|
|
while IFS=',' read -ra TIMES; do
|
|
for ts in "${TIMES[@]}"; do
|
|
[ "$ts" -ge "$five_min_ago" ] && ((rapid_count++))
|
|
done
|
|
done <<< "$timestamps"
|
|
|
|
if [ "$rapid_count" -ge 10 ]; then
|
|
bonus=$((bonus + 15))
|
|
reason="${reason}+RAPID:${rapid_count}/5min"
|
|
fi
|
|
|
|
echo "${recent_count}|${bonus}|${reason}"
|
|
}
|
|
|
|
# Record attack vector for diversity tracking
|
|
record_attack_vector() {
|
|
local ip="$1"
|
|
local vector="$2" # SSH, WEB, EMAIL, FTP, DATABASE, FIREWALL
|
|
|
|
local vectors="${IP_ATTACK_VECTORS[$ip]}"
|
|
|
|
# Add if not already present
|
|
if [[ ! "$vectors" =~ $vector ]]; then
|
|
if [ -z "$vectors" ]; then
|
|
vectors="$vector"
|
|
else
|
|
vectors="$vectors,$vector"
|
|
fi
|
|
IP_ATTACK_VECTORS[$ip]="$vectors"
|
|
fi
|
|
}
|
|
|
|
# Calculate diversity bonus
|
|
# Returns: vector_count|bonus_points|reason
|
|
calculate_diversity_bonus() {
|
|
local ip="$1"
|
|
local vectors="${IP_ATTACK_VECTORS[$ip]}"
|
|
|
|
[ -z "$vectors" ] && echo "0|0|" && return
|
|
|
|
local count=$(echo "$vectors" | tr ',' '\n' 2>/dev/null | wc -l 2>/dev/null || echo "0")
|
|
local bonus=0
|
|
local reason=""
|
|
|
|
if [ "$count" -ge 4 ]; then
|
|
bonus=35
|
|
reason="MULTI_VECTOR:${count}_types"
|
|
elif [ "$count" -eq 3 ]; then
|
|
bonus=25
|
|
reason="COORDINATED:${count}_types"
|
|
elif [ "$count" -eq 2 ]; then
|
|
bonus=10
|
|
reason="DUAL_VECTOR:${count}_types"
|
|
fi
|
|
|
|
echo "${count}|${bonus}|${reason}"
|
|
}
|
|
|
|
# Detect timing patterns (bot signatures)
|
|
# Returns: pattern_type|confidence|bonus_points|reason
|
|
detect_timing_pattern() {
|
|
local ip="$1"
|
|
local timestamps="${IP_TIMESTAMPS[$ip]}"
|
|
|
|
[ -z "$timestamps" ] && echo "NONE|0|0|" && return
|
|
|
|
# Need at least 5 attacks to detect pattern
|
|
local count=$(echo "$timestamps" | tr ',' '\n' | wc -l)
|
|
[ "$count" -lt 5 ] && echo "INSUFFICIENT|0|0|" && return
|
|
|
|
# Calculate gaps between attacks
|
|
local prev_ts=""
|
|
local gaps=()
|
|
|
|
while IFS=',' read -ra TIMES; do
|
|
for ts in "${TIMES[@]}"; do
|
|
if [ -n "$prev_ts" ]; then
|
|
local gap=$((ts - prev_ts))
|
|
gaps+=("$gap")
|
|
fi
|
|
prev_ts="$ts"
|
|
done
|
|
done <<< "$timestamps"
|
|
|
|
# Check for consistent intervals (bot signature)
|
|
local total_gap=0
|
|
local gap_count=${#gaps[@]}
|
|
|
|
for gap in "${gaps[@]}"; do
|
|
total_gap=$((total_gap + gap))
|
|
done
|
|
|
|
if [ "$gap_count" -gt 0 ]; then
|
|
local avg_gap=$((total_gap / gap_count))
|
|
|
|
# Check variance - if all gaps are similar, it's a bot
|
|
local variance=0
|
|
for gap in "${gaps[@]}"; do
|
|
local diff=$((gap - avg_gap))
|
|
[ "$diff" -lt 0 ] && diff=$((diff * -1))
|
|
variance=$((variance + diff))
|
|
done
|
|
|
|
local avg_variance=$((variance / gap_count))
|
|
|
|
# If average variance is low (gaps are consistent), it's automated
|
|
if [ "$avg_variance" -lt 3 ]; then
|
|
# Very consistent timing = bot
|
|
echo "BOT_PATTERN|HIGH|20|AUTOMATED:${avg_gap}s_intervals"
|
|
return
|
|
elif [ "$avg_variance" -lt 10 ]; then
|
|
# Somewhat consistent = likely bot
|
|
echo "LIKELY_BOT|MEDIUM|10|PATTERN:${avg_gap}s_avg"
|
|
return
|
|
fi
|
|
fi
|
|
|
|
echo "HUMAN_LIKE|LOW|0|RANDOM_TIMING"
|
|
}
|
|
|
|
# Check if attack was successful
|
|
# Returns: success_detected|bonus_points|reason
|
|
detect_attack_success() {
|
|
local ip="$1"
|
|
local url="$2"
|
|
local status="${3:-0}"
|
|
local method="${4:-GET}"
|
|
|
|
local bonus=0
|
|
local reason=""
|
|
local success=0
|
|
|
|
# Check for successful login attempts
|
|
if [[ "$url" =~ wp-login\.php ]] && [ "$status" -eq 302 ]; then
|
|
# 302 redirect on wp-login = successful login
|
|
success=1
|
|
bonus=50
|
|
reason="WORDPRESS_BREACH"
|
|
elif [[ "$url" =~ wp-admin ]] && [ "$status" -eq 200 ] && [[ "$method" == "POST" ]]; then
|
|
# POST to wp-admin with 200 = potential successful action
|
|
success=1
|
|
bonus=40
|
|
reason="ADMIN_ACCESS"
|
|
elif [ "$status" -eq 200 ] && [[ "$url" =~ \.(php|asp|aspx|jsp)$ ]] && [[ "$url" =~ (shell|cmd|exec|eval) ]]; then
|
|
# Successful request to shell-like file = breach
|
|
success=1
|
|
bonus=60
|
|
reason="SHELL_ACCESS"
|
|
fi
|
|
|
|
echo "${success}|${bonus}|${reason}"
|
|
}
|
|
|
|
# Track subnet attacks
|
|
track_subnet_attack() {
|
|
local ip="$1"
|
|
|
|
# Extract /24 subnet (bash built-in, 100x faster than cut)
|
|
local subnet="${ip%.*}" # Remove last octet: 1.2.3.4 → 1.2.3
|
|
|
|
# Increment subnet counter
|
|
local count=${SUBNET_ATTACKS[$subnet]:-0}
|
|
count=$((count + 1))
|
|
SUBNET_ATTACKS[$subnet]=$count
|
|
|
|
echo "$count"
|
|
}
|
|
|
|
# Calculate subnet attack bonus
|
|
# Returns: subnet_count|bonus_points|reason
|
|
calculate_subnet_bonus() {
|
|
local ip="$1"
|
|
local subnet="${ip%.*}" # Bash built-in: 1.2.3.4 → 1.2.3 (100x faster than cut)
|
|
|
|
local count=${SUBNET_ATTACKS[$subnet]:-0}
|
|
local bonus=0
|
|
local reason=""
|
|
|
|
if [ "$count" -ge 10 ]; then
|
|
bonus=40
|
|
reason="SUBNET_SWARM:${count}_IPs_in_${subnet}.0/24"
|
|
elif [ "$count" -ge 5 ]; then
|
|
bonus=25
|
|
reason="SUBNET_ATTACK:${count}_IPs_in_${subnet}.0/24"
|
|
elif [ "$count" -ge 3 ]; then
|
|
bonus=15
|
|
reason="RELATED_IPS:${count}_in_${subnet}.0/24"
|
|
fi
|
|
|
|
echo "${count}|${bonus}|${reason}"
|
|
}
|
|
|
|
# Assess target criticality
|
|
# Returns: criticality_level|bonus_points|reason
|
|
assess_target_criticality() {
|
|
local url="$1"
|
|
local method="${2:-GET}"
|
|
|
|
local bonus=0
|
|
local reason=""
|
|
local level="LOW"
|
|
|
|
# Critical admin paths
|
|
if [[ "$url" =~ (wp-admin|admin|administrator|manager|phpmyadmin|cpanel|whm) ]]; then
|
|
bonus=15
|
|
reason="ADMIN_TARGET"
|
|
level="HIGH"
|
|
fi
|
|
|
|
# Authentication endpoints
|
|
if [[ "$url" =~ (wp-login|login|signin|auth|session) ]]; then
|
|
bonus=12
|
|
reason="AUTH_TARGET"
|
|
level="HIGH"
|
|
fi
|
|
|
|
# Config/sensitive files
|
|
if [[ "$url" =~ (config|\.env|\.git|\.sql|backup|database|credentials) ]]; then
|
|
bonus=18
|
|
reason="SENSITIVE_FILE"
|
|
level="CRITICAL"
|
|
fi
|
|
|
|
# Shell/exploit attempts
|
|
if [[ "$url" =~ (shell|cmd|exec|eval|system|phpinfo) ]]; then
|
|
bonus=20
|
|
reason="EXPLOIT_ATTEMPT"
|
|
level="CRITICAL"
|
|
fi
|
|
|
|
# Upload endpoints (RCE risk)
|
|
if [[ "$url" =~ upload ]] && [[ "$method" == "POST" ]]; then
|
|
bonus=15
|
|
reason="UPLOAD_TARGET"
|
|
level="HIGH"
|
|
fi
|
|
|
|
echo "${level}|${bonus}|${reason}"
|
|
}
|
|
|
|
# Apply reputation decay
|
|
apply_reputation_decay() {
|
|
local now=$(date +%s)
|
|
local time_since_last=$((now - LAST_DECAY_CHECK))
|
|
|
|
# Only check every 30 minutes
|
|
[ "$time_since_last" -lt "$DECAY_CHECK_INTERVAL" ] && return
|
|
|
|
LAST_DECAY_CHECK=$now
|
|
|
|
# Decay scores for IPs with no recent activity
|
|
for ip in "${!IP_DATA[@]}"; do
|
|
local timestamps="${IP_TIMESTAMPS[$ip]}"
|
|
|
|
[ -z "$timestamps" ] && continue
|
|
|
|
# Get most recent attack time
|
|
local last_attack=$(echo "$timestamps" | tr ',' '\n' 2>/dev/null | tail -1 2>/dev/null || echo "0")
|
|
local time_since_attack=$((now - ${last_attack:-0}))
|
|
|
|
# If no activity for 6 hours, start decay
|
|
if [ "$time_since_attack" -gt 21600 ]; then
|
|
IFS='|' read -r score hits bot_type attacks ban_count rep_score <<< "${IP_DATA[$ip]}"
|
|
|
|
# Reduce score by 20% (but not below 0)
|
|
local decay_amount=$((score / 5))
|
|
[ "$decay_amount" -lt 5 ] && decay_amount=5
|
|
|
|
score=$((score - decay_amount))
|
|
[ "$score" -lt 0 ] && score=0
|
|
|
|
# Update data
|
|
IP_DATA[$ip]="$score|$hits|$bot_type|$attacks|$ban_count|$rep_score"
|
|
fi
|
|
done
|
|
}
|
|
|
|
# Context-aware scoring (geo, ISP, time-of-day)
|
|
# Returns: context_bonus|reason
|
|
calculate_context_bonus() {
|
|
local ip="$1"
|
|
local now=$(date +%s)
|
|
|
|
local bonus=0
|
|
local reasons=""
|
|
|
|
# Time-of-day analysis (attacks at odd hours = suspicious)
|
|
local hour=$(date +%H)
|
|
if [ "$hour" -ge 2 ] && [ "$hour" -le 5 ]; then
|
|
# Attacks between 2am-5am (server timezone) = suspicious
|
|
bonus=$((bonus + 8))
|
|
reasons="NIGHT_ATTACK:${hour}h"
|
|
fi
|
|
|
|
# Check geolocation if available (from threat intelligence)
|
|
if [ -f "$TEMP_DIR/threat_enrich_${ip//\./_}" ]; then
|
|
local threat_data=$(cat "$TEMP_DIR/threat_enrich_${ip//\./_}")
|
|
IFS='|' read -r abuse_conf abuse_rpts country isp geo timing whitelisted <<< "$threat_data"
|
|
|
|
# High-risk country already detected
|
|
if is_high_risk_country "${geo:-XX}" 2>/dev/null; then
|
|
bonus=$((bonus + 5))
|
|
[ -n "$reasons" ] && reasons="${reasons}+" || reasons=""
|
|
reasons="${reasons}HIGH_RISK_GEO:${geo}"
|
|
fi
|
|
|
|
# Residential ISP (suspicious for server attacks)
|
|
# Bash pattern matching (faster than grep subprocess)
|
|
local isp_lower="${isp,,}" # Convert to lowercase
|
|
if [[ "$isp_lower" =~ (comcast|verizon|att|residential|cable|dsl|fiber|broadband) ]]; then
|
|
bonus=$((bonus + 10))
|
|
[ -n "$reasons" ] && reasons="${reasons}+" || reasons=""
|
|
reasons="${reasons}RESIDENTIAL_ISP"
|
|
fi
|
|
fi
|
|
|
|
echo "${bonus}|${reasons}"
|
|
}
|
|
|
|
# Atomically increment block counter (prevents race conditions)
|
|
increment_block_counter() {
|
|
local increment="${1:-1}"
|
|
(
|
|
flock -x 200
|
|
local current=$(cat "$TEMP_DIR/total_blocks" 2>/dev/null || echo "0")
|
|
echo $((current + increment)) > "$TEMP_DIR/total_blocks"
|
|
) 200>"$TEMP_DIR/counter.lock"
|
|
}
|
|
|
|
# Record blocked IP to reputation database (for permanent tracking)
|
|
record_blocked_ip() {
|
|
local ip="$1"
|
|
local reason="${2:-Auto-blocked}"
|
|
|
|
# Update IP_DATA to increment ban_count
|
|
if [ -n "${IP_DATA[$ip]}" ]; then
|
|
IFS='|' read -r score hits bot_type attacks ban_count rep_score <<< "${IP_DATA[$ip]}"
|
|
ban_count=$((ban_count + 1))
|
|
IP_DATA[$ip]="$score|$hits|$bot_type|$attacks|$ban_count|$rep_score"
|
|
|
|
# Also save to IP reputation database (in background to avoid blocking)
|
|
if type record_ip_data &>/dev/null; then
|
|
(record_ip_data "$ip" "$score" "$hits" "$attacks" "$ban_count" 2>/dev/null) &
|
|
fi
|
|
fi
|
|
|
|
# Log to permanent block history file
|
|
echo "$(date '+%Y-%m-%d %H:%M:%S')|$ip|$reason" >> "$SNAPSHOT_DIR/block_history.log"
|
|
}
|
|
|
|
# Batch block multiple IPs at once (optimized for DDoS scenarios)
|
|
batch_block_ips() {
|
|
local -a ip_list=("$@")
|
|
local blocked=0
|
|
local failed=0
|
|
|
|
if [ ${#ip_list[@]} -eq 0 ]; then
|
|
return 0
|
|
fi
|
|
|
|
# DEBUG: Log function entry
|
|
echo "[$(date +"%H:%M:%S")] BATCH_BLOCK: Starting batch block for ${#ip_list[@]} IPs: ${ip_list[*]}" >> "$TEMP_DIR/debug.log" 2>/dev/null || true
|
|
|
|
echo "Batch blocking ${#ip_list[@]} IPs..."
|
|
|
|
# Use IPset for instant batch blocking if available
|
|
if [ "$IPSET_AVAILABLE" -eq 1 ]; then
|
|
echo "[$(date +"%H:%M:%S")] BATCH_BLOCK: Using IPSET path" >> "$TEMP_DIR/debug.log" 2>/dev/null || true
|
|
for ip in "${ip_list[@]}"; do
|
|
# Validate IP format
|
|
if ! is_valid_ip "$ip"; then
|
|
((failed++))
|
|
continue
|
|
fi
|
|
|
|
# Add to IPset with 1-hour timeout (instant, no verification needed)
|
|
if ipset add "$IPSET_NAME" "$ip" timeout 3600 2>/dev/null; then
|
|
((blocked++))
|
|
echo "$ip" >> "$TEMP_DIR/blocked_ips_cache"
|
|
else
|
|
# Already in set or error
|
|
((failed++))
|
|
fi
|
|
done
|
|
|
|
# Single cache update after batch
|
|
sort -u "$TEMP_DIR/blocked_ips_cache" -o "$TEMP_DIR/blocked_ips_cache" 2>/dev/null
|
|
|
|
echo "✓ IPset batch: $blocked blocked, $failed skipped"
|
|
else
|
|
# Fallback to CSF - use chain_DENY ipset directly for speed
|
|
echo "[$(date +"%H:%M:%S")] BATCH_BLOCK: Using CSF chain_DENY ipset path (IPSET_AVAILABLE=$IPSET_AVAILABLE)" >> "$TEMP_DIR/debug.log" 2>/dev/null || true
|
|
|
|
# Try CSF's chain_DENY ipset first (much faster than csf -td for batches)
|
|
if ipset list chain_DENY &>/dev/null 2>&1; then
|
|
for ip in "${ip_list[@]}"; do
|
|
if ! is_valid_ip "$ip"; then
|
|
echo "[$(date +"%H:%M:%S")] BATCH_BLOCK: Invalid IP format: $ip" >> "$TEMP_DIR/debug.log" 2>/dev/null || true
|
|
((failed++))
|
|
continue
|
|
fi
|
|
|
|
# Add directly to CSF's chain_DENY ipset (instant kernel-level blocking)
|
|
# Include 1-hour timeout if chain_DENY supports it
|
|
if ipset add chain_DENY "$ip" timeout 3600 -exist 2>/dev/null; then
|
|
echo "[$(date +"%H:%M:%S")] BATCH_BLOCK: chain_DENY ipset SUCCESS for $ip (timeout 1h)" >> "$TEMP_DIR/debug.log" 2>/dev/null || true
|
|
((blocked++))
|
|
echo "$ip" >> "$TEMP_DIR/blocked_ips_cache"
|
|
elif ipset add chain_DENY "$ip" -exist 2>/dev/null; then
|
|
# Fallback: chain_DENY doesn't support timeout (CSF will manage via csf -td in background)
|
|
echo "[$(date +"%H:%M:%S")] BATCH_BLOCK: chain_DENY ipset SUCCESS for $ip (no timeout - CSF managed)" >> "$TEMP_DIR/debug.log" 2>/dev/null || true
|
|
((blocked++))
|
|
echo "$ip" >> "$TEMP_DIR/blocked_ips_cache"
|
|
else
|
|
echo "[$(date +"%H:%M:%S")] BATCH_BLOCK: chain_DENY ipset FAILED for $ip" >> "$TEMP_DIR/debug.log" 2>/dev/null || true
|
|
((failed++))
|
|
fi
|
|
done
|
|
sort -u "$TEMP_DIR/blocked_ips_cache" -o "$TEMP_DIR/blocked_ips_cache" 2>/dev/null
|
|
echo "✓ CSF chain_DENY ipset batch: $blocked blocked, $failed failed"
|
|
else
|
|
# Fallback to csf -td if chain_DENY ipset not available
|
|
echo "[$(date +"%H:%M:%S")] BATCH_BLOCK: chain_DENY ipset not available, falling back to csf -td" >> "$TEMP_DIR/debug.log" 2>/dev/null || true
|
|
for ip in "${ip_list[@]}"; do
|
|
if ! is_valid_ip "$ip"; then
|
|
((failed++))
|
|
continue
|
|
fi
|
|
|
|
# Use csf -td as last resort (slower)
|
|
if csf -td "$ip" 3600 "Auto-block: SYN attack" >/dev/null 2>&1; then
|
|
((blocked++))
|
|
else
|
|
((failed++))
|
|
fi
|
|
done
|
|
echo "✓ CSF -td batch: $blocked blocked, $failed failed"
|
|
fi
|
|
echo "[$(date +"%H:%M:%S")] BATCH_BLOCK: CSF fallback batch complete - blocked=$blocked, failed=$failed" >> "$TEMP_DIR/debug.log" 2>/dev/null || true
|
|
fi
|
|
|
|
# Update total counter atomically
|
|
echo "[$(date +"%H:%M:%S")] BATCH_BLOCK: Incrementing counter by $blocked" >> "$TEMP_DIR/debug.log" 2>/dev/null || true
|
|
increment_block_counter "$blocked"
|
|
|
|
return 0
|
|
}
|
|
|
|
# Block IP temporarily with CSF
|
|
block_ip_temporary() {
|
|
local ip="$1"
|
|
local hours="${2:-1}"
|
|
local reason="${3:-Auto-block by live monitor}"
|
|
local seconds=$((hours * 3600))
|
|
|
|
# Validate IP format before blocking
|
|
if ! is_valid_ip "$ip"; then
|
|
echo "✗ Error: Invalid IP format: $ip"
|
|
return 1
|
|
fi
|
|
|
|
# PRIORITY 1: Use IPset for instant kernel-level blocking (performance critical)
|
|
if [ "$IPSET_AVAILABLE" -eq 1 ]; then
|
|
# Try IPset with timeout if supported
|
|
if [ "$IPSET_SUPPORTS_TIMEOUT" -eq 1 ]; then
|
|
if ipset add "$IPSET_NAME" "$ip" timeout "$seconds" -exist 2>/dev/null; then
|
|
echo "✓ $ip blocked via IPset $IPSET_NAME (expires in ${hours}h)"
|
|
echo "$ip" >> "$TEMP_DIR/blocked_ips_cache"
|
|
increment_block_counter 1
|
|
record_blocked_ip "$ip" "$reason"
|
|
return 0
|
|
fi
|
|
else
|
|
# IPset without timeout (CSF's chain_DENY) - add to IPset for instant block,
|
|
# then let CSF manage the timeout removal
|
|
if ipset add "$IPSET_NAME" "$ip" -exist 2>/dev/null; then
|
|
echo "$ip" >> "$TEMP_DIR/blocked_ips_cache"
|
|
increment_block_counter 1
|
|
record_blocked_ip "$ip" "$reason"
|
|
|
|
# Let CSF manage the timeout in background (IPset already blocking)
|
|
if command -v csf &>/dev/null; then
|
|
csf -td "$ip" "$seconds" "$reason" >/dev/null 2>&1 &
|
|
echo "✓ $ip blocked via IPset $IPSET_NAME (CSF managing timeout: ${hours}h)"
|
|
else
|
|
echo "✓ $ip blocked via IPset $IPSET_NAME (permanent - no CSF timeout)"
|
|
fi
|
|
return 0
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# FALLBACK: CSF-only blocking (slower, but still works)
|
|
if command -v csf &>/dev/null; then
|
|
if csf -td "$ip" "$seconds" "$reason" >/dev/null 2>&1; then
|
|
echo "✓ $ip blocked via CSF (expires in ${hours}h)"
|
|
echo "$ip" >> "$TEMP_DIR/blocked_ips_cache"
|
|
increment_block_counter 1
|
|
record_blocked_ip "$ip" "$reason"
|
|
return 0
|
|
else
|
|
echo "✗ Warning: CSF block failed for $ip"
|
|
return 1
|
|
fi
|
|
fi
|
|
|
|
echo "✗ Error: No blocking method available"
|
|
return 1
|
|
}
|
|
|
|
# Quick block IP (wrapper for background auto-blocking)
|
|
# Used by ET detection and auto-mitigation engine
|
|
quick_block_ip() {
|
|
local ip="$1"
|
|
local reason="${2:-Auto-block: Critical threat}"
|
|
|
|
# Validate IP
|
|
if ! is_valid_ip "$ip"; then
|
|
return 1
|
|
fi
|
|
|
|
# Block for 1 hour using IPset or CSF
|
|
block_ip_temporary "$ip" 1 "$reason" >/dev/null 2>&1
|
|
}
|
|
|
|
# Block IP permanently with CSF
|
|
block_ip_permanent() {
|
|
local ip="$1"
|
|
local reason="${2:-Permanent block by live monitor}"
|
|
|
|
# Validate IP format before blocking
|
|
if ! is_valid_ip "$ip"; then
|
|
echo "✗ Error: Invalid IP format: $ip"
|
|
return 1
|
|
fi
|
|
|
|
# PRIORITY: Add to IPset immediately for instant kernel-level blocking
|
|
if [ "$IPSET_AVAILABLE" -eq 1 ]; then
|
|
if [ "$IPSET_SUPPORTS_TIMEOUT" -eq 1 ]; then
|
|
# IPset with timeout - use max timeout (24 hours)
|
|
ipset add "$IPSET_NAME" "$ip" timeout 86400 -exist 2>/dev/null
|
|
else
|
|
# IPset without timeout (CSF's chain_DENY) - permanent add
|
|
ipset add "$IPSET_NAME" "$ip" -exist 2>/dev/null
|
|
fi
|
|
fi
|
|
|
|
# CSF for persistent management (runs after IPset for immediate effect)
|
|
if command -v csf &>/dev/null; then
|
|
echo "Permanently blocking $ip: $reason"
|
|
if csf -d "$ip" "$reason" >/dev/null 2>&1; then
|
|
echo "✓ $ip permanently blocked via CSF"
|
|
echo "$ip" >> "$TEMP_DIR/blocked_ips_cache"
|
|
|
|
# Update counter atomically
|
|
increment_block_counter 1
|
|
|
|
# Record to reputation database
|
|
record_blocked_ip "$ip" "PERMANENT:$reason"
|
|
|
|
return 0
|
|
else
|
|
echo "✗ Warning: CSF permanent block failed for $ip"
|
|
return 1
|
|
fi
|
|
fi
|
|
|
|
echo "✗ Error: CSF not available"
|
|
return 1
|
|
}
|
|
|
|
# Check if IP is currently blocked in CSF/iptables (optimized with caching)
|
|
is_ip_blocked() {
|
|
local ip="$1"
|
|
|
|
# Use cached blocked IPs list (refreshed every 10 seconds by background process)
|
|
if [ -f "$TEMP_DIR/blocked_ips_cache" ]; then
|
|
if grep -q "^$ip$" "$TEMP_DIR/blocked_ips_cache" 2>/dev/null; then
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
return 1
|
|
}
|
|
|
|
# Real-time verification (no cache) for immediate confirmation after blocking
|
|
verify_ip_blocked() {
|
|
local ip="$1"
|
|
|
|
# Check CSF temporary blocks
|
|
if command -v csf &>/dev/null; then
|
|
# CRITICAL FIX: Use -w flag for word boundary matching
|
|
if csf -t 2>/dev/null | grep -q -w "$ip"; then
|
|
return 0
|
|
fi
|
|
|
|
# Check CSF permanent deny list
|
|
if [ -f /etc/csf/csf.deny ]; then
|
|
if grep -q "^$ip" /etc/csf/csf.deny 2>/dev/null; then
|
|
return 0
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Check iptables directly
|
|
if command -v iptables &>/dev/null; then
|
|
# CRITICAL FIX: Use -w flag for word boundary matching
|
|
if iptables -L INPUT -n 2>/dev/null | grep -q -w "$ip"; then
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
return 1
|
|
}
|
|
|
|
# Get threat level from score
|
|
get_threat_level() {
|
|
local score="${1:-0}"
|
|
|
|
if [ "$score" -ge "$THREAT_THRESHOLD_CRITICAL" ]; then
|
|
echo "CRITICAL"
|
|
elif [ "$score" -ge "$THREAT_THRESHOLD_HIGH" ]; then
|
|
echo "HIGH"
|
|
elif [ "$score" -ge "$THREAT_THRESHOLD_MEDIUM" ]; then
|
|
echo "MEDIUM"
|
|
else
|
|
echo "LOW"
|
|
fi
|
|
}
|
|
export -f get_threat_level
|
|
|
|
# Get color for threat level
|
|
get_threat_color() {
|
|
local level="$1"
|
|
|
|
case "$level" in
|
|
CRITICAL) echo "$CRITICAL_COLOR" ;;
|
|
HIGH) echo "$HIGH_COLOR" ;;
|
|
MEDIUM) echo "$MEDIUM_COLOR" ;;
|
|
LOW) echo "$LOW_COLOR" ;;
|
|
SAFE) echo "$SAFE_COLOR" ;;
|
|
*) echo "$INFO_COLOR" ;;
|
|
esac
|
|
}
|
|
export -f get_threat_color
|
|
|
|
# Get bot color
|
|
get_bot_color() {
|
|
local bot_type="$1"
|
|
|
|
case "$bot_type" in
|
|
legit) echo "$SAFE_COLOR" ;;
|
|
ai) echo '\033[0;34m' ;; # Blue
|
|
monitor) echo "$MEDIUM_COLOR" ;;
|
|
suspicious) echo "$HIGH_COLOR" ;;
|
|
*) echo "$INFO_COLOR" ;;
|
|
esac
|
|
}
|
|
|
|
################################################################################
|
|
# Dashboard Display Functions
|
|
################################################################################
|
|
|
|
draw_header() {
|
|
clear
|
|
local uptime=$(($(date +%s) - START_TIME))
|
|
local uptime_str=$(printf "%02d:%02d:%02d" $((uptime/3600)) $((uptime%3600/60)) $((uptime%60)))
|
|
|
|
# Read event counter from file (updated by subshell)
|
|
local event_count=$(cat "$TEMP_DIR/event_counter" 2>/dev/null || echo "0")
|
|
|
|
echo -e "${CRITICAL_COLOR}╔════════════════════════════════════════════════════════════════════════════╗${NC}"
|
|
echo -e "${CRITICAL_COLOR}║ 🚨 LIVE SECURITY MONITOR - INTELLIGENCE MODE 🧠 ║${NC}"
|
|
echo -e "${CRITICAL_COLOR}╚════════════════════════════════════════════════════════════════════════════╝${NC}"
|
|
echo -e "${INFO_COLOR}Runtime: ${uptime_str} | Events: ${event_count} | Threats: ${TOTAL_THREATS} | Blocks: ${TOTAL_BLOCKS} | Monitoring...${NC}"
|
|
echo ""
|
|
}
|
|
|
|
draw_intelligence_panel() {
|
|
echo -e "${HIGH_COLOR}┌─ THREAT INTELLIGENCE ──────────────────────────────────────────────────────┐${NC}"
|
|
|
|
# Debug: Show cache status
|
|
if [ -f "$TEMP_DIR/blocked_ips_cache" ]; then
|
|
CACHED_IPS=$(wc -l < "$TEMP_DIR/blocked_ips_cache" 2>/dev/null || echo 0)
|
|
echo -e "${INFO_COLOR} Cache: $CACHED_IPS blocked IPs${NC}" >> "$TEMP_DIR/debug.log"
|
|
else
|
|
echo -e "${INFO_COLOR} Cache: NOT FOUND${NC}" >> "$TEMP_DIR/debug.log"
|
|
fi
|
|
|
|
# Get top IPs by threat score (exclude already blocked IPs)
|
|
# Load blocked IPs cache into associative array for O(1) lookups
|
|
declare -A blocked_ips_lookup
|
|
if [ -f "$TEMP_DIR/blocked_ips_cache" ]; then
|
|
while IFS= read -r blocked_ip; do
|
|
[ -n "$blocked_ip" ] && blocked_ips_lookup[$blocked_ip]=1
|
|
done < "$TEMP_DIR/blocked_ips_cache"
|
|
fi
|
|
|
|
local ip_list=""
|
|
local blocked_count=0
|
|
local displayed_count=0
|
|
for ip in "${!IP_DATA[@]}"; do
|
|
# Skip IPs that are already blocked (O(1) lookup in hash)
|
|
if [ -n "${blocked_ips_lookup[$ip]}" ]; then
|
|
((blocked_count++))
|
|
echo " Filtering out blocked IP: $ip" >> "$TEMP_DIR/debug.log"
|
|
continue
|
|
fi
|
|
|
|
((displayed_count++))
|
|
IFS='|' read -r score hits bot_type attacks ban_count rep_score <<< "${IP_DATA[$ip]}"
|
|
ip_list+="$score|$ip|$hits|$bot_type|$attacks|$ban_count|$rep_score"$'\n'
|
|
done
|
|
|
|
echo " Blocked/filtered: $blocked_count, Displaying: $displayed_count" >> "$TEMP_DIR/debug.log"
|
|
|
|
if [ -n "$ip_list" ]; then
|
|
# Show fewer IPs in compact mode
|
|
local max_ips=10
|
|
[ "$COMPACT_MODE" -eq 1 ] && max_ips=5
|
|
|
|
echo "$ip_list" | sort -t'|' -k1 -rn | head -$max_ips | while IFS='|' read -r score ip hits bot_type attacks ban_count rep_score; do
|
|
# Set defaults for empty values
|
|
score="${score:-0}"
|
|
hits="${hits:-0}"
|
|
ban_count="${ban_count:-0}"
|
|
rep_score="${rep_score:-0}"
|
|
|
|
local level=$(get_threat_level "$score")
|
|
local color=$(get_threat_color "$level")
|
|
local bot_color=$(get_bot_color "$bot_type")
|
|
|
|
# Build status line
|
|
local status_line=$(printf "%-15s" "$ip")
|
|
status_line+=$(printf " Score:%-3s" "$score")
|
|
status_line+=$(printf " Hits:%-4s" "$hits")
|
|
|
|
# Bot type indicator
|
|
case "$bot_type" in
|
|
legit) status_line+=" ✅BOT" ;;
|
|
ai) status_line+=" 🤖AI" ;;
|
|
monitor) status_line+=" 📊MON" ;;
|
|
suspicious) status_line+=" ⚠️ SUS" ;;
|
|
*) status_line+="" ;;
|
|
esac
|
|
|
|
# Threat level
|
|
status_line+=$(printf " [%-8s]" "$level")
|
|
|
|
# Attacks (use bash parameter expansion instead of cut)
|
|
if [ -n "$attacks" ]; then
|
|
# Show first attack type
|
|
local first_attack="${attacks%%,*}"
|
|
local icon=$(get_attack_icon "$first_attack")
|
|
status_line+=" $icon$first_attack"
|
|
fi
|
|
|
|
# Ban count
|
|
if [ "$ban_count" -gt 0 ]; then
|
|
status_line+=" 🚫x$ban_count"
|
|
fi
|
|
|
|
# Known threat indicator
|
|
if [ "$rep_score" -gt 0 ]; then
|
|
status_line+=" [KNOWN]"
|
|
fi
|
|
|
|
echo -e "${color}${status_line}${NC}"
|
|
done
|
|
else
|
|
# Show appropriate message
|
|
if [ ${#IP_DATA[@]} -gt 0 ]; then
|
|
echo -e "${SAFE_COLOR} ✓ All detected threats have been blocked${NC}"
|
|
else
|
|
echo -e "${LOW_COLOR} No threats detected yet...${NC}"
|
|
fi
|
|
fi
|
|
|
|
echo -e "${HIGH_COLOR}└────────────────────────────────────────────────────────────────────────────┘${NC}"
|
|
echo ""
|
|
}
|
|
|
|
draw_attack_breakdown() {
|
|
# Skip this section entirely in compact mode
|
|
[ "$COMPACT_MODE" -eq 1 ] && return
|
|
|
|
echo -e "${MEDIUM_COLOR}┌─ ATTACK VECTORS ───────────────────────────────────────────────────────────┐${NC}"
|
|
|
|
if [ ${#ATTACK_TYPE_COUNTER[@]} -eq 0 ]; then
|
|
echo -e "${LOW_COLOR} No attacks detected yet...${NC}"
|
|
else
|
|
for attack_type in "${!ATTACK_TYPE_COUNTER[@]}"; do
|
|
local count="${ATTACK_TYPE_COUNTER[$attack_type]}"
|
|
local icon=$(get_attack_icon "$attack_type")
|
|
local color=$(get_attack_color "$attack_type")
|
|
printf "${color} ${icon} %-20s %5d${NC}\n" "$attack_type" "$count"
|
|
done | sort -t' ' -k3 -rn | head -5
|
|
fi
|
|
|
|
echo -e "${MEDIUM_COLOR}└────────────────────────────────────────────────────────────────────────────┘${NC}"
|
|
echo ""
|
|
}
|
|
|
|
draw_live_feed() {
|
|
echo -e "${HIGH_COLOR}┌─ LIVE THREAT FEED ─────────────────────────────────────────────────────────┐${NC}"
|
|
|
|
# Adaptive line count based on mode
|
|
local feed_lines=$MAX_DISPLAY_LINES
|
|
[ "$COMPACT_MODE" -eq 1 ] && feed_lines=8
|
|
|
|
if [ -f "$TEMP_DIR/recent_events" ] && [ -s "$TEMP_DIR/recent_events" ]; then
|
|
tail -n "$feed_lines" "$TEMP_DIR/recent_events"
|
|
else
|
|
echo -e "${LOW_COLOR} Waiting for events...${NC}"
|
|
fi
|
|
|
|
echo -e "${HIGH_COLOR}└────────────────────────────────────────────────────────────────────────────┘${NC}"
|
|
echo ""
|
|
}
|
|
|
|
draw_quick_actions() {
|
|
echo -e "${MEDIUM_COLOR}┌─ QUICK ACTIONS & RECOMMENDATIONS ─────────────────────────────────────────┐${NC}"
|
|
|
|
# Get blockable IPs (score >= 60, not already blocked)
|
|
local blockable_count=0
|
|
local blockable_ips=""
|
|
local has_ddos=0
|
|
local has_ssh_bruteforce=0
|
|
local high_conn_count=0
|
|
|
|
# Load blocked IPs cache once for efficient lookups
|
|
declare -A blocked_ips_check
|
|
if [ -f "$TEMP_DIR/blocked_ips_cache" ]; then
|
|
while IFS= read -r blocked_ip; do
|
|
[ -n "$blocked_ip" ] && blocked_ips_check[$blocked_ip]=1
|
|
done < "$TEMP_DIR/blocked_ips_cache"
|
|
fi
|
|
|
|
for ip in "${!IP_DATA[@]}"; do
|
|
IFS='|' read -r score hits bot_type attacks ban_count rep_score <<< "${IP_DATA[$ip]}"
|
|
|
|
# Check attack patterns
|
|
[[ "$attacks" =~ DDOS ]] && has_ddos=1
|
|
[[ "$attacks" =~ BRUTEFORCE ]] && has_ssh_bruteforce=1
|
|
|
|
# Skip if score too low for blocking
|
|
[ "$score" -lt 60 ] && continue
|
|
|
|
# Skip if already blocked
|
|
[ -n "${blocked_ips_check[$ip]}" ] && continue
|
|
|
|
# Count as blockable
|
|
blockable_count=$((blockable_count + 1))
|
|
blockable_ips+="$ip "
|
|
done
|
|
|
|
# Check for high connection counts
|
|
if [ -f "$TEMP_DIR/recent_events" ]; then
|
|
high_conn_count=$(grep -c "HIGH_CONN_COUNT" "$TEMP_DIR/recent_events" 2>/dev/null)
|
|
else
|
|
high_conn_count=0
|
|
fi
|
|
|
|
# Ensure it's a valid number (strip whitespace and validate)
|
|
high_conn_count=$(echo "$high_conn_count" | tr -d '[:space:]')
|
|
[[ ! "$high_conn_count" =~ ^[0-9]+$ ]] && high_conn_count=0
|
|
|
|
# IP Blocking Recommendations
|
|
if [ "$blockable_count" -gt 0 ]; then
|
|
echo -e "${HIGH_COLOR} ⚠️ $blockable_count high-threat IPs ready to block${NC}"
|
|
echo -e "${MEDIUM_COLOR} → Press 'b' to open blocking menu${NC}"
|
|
else
|
|
echo -e "${SAFE_COLOR} ✓ No IPs requiring immediate blocks${NC}"
|
|
fi
|
|
|
|
# Intelligent Firewall Recommendations
|
|
local recommendations=0
|
|
|
|
if [ "$has_ddos" -eq 1 ] || [ "$high_conn_count" -gt 0 ]; then
|
|
# Check current security settings
|
|
local synflood_status=$(grep "^SYNFLOOD\s*=" /etc/csf/csf.conf 2>/dev/null | cut -d'"' -f2)
|
|
local ct_limit=$(grep -oP "^CT_LIMIT\s*=\s*\"\K[0-9]+" /etc/csf/csf.conf 2>/dev/null | head -1)
|
|
|
|
local needs_config=0
|
|
|
|
# Check if SYNFLOOD needs enabling
|
|
if [ "$synflood_status" != "1" ]; then
|
|
needs_config=1
|
|
fi
|
|
|
|
# Check if CT_LIMIT needs optimization (not set or set to 0)
|
|
if [ -z "$ct_limit" ] || [ "$ct_limit" -eq 0 ]; then
|
|
needs_config=1
|
|
fi
|
|
|
|
# Only show recommendation if something needs fixing
|
|
if [ "${needs_config:-0}" -eq 1 ]; then
|
|
echo -e "${HIGH_COLOR} ⚠️ DDoS/SYN Flood Detected - Firewall Protection Recommended${NC}"
|
|
echo -e "${MEDIUM_COLOR} → Press 'c' for Security Hardening menu${NC}"
|
|
recommendations=1
|
|
fi
|
|
fi
|
|
|
|
if [ "$has_ssh_bruteforce" -eq 1 ]; then
|
|
local ssh_attacks=0
|
|
if [ -f "$TEMP_DIR/recent_events" ]; then
|
|
ssh_attacks=$(grep -c "SSH_BRUTEFORCE" "$TEMP_DIR/recent_events" 2>/dev/null)
|
|
fi
|
|
ssh_attacks=$(echo "$ssh_attacks" | tr -d '[:space:]')
|
|
[[ ! "$ssh_attacks" =~ ^[0-9]+$ ]] && ssh_attacks=0
|
|
if [ "$ssh_attacks" -gt 5 ]; then
|
|
# Check if SSH hardening is already applied
|
|
local current_lf=$(grep -oP "^LF_SSHD\s*=\s*\"\K[0-9]+" /etc/csf/csf.conf 2>/dev/null | head -1)
|
|
[ -z "$current_lf" ] && current_lf="5"
|
|
|
|
# Only show recommendation if not already hardened
|
|
if [ "$current_lf" -gt 3 ]; then
|
|
echo -e "${HIGH_COLOR} ⚠️ SSH Bruteforce ($ssh_attacks attempts) - Strengthen SSH Security${NC}"
|
|
echo -e "${MEDIUM_COLOR} → Press 'c' for Security Hardening menu${NC}"
|
|
recommendations=1
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
if [ "${recommendations:-0}" -eq 0 ]; then
|
|
echo ""
|
|
fi
|
|
|
|
# Show different keys based on mode
|
|
if [ "$COMPACT_MODE" -eq 1 ]; then
|
|
echo -e "${INFO_COLOR} Keys: 'b' Block | 'c' Security | 'v' Verbose | 'r' Refresh | 'q' Quit${NC}"
|
|
else
|
|
echo -e "${INFO_COLOR} Keys: 'b' Block | 'c' Security | 'v' Compact | 's' Stats | 'q' Quit${NC}"
|
|
fi
|
|
|
|
echo -e "${MEDIUM_COLOR}└────────────────────────────────────────────────────────────────────────────┘${NC}"
|
|
}
|
|
|
|
################################################################################
|
|
# Quick Action Menu
|
|
################################################################################
|
|
|
|
show_blocking_menu() {
|
|
# Pause monitoring
|
|
local monitoring_paused=1
|
|
|
|
clear
|
|
print_banner "Quick IP Blocking"
|
|
echo ""
|
|
echo "Select IPs to block (1-hour temporary ban):"
|
|
echo ""
|
|
|
|
# Build array of blockable IPs
|
|
local -a blockable_list=()
|
|
for ip in "${!IP_DATA[@]}"; do
|
|
IFS='|' read -r score hits bot_type attacks ban_count rep_score <<< "${IP_DATA[$ip]}"
|
|
|
|
# Set defaults for empty values
|
|
score="${score:-0}"
|
|
hits="${hits:-0}"
|
|
attacks="${attacks:-none}"
|
|
|
|
# Skip if score too low or already blocked
|
|
[ "$score" -lt 60 ] && continue
|
|
is_ip_blocked "$ip" 2>/dev/null && continue
|
|
|
|
blockable_list+=("$ip|$score|$hits|$attacks")
|
|
done
|
|
|
|
if [ ${#blockable_list[@]} -eq 0 ]; then
|
|
echo "No IPs meet blocking criteria (score >= 60)"
|
|
echo ""
|
|
read -p "Press Enter to continue..."
|
|
return
|
|
fi
|
|
|
|
# Check if any IPs to block
|
|
if [ ${#blockable_list[@]} -eq 0 ]; then
|
|
echo ""
|
|
echo -e "${SAFE_COLOR}No IPs meet blocking criteria (score >= 60 and not already blocked)${NC}"
|
|
echo ""
|
|
read -p "Press Enter to continue..."
|
|
return
|
|
fi
|
|
|
|
# Sort by score
|
|
IFS=$'\n' blockable_list=($(sort -t'|' -k2 -rn <<<"${blockable_list[*]}"))
|
|
unset IFS
|
|
|
|
# Display IPs
|
|
local idx=1
|
|
for entry in "${blockable_list[@]}"; do
|
|
IFS='|' read -r ip score hits attacks <<< "$entry"
|
|
local level=$(get_threat_level "$score")
|
|
local color=$(get_threat_color "$level")
|
|
|
|
printf "${color} %2d) %-15s Score:%-3s Hits:%-5s Attacks: %s${NC}\n" \
|
|
"$idx" "$ip" "$score" "$hits" "${attacks:-none}"
|
|
|
|
((idx++))
|
|
done
|
|
|
|
echo ""
|
|
echo -e "${BOLD}Options:${NC}"
|
|
echo " 1-${#blockable_list[@]}) Block specific IP"
|
|
echo " a) Block ALL high-threat IPs (score >= 80)"
|
|
echo -e " ${RED}0)${NC} Back"
|
|
echo ""
|
|
read -p "Select option: " choice
|
|
|
|
if [ "$choice" = "0" ]; then
|
|
return
|
|
elif [ "$choice" = "a" ]; then
|
|
# Block all IPs with score >= 80
|
|
local blocked=0
|
|
local failed=0
|
|
for entry in "${blockable_list[@]}"; do
|
|
IFS='|' read -r ip score hits attacks <<< "$entry"
|
|
[ "$score" -lt 80 ] && continue
|
|
|
|
echo ""
|
|
if block_ip_temporary "$ip" 1 "Auto-block: High threat (score $score)"; then
|
|
((blocked++))
|
|
else
|
|
((failed++))
|
|
fi
|
|
done
|
|
|
|
echo ""
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
echo "✓ Successfully blocked: $blocked IPs"
|
|
[ "${failed:-0}" -gt 0 ] && echo "✗ Failed to block: $failed IPs"
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
read -p "Press Enter to continue..."
|
|
elif [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le ${#blockable_list[@]} ]; then
|
|
# Block specific IP
|
|
local entry="${blockable_list[$((choice-1))]}"
|
|
IFS='|' read -r ip score hits attacks <<< "$entry"
|
|
|
|
echo ""
|
|
block_ip_temporary "$ip" 1 "Manual block from live monitor (score $score)"
|
|
|
|
echo ""
|
|
read -p "Press Enter to continue..."
|
|
else
|
|
echo "Invalid option"
|
|
read -p "Press Enter to continue..."
|
|
fi
|
|
}
|
|
|
|
show_security_hardening_menu() {
|
|
clear
|
|
print_banner "Security Hardening & Firewall Optimization"
|
|
echo ""
|
|
|
|
# Check if CSF is available
|
|
if ! command -v csf &>/dev/null; then
|
|
echo -e "${HIGH_COLOR}⚠️ CSF/LFD firewall not detected${NC}"
|
|
echo " Security hardening options require CSF to be installed"
|
|
echo ""
|
|
read -p "Press Enter to return to monitor..."
|
|
return
|
|
fi
|
|
|
|
# Check current settings
|
|
local synflood_status=$(grep "^SYNFLOOD\s*=" /etc/csf/csf.conf 2>/dev/null | cut -d'"' -f2)
|
|
local current_lf=$(grep -oP "^LF_SSHD\s*=\s*\"\K[0-9]+" /etc/csf/csf.conf 2>/dev/null | head -1)
|
|
[ -z "$current_lf" ] && current_lf="5"
|
|
|
|
echo "Current Security Status:"
|
|
echo ""
|
|
|
|
# SYNFLOOD status
|
|
if [ "$synflood_status" = "1" ]; then
|
|
echo -e " ${SAFE_COLOR}✓${NC} SYNFLOOD Protection: ${BOLD}Enabled${NC}"
|
|
else
|
|
echo -e " ${HIGH_COLOR}✗${NC} SYNFLOOD Protection: ${BOLD}Disabled${NC}"
|
|
fi
|
|
|
|
# SSH hardening status
|
|
if [ "$current_lf" -le 3 ]; then
|
|
echo -e " ${SAFE_COLOR}✓${NC} SSH Security: ${BOLD}Hardened${NC} (LF_SSHD=$current_lf)"
|
|
else
|
|
echo -e " ${HIGH_COLOR}✗${NC} SSH Security: ${BOLD}Default${NC} (LF_SSHD=$current_lf, recommend ≤3)"
|
|
fi
|
|
|
|
# CT_LIMIT status (basic check)
|
|
local ct_limit=$(grep -oP "^CT_LIMIT\s*=\s*\"\K[0-9]+" /etc/csf/csf.conf 2>/dev/null | head -1)
|
|
if [ -n "$ct_limit" ] && [ "$ct_limit" -gt 0 ]; then
|
|
echo -e " ${SAFE_COLOR}✓${NC} Connection Tracking: ${BOLD}Configured${NC} (CT_LIMIT=$ct_limit)"
|
|
else
|
|
echo -e " ${HIGH_COLOR}✗${NC} Connection Tracking: ${BOLD}Not Optimized${NC}"
|
|
fi
|
|
|
|
echo ""
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
echo ""
|
|
echo "Available Hardening Options:"
|
|
echo ""
|
|
echo -e " ${BOLD}1${NC} - Enable SYNFLOOD Protection (DDoS defense)"
|
|
echo -e " ${BOLD}2${NC} - Harden SSH Security (Lower LF_SSHD to 3)"
|
|
echo -e " ${BOLD}3${NC} - Optimize CT_LIMIT (Auto-analyze & apply)"
|
|
echo -e " ${BOLD}4${NC} - Configure Port Knocking (Coming soon)"
|
|
echo ""
|
|
echo -e " ${BOLD}a${NC} - Apply All Needed Fixes"
|
|
echo ""
|
|
echo -e " ${RED}0)${NC} Back"
|
|
echo ""
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
read -p "Select option: " choice
|
|
echo ""
|
|
|
|
case "$choice" in
|
|
1)
|
|
if [ "$synflood_status" = "1" ]; then
|
|
echo "✓ SYNFLOOD is already enabled"
|
|
echo ""
|
|
read -p "Press Enter to continue..."
|
|
else
|
|
apply_synflood_fix
|
|
fi
|
|
;;
|
|
2)
|
|
if [ "$current_lf" -le 3 ]; then
|
|
echo "✓ SSH is already hardened (LF_SSHD=$current_lf)"
|
|
echo ""
|
|
read -p "Press Enter to continue..."
|
|
else
|
|
apply_ssh_hardening
|
|
fi
|
|
;;
|
|
3)
|
|
clear
|
|
"$SCRIPT_DIR/modules/security/optimize-ct-limit.sh" --auto
|
|
echo ""
|
|
read -p "Press Enter to return to monitor..."
|
|
;;
|
|
4)
|
|
echo "Port Knocking configuration coming soon..."
|
|
echo ""
|
|
echo "For now, you can manually configure port knocking in CSF:"
|
|
echo "1. Edit /etc/csf/csf.conf"
|
|
echo "2. Set: PORTKNOCKING = \"1\""
|
|
echo "3. Define sequence: PORTKNOCKING_ALERT = \"1\""
|
|
echo "4. Restart: csf -r"
|
|
echo ""
|
|
read -p "Press Enter to continue..."
|
|
;;
|
|
a|A)
|
|
echo "Applying all needed fixes..."
|
|
echo ""
|
|
local applied=0
|
|
|
|
# Apply SYNFLOOD if needed
|
|
if [ "$synflood_status" != "1" ]; then
|
|
apply_synflood_fix
|
|
((applied++))
|
|
fi
|
|
|
|
# Apply SSH hardening if needed
|
|
if [ "$current_lf" -gt 3 ]; then
|
|
apply_ssh_hardening
|
|
((applied++))
|
|
fi
|
|
|
|
# Always offer CT_LIMIT
|
|
echo ""
|
|
echo "Running CT_LIMIT optimizer..."
|
|
"$SCRIPT_DIR/modules/security/optimize-ct-limit.sh" --auto
|
|
((applied++))
|
|
|
|
echo ""
|
|
if [ "${applied:-0}" -gt 0 ]; then
|
|
echo "✓ Applied $applied security fix(es)"
|
|
else
|
|
echo "✓ All security settings already optimized"
|
|
fi
|
|
echo ""
|
|
read -p "Press Enter to return to monitor..."
|
|
;;
|
|
0)
|
|
return
|
|
;;
|
|
*)
|
|
echo "Invalid option"
|
|
read -p "Press Enter to continue..."
|
|
;;
|
|
esac
|
|
}
|
|
|
|
apply_synflood_fix() {
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
echo "Enabling SYNFLOOD Protection..."
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
echo ""
|
|
|
|
# Check current status
|
|
local current_status=$(grep "^SYNFLOOD\s*=" /etc/csf/csf.conf 2>/dev/null | cut -d'"' -f2)
|
|
|
|
if [ "$current_status" = "1" ]; then
|
|
echo "✓ SYNFLOOD protection is already enabled"
|
|
else
|
|
echo "Current setting: SYNFLOOD = \"$current_status\""
|
|
echo "Enabling SYNFLOOD protection..."
|
|
|
|
# Backup config
|
|
cp /etc/csf/csf.conf /etc/csf/csf.conf.bak.$(date +%Y%m%d_%H%M%S)
|
|
|
|
# Enable SYNFLOOD
|
|
sed -i 's/^SYNFLOOD\s*=.*/SYNFLOOD = "1"/' /etc/csf/csf.conf
|
|
|
|
# Set reasonable defaults if not already set
|
|
if ! grep -q "^SYNFLOOD_RATE\s*=" /etc/csf/csf.conf; then
|
|
echo 'SYNFLOOD_RATE = "100/s"' >> /etc/csf/csf.conf
|
|
fi
|
|
if ! grep -q "^SYNFLOOD_BURST\s*=" /etc/csf/csf.conf; then
|
|
echo 'SYNFLOOD_BURST = "150"' >> /etc/csf/csf.conf
|
|
fi
|
|
|
|
# Restart CSF
|
|
echo ""
|
|
echo "Restarting CSF to apply changes..."
|
|
csf -r >/dev/null 2>&1
|
|
|
|
echo ""
|
|
echo "✓ SYNFLOOD protection enabled successfully"
|
|
echo " Rate limit: 100 connections per second"
|
|
echo " Burst: 150 connections"
|
|
fi
|
|
echo ""
|
|
read -p "Press Enter to continue..."
|
|
}
|
|
|
|
apply_ssh_hardening() {
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
echo "Hardening SSH Security..."
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
echo ""
|
|
|
|
# Check current LF_SSHD setting
|
|
local current_lf=$(grep -oP "^LF_SSHD\s*=\s*\"\K[0-9]+" /etc/csf/csf.conf 2>/dev/null | head -1)
|
|
|
|
if [ -z "$current_lf" ]; then
|
|
current_lf="5" # CSF default
|
|
fi
|
|
|
|
echo "Current SSH failure threshold: LF_SSHD = \"$current_lf\""
|
|
|
|
if [ "$current_lf" -le 3 ]; then
|
|
echo "✓ SSH security is already hardened (threshold ≤ 3)"
|
|
else
|
|
echo "Lowering threshold to 3 failed attempts..."
|
|
|
|
# Backup config
|
|
cp /etc/csf/csf.conf /etc/csf/csf.conf.bak.$(date +%Y%m%d_%H%M%S)
|
|
|
|
# Update LF_SSHD
|
|
sed -i 's/^LF_SSHD\s*=.*/LF_SSHD = "3"/' /etc/csf/csf.conf
|
|
|
|
# Also lower LF_SSHD_PERM if it exists (permanent blocks after X temp blocks)
|
|
if grep -q "^LF_SSHD_PERM\s*=" /etc/csf/csf.conf; then
|
|
sed -i 's/^LF_SSHD_PERM\s*=.*/LF_SSHD_PERM = "3"/' /etc/csf/csf.conf
|
|
fi
|
|
|
|
# Restart LFD to apply changes
|
|
echo ""
|
|
echo "Restarting LFD to apply changes..."
|
|
csf -r >/dev/null 2>&1
|
|
|
|
echo ""
|
|
echo "✓ SSH security hardened successfully"
|
|
echo " New threshold: 3 failed attempts before temp block"
|
|
echo " Block duration: As configured in LF_TRIGGER (default: 1 hour)"
|
|
fi
|
|
echo ""
|
|
read -p "Press Enter to continue..."
|
|
}
|
|
|
|
################################################################################
|
|
# Log Monitoring
|
|
################################################################################
|
|
|
|
monitor_apache_logs() {
|
|
# Try multiple log locations based on control panel
|
|
local log_files=()
|
|
|
|
# Use system-detected log directory (no fallback)
|
|
local LOG_DIR="${SYS_LOG_DIR}"
|
|
|
|
if [ "$SYS_CONTROL_PANEL" = "interworx" ]; then
|
|
# InterWorx: Monitor per-domain access logs
|
|
# Find recent domain logs (modified in last hour for performance, InterWorx uses 'transfer.log')
|
|
while IFS= read -r domain_log; do
|
|
[ -f "$domain_log" ] && log_files+=("$domain_log")
|
|
done < <(find /home/*/var/*/logs -type f -name "transfer.log" -mmin -60 2>/dev/null | head -10)
|
|
|
|
elif [ -n "$LOG_DIR" ]; then
|
|
# cPanel/Plesk: Use detected log directory
|
|
|
|
# Main access log
|
|
if [ -f "${LOG_DIR}/access_log" ]; then
|
|
log_files+=("${LOG_DIR}/access_log")
|
|
elif [ -f "/var/log/httpd/access_log" ]; then
|
|
log_files+=("/var/log/httpd/access_log")
|
|
elif [ -f "/var/log/apache2/access.log" ]; then
|
|
log_files+=("/var/log/apache2/access.log")
|
|
fi
|
|
|
|
# Domain logs
|
|
if [ -d "${LOG_DIR}" ]; then
|
|
# Find recent domain logs (modified in last hour)
|
|
while IFS= read -r domain_log; do
|
|
[ -f "$domain_log" ] && log_files+=("$domain_log")
|
|
done < <(find "${LOG_DIR}" -type f \( -name "*.com" -o -name "*.net" -o -name "*.org" \) -mmin -60 2>/dev/null | head -10)
|
|
fi
|
|
fi
|
|
|
|
if [ ${#log_files[@]} -eq 0 ]; then
|
|
# Apache logs not found - skip HTTP monitoring but continue with other monitoring
|
|
# This is non-fatal; other monitors (SYN, SSH, email, etc.) will continue
|
|
echo "[WARNING] No accessible Apache log files found (control panel: ${SYS_CONTROL_PANEL}, log dir: ${LOG_DIR})" >> "$TEMP_DIR/debug.log" 2>/dev/null
|
|
return 0 # Don't fail - let other monitoring continue
|
|
fi
|
|
|
|
# Monitor all log files
|
|
local event_count=0
|
|
tail -n 0 -F "${log_files[@]}" 2>/dev/null | while read -r line; do
|
|
# Increment event counter (update file every 10 events for performance)
|
|
((event_count++))
|
|
if [ $((event_count % 10)) -eq 0 ]; then
|
|
echo "$event_count" > "$TEMP_DIR/event_counter"
|
|
fi
|
|
|
|
# Parse Apache combined log format (supports IPv4 and IPv6)
|
|
# Note: bytes field can be - or number, so use [0-9-]+
|
|
if [[ "$line" =~ ^([0-9a-f.:]+)\ -\ -\ \[([^\]]+)\]\ \"([A-Z]+)\ ([^\"]+)\ [^\"]+\"\ ([0-9]+)\ ([0-9-]+)\ \"[^\"]*\"\ \"([^\"]+)\" ]]; then
|
|
local ip="${BASH_REMATCH[1]}"
|
|
local timestamp="${BASH_REMATCH[2]}"
|
|
local method="${BASH_REMATCH[3]}"
|
|
local url="${BASH_REMATCH[4]}"
|
|
local status="${BASH_REMATCH[5]}"
|
|
local bytes="${BASH_REMATCH[6]}"
|
|
local user_agent="${BASH_REMATCH[7]}"
|
|
|
|
# Skip local/private IPs and server's own IP
|
|
if [[ "$ip" =~ ^127\. ]] || \
|
|
[[ "$ip" =~ ^10\. ]] || \
|
|
[[ "$ip" =~ ^192\.168\. ]] || \
|
|
[[ "$ip" =~ ^172\.(1[6-9]|2[0-9]|3[01])\. ]] || \
|
|
[[ "$ip" =~ ^169\.254\. ]] || \
|
|
[[ "$ip" == "localhost" ]] || \
|
|
[[ "$ip" == "::1" ]]; then
|
|
continue
|
|
fi
|
|
|
|
# Update intelligence
|
|
update_ip_intelligence "$ip" "$url" "$user_agent" "$method"
|
|
|
|
# Enhanced attack detection using ET Open signatures
|
|
local et_attack_score=0
|
|
local et_attack_types=""
|
|
local et_signatures=""
|
|
local et_rate_score=0
|
|
|
|
if type analyze_http_log_line &>/dev/null; then
|
|
local attack_result=$(analyze_http_log_line "$line" 2>/dev/null)
|
|
if [ -n "$attack_result" ]; then
|
|
et_attack_score="${attack_result%%||*}"
|
|
if [ "$et_attack_score" -gt 0 ]; then
|
|
local temp="${attack_result#*||}"
|
|
et_attack_types="${temp%%||*}"
|
|
temp="${temp#*||}"
|
|
et_signatures="${temp%%||*}"
|
|
|
|
# Update IP intelligence with ET attack info
|
|
update_ip_intelligence "$ip" "$url|ET:$et_attack_types|$et_signatures" "attack" "HTTP"
|
|
|
|
# Replace IP threat score with ET detection score
|
|
# Note: We use ET score instead of adding it to avoid double-counting
|
|
# (update_ip_intelligence already detected the same attack via legacy patterns)
|
|
local current_intel=$(get_ip_intelligence "$ip")
|
|
IFS='|' read -r curr_score curr_hits curr_bot curr_attacks curr_ban curr_rep <<< "$current_intel"
|
|
|
|
# Use ET score if it's higher than current score
|
|
local new_score="$et_attack_score"
|
|
if [ "$curr_score" -gt "$et_attack_score" ]; then
|
|
# Keep higher score (e.g., from AbuseIPDB reputation boost)
|
|
new_score="$curr_score"
|
|
fi
|
|
[ "$new_score" -gt 100 ] && new_score=100
|
|
|
|
# Update IP data with ET-based score
|
|
IP_DATA[$ip]="$new_score|$curr_hits|$curr_bot|$curr_attacks|$curr_ban|$curr_rep"
|
|
|
|
# CRITICAL FIX: Write to file for cross-process communication
|
|
write_ip_data_to_file "$ip" "$new_score|$curr_hits|$curr_bot|$curr_attacks|$curr_ban|$curr_rep" 2>/dev/null &
|
|
|
|
# CRITICAL: Immediate block for severe threats (RCE, WEBSHELL, etc.)
|
|
if [[ "$et_attack_types" =~ (RCE|WEBSHELL|ECOMMERCE_EXPLOIT) ]]; then
|
|
# These are ALWAYS critical - block immediately regardless of score
|
|
echo "[CRITICAL] INSTANT_BLOCK_RCE | $ip | Score:$et_attack_score | Attacks:$et_attack_types" >> "$TEMP_DIR/recent_events"
|
|
# BUG FIX: Increment block counter for RCE blocks
|
|
increment_block_counter 1
|
|
if type quick_block_ip &>/dev/null; then
|
|
quick_block_ip "$ip" "CRITICAL_RCE: $et_attack_types" &
|
|
fi
|
|
fi
|
|
|
|
# Check rate anomaly
|
|
if type record_request &>/dev/null && type detect_rate_anomaly &>/dev/null; then
|
|
record_request "$ip"
|
|
local rate_result=$(detect_rate_anomaly "$ip" 2>/dev/null)
|
|
et_rate_score="${rate_result%%||*}"
|
|
|
|
# Combine scores
|
|
local combined_score=$((et_attack_score + et_rate_score))
|
|
[ "$combined_score" -gt 100 ] && combined_score=100
|
|
|
|
# Auto-block critical attacks
|
|
if [ "$combined_score" -ge 90 ]; then
|
|
echo "[CRITICAL] Auto-blocking $ip (Score: $combined_score, Attacks: $et_attack_types)" >> "$TEMP_DIR/recent_events"
|
|
if type quick_block_ip &>/dev/null; then
|
|
quick_block_ip "$ip" "ET:$et_attack_types" &
|
|
fi
|
|
fi
|
|
fi
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Get updated data
|
|
local intel=$(get_ip_intelligence "$ip")
|
|
IFS='|' read -r score hits bot_type attacks ban_count rep_score <<< "$intel"
|
|
|
|
# Determine if this is a threat
|
|
local level=$(get_threat_level "$score")
|
|
|
|
# Log all traffic with attacks, or score > 0, or suspicious bots, or ET detection
|
|
# This ensures we see everything interesting, not just high scores
|
|
if [ "$score" -gt 0 ] || [ -n "$attacks" ] || [ "$bot_type" = "suspicious" ] || [ "$et_attack_score" -gt 0 ]; then
|
|
local color=$(get_threat_color "$level")
|
|
local time_str=$(date +"%H:%M:%S")
|
|
|
|
# Use ET score if higher than regular score
|
|
local display_score="$score"
|
|
if [ "$et_attack_score" -gt "$score" ]; then
|
|
display_score="$et_attack_score"
|
|
level=$(get_threat_level "$et_attack_score")
|
|
color=$(get_threat_color "$level")
|
|
fi
|
|
|
|
# Build log line
|
|
local log_line="${color}[${time_str}] $ip"
|
|
log_line+=" | Score:$display_score [$level]"
|
|
|
|
# Show ET detection if found
|
|
if [ "$et_attack_score" -gt 0 ]; then
|
|
# Show primary attack type (cleaner than full list)
|
|
local primary_type=$(echo "$et_attack_types" | grep -oE 'SQLI|XSS|CMD|TRAVERSAL|WEBSHELL|RCE|UPLOAD|CVE' | head -1 2>/dev/null || echo "")
|
|
if [ -z "$primary_type" ]; then
|
|
# Bash built-in: Get first field (100x faster than cut)
|
|
primary_type="${et_attack_types%%,*}"
|
|
fi
|
|
log_line+=" | 🛡️ET:$primary_type"
|
|
|
|
# Show signature names (the key improvement!)
|
|
if [ -n "$et_signatures" ]; then
|
|
# Limit to first 3 signatures to keep display clean
|
|
local sig_display=$(echo "$et_signatures" | tr ',' '\n' | head -3 | tr '\n' ',' | sed 's/,$//')
|
|
log_line+=" | Sigs:$sig_display"
|
|
fi
|
|
|
|
# Show rate info if elevated
|
|
if [ "$et_rate_score" -gt 0 ]; then
|
|
log_line+=" | 🌊Rate:+$et_rate_score"
|
|
fi
|
|
fi
|
|
|
|
# Show bot type if interesting
|
|
if [ "$bot_type" = "suspicious" ] || [ "$bot_type" = "ai" ]; then
|
|
log_line+=" | Bot:$bot_type"
|
|
fi
|
|
|
|
# Show legacy attacks if no ET detection
|
|
if [ -n "$attacks" ] && [ "$et_attack_score" -eq 0 ]; then
|
|
# Bash built-in: Get first field (100x faster than cut)
|
|
local first_attack="${attacks%%,*}"
|
|
local icon=$(get_attack_icon "$first_attack")
|
|
log_line+=" | $icon$first_attack"
|
|
fi
|
|
|
|
log_line+=" | $url${NC}"
|
|
|
|
echo -e "$log_line" >> "$TEMP_DIR/recent_events"
|
|
fi
|
|
fi
|
|
done &
|
|
}
|
|
|
|
################################################################################
|
|
# Main Loop
|
|
################################################################################
|
|
|
|
################################################################################
|
|
# SSH Attack Monitoring
|
|
################################################################################
|
|
|
|
monitor_ssh_attacks() {
|
|
# Monitor SSH brute force attempts from /var/log/secure
|
|
local secure_log="/var/log/secure"
|
|
|
|
if [ ! -f "$secure_log" ]; then
|
|
# Try alternative location (Debian/Ubuntu)
|
|
secure_log="/var/log/auth.log"
|
|
fi
|
|
|
|
if [ -f "$secure_log" ]; then
|
|
tail -n 0 -F "$secure_log" 2>/dev/null | while read -r line; do
|
|
# Detect failed SSH login attempts (use bash regex for performance)
|
|
if [[ "$line" =~ [Ff]ailed\ password|[Aa]uthentication\ failure|[Ii]nvalid\ user ]]; then
|
|
# Extract IP address using bash regex
|
|
if [[ "$line" =~ ([0-9]{1,3}\.){3}[0-9]{1,3} ]]; then
|
|
local ip="${BASH_REMATCH[0]}"
|
|
else
|
|
continue
|
|
fi
|
|
|
|
if [ -n "$ip" ]; then
|
|
# Skip local/private IPs
|
|
if [[ "$ip" =~ ^127\. ]] || \
|
|
[[ "$ip" =~ ^10\. ]] || \
|
|
[[ "$ip" =~ ^192\.168\. ]] || \
|
|
[[ "$ip" =~ ^172\.(1[6-9]|2[0-9]|3[01])\. ]]; then
|
|
continue
|
|
fi
|
|
|
|
# Process as BRUTEFORCE attack
|
|
# Read from file (subshells can't access IP_DATA array)
|
|
local ip_file="$TEMP_DIR/ip_${ip//\./_}"
|
|
local current_data="0|0|human||0|0"
|
|
if [ -f "$ip_file" ]; then
|
|
current_data=$(cat "$ip_file")
|
|
fi
|
|
IFS='|' read -r score hits bot_type attacks ban_count rep_score <<< "$current_data"
|
|
|
|
# Increment hits
|
|
hits=$((hits + 1))
|
|
|
|
# Record timestamp and vector for intelligence
|
|
record_attack_timestamp "$ip"
|
|
record_attack_vector "$ip" "SSH"
|
|
track_subnet_attack "$ip"
|
|
|
|
# Add BRUTEFORCE to attacks if not already present
|
|
if [[ ! "$attacks" =~ BRUTEFORCE ]]; then
|
|
if [ -z "$attacks" ]; then
|
|
attacks="BRUTEFORCE"
|
|
else
|
|
attacks="${attacks},BRUTEFORCE"
|
|
fi
|
|
# Update attack type counter for display
|
|
((ATTACK_TYPE_COUNTER["BRUTEFORCE"]++))
|
|
fi
|
|
|
|
# Progressive scoring for bruteforce: Each attempt adds points
|
|
# First attempt: 10 pts, subsequent attempts: +8 pts each
|
|
if [ "${hits:-0}" -eq 1 ]; then
|
|
score=10
|
|
else
|
|
score=$((score + 8))
|
|
fi
|
|
|
|
# Apply advanced intelligence bonuses
|
|
local block_reasons=""
|
|
|
|
# 1. Attack velocity bonus
|
|
local velocity_data=$(calculate_attack_velocity "$ip")
|
|
IFS='|' read -r vel_count vel_bonus vel_reason <<< "$velocity_data"
|
|
if [ "$vel_bonus" -gt 0 ]; then
|
|
score=$((score + vel_bonus))
|
|
block_reasons="${vel_reason}"
|
|
fi
|
|
|
|
# 2. Diversity bonus (multi-vector attack)
|
|
local div_data=$(calculate_diversity_bonus "$ip")
|
|
IFS='|' read -r div_count div_bonus div_reason <<< "$div_data"
|
|
if [ "$div_bonus" -gt 0 ]; then
|
|
score=$((score + div_bonus))
|
|
[ -n "$block_reasons" ] && block_reasons="${block_reasons}+" || block_reasons=""
|
|
block_reasons="${block_reasons}${div_reason}"
|
|
fi
|
|
|
|
# 3. Timing pattern detection
|
|
local pattern_data=$(detect_timing_pattern "$ip")
|
|
IFS='|' read -r pat_type pat_conf pat_bonus pat_reason <<< "$pattern_data"
|
|
if [ "$pat_bonus" -gt 0 ]; then
|
|
score=$((score + pat_bonus))
|
|
[ -n "$block_reasons" ] && block_reasons="${block_reasons}+" || block_reasons=""
|
|
block_reasons="${block_reasons}${pat_reason}"
|
|
fi
|
|
|
|
# 4. Subnet attack bonus
|
|
local subnet_data=$(calculate_subnet_bonus "$ip")
|
|
IFS='|' read -r subnet_count subnet_bonus subnet_reason <<< "$subnet_data"
|
|
if [ "$subnet_bonus" -gt 0 ]; then
|
|
score=$((score + subnet_bonus))
|
|
[ -n "$block_reasons" ] && block_reasons="${block_reasons}+" || block_reasons=""
|
|
block_reasons="${block_reasons}${subnet_reason}"
|
|
fi
|
|
|
|
# 5. Context-aware bonus (geo, ISP, time)
|
|
local context_data=$(calculate_context_bonus "$ip")
|
|
IFS='|' read -r context_bonus context_reason <<< "$context_data"
|
|
if [ "$context_bonus" -gt 0 ]; then
|
|
score=$((score + context_bonus))
|
|
[ -n "$block_reasons" ] && block_reasons="${block_reasons}+" || block_reasons=""
|
|
block_reasons="${block_reasons}${context_reason}"
|
|
fi
|
|
|
|
# Cap at 100
|
|
[ "${score:-0}" -gt 100 ] && score=100
|
|
|
|
# Update ip_data file directly (subshells can't access IP_DATA array)
|
|
local ip_file="$TEMP_DIR/ip_${ip//\./_}"
|
|
echo "$score|$hits|$bot_type|$attacks|$ban_count|$rep_score" > "$ip_file"
|
|
|
|
# Store block reasons for CSF
|
|
if [ -n "$block_reasons" ]; then
|
|
echo "$block_reasons" > "$TEMP_DIR/block_reason_${ip//\./_}"
|
|
fi
|
|
|
|
# Log to reputation DB
|
|
flag_ip_attack "$ip" "BRUTEFORCE" 0 "SSH failed login attempt" >/dev/null 2>&1 &
|
|
|
|
# Log event
|
|
local time_str=$(date +"%H:%M:%S")
|
|
local level=$(get_threat_level "$score")
|
|
local color=$(get_threat_color "$level")
|
|
local icon=$(get_attack_icon "BRUTEFORCE")
|
|
|
|
echo -e "${color}[${time_str}] $ip | Score:$score [$level] | ${icon}SSH_BRUTEFORCE | Hits:$hits${NC}" >> "$TEMP_DIR/recent_events"
|
|
fi
|
|
fi
|
|
done &
|
|
fi
|
|
}
|
|
|
|
################################################################################
|
|
# Firewall Block Monitoring
|
|
################################################################################
|
|
|
|
monitor_firewall_blocks() {
|
|
# Monitor CSF/iptables blocks in real-time from /var/log/messages
|
|
local messages_log="/var/log/messages"
|
|
|
|
if [ ! -f "$messages_log" ]; then
|
|
# Try alternative location
|
|
messages_log="/var/log/syslog"
|
|
fi
|
|
|
|
if [ -f "$messages_log" ]; then
|
|
tail -n 0 -F "$messages_log" 2>/dev/null | while read -r line; do
|
|
# Detect firewall blocks (use bash regex for performance)
|
|
if [[ "$line" =~ [Ff]irewall|iptables.*(DENY|DROP)|CSF.*block ]]; then
|
|
# Extract IP address using bash regex
|
|
if [[ "$line" =~ ([0-9]{1,3}\.){3}[0-9]{1,3} ]]; then
|
|
local ip="${BASH_REMATCH[0]}"
|
|
else
|
|
continue
|
|
fi
|
|
|
|
if [ -n "$ip" ]; then
|
|
# Skip local/private IPs
|
|
if [[ "$ip" =~ ^127\. ]] || \
|
|
[[ "$ip" =~ ^10\. ]] || \
|
|
[[ "$ip" =~ ^192\.168\. ]] || \
|
|
[[ "$ip" =~ ^172\.(1[6-9]|2[0-9]|3[01])\. ]]; then
|
|
continue
|
|
fi
|
|
|
|
# Log firewall block
|
|
local time_str=$(date +"%H:%M:%S")
|
|
echo -e "${LOW_COLOR}[${time_str}] $ip | FIREWALL_BLOCK | Blocked by firewall${NC}" >> "$TEMP_DIR/recent_events"
|
|
# BUG FIX: Increment block counter when block is detected
|
|
increment_block_counter 1
|
|
fi
|
|
fi
|
|
done &
|
|
fi
|
|
}
|
|
|
|
################################################################################
|
|
# cPHulk Monitoring
|
|
################################################################################
|
|
|
|
monitor_cphulk_blocks() {
|
|
# Monitor cPHulk blocks (cPanel security system - cPanel ONLY)
|
|
# Skip if not cPanel
|
|
if [ "$SYS_CONTROL_PANEL" != "cpanel" ]; then
|
|
return 0
|
|
fi
|
|
|
|
if [ -x "/usr/local/cpanel/bin/cphulk_pam_ctl" ] || command -v whmapi1 &>/dev/null; then
|
|
(
|
|
declare -A SEEN_BLOCKS
|
|
while true; do
|
|
# Query cPHulk for blocked IPs
|
|
whmapi1 cphulkd_list_blocks 2>/dev/null | grep -E "ip:" | while read -r line; do
|
|
local ip=$(echo "$line" | awk '{print $2}')
|
|
|
|
if [ -n "$ip" ] && [ -z "${SEEN_BLOCKS[$ip]}" ]; then
|
|
SEEN_BLOCKS[$ip]=1
|
|
|
|
# Skip local/private IPs
|
|
if [[ "$ip" =~ ^127\. ]] || \
|
|
[[ "$ip" =~ ^10\. ]] || \
|
|
[[ "$ip" =~ ^192\.168\. ]] || \
|
|
[[ "$ip" =~ ^172\.(1[6-9]|2[0-9]|3[01])\. ]]; then
|
|
continue
|
|
fi
|
|
|
|
# Process as BRUTEFORCE attack (cPHulk blocks login attempts)
|
|
local current_data="${IP_DATA[$ip]:-0|0|human||0|0}"
|
|
IFS='|' read -r score hits bot_type attacks ban_count rep_score <<< "$current_data"
|
|
|
|
# Add BRUTEFORCE to attacks
|
|
if [[ ! "$attacks" =~ BRUTEFORCE ]]; then
|
|
if [ -z "$attacks" ]; then
|
|
attacks="BRUTEFORCE"
|
|
else
|
|
attacks="${attacks},BRUTEFORCE"
|
|
fi
|
|
fi
|
|
|
|
# Calculate score
|
|
score=$(calculate_attack_score "$attacks")
|
|
hits=$((hits + 1))
|
|
|
|
# Update IP_DATA
|
|
IP_DATA[$ip]="$score|$hits|$bot_type|$attacks|$ban_count|$rep_score"
|
|
|
|
# CRITICAL FIX: Write to file for cross-process communication
|
|
write_ip_data_to_file "$ip" "$score|$hits|$bot_type|$attacks|$ban_count|$rep_score" 2>/dev/null &
|
|
|
|
# Log event
|
|
local time_str=$(date +"%H:%M:%S")
|
|
local level=$(get_threat_level "$score")
|
|
local color=$(get_threat_color "$level")
|
|
|
|
echo -e "${color}[${time_str}] $ip | Score:$score [$level] | 🔐CPHULK_BLOCK | Blocked by cPHulk${NC}" >> "$TEMP_DIR/recent_events"
|
|
# BUG FIX: Increment block counter for cPHulk blocks
|
|
increment_block_counter 1
|
|
fi
|
|
done
|
|
sleep 10 # Poll every 10 seconds
|
|
done
|
|
) &
|
|
fi
|
|
}
|
|
|
|
################################################################################
|
|
# Network Attack Monitoring (SYN floods, port scans, DDoS)
|
|
################################################################################
|
|
|
|
monitor_network_attacks() {
|
|
# Monitor kernel logs and network statistics for SYN floods, port scans, etc.
|
|
local kern_log="/var/log/kern.log"
|
|
|
|
# Try different log locations
|
|
if [ ! -f "$kern_log" ]; then
|
|
kern_log="/var/log/messages"
|
|
fi
|
|
|
|
# Monitor kernel/firewall logs for network attacks
|
|
if [ -f "$kern_log" ]; then
|
|
tail -n 0 -F "$kern_log" 2>/dev/null | while read -r line; do
|
|
# Detect SYN flood patterns (use bash regex for performance)
|
|
if [[ "$line" =~ SYN\ flood|possible\ SYN\ flooding|TCP:\ Possible\ SYN\ flooding ]]; then
|
|
# Extract IP address using bash regex
|
|
if [[ "$line" =~ ([0-9]{1,3}\.){3}[0-9]{1,3} ]]; then
|
|
local ip="${BASH_REMATCH[0]}"
|
|
else
|
|
continue
|
|
fi
|
|
|
|
if [ -n "$ip" ]; then
|
|
# Skip local/private IPs
|
|
if [[ "$ip" =~ ^127\. ]] || \
|
|
[[ "$ip" =~ ^10\. ]] || \
|
|
[[ "$ip" =~ ^192\.168\. ]] || \
|
|
[[ "$ip" =~ ^172\.(1[6-9]|2[0-9]|3[01])\. ]]; then
|
|
continue
|
|
fi
|
|
|
|
# Process as DDOS attack
|
|
local current_data="${IP_DATA[$ip]:-0|0|human||0|0}"
|
|
IFS='|' read -r score hits bot_type attacks ban_count rep_score <<< "$current_data"
|
|
|
|
# Add DDOS to attacks
|
|
if [[ ! "$attacks" =~ DDOS ]]; then
|
|
if [ -z "$attacks" ]; then
|
|
attacks="DDOS"
|
|
else
|
|
attacks="${attacks},DDOS"
|
|
fi
|
|
fi
|
|
|
|
# Calculate score (DDOS is high severity)
|
|
score=$(calculate_attack_score "$attacks")
|
|
hits=$((hits + 1))
|
|
|
|
# Update IP_DATA
|
|
IP_DATA[$ip]="$score|$hits|$bot_type|$attacks|$ban_count|$rep_score"
|
|
|
|
# CRITICAL FIX: Write to file for cross-process communication
|
|
write_ip_data_to_file "$ip" "$score|$hits|$bot_type|$attacks|$ban_count|$rep_score" 2>/dev/null &
|
|
|
|
# Log to reputation DB
|
|
flag_ip_attack "$ip" "DDOS" 0 "SYN flood detected" >/dev/null 2>&1 &
|
|
|
|
# Log event
|
|
local time_str=$(date +"%H:%M:%S")
|
|
local level=$(get_threat_level "$score")
|
|
local color=$(get_threat_color "$level")
|
|
|
|
echo -e "${color}[${time_str}] $ip | Score:$score [$level] | 💥SYN_FLOOD | Network attack${NC}" >> "$TEMP_DIR/recent_events"
|
|
fi
|
|
fi
|
|
|
|
# Detect port scan attempts (use bash regex for performance)
|
|
if [[ "$line" =~ port.*scan|stealth\ scan|SYN-FIN\ scan|NULL\ scan ]]; then
|
|
# Extract IP address using bash regex
|
|
if [[ "$line" =~ ([0-9]{1,3}\.){3}[0-9]{1,3} ]]; then
|
|
local ip="${BASH_REMATCH[0]}"
|
|
else
|
|
continue
|
|
fi
|
|
|
|
if [ -n "$ip" ]; then
|
|
# Skip local/private IPs
|
|
if [[ "$ip" =~ ^127\. ]] || \
|
|
[[ "$ip" =~ ^10\. ]] || \
|
|
[[ "$ip" =~ ^192\.168\. ]] || \
|
|
[[ "$ip" =~ ^172\.(1[6-9]|2[0-9]|3[01])\. ]]; then
|
|
continue
|
|
fi
|
|
|
|
# Process as SCANNER attack
|
|
local current_data="${IP_DATA[$ip]:-0|0|human||0|0}"
|
|
IFS='|' read -r score hits bot_type attacks ban_count rep_score <<< "$current_data"
|
|
|
|
# Add PORT_SCAN to attacks (using ADMIN_PROBE for now - 5 points)
|
|
if [[ ! "$attacks" =~ ADMIN_PROBE ]]; then
|
|
if [ -z "$attacks" ]; then
|
|
attacks="ADMIN_PROBE"
|
|
else
|
|
attacks="${attacks},ADMIN_PROBE"
|
|
fi
|
|
fi
|
|
|
|
# Calculate score
|
|
score=$(calculate_attack_score "$attacks")
|
|
hits=$((hits + 1))
|
|
|
|
# Update IP_DATA
|
|
IP_DATA[$ip]="$score|$hits|$bot_type|$attacks|$ban_count|$rep_score"
|
|
|
|
# CRITICAL FIX: Write to file for cross-process communication
|
|
write_ip_data_to_file "$ip" "$score|$hits|$bot_type|$attacks|$ban_count|$rep_score" 2>/dev/null &
|
|
|
|
# Log event
|
|
local time_str=$(date +"%H:%M:%S")
|
|
local level=$(get_threat_level "$score")
|
|
local color=$(get_threat_color "$level")
|
|
|
|
echo -e "${color}[${time_str}] $ip | Score:$score [$level] | 🔎PORT_SCAN | Network reconnaissance${NC}" >> "$TEMP_DIR/recent_events"
|
|
fi
|
|
fi
|
|
done &
|
|
fi
|
|
|
|
# Monitor netstat for high connection counts (possible DDoS)
|
|
if command -v netstat &>/dev/null || command -v ss &>/dev/null; then
|
|
(
|
|
declare -A CONNECTION_COUNT
|
|
declare -A ALERT_SENT
|
|
local ss_cache=""
|
|
local ss_cache_time=0
|
|
while true; do
|
|
# Use ss if available (faster), otherwise netstat
|
|
if command -v ss &>/dev/null; then
|
|
# PERFORMANCE: Cache ss output during high-severity attacks
|
|
# During Tier 3+ attacks, cache for 5 seconds to reduce CPU usage by 50%
|
|
local current_time=$(date +%s 2>/dev/null || echo "${ss_cache_time:-0}")
|
|
local cache_age=$((${current_time:-0} - ${ss_cache_time:-0}))
|
|
|
|
# Refresh cache if: (1) no cache, (2) cache > 5s old, (3) not in attack (always fresh)
|
|
local prev_severity="${ATTACK_SEVERITY:-0}"
|
|
if [ -z "$ss_cache" ] || [ "$cache_age" -gt 5 ] || [ "${prev_severity}" -lt 3 ]; then
|
|
ss_cache=$(ss -tn state syn-recv 2>/dev/null)
|
|
ss_cache_time=$current_time
|
|
fi
|
|
|
|
# Get total SYN_RECV count from cache
|
|
# CRITICAL FIX: Subtract 1 to exclude header line "Recv-Q Send-Q Local Address:Port Peer Address:Port"
|
|
# Bug: wc -l was counting header + data lines, causing false severity = 0 when connections < 75
|
|
# Result: 40 real connections + header = 41 lines, 41 < 75, so severity stays 0, threshold stays 20
|
|
# Fix: Skip the first line (header) to get accurate connection count
|
|
local total_syn=$(($(echo "$ss_cache" | wc -l) - 1))
|
|
[ "$total_syn" -lt 0 ] && total_syn=0 # Handle case where ss_cache is empty/only header
|
|
local attack_severity=0
|
|
local unique_ips=0
|
|
|
|
# Multi-tier distributed DDoS detection with adaptive learning
|
|
# CRITICAL FIX: Use >= not > to include boundary values
|
|
# Bug: total_syn=500 was severity 0 instead of 4 (off-by-one)
|
|
if [ "$total_syn" -ge 500 ]; then
|
|
attack_severity=4 # Critical DDoS (new tier)
|
|
elif [ "$total_syn" -ge 300 ]; then
|
|
attack_severity=3 # Severe DDoS
|
|
elif [ "$total_syn" -ge 150 ]; then
|
|
attack_severity=2 # Major DDoS
|
|
elif [ "$total_syn" -ge 75 ]; then
|
|
attack_severity=1 # Moderate DDoS
|
|
fi
|
|
ATTACK_SEVERITY=$attack_severity # Store for next iteration
|
|
|
|
# Attack momentum tracking: Check if attack is growing
|
|
local prev_total="${PREV_TOTAL_SYN:-0}"
|
|
local attack_momentum=0
|
|
if [ "$total_syn" -gt "$prev_total" ] && [ "$prev_total" -gt 0 ]; then
|
|
local growth=$((total_syn - prev_total))
|
|
if [ "$growth" -gt 100 ]; then
|
|
attack_momentum=2 # Rapidly accelerating
|
|
elif [ "$growth" -gt 30 ]; then
|
|
attack_momentum=1 # Accelerating
|
|
fi
|
|
fi
|
|
PREV_TOTAL_SYN=$total_syn
|
|
|
|
# Count unique attacker IPs and track /24 subnets (use cached data)
|
|
declare -A subnet_counts
|
|
local attacker_ips=$(echo "$ss_cache" | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' | sort -u)
|
|
while IFS= read -r attacker_ip; do
|
|
[ -z "$attacker_ip" ] && continue
|
|
((unique_ips++))
|
|
|
|
# Track /24 subnets to detect coordinated attacks
|
|
local subnet="${attacker_ip%.*}" # Bash built-in (100x faster)
|
|
((subnet_counts[$subnet]++))
|
|
done <<< "$attacker_ips"
|
|
|
|
# Coordinated botnet detection: 3+ IPs from same /24
|
|
local coordinated_attack=0
|
|
declare -A hostile_subnets
|
|
for subnet in "${!subnet_counts[@]}"; do
|
|
if [ "${subnet_counts[$subnet]}" -ge 3 ]; then
|
|
coordinated_attack=1
|
|
hostile_subnets[$subnet]=${subnet_counts[$subnet]}
|
|
fi
|
|
done
|
|
|
|
# Subnet-level auto-blocking for severe attacks
|
|
# If attack_severity >= 3 AND subnet has 10+ attacking IPs, block entire /24
|
|
if [ "$attack_severity" -ge 3 ]; then
|
|
for subnet in "${!hostile_subnets[@]}"; do
|
|
local subnet_ip_count=${hostile_subnets[$subnet]}
|
|
if [ "$subnet_ip_count" -ge 10 ]; then
|
|
# Block entire /24 subnet via IPset (PRIORITY) then CSF
|
|
local subnet_cidr="${subnet}.0/24"
|
|
if ! grep -q "^${subnet_cidr}\$" "$TEMP_DIR/blocked_subnets" 2>/dev/null; then
|
|
echo "$subnet_cidr" >> "$TEMP_DIR/blocked_subnets"
|
|
(
|
|
# PRIORITY: Add to IPset for instant kernel-level blocking
|
|
if [ "$IPSET_AVAILABLE" -eq 1 ]; then
|
|
ipset add "$IPSET_NAME" "$subnet_cidr" -exist 2>/dev/null
|
|
fi
|
|
# CSF for persistent management (runs in background after IPset)
|
|
if command -v csf &>/dev/null; then
|
|
csf -d "$subnet_cidr" "SUBNET_DDOS:${subnet_ip_count}IPs" 2>/dev/null
|
|
fi
|
|
) &
|
|
local time_str=$(date +"%H:%M:%S")
|
|
echo -e "${CRITICAL_COLOR}[${time_str}] SUBNET_BLOCK | $subnet_cidr | IPs:${subnet_ip_count} | Severity:${attack_severity}${NC}" >> "$TEMP_DIR/recent_events"
|
|
# BUG FIX: Increment block counter when subnet block is detected
|
|
increment_block_counter 1
|
|
fi
|
|
fi
|
|
done
|
|
fi
|
|
|
|
# Count SYN_RECV connections per IP (sign of SYN flood)
|
|
while read -r ip count; do
|
|
# Skip local/private IPs first
|
|
if [[ "$ip" =~ ^127\. ]] || \
|
|
[[ "$ip" =~ ^10\. ]] || \
|
|
[[ "$ip" =~ ^192\.168\. ]] || \
|
|
[[ "$ip" =~ ^172\.(1[6-9]|2[0-9]|3[01])\. ]]; then
|
|
continue
|
|
fi
|
|
|
|
# Track connection count for this IP
|
|
CONNECTION_COUNT[$ip]=$count
|
|
|
|
# Load IP's persistent data FIRST (before threshold calculation)
|
|
# This gets the current lifetime hits count from ip_data
|
|
local current_data="0|0|human||0|0"
|
|
if [ -f "$TEMP_DIR/ip_data" ]; then
|
|
current_data=$(grep "^${ip}=" "$TEMP_DIR/ip_data" 2>/dev/null | cut -d= -f2 || echo "0|0|human||0|0")
|
|
fi
|
|
IFS='|' read -r score hits bot_type attacks ban_count rep_score <<< "$current_data"
|
|
|
|
# Dynamic threshold based on attack severity + momentum:
|
|
# CRITICAL FIX: Changed Tier 0 threshold from 20 to 3
|
|
# Bug: Tier 0 (< 75 total SYN) had threshold=20, preventing detection of distributed attacks
|
|
# With 8-41 total connections spread across IPs, no single IP reaches 20, so ZERO detection
|
|
# Fix: Lower Tier 0 to 3 to detect any suspicious SYN activity
|
|
# Tier 0: >3 connections (low-level activity, may be distributed)
|
|
# Tier 1: >10 connections (75-150 total, moderate DDoS)
|
|
# Tier 2: >6 connections (150-300 total, major DDoS)
|
|
# Tier 3: >4 connections (300-500 total, severe DDoS)
|
|
# Tier 4: >3 connections (500+ total, CRITICAL DDoS)
|
|
local threshold=3
|
|
case "$attack_severity" in
|
|
4) threshold=3 ;; # Critical: Very aggressive (safe for production)
|
|
3) threshold=4 ;; # Severe: Aggressive
|
|
2) threshold=6 ;; # Major: Balanced
|
|
1) threshold=10 ;; # Moderate: Conservative
|
|
esac
|
|
|
|
# Attack momentum adaptation: Lower threshold if attack is growing
|
|
if [ "$attack_momentum" -eq 2 ] && [ "$threshold" -gt 3 ]; then
|
|
threshold=$((threshold - 2)) # Rapidly accelerating attack
|
|
elif [ "$attack_momentum" -eq 1 ] && [ "$threshold" -gt 3 ]; then
|
|
threshold=$((threshold - 1)) # Accelerating attack
|
|
fi
|
|
|
|
# Coordinated attack bonus: Lower threshold by 1 (stacks with momentum)
|
|
if [ "$coordinated_attack" -eq 1 ] && [ "$threshold" -gt 3 ]; then
|
|
threshold=$((threshold - 1))
|
|
fi
|
|
|
|
# Minimum threshold of 3 to prevent false positives on busy web servers
|
|
[ "$threshold" -lt 3 ] && threshold=3
|
|
|
|
# CRITICAL FIX: Adaptive threshold based on LIFETIME detection history
|
|
# Use persistent hits from ip_data (central database) - survives monitor restarts
|
|
# An IP that attacks 5-10 times over days should be detected at lower threshold
|
|
# This catches distributed/low-level probes that space out attempts over time
|
|
# NOTE: hits variable now loaded from persistent ip_data storage
|
|
local lifetime_hits="${hits:-0}"
|
|
if [ "$lifetime_hits" -ge 10 ]; then
|
|
threshold=1 # Seen 10+ times across ALL TIME: auto-block even 1 connection
|
|
[ "$threshold" -lt 1 ] && threshold=1
|
|
elif [ "$lifetime_hits" -ge 5 ]; then
|
|
threshold=$((threshold - 2)) # 5-9 times: lower threshold by 2 (from 3 to 1)
|
|
[ "$threshold" -lt 1 ] && threshold=1
|
|
elif [ "$lifetime_hits" -ge 3 ]; then
|
|
threshold=$((threshold - 1)) # 3-4 times: lower threshold by 1
|
|
[ "$threshold" -lt 2 ] && threshold=2
|
|
elif [ "$lifetime_hits" -ge 2 ]; then
|
|
threshold=$((threshold - 1)) # 2 times: lower threshold slightly
|
|
[ "$threshold" -lt 2 ] && threshold=2
|
|
fi
|
|
|
|
if [ "$count" -gt "$threshold" ]; then
|
|
# Only process once per detection window
|
|
if [ -z "${ALERT_SENT[$ip]}" ]; then
|
|
ALERT_SENT[$ip]=1
|
|
|
|
# Define ip_file for this IP's individual tracking file
|
|
local ip_file="$TEMP_DIR/ip_${ip//\./_}"
|
|
|
|
# Smart whitelisting: Skip SCORING for IPs with MANY successful established connections
|
|
# But still track them - don't skip the write!
|
|
# Only whitelist if IP has 20+ established connections (highly unlikely for attacker)
|
|
# CRITICAL FIX: Use -w flag to match whole word (prevent partial IP matches)
|
|
# Example: "1.1.1.1" should not match "11.1.1.1" or "119.1.1.1"
|
|
local established_conns=$(ss -tn state established 2>/dev/null | grep -w "$ip" | wc -l)
|
|
[ -z "$established_conns" ] && established_conns=0
|
|
local skip_scoring=0
|
|
if [ "$established_conns" -ge 20 ]; then
|
|
# IP has 20+ established connections = highly likely legitimate user
|
|
# Skip scoring but STILL write/track (for historical hits)
|
|
skip_scoring=1
|
|
fi
|
|
|
|
# Check if whitelisted service
|
|
# CRITICAL FIX: Changed hits check from -eq 1 to -eq 0
|
|
# Bug: hits=0 means NEW IP (first detection), hits=1 means repeat detection
|
|
# Whitelist should only be checked on FIRST detection (hits=0), not repeat
|
|
# Previous: only checked on 2nd+ detection, causing false alerts on initial detection
|
|
if [ "$skip_scoring" -eq 0 ] && [ "${hits:-0}" -eq 0 ]; then
|
|
# Only check whitelist on first detection, and only if not already skipped
|
|
if is_whitelisted_service "$ip" 2>/dev/null; then
|
|
skip_scoring=1 # Skip scoring but STILL write/track
|
|
fi
|
|
fi
|
|
|
|
# Enhanced threat intelligence on first detection
|
|
# CRITICAL FIX: Changed hits check from -eq 1 to -eq 0
|
|
# Only query threat intelligence on FIRST detection to avoid redundant API calls
|
|
# CRITICAL FIX #2: Moved reputation bonus calculation OUT of background subshell
|
|
# Bug: Bonuses were calculated in background and written to $ip_file, but never added to final score
|
|
# Fix: Calculate bonuses synchronously and add directly to $score variable
|
|
local threat_intel_bonus=0
|
|
if [ "$skip_scoring" -eq 0 ] && [ "${hits:-0}" -eq 0 ]; then
|
|
|
|
local threat_intel=$(get_threat_intelligence "$ip" 2>/dev/null)
|
|
IFS='|' read -r abuse_conf abuse_rpts country isp geo timing whitelisted <<< "$threat_intel"
|
|
|
|
# Store enrichment for later use
|
|
echo "$threat_intel" > "$TEMP_DIR/threat_enrich_${ip//\./_}"
|
|
|
|
# Geographic clustering detection (still in background to avoid blocking)
|
|
(
|
|
# Check country/ASN clustering
|
|
if [ -n "$geo" ] && [ "$geo" != "XX" ]; then
|
|
echo "$geo" >> "$TEMP_DIR/attack_countries"
|
|
local country_count=$(grep -c "^${geo}$" "$TEMP_DIR/attack_countries" 2>/dev/null || echo "0")
|
|
if [ "$country_count" -ge 5 ]; then
|
|
echo "$geo" >> "$TEMP_DIR/hostile_countries"
|
|
fi
|
|
fi
|
|
|
|
# ASN clustering detection
|
|
if [ -n "$isp" ]; then
|
|
local asn=$(echo "$isp" | grep -oP 'AS\K\d+' 2>/dev/null | head -1 2>/dev/null || echo "")
|
|
if [ -n "$asn" ]; then
|
|
echo "$asn" >> "$TEMP_DIR/attack_asns"
|
|
local asn_count=$(grep -c "^${asn}$" "$TEMP_DIR/attack_asns" 2>/dev/null || echo "0")
|
|
if [ "$asn_count" -ge 3 ]; then
|
|
echo "$asn" >> "$TEMP_DIR/hostile_asns"
|
|
fi
|
|
fi
|
|
fi
|
|
) &
|
|
|
|
# Calculate reputation bonuses NOW (synchronously) so they get added to score
|
|
# Apply reputation boosts based on AbuseIPDB
|
|
if [ "${abuse_conf:-0}" -ge 75 ]; then
|
|
# High confidence malicious - add 30 points
|
|
threat_intel_bonus=30
|
|
elif [ "${abuse_conf:-0}" -ge 50 ]; then
|
|
# Medium confidence - add 15 points
|
|
threat_intel_bonus=15
|
|
fi
|
|
|
|
# High-risk country adds 5 points
|
|
if is_high_risk_country "${geo:-XX}" 2>/dev/null; then
|
|
threat_intel_bonus=$((threat_intel_bonus + 5))
|
|
fi
|
|
fi
|
|
|
|
# Reputation pre-boost: IPs with existing HTTP attacks get higher SYN scoring
|
|
local http_attack_bonus=0
|
|
if [[ "$attacks" =~ (SQLI|XSS|RCE|LFI|RFI|WEBSHELL|XXE|SSRF) ]]; then
|
|
http_attack_bonus=25 # Already known attacker, very suspicious
|
|
fi
|
|
|
|
# CRITICAL FIX: Declare variables before skip_scoring block
|
|
# Bug: multi_vector and geo_bonus were declared inside skip_scoring but used outside
|
|
# When skip_scoring=1, local vars never initialized, causing undefined variable in intel_tags logic
|
|
# Fix: Move declarations outside skip_scoring so they're always available
|
|
local multi_vector=0
|
|
local geo_bonus=0
|
|
|
|
# Only do scoring/tracking if not whitelisted
|
|
if [ "$skip_scoring" -eq 0 ]; then
|
|
# Record attack intelligence
|
|
record_attack_timestamp "$ip"
|
|
record_attack_vector "$ip" "NETWORK"
|
|
track_subnet_attack "$ip"
|
|
|
|
# Add SYN_FLOOD to attacks if not already present
|
|
if [[ ! "$attacks" =~ SYN_FLOOD ]]; then
|
|
[ -z "$attacks" ] && attacks="SYN_FLOOD" || attacks="${attacks},SYN_FLOOD"
|
|
fi
|
|
|
|
# CRITICAL FIX: Fixed indentation - these lines should be INSIDE skip_scoring check
|
|
# Bug: Scoring calculations were outside the if block, still running for whitelisted IPs
|
|
# Progressive scoring based on connection count
|
|
# 20-50 conns: +15 pts, 50-100: +25 pts, 100+: +40 pts
|
|
local conn_bonus=0
|
|
if [ "$count" -ge 100 ]; then
|
|
conn_bonus=40
|
|
elif [ "$count" -ge 50 ]; then
|
|
conn_bonus=25
|
|
else
|
|
conn_bonus=15
|
|
fi
|
|
|
|
# Distributed attack severity bonus
|
|
# Higher severity = more dangerous, boost scores
|
|
# Tier 4 (500+ SYN) is extreme - should auto-block immediately
|
|
case "$attack_severity" in
|
|
4) conn_bonus=$((conn_bonus + 50)) ;; # Critical DDoS (INSTANT BLOCK)
|
|
3) conn_bonus=$((conn_bonus + 30)) ;; # Severe DDoS
|
|
2) conn_bonus=$((conn_bonus + 15)) ;; # Major DDoS
|
|
1) conn_bonus=$((conn_bonus + 8)) ;; # Moderate DDoS
|
|
esac
|
|
|
|
# Attack momentum bonus (growing attack = more dangerous)
|
|
if [ "$attack_momentum" -eq 2 ]; then
|
|
conn_bonus=$((conn_bonus + 15)) # Rapidly accelerating
|
|
elif [ "$attack_momentum" -eq 1 ]; then
|
|
conn_bonus=$((conn_bonus + 8)) # Accelerating
|
|
fi
|
|
|
|
# SYN FLOOD SPECIFIC INTELLIGENCE METRICS
|
|
|
|
# 1. Pure SYN attacker (no ESTABLISHED connections)
|
|
# Legitimate users always have some established connections
|
|
# Pure SYN = 100% attack traffic
|
|
if [ "$established_conns" -eq 0 ] && [ "$count" -ge 5 ]; then
|
|
conn_bonus=$((conn_bonus + 20)) # Pure SYN flood, no legitimate traffic
|
|
fi
|
|
|
|
# 2. SYN/ESTABLISHED ratio detection
|
|
# Normal: More ESTABLISHED than SYN_RECV
|
|
# Attacker: More SYN_RECV than ESTABLISHED (or 0 established)
|
|
if [ "$established_conns" -gt 0 ]; then
|
|
# Calculate ratio (multiply by 10 for integer math)
|
|
local ratio=$((count * 10 / established_conns))
|
|
if [ "$ratio" -ge 30 ]; then
|
|
conn_bonus=$((conn_bonus + 15)) # 3:1 ratio = suspicious
|
|
elif [ "$ratio" -ge 20 ]; then
|
|
conn_bonus=$((conn_bonus + 10)) # 2:1 ratio = questionable
|
|
fi
|
|
fi
|
|
|
|
# 3. Connection persistence without completion
|
|
# Check if IP has been seen before with SYN but never completed
|
|
if [ "${hits:-0}" -ge 2 ] && [ "$established_conns" -eq 0 ]; then
|
|
conn_bonus=$((conn_bonus + 15)) # Repeated SYN, never establishes = bot
|
|
fi
|
|
|
|
# 4. Spoofed source detection (high SYN, low other traffic)
|
|
# Check if IP has ANY other traffic (HTTP requests, DNS, etc)
|
|
# CRITICAL FIX: Use already-loaded $attacks variable from ip_data (line 2597)
|
|
# Bug: was trying to read from individual ip_* file which may not exist
|
|
# If this is first SYN detection of an IP with prior HTTP attacks, file won't exist
|
|
# Result: has_other_traffic stays 0, missing indicator of multi-attack IP
|
|
local has_other_traffic=0
|
|
# If has HTTP attacks in history, not spoofed
|
|
if [[ "$attacks" =~ (SQLI|XSS|BRUTE|SCAN) ]]; then
|
|
has_other_traffic=1
|
|
fi
|
|
|
|
# High SYN but no other traffic = likely spoofed source
|
|
if [ "$has_other_traffic" -eq 0 ] && [ "$count" -ge 10 ] && [ "${hits:-0}" -ge 2 ]; then
|
|
conn_bonus=$((conn_bonus + 20)) # Spoofed source IP
|
|
fi
|
|
|
|
# 5. Single-target focus detection
|
|
# Botnet usually targets one service/port
|
|
# Check if connections are all to same port (80/443)
|
|
# CRITICAL FIX: Quote the ss EXPRESSION filter for correct syntax
|
|
# Bug: Unquoted 'src "$ip"' was treated as separate arguments, not a filter expression
|
|
# Result: ss silently ignores the filter and returns ALL syn-recv (giving wrong port count)
|
|
# Fix: Quote the expression so ss parses it correctly: 'src IP'
|
|
local target_ports=$(ss -tn "state syn-recv src $ip" 2>/dev/null | grep -oP ':\d+\s+' | sort -u | wc -l)
|
|
[ -z "$target_ports" ] && target_ports=0
|
|
if [ "$target_ports" -eq 1 ] && [ "$count" -ge 8 ]; then
|
|
conn_bonus=$((conn_bonus + 10)) # Single port = targeted attack
|
|
elif [ "$target_ports" -le 2 ] && [ "$count" -ge 15 ]; then
|
|
conn_bonus=$((conn_bonus + 5)) # 1-2 ports = focused attack
|
|
fi
|
|
|
|
# Multi-vector attack detection: Check if IP also has HTTP attacks
|
|
# This indicates sophisticated attacker (SYN flood + application layer)
|
|
# CRITICAL FIX: Use already-loaded $attacks variable from ip_data (line 2597)
|
|
# Bug: was trying to read from individual ip_* file which may not exist
|
|
# If this is first SYN detection of an IP with prior HTTP attacks, file won't exist
|
|
# Result: multi_vector stays 0, missing the sophisticated attacker indicator
|
|
# Note: multi_vector declared outside skip_scoring block (line ~2755) for scope
|
|
if [[ "$attacks" =~ (SQLI|XSS|RCE|LFI|RFI|WEBSHELL) ]]; then
|
|
multi_vector=1
|
|
conn_bonus=$((conn_bonus + 30)) # Multi-vector = very dangerous
|
|
fi
|
|
|
|
# Connection persistence bonus (repeated detections of same IP)
|
|
# This indicates sustained attack vs transient spike
|
|
if [ "${hits:-0}" -ge 5 ]; then
|
|
conn_bonus=$((conn_bonus + 20)) # Persistent attacker
|
|
elif [ "${hits:-0}" -ge 3 ]; then
|
|
conn_bonus=$((conn_bonus + 10)) # Repeated attack
|
|
fi
|
|
|
|
# Connection escalation detection
|
|
# Check if connection count is increasing (more aggressive attack)
|
|
local prev_count="${CONNECTION_COUNT[$ip]:-0}"
|
|
if [ "$count" -gt "$prev_count" ] && [ "$prev_count" -gt 0 ]; then
|
|
local increase=$((count - prev_count))
|
|
if [ "$increase" -ge 50 ]; then
|
|
conn_bonus=$((conn_bonus + 25)) # Rapidly escalating
|
|
elif [ "$increase" -ge 20 ]; then
|
|
conn_bonus=$((conn_bonus + 15)) # Escalating
|
|
fi
|
|
fi
|
|
|
|
# Add HTTP attack pre-boost
|
|
conn_bonus=$((conn_bonus + http_attack_bonus))
|
|
|
|
# Geographic clustering bonus
|
|
# Note: geo_bonus declared outside skip_scoring block (line ~2755) for scope
|
|
if [ -f "$TEMP_DIR/threat_enrich_${ip//\./_}" ]; then
|
|
local threat_data=$(cat "$TEMP_DIR/threat_enrich_${ip//\./_}" 2>/dev/null || echo "")
|
|
# Bash IFS field splitting (100x faster than cut)
|
|
IFS='|' read -r _ _ _ ip_isp ip_geo _ <<< "$threat_data"
|
|
|
|
# Check if from hostile country (5+ attackers)
|
|
if [ -n "$ip_geo" ] && grep -q "^${ip_geo}$" "$TEMP_DIR/hostile_countries" 2>/dev/null; then
|
|
geo_bonus=$((geo_bonus + 10)) # Part of coordinated country-level attack
|
|
fi
|
|
|
|
# Check if from hostile ASN (3+ attackers)
|
|
if [ -n "$ip_isp" ]; then
|
|
local ip_asn=$(echo "$ip_isp" | grep -oP 'AS\K\d+' 2>/dev/null | head -1 2>/dev/null || echo "")
|
|
if [ -n "$ip_asn" ] && grep -q "^${ip_asn}$" "$TEMP_DIR/hostile_asns" 2>/dev/null; then
|
|
geo_bonus=$((geo_bonus + 15)) # Same botnet infrastructure
|
|
fi
|
|
fi
|
|
fi
|
|
conn_bonus=$((conn_bonus + geo_bonus))
|
|
|
|
# First hit or add to existing score
|
|
# CRITICAL FIX: Reversed the condition - repeat detections should ADD, not RESET
|
|
# Bug: hits=0 means NEW IP (initialize score), hits=1+ means REPEAT (accumulate)
|
|
# Previous: reset score on repeat detection, losing threat history
|
|
# Now: initialize only on first detection, accumulate on repeats
|
|
if [ "${hits:-0}" -eq 0 ]; then
|
|
score=$conn_bonus # First detection: initialize to connection bonus
|
|
else
|
|
score=$((score + conn_bonus)) # Repeat detection: ADD to accumulated score
|
|
fi
|
|
|
|
# Apply advanced intelligence bonuses
|
|
local block_reasons=""
|
|
local velocity_data=$(calculate_attack_velocity "$ip")
|
|
IFS='|' read -r vel_count vel_bonus vel_reason <<< "$velocity_data"
|
|
[ "$vel_bonus" -gt 0 ] && score=$((score + vel_bonus)) && block_reasons="${vel_reason}"
|
|
|
|
# Apply threat intelligence bonuses (AbuseIPDB, geolocation)
|
|
if [ "$threat_intel_bonus" -gt 0 ]; then
|
|
score=$((score + threat_intel_bonus))
|
|
[ -n "$block_reasons" ] && block_reasons="${block_reasons}+" || block_reasons=""
|
|
block_reasons="${block_reasons}THREAT_INTEL(+${threat_intel_bonus})"
|
|
fi
|
|
|
|
local div_data=$(calculate_diversity_bonus "$ip")
|
|
IFS='|' read -r div_count div_bonus div_reason <<< "$div_data"
|
|
if [ "$div_bonus" -gt 0 ]; then
|
|
score=$((score + div_bonus))
|
|
[ -n "$block_reasons" ] && block_reasons="${block_reasons}+" || block_reasons=""
|
|
block_reasons="${block_reasons}${div_reason}"
|
|
fi
|
|
|
|
local subnet_data=$(calculate_subnet_bonus "$ip")
|
|
IFS='|' read -r subnet_count subnet_bonus subnet_reason <<< "$subnet_data"
|
|
if [ "${subnet_bonus:-0}" -gt 0 ]; then
|
|
score=$((score + subnet_bonus))
|
|
[ -n "$block_reasons" ] && block_reasons="${block_reasons}+" || block_reasons=""
|
|
block_reasons="${block_reasons}${subnet_reason}"
|
|
fi
|
|
|
|
# Detect timing patterns
|
|
local timing_result=$(detect_timing_pattern "$ip")
|
|
IFS='|' read -r timing_type timing_bonus timing_reason <<< "$timing_result"
|
|
if [ "$timing_bonus" -gt 0 ]; then
|
|
score=$((score + timing_bonus))
|
|
[ -n "$block_reasons" ] && block_reasons="${block_reasons}+" || block_reasons=""
|
|
block_reasons="${block_reasons}${timing_reason}"
|
|
fi
|
|
|
|
# Cap at 100
|
|
[ "$score" -gt 100 ] && score=100
|
|
fi # End of skip_scoring check
|
|
|
|
# INCREMENT HITS AFTER ALL SCORING
|
|
# Moved from before whitelisting to ensure we have complete data
|
|
# Now hits is incremented with full score calculated and ready to persist
|
|
hits=$((hits + 1))
|
|
|
|
# CRITICAL FIX: Write to centralized ip_data file (not individual ip_*.files)
|
|
# auto_mitigation_engine() reads from $TEMP_DIR/ip_data, not individual files
|
|
# Without this, SYN-detected IPs are never auto-blocked!
|
|
# SINGLE WRITE: Complete data with correct score and incremented hits
|
|
write_ip_data_to_file "$ip" "$score|$hits|$bot_type|$attacks|$ban_count|$rep_score" 2>/dev/null &
|
|
|
|
# Also write to individual file for debugging/tracking
|
|
echo "$score|$hits|$bot_type|$attacks|$ban_count|$rep_score" > "$ip_file"
|
|
|
|
# Store block reasons for auto-mitigation
|
|
if [ -n "$block_reasons" ]; then
|
|
echo "$block_reasons" > "$TEMP_DIR/block_reason_${ip//\./_}"
|
|
fi
|
|
|
|
# Log to reputation DB
|
|
flag_ip_attack "$ip" "SYN_FLOOD" 0 "SYN flood: $count connections" >/dev/null 2>&1 &
|
|
|
|
# Log event with reputation score and attack intelligence
|
|
local time_str=$(date +"%H:%M:%S")
|
|
local level=$(get_threat_level "$score")
|
|
local color=$(get_threat_color "$level")
|
|
|
|
# Build intelligence summary
|
|
local intel_tags=""
|
|
[ "$attack_severity" -ge 1 ] && intel_tags="${intel_tags}DDoS:T${attack_severity} "
|
|
[ "$attack_momentum" -ge 1 ] && intel_tags="${intel_tags}ACCEL "
|
|
[ "$coordinated_attack" -eq 1 ] && intel_tags="${intel_tags}BOTNET "
|
|
[ "$multi_vector" -eq 1 ] && intel_tags="${intel_tags}MULTI-VECTOR "
|
|
[ "$http_attack_bonus" -gt 0 ] && intel_tags="${intel_tags}HTTP-ATTACKER "
|
|
# CRITICAL FIX: Fixed conditional precedence for geo tagging
|
|
# Bug: Using elif logic caused mutual exclusion - couldn't show both tags
|
|
# If geo_bonus = 25 (both hostile country + ASN), only showed "HOSTILE-ASN"
|
|
# Should show BOTH tags if both conditions are true
|
|
local is_hostile_asn=0
|
|
local is_hostile_geo=0
|
|
if [ "$geo_bonus" -ge 15 ]; then
|
|
is_hostile_asn=1
|
|
fi
|
|
if [ "$geo_bonus" -ge 10 ] && [ "$geo_bonus" -lt 15 ]; then
|
|
is_hostile_geo=1
|
|
fi
|
|
# Special case: if geo_bonus >= 25, it's from BOTH sources (10 + 15)
|
|
if [ "$geo_bonus" -ge 25 ]; then
|
|
is_hostile_asn=1
|
|
is_hostile_geo=1
|
|
fi
|
|
[ "$is_hostile_asn" -eq 1 ] && intel_tags="${intel_tags}HOSTILE-ASN "
|
|
[ "$is_hostile_geo" -eq 1 ] && intel_tags="${intel_tags}HOSTILE-GEO "
|
|
|
|
# SYN-specific intelligence tags
|
|
[ "$established_conns" -eq 0 ] && [ "$count" -ge 5 ] && intel_tags="${intel_tags}PURE-SYN "
|
|
[ "${ratio:-0}" -ge 30 ] && intel_tags="${intel_tags}BAD-RATIO "
|
|
[ "$has_other_traffic" -eq 0 ] && [ "$count" -ge 10 ] && intel_tags="${intel_tags}SPOOFED "
|
|
[ "${target_ports:-0}" -eq 1 ] && [ "$count" -ge 8 ] && intel_tags="${intel_tags}TARGETED "
|
|
|
|
echo -e "${color}[${time_str}] $ip | Score:$score [$level] | 💥SYN_FLOOD | Conns:$count Est:$established_conns | ${intel_tags}${NC}" >> "$TEMP_DIR/recent_events"
|
|
fi
|
|
else
|
|
# Reset alert if connections drop below threshold
|
|
unset ALERT_SENT[$ip]
|
|
fi
|
|
# CRITICAL FIX: Change awk filter from '$1 > 5' to '$1 >= 3'
|
|
# Reason: Minimum threshold is 3 connections (Tier 4 attacks), so IPs with 3-5 connections must be processed
|
|
# Before fix: IPs with <6 connections were silently skipped, preventing detection in high-severity attacks
|
|
done < <(ss -tn state syn-recv 2>/dev/null | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' | sort | uniq -c | awk '$1 >= 3 {print $2, $1}')
|
|
fi
|
|
|
|
sleep 5 # Check every 5 seconds (faster detection during active attacks)
|
|
done
|
|
) &
|
|
fi
|
|
}
|
|
|
|
################################################################################
|
|
# Email/SMTP Attack Monitoring
|
|
################################################################################
|
|
|
|
monitor_email_attacks() {
|
|
# Monitor mail logs for SMTP/IMAP/POP3 bruteforce
|
|
local mail_log="/var/log/maillog"
|
|
|
|
if [ ! -f "$mail_log" ]; then
|
|
mail_log="/var/log/mail.log"
|
|
fi
|
|
|
|
if [ -f "$mail_log" ]; then
|
|
tail -n 0 -F "$mail_log" 2>/dev/null | while read -r line; do
|
|
# Dovecot authentication failures (use bash regex for performance)
|
|
if [[ "$line" =~ auth.*failed|authentication\ failed|password\ mismatch ]]; then
|
|
# Extract IP address using bash regex
|
|
if [[ "$line" =~ ([0-9]{1,3}\.){3}[0-9]{1,3} ]]; then
|
|
local ip="${BASH_REMATCH[0]}"
|
|
else
|
|
continue
|
|
fi
|
|
|
|
if [ -n "$ip" ]; then
|
|
# Skip local/private IPs
|
|
[[ "$ip" =~ ^127\. ]] || [[ "$ip" =~ ^10\. ]] || [[ "$ip" =~ ^192\.168\. ]] || [[ "$ip" =~ ^172\.(1[6-9]|2[0-9]|3[01])\. ]] && continue
|
|
|
|
# Process as BRUTEFORCE attack
|
|
# Read from file (subshells can't access IP_DATA array)
|
|
local ip_file="$TEMP_DIR/ip_${ip//\./_}"
|
|
local current_data="0|0|human||0|0"
|
|
if [ -f "$ip_file" ]; then
|
|
current_data=$(cat "$ip_file")
|
|
fi
|
|
IFS='|' read -r score hits bot_type attacks ban_count rep_score <<< "$current_data"
|
|
|
|
hits=$((hits + 1))
|
|
|
|
# Record timestamp and vector for intelligence
|
|
record_attack_timestamp "$ip"
|
|
record_attack_vector "$ip" "EMAIL"
|
|
track_subnet_attack "$ip"
|
|
|
|
# Add BRUTEFORCE to attacks
|
|
if [[ ! "$attacks" =~ BRUTEFORCE ]]; then
|
|
[ -z "$attacks" ] && attacks="BRUTEFORCE" || attacks="${attacks},BRUTEFORCE"
|
|
fi
|
|
|
|
# Progressive scoring: Each email bruteforce attempt adds points
|
|
if [ "${hits:-0}" -eq 1 ]; then
|
|
score=10
|
|
else
|
|
score=$((score + 8))
|
|
fi
|
|
|
|
# Apply advanced intelligence bonuses
|
|
local block_reasons=""
|
|
local velocity_data=$(calculate_attack_velocity "$ip")
|
|
IFS='|' read -r vel_count vel_bonus vel_reason <<< "$velocity_data"
|
|
[ "$vel_bonus" -gt 0 ] && score=$((score + vel_bonus)) && block_reasons="${vel_reason}"
|
|
|
|
local div_data=$(calculate_diversity_bonus "$ip")
|
|
IFS='|' read -r div_count div_bonus div_reason <<< "$div_data"
|
|
if [ "$div_bonus" -gt 0 ]; then
|
|
score=$((score + div_bonus))
|
|
[ -n "$block_reasons" ] && block_reasons="${block_reasons}+" || block_reasons=""
|
|
block_reasons="${block_reasons}${div_reason}"
|
|
fi
|
|
|
|
local pattern_data=$(detect_timing_pattern "$ip")
|
|
IFS='|' read -r pat_type pat_conf pat_bonus pat_reason <<< "$pattern_data"
|
|
if [ "$pat_bonus" -gt 0 ]; then
|
|
score=$((score + pat_bonus))
|
|
[ -n "$block_reasons" ] && block_reasons="${block_reasons}+" || block_reasons=""
|
|
block_reasons="${block_reasons}${pat_reason}"
|
|
fi
|
|
|
|
local subnet_data=$(calculate_subnet_bonus "$ip")
|
|
IFS='|' read -r subnet_count subnet_bonus subnet_reason <<< "$subnet_data"
|
|
if [ "$subnet_bonus" -gt 0 ]; then
|
|
score=$((score + subnet_bonus))
|
|
[ -n "$block_reasons" ] && block_reasons="${block_reasons}+" || block_reasons=""
|
|
block_reasons="${block_reasons}${subnet_reason}"
|
|
fi
|
|
|
|
local context_data=$(calculate_context_bonus "$ip")
|
|
IFS='|' read -r context_bonus context_reason <<< "$context_data"
|
|
if [ "$context_bonus" -gt 0 ]; then
|
|
score=$((score + context_bonus))
|
|
[ -n "$block_reasons" ] && block_reasons="${block_reasons}+" || block_reasons=""
|
|
block_reasons="${block_reasons}${context_reason}"
|
|
fi
|
|
|
|
[ "${score:-0}" -gt 100 ] && score=100
|
|
|
|
# Update ip_data file directly (subshells can't access IP_DATA array)
|
|
local ip_file="$TEMP_DIR/ip_${ip//\./_}"
|
|
echo "$score|$hits|$bot_type|$attacks|$ban_count|$rep_score" > "$ip_file"
|
|
|
|
# Store block reasons for CSF
|
|
if [ -n "$block_reasons" ]; then
|
|
echo "$block_reasons" > "$TEMP_DIR/block_reason_${ip//\./_}"
|
|
fi
|
|
|
|
# Log to reputation DB
|
|
flag_ip_attack "$ip" "BRUTEFORCE" 0 "Email authentication failure" >/dev/null 2>&1 &
|
|
|
|
# Log event
|
|
local time_str=$(date +"%H:%M:%S")
|
|
local level=$(get_threat_level "$score")
|
|
local color=$(get_threat_color "$level")
|
|
|
|
echo -e "${color}[${time_str}] $ip | Score:$score [$level] | 📧EMAIL_BRUTEFORCE | Hits:$hits${NC}" >> "$TEMP_DIR/recent_events"
|
|
fi
|
|
fi
|
|
done &
|
|
fi
|
|
}
|
|
|
|
################################################################################
|
|
# FTP Attack Monitoring
|
|
################################################################################
|
|
|
|
monitor_ftp_attacks() {
|
|
# Monitor FTP logs for bruteforce attempts
|
|
local ftp_log="/var/log/vsftpd.log"
|
|
|
|
if [ ! -f "$ftp_log" ]; then
|
|
ftp_log="/var/log/xferlog"
|
|
fi
|
|
|
|
if [ -f "$ftp_log" ]; then
|
|
tail -n 0 -F "$ftp_log" 2>/dev/null | while read -r line; do
|
|
# FTP authentication failures (use bash regex for performance)
|
|
if [[ "$line" =~ FAIL\ LOGIN|authentication\ failed|530\ Login\ incorrect ]]; then
|
|
# Extract IP address using bash regex
|
|
if [[ "$line" =~ ([0-9]{1,3}\.){3}[0-9]{1,3} ]]; then
|
|
local ip="${BASH_REMATCH[0]}"
|
|
else
|
|
continue
|
|
fi
|
|
|
|
if [ -n "$ip" ]; then
|
|
# Skip local/private IPs
|
|
[[ "$ip" =~ ^127\. ]] || [[ "$ip" =~ ^10\. ]] || [[ "$ip" =~ ^192\.168\. ]] || [[ "$ip" =~ ^172\.(1[6-9]|2[0-9]|3[01])\. ]] && continue
|
|
|
|
# Process as BRUTEFORCE attack
|
|
# Read from file (subshells can't access IP_DATA array)
|
|
local ip_file="$TEMP_DIR/ip_${ip//\./_}"
|
|
local current_data="0|0|human||0|0"
|
|
if [ -f "$ip_file" ]; then
|
|
current_data=$(cat "$ip_file")
|
|
fi
|
|
IFS='|' read -r score hits bot_type attacks ban_count rep_score <<< "$current_data"
|
|
|
|
hits=$((hits + 1))
|
|
|
|
# Record timestamp and vector for intelligence
|
|
record_attack_timestamp "$ip"
|
|
record_attack_vector "$ip" "FTP"
|
|
track_subnet_attack "$ip"
|
|
|
|
# Add BRUTEFORCE to attacks
|
|
if [[ ! "$attacks" =~ BRUTEFORCE ]]; then
|
|
[ -z "$attacks" ] && attacks="BRUTEFORCE" || attacks="${attacks},BRUTEFORCE"
|
|
fi
|
|
|
|
# Progressive scoring: Each FTP bruteforce attempt adds points
|
|
if [ "${hits:-0}" -eq 1 ]; then
|
|
score=10
|
|
else
|
|
score=$((score + 8))
|
|
fi
|
|
|
|
# Apply advanced intelligence bonuses
|
|
local block_reasons=""
|
|
local velocity_data=$(calculate_attack_velocity "$ip")
|
|
IFS='|' read -r vel_count vel_bonus vel_reason <<< "$velocity_data"
|
|
[ "$vel_bonus" -gt 0 ] && score=$((score + vel_bonus)) && block_reasons="${vel_reason}"
|
|
|
|
local div_data=$(calculate_diversity_bonus "$ip")
|
|
IFS='|' read -r div_count div_bonus div_reason <<< "$div_data"
|
|
if [ "$div_bonus" -gt 0 ]; then
|
|
score=$((score + div_bonus))
|
|
[ -n "$block_reasons" ] && block_reasons="${block_reasons}+" || block_reasons=""
|
|
block_reasons="${block_reasons}${div_reason}"
|
|
fi
|
|
|
|
local pattern_data=$(detect_timing_pattern "$ip")
|
|
IFS='|' read -r pat_type pat_conf pat_bonus pat_reason <<< "$pattern_data"
|
|
if [ "$pat_bonus" -gt 0 ]; then
|
|
score=$((score + pat_bonus))
|
|
[ -n "$block_reasons" ] && block_reasons="${block_reasons}+" || block_reasons=""
|
|
block_reasons="${block_reasons}${pat_reason}"
|
|
fi
|
|
|
|
local subnet_data=$(calculate_subnet_bonus "$ip")
|
|
IFS='|' read -r subnet_count subnet_bonus subnet_reason <<< "$subnet_data"
|
|
if [ "$subnet_bonus" -gt 0 ]; then
|
|
score=$((score + subnet_bonus))
|
|
[ -n "$block_reasons" ] && block_reasons="${block_reasons}+" || block_reasons=""
|
|
block_reasons="${block_reasons}${subnet_reason}"
|
|
fi
|
|
|
|
local context_data=$(calculate_context_bonus "$ip")
|
|
IFS='|' read -r context_bonus context_reason <<< "$context_data"
|
|
if [ "$context_bonus" -gt 0 ]; then
|
|
score=$((score + context_bonus))
|
|
[ -n "$block_reasons" ] && block_reasons="${block_reasons}+" || block_reasons=""
|
|
block_reasons="${block_reasons}${context_reason}"
|
|
fi
|
|
|
|
[ "${score:-0}" -gt 100 ] && score=100
|
|
|
|
# Update ip_data file directly (subshells can't access IP_DATA array)
|
|
local ip_file="$TEMP_DIR/ip_${ip//\./_}"
|
|
echo "$score|$hits|$bot_type|$attacks|$ban_count|$rep_score" > "$ip_file"
|
|
|
|
# Store block reasons for CSF
|
|
if [ -n "$block_reasons" ]; then
|
|
echo "$block_reasons" > "$TEMP_DIR/block_reason_${ip//\./_}"
|
|
fi
|
|
|
|
# Log to reputation DB
|
|
flag_ip_attack "$ip" "BRUTEFORCE" 0 "FTP login failure" >/dev/null 2>&1 &
|
|
|
|
# Log event
|
|
local time_str=$(date +"%H:%M:%S")
|
|
local level=$(get_threat_level "$score")
|
|
local color=$(get_threat_color "$level")
|
|
|
|
echo -e "${color}[${time_str}] $ip | Score:$score [$level] | 📁FTP_BRUTEFORCE | Hits:$hits${NC}" >> "$TEMP_DIR/recent_events"
|
|
fi
|
|
fi
|
|
done &
|
|
fi
|
|
}
|
|
|
|
################################################################################
|
|
# Database Attack Monitoring
|
|
################################################################################
|
|
|
|
monitor_database_attacks() {
|
|
# Monitor MySQL logs for authentication failures
|
|
local mysql_log="/var/log/mysqld.log"
|
|
|
|
if [ ! -f "$mysql_log" ]; then
|
|
mysql_log="/var/log/mysql/error.log"
|
|
fi
|
|
|
|
if [ -f "$mysql_log" ]; then
|
|
tail -n 0 -F "$mysql_log" 2>/dev/null | while read -r line; do
|
|
# MySQL authentication failures (use bash regex for performance)
|
|
if [[ "$line" =~ Access\ denied\ for\ user|Failed\ password\ for ]]; then
|
|
# Extract IP address using bash regex
|
|
if [[ "$line" =~ ([0-9]{1,3}\.){3}[0-9]{1,3} ]]; then
|
|
local ip="${BASH_REMATCH[0]}"
|
|
else
|
|
continue
|
|
fi
|
|
|
|
if [ -n "$ip" ]; then
|
|
# Skip local/private IPs
|
|
[[ "$ip" =~ ^127\. ]] || [[ "$ip" =~ ^10\. ]] || [[ "$ip" =~ ^192\.168\. ]] || [[ "$ip" =~ ^172\.(1[6-9]|2[0-9]|3[01])\. ]] && continue
|
|
|
|
# Process as SQL_INJECTION attack (database level)
|
|
# Read from file (subshells can't access IP_DATA array)
|
|
local ip_file="$TEMP_DIR/ip_${ip//\./_}"
|
|
local current_data="0|0|human||0|0"
|
|
if [ -f "$ip_file" ]; then
|
|
current_data=$(cat "$ip_file")
|
|
fi
|
|
IFS='|' read -r score hits bot_type attacks ban_count rep_score <<< "$current_data"
|
|
|
|
hits=$((hits + 1))
|
|
|
|
# Record timestamp and vector for intelligence
|
|
record_attack_timestamp "$ip"
|
|
record_attack_vector "$ip" "DATABASE"
|
|
track_subnet_attack "$ip"
|
|
|
|
# Add SQL_INJECTION to attacks
|
|
local is_new_attack=0
|
|
if [[ ! "$attacks" =~ SQL_INJECTION ]]; then
|
|
[ -z "$attacks" ] && attacks="SQL_INJECTION" || attacks="${attacks},SQL_INJECTION"
|
|
is_new_attack=1
|
|
fi
|
|
|
|
# Progressive scoring: First DB attack = 15pts, each additional = 12pts
|
|
if [ "${is_new_attack:-0}" -eq 1 ]; then
|
|
score=$((score + 15))
|
|
else
|
|
score=$((score + 12))
|
|
fi
|
|
|
|
# Apply advanced intelligence bonuses
|
|
local block_reasons=""
|
|
local velocity_data=$(calculate_attack_velocity "$ip")
|
|
IFS='|' read -r vel_count vel_bonus vel_reason <<< "$velocity_data"
|
|
[ "$vel_bonus" -gt 0 ] && score=$((score + vel_bonus)) && block_reasons="${vel_reason}"
|
|
|
|
local div_data=$(calculate_diversity_bonus "$ip")
|
|
IFS='|' read -r div_count div_bonus div_reason <<< "$div_data"
|
|
if [ "$div_bonus" -gt 0 ]; then
|
|
score=$((score + div_bonus))
|
|
[ -n "$block_reasons" ] && block_reasons="${block_reasons}+" || block_reasons=""
|
|
block_reasons="${block_reasons}${div_reason}"
|
|
fi
|
|
|
|
local pattern_data=$(detect_timing_pattern "$ip")
|
|
IFS='|' read -r pat_type pat_conf pat_bonus pat_reason <<< "$pattern_data"
|
|
if [ "$pat_bonus" -gt 0 ]; then
|
|
score=$((score + pat_bonus))
|
|
[ -n "$block_reasons" ] && block_reasons="${block_reasons}+" || block_reasons=""
|
|
block_reasons="${block_reasons}${pat_reason}"
|
|
fi
|
|
|
|
local subnet_data=$(calculate_subnet_bonus "$ip")
|
|
IFS='|' read -r subnet_count subnet_bonus subnet_reason <<< "$subnet_data"
|
|
if [ "$subnet_bonus" -gt 0 ]; then
|
|
score=$((score + subnet_bonus))
|
|
[ -n "$block_reasons" ] && block_reasons="${block_reasons}+" || block_reasons=""
|
|
block_reasons="${block_reasons}${subnet_reason}"
|
|
fi
|
|
|
|
local context_data=$(calculate_context_bonus "$ip")
|
|
IFS='|' read -r context_bonus context_reason <<< "$context_data"
|
|
if [ "$context_bonus" -gt 0 ]; then
|
|
score=$((score + context_bonus))
|
|
[ -n "$block_reasons" ] && block_reasons="${block_reasons}+" || block_reasons=""
|
|
block_reasons="${block_reasons}${context_reason}"
|
|
fi
|
|
|
|
[ "${score:-0}" -gt 100 ] && score=100
|
|
|
|
# Update ip_data file directly (subshells can't access IP_DATA array)
|
|
local ip_file="$TEMP_DIR/ip_${ip//\./_}"
|
|
echo "$score|$hits|$bot_type|$attacks|$ban_count|$rep_score" > "$ip_file"
|
|
|
|
# Store block reasons for CSF
|
|
if [ -n "$block_reasons" ]; then
|
|
echo "$block_reasons" > "$TEMP_DIR/block_reason_${ip//\./_}"
|
|
fi
|
|
|
|
# Log to reputation DB
|
|
flag_ip_attack "$ip" "SQL_INJECTION" 0 "MySQL authentication failure" >/dev/null 2>&1 &
|
|
|
|
# Log event
|
|
local time_str=$(date +"%H:%M:%S")
|
|
local level=$(get_threat_level "$score")
|
|
local color=$(get_threat_color "$level")
|
|
|
|
echo -e "${color}[${time_str}] $ip | Score:$score [$level] | 🗄️ DB_BRUTEFORCE | Hits:$hits${NC}" >> "$TEMP_DIR/recent_events"
|
|
fi
|
|
fi
|
|
done &
|
|
fi
|
|
}
|
|
|
|
################################################################################
|
|
# Distributed Attack Detection
|
|
################################################################################
|
|
|
|
detect_distributed_attacks() {
|
|
# Run in background, check every 30 seconds
|
|
(
|
|
while true; do
|
|
sleep 30
|
|
|
|
# Look for same attack pattern from multiple IPs in short time
|
|
if [ -f "$TEMP_DIR/recent_events" ]; then
|
|
# Get recent attacks (last 2 minutes)
|
|
local recent=$(tail -200 "$TEMP_DIR/recent_events" 2>/dev/null)
|
|
|
|
# Check for same attack type from 5+ different IPs (use awk for performance)
|
|
for attack_type in RCE SQL_INJECTION XSS PATH_TRAVERSAL BRUTEFORCE; do
|
|
# Single AWK pass to extract all attacking IPs
|
|
local attacking_ips=$(echo "$recent" | awk -v pattern="$attack_type" '
|
|
$0 ~ pattern {
|
|
# Extract IP (first field matching IP pattern)
|
|
for(i=1; i<=NF; i++) {
|
|
if($i ~ /^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/) {
|
|
print $i
|
|
break
|
|
}
|
|
}
|
|
}
|
|
' | sort -u)
|
|
|
|
# Count unique IPs
|
|
local unique_ips=$(echo "$attacking_ips" | grep -c "^[0-9]" 2>/dev/null || echo "0")
|
|
|
|
if [ "${unique_ips:-0}" -ge 5 ]; then
|
|
# Distributed attack detected!
|
|
local time_str=$(date +"%H:%M:%S")
|
|
|
|
# BLOCK ALL INDIVIDUAL IPs IN THE ATTACK
|
|
local -a batch_ips=()
|
|
while IFS= read -r ip; do
|
|
[ -n "$ip" ] && batch_ips+=("$ip")
|
|
done <<< "$attacking_ips"
|
|
|
|
if [ ${#batch_ips[@]} -gt 0 ]; then
|
|
batch_block_ips "${batch_ips[@]}"
|
|
echo -e "${CRITICAL_COLOR}[${time_str}] DISTRIBUTED_ATTACK | ${attack_type} from ${unique_ips} IPs | BLOCKED ALL${NC}" >> "$TEMP_DIR/recent_events"
|
|
# CRITICAL FIX: Removed duplicate increment_block_counter call
|
|
# batch_block_ips() already calls increment_block_counter with the actual count on line 1027
|
|
# Adding another increment_block_counter 1 here causes double-counting
|
|
# (If 10 IPs blocked: would count as 11 instead of 10)
|
|
fi
|
|
|
|
# Check for subnet-level coordination (25+ IPs from same /24)
|
|
declare -A subnet_counts
|
|
while IFS= read -r ip; do
|
|
[ -z "$ip" ] && continue
|
|
local subnet="${ip%.*}" # Get /24 subnet (bash built-in)
|
|
((subnet_counts[$subnet]++))
|
|
done <<< "$attacking_ips"
|
|
|
|
# Block entire subnets with 25+ attacking IPs
|
|
for subnet in "${!subnet_counts[@]}"; do
|
|
local subnet_ip_count=${subnet_counts[$subnet]}
|
|
if [ "$subnet_ip_count" -ge 25 ]; then
|
|
local subnet_cidr="${subnet}.0/24"
|
|
|
|
# Check if not already blocked
|
|
if ! grep -q "^${subnet_cidr}\$" "$TEMP_DIR/blocked_subnets" 2>/dev/null; then
|
|
echo "$subnet_cidr" >> "$TEMP_DIR/blocked_subnets"
|
|
|
|
# Add to IPset (kernel-level blocking)
|
|
if [ "$IPSET_AVAILABLE" -eq 1 ]; then
|
|
ipset add "$IPSET_NAME" "$subnet_cidr" -exist 2>/dev/null
|
|
echo -e "${CRITICAL_COLOR}[${time_str}] SUBNET_BLOCK | $subnet_cidr | ${attack_type} from ${subnet_ip_count} IPs | BLOCKED${NC}" >> "$TEMP_DIR/recent_events"
|
|
# BUG FIX: Increment block counter for subnet blocks
|
|
increment_block_counter 1
|
|
fi
|
|
fi
|
|
fi
|
|
done
|
|
|
|
# Mark in a file for Quick Actions to see
|
|
echo "${attack_type}|${unique_ips}|$(date +%s)" >> "$TEMP_DIR/distributed_attacks"
|
|
fi
|
|
done
|
|
fi
|
|
done
|
|
) &
|
|
}
|
|
|
|
################################################################################
|
|
# Automatic Mitigation Engine
|
|
################################################################################
|
|
|
|
auto_mitigation_engine() {
|
|
# Run in background, check every 10 seconds
|
|
(
|
|
# Track already blocked IPs in this session
|
|
declare -A BLOCKED_THIS_SESSION
|
|
|
|
while true; do
|
|
# Batch blocking arrays (collect IPs, block in batches of 50)
|
|
local -a batch_instant=()
|
|
local -a batch_critical=()
|
|
|
|
# DEBUG: Log that we're checking
|
|
echo "[$(date +"%H:%M:%S")] AUTO_MIT: Checking for IPs to block..." >> "$TEMP_DIR/debug.log" 2>/dev/null || true
|
|
|
|
# Read current IP data from snapshot file (updated by main process)
|
|
if [ -f "$TEMP_DIR/ip_data" ]; then
|
|
# DEBUG: File exists
|
|
local ip_count=$(wc -l < "$TEMP_DIR/ip_data" 2>/dev/null || echo "0")
|
|
echo "[$(date +"%H:%M:%S")] AUTO_MIT: ip_data exists with $ip_count IPs" >> "$TEMP_DIR/debug.log" 2>/dev/null || true
|
|
while IFS='=' read -r ip data; do
|
|
[ -z "$ip" ] && continue
|
|
|
|
IFS='|' read -r score hits bot_type attacks ban_count rep_score <<< "$data"
|
|
|
|
# DEBUG: Log parsed data
|
|
echo "[$(date +"%H:%M:%S")] AUTO_MIT: Parsing IP $ip | score=$score | hits=$hits | attacks=$attacks" >> "$TEMP_DIR/debug.log" 2>/dev/null || true
|
|
|
|
# Validate score is numeric
|
|
[ -z "$score" ] && score=0
|
|
[[ ! "$score" =~ ^[0-9]+$ ]] && score=0
|
|
|
|
# Skip if already blocked in this session
|
|
[ -n "${BLOCKED_THIS_SESSION[$ip]}" ] && continue
|
|
|
|
# INSTANT block at score 100 (MAXIMUM threat via IPset)
|
|
if [ "${score:-0}" -ge 100 ]; then
|
|
# DEBUG: Log score 100 detection
|
|
echo "[$(date +"%H:%M:%S")] AUTO_MIT: Found score 100 IP: $ip" >> "$TEMP_DIR/debug.log" 2>/dev/null || true
|
|
|
|
# Mark as blocked
|
|
BLOCKED_THIS_SESSION[$ip]=1
|
|
|
|
# Add to instant batch
|
|
batch_instant+=("$ip")
|
|
|
|
# Log event
|
|
local time_str=$(date +"%H:%M:%S")
|
|
echo -e "${CRITICAL_COLOR}[${time_str}] INSTANT_BLOCK | $ip | Score:100 | ${attacks}${NC}" >> "$TEMP_DIR/recent_events"
|
|
continue
|
|
fi
|
|
|
|
# Auto-block at score >= 80 (CRITICAL)
|
|
if [ "${score:-0}" -ge 80 ]; then
|
|
# Mark as blocked
|
|
BLOCKED_THIS_SESSION[$ip]=1
|
|
|
|
# Add to critical batch
|
|
batch_critical+=("$ip")
|
|
|
|
# Log event
|
|
local time_str=$(date +"%H:%M:%S")
|
|
echo -e "${CRITICAL_COLOR}[${time_str}] AUTO_BLOCK | $ip | Score:$score | ${attacks}${NC}" >> "$TEMP_DIR/recent_events"
|
|
fi
|
|
done < "$TEMP_DIR/ip_data"
|
|
else
|
|
# DEBUG: File doesn't exist
|
|
echo "[$(date +"%H:%M:%S")] AUTO_MIT: WARNING - ip_data file not found at $TEMP_DIR/ip_data" >> "$TEMP_DIR/debug.log" 2>/dev/null || true
|
|
fi
|
|
|
|
# BATCH BLOCK - Instant (score 100)
|
|
if [ ${#batch_instant[@]} -gt 0 ]; then
|
|
echo "[$(date +"%H:%M:%S")] AUTO_MIT: Blocking ${#batch_instant[@]} instant IPs: ${batch_instant[*]}" >> "$TEMP_DIR/debug.log" 2>/dev/null || true
|
|
batch_block_ips "${batch_instant[@]}"
|
|
else
|
|
echo "[$(date +"%H:%M:%S")] AUTO_MIT: No instant IPs to block" >> "$TEMP_DIR/debug.log" 2>/dev/null || true
|
|
fi
|
|
|
|
# BATCH BLOCK - Critical (score 80-99)
|
|
if [ ${#batch_critical[@]} -gt 0 ]; then
|
|
echo "[$(date +"%H:%M:%S")] AUTO_MIT: Blocking ${#batch_critical[@]} critical IPs: ${batch_critical[*]}" >> "$TEMP_DIR/debug.log" 2>/dev/null || true
|
|
batch_block_ips "${batch_critical[@]}"
|
|
fi
|
|
|
|
# Sleep at END of loop to check immediately on startup
|
|
# Faster checks during active attack scenarios (5 sec vs 10 sec)
|
|
sleep 5
|
|
done
|
|
) &
|
|
}
|
|
|
|
# Start all log monitoring sources
|
|
# Start all monitoring subprocesses in background
|
|
monitor_apache_logs &
|
|
monitor_ssh_attacks &
|
|
monitor_email_attacks &
|
|
monitor_ftp_attacks &
|
|
monitor_database_attacks &
|
|
monitor_firewall_blocks &
|
|
monitor_cphulk_blocks &
|
|
monitor_network_attacks &
|
|
|
|
# Display IPset initialization status
|
|
if [ -n "$IPSET_INIT_ERROR" ]; then
|
|
echo ""
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
echo -e "${HIGH_COLOR}⚠️ IPset Initialization Warning${NC}"
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
echo ""
|
|
echo " IPset fast blocking is NOT available"
|
|
echo " Reason: $IPSET_INIT_ERROR"
|
|
echo ""
|
|
echo " ${BOLD}Impact:${NC}"
|
|
echo " • Blocking will use CSF (slower than IPset)"
|
|
echo " • Large-scale attacks (500+ IPs) will be slower to block"
|
|
echo " • Performance: ~50x slower blocking vs IPset"
|
|
echo ""
|
|
echo " ${BOLD}To enable IPset fast blocking:${NC}"
|
|
|
|
if echo "$IPSET_INIT_ERROR" | grep -q "not found"; then
|
|
echo " 1. Install ipset: yum install ipset -y (or apt-get install ipset)"
|
|
echo " 2. Restart this script"
|
|
elif echo "$IPSET_INIT_ERROR" | grep -qi "module"; then
|
|
echo " 1. Load kernel modules: modprobe ip_set ip_set_hash_ip xt_set"
|
|
echo " 2. Restart this script"
|
|
elif echo "$IPSET_INIT_ERROR" | grep -qi "permission"; then
|
|
echo " 1. Run script as root: sudo $0"
|
|
elif echo "$IPSET_INIT_ERROR" | grep -q "iptables"; then
|
|
echo " 1. Check iptables: iptables -L -n"
|
|
echo " 2. Install iptables if missing: yum install iptables -y"
|
|
echo " 3. Ensure xt_set kernel module is loaded: modprobe xt_set"
|
|
else
|
|
echo " 1. Check debug log: $TEMP_DIR/debug.log"
|
|
echo " 2. Ensure ipset and iptables are installed"
|
|
echo " 3. Run as root"
|
|
fi
|
|
|
|
echo ""
|
|
echo " Fallback: Using CSF for all blocking (still functional)"
|
|
echo ""
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
echo ""
|
|
sleep 3 # Give user time to read
|
|
fi
|
|
|
|
# Start intelligence engines
|
|
detect_distributed_attacks
|
|
auto_mitigation_engine
|
|
|
|
# Reputation decay engine (runs every 30 min)
|
|
(
|
|
while true; do
|
|
sleep $DECAY_CHECK_INTERVAL
|
|
apply_reputation_decay
|
|
done
|
|
) &
|
|
|
|
# Blocked IPs cache updater (only needed in CSF mode - IPset mode appends to cache on each block)
|
|
if [ "$IPSET_AVAILABLE" -eq 0 ]; then
|
|
(
|
|
while true; do
|
|
{
|
|
# Get CSF temporary blocks - extract just the IP address
|
|
if command -v csf &>/dev/null; then
|
|
csf -t 2>/dev/null | awk '/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/ {print $1}'
|
|
fi
|
|
|
|
# Get CSF permanent denies
|
|
if [ -f /etc/csf/csf.deny ]; then
|
|
awk '/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/ {print $1}' /etc/csf/csf.deny 2>/dev/null
|
|
fi
|
|
|
|
# Get iptables DROP rules
|
|
if command -v iptables &>/dev/null; then
|
|
iptables -L INPUT -n -v 2>/dev/null | awk '/DROP/ && $8 ~ /^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/ {print $8}'
|
|
fi
|
|
} | sort -u > "$TEMP_DIR/blocked_ips_cache.tmp" 2>/dev/null
|
|
mv "$TEMP_DIR/blocked_ips_cache.tmp" "$TEMP_DIR/blocked_ips_cache" 2>/dev/null
|
|
sleep 10
|
|
done
|
|
) &
|
|
fi
|
|
|
|
# Periodic snapshot saving in background
|
|
(
|
|
while true; do
|
|
sleep 300 # Save every 5 minutes
|
|
save_snapshot
|
|
done
|
|
) &
|
|
|
|
# Main dashboard loop
|
|
LOOP_COUNT=0
|
|
while true; do
|
|
# Sync individual IP files into IP_DATA array (for data from subshell processes like SSH monitoring)
|
|
for ip_file in "$TEMP_DIR"/ip_*; do
|
|
[ -f "$ip_file" ] || continue
|
|
basename_file="${ip_file##*/}"
|
|
|
|
# Skip non-IP files explicitly
|
|
case "$basename_file" in
|
|
ip_data|ip_database.db|*cache*|*blocked*|*debug*)
|
|
continue
|
|
;;
|
|
esac
|
|
|
|
# Validate it's an IP file (should match pattern ip_N_N_N_N)
|
|
# Using bash pattern matching instead of grep for performance
|
|
if [[ ! "$basename_file" =~ ^ip_[0-9]{1,3}_[0-9]{1,3}_[0-9]{1,3}_[0-9]{1,3}$ ]]; then
|
|
continue
|
|
fi
|
|
|
|
# Extract IP from filename (ip_1_2_3_4 -> 1.2.3.4)
|
|
# Using bash string manipulation for performance
|
|
ip="${basename_file#ip_}" # Remove 'ip_' prefix
|
|
ip="${ip//_/.}" # Replace all underscores with dots
|
|
data=$(cat "$ip_file" 2>/dev/null)
|
|
|
|
# Validate data format (should be score|hits|bot_type|attacks|ban_count|rep_score)
|
|
# Using bash pattern matching instead of grep for performance
|
|
if [ -n "$data" ] && [[ "$data" == *"|"* ]]; then
|
|
# Update IP_DATA array with data from file
|
|
IP_DATA[$ip]="$data"
|
|
fi
|
|
done
|
|
|
|
draw_header
|
|
draw_intelligence_panel
|
|
draw_attack_breakdown
|
|
draw_live_feed
|
|
draw_quick_actions
|
|
|
|
# Write IP_DATA to ip_data file for auto-mitigation engine
|
|
# NOTE: Subprocesses use write_ip_data_to_file() for real-time updates
|
|
# This merges parent process data without overwriting subprocess updates
|
|
{
|
|
flock -w 2 200 || exit 1
|
|
|
|
# Read existing file (contains subprocess updates)
|
|
declare -A existing_ips
|
|
if [ -f "$TEMP_DIR/ip_data" ]; then
|
|
while IFS='=' read -r ip data; do
|
|
[ -n "$ip" ] && existing_ips[$ip]="$data"
|
|
done < "$TEMP_DIR/ip_data"
|
|
fi
|
|
|
|
# Merge parent's IP_DATA with existing (subprocess updates take priority)
|
|
for ip in "${!IP_DATA[@]}"; do
|
|
# Only write if not already in file (subprocess updates are fresher)
|
|
if [ -z "${existing_ips[$ip]}" ]; then
|
|
echo "$ip=${IP_DATA[$ip]}"
|
|
fi
|
|
done
|
|
|
|
# Write back existing entries (from subprocesses)
|
|
for ip in "${!existing_ips[@]}"; do
|
|
echo "$ip=${existing_ips[$ip]}"
|
|
done
|
|
|
|
} > "$TEMP_DIR/ip_data.new" 2>/dev/null 200>"$TEMP_DIR/ip_data.lock"
|
|
|
|
mv "$TEMP_DIR/ip_data.new" "$TEMP_DIR/ip_data" 2>/dev/null
|
|
|
|
# Update total blocks from file
|
|
if [ -f "$TEMP_DIR/total_blocks" ]; then
|
|
TOTAL_BLOCKS=$(cat "$TEMP_DIR/total_blocks")
|
|
fi
|
|
|
|
# Periodic cleanup (every 50 loops = ~100 seconds)
|
|
((LOOP_COUNT++))
|
|
if [ $((LOOP_COUNT % 50)) -eq 0 ]; then
|
|
# Trim event log to last 1000 lines
|
|
if [ -f "$TEMP_DIR/recent_events" ]; then
|
|
tail -1000 "$TEMP_DIR/recent_events" > "$TEMP_DIR/recent_events.tmp" 2>/dev/null
|
|
mv "$TEMP_DIR/recent_events.tmp" "$TEMP_DIR/recent_events" 2>/dev/null
|
|
fi
|
|
fi
|
|
|
|
# Non-blocking input with timeout
|
|
read -t $REFRESH_INTERVAL -n 1 key
|
|
|
|
case "$key" in
|
|
b|B)
|
|
show_blocking_menu
|
|
;;
|
|
c|C)
|
|
# Security hardening menu
|
|
show_security_hardening_menu
|
|
;;
|
|
v|V)
|
|
# Toggle compact/verbose mode
|
|
if [ "$COMPACT_MODE" -eq 1 ]; then
|
|
COMPACT_MODE=0
|
|
else
|
|
COMPACT_MODE=1
|
|
fi
|
|
;;
|
|
i|I)
|
|
# Show threat intelligence for specific IP
|
|
clear
|
|
print_banner "Threat Intelligence Lookup"
|
|
echo ""
|
|
read -p "Enter IP address: " lookup_ip
|
|
if [ -n "$lookup_ip" ]; then
|
|
echo ""
|
|
echo "Querying threat intelligence for $lookup_ip..."
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
threat_intel=$(get_threat_intelligence "$lookup_ip")
|
|
IFS='|' read -r abuse_conf abuse_rpts country isp geo timing whitelisted <<< "$threat_intel"
|
|
echo ""
|
|
echo "${BOLD}Threat Intelligence:${NC}"
|
|
echo " AbuseIPDB Confidence: ${abuse_conf}%"
|
|
echo " Total Abuse Reports: $abuse_rpts"
|
|
echo " Country: ${geo:-$country}"
|
|
echo " ISP: $isp"
|
|
echo " Timing Pattern: $timing"
|
|
echo " Whitelisted: $whitelisted"
|
|
echo ""
|
|
if is_high_risk_country "${geo:-XX}"; then
|
|
echo -e "${HIGH_COLOR} ⚠️ HIGH RISK COUNTRY${NC}"
|
|
fi
|
|
if [ "${abuse_conf:-0}" -ge 75 ]; then
|
|
echo -e "${CRITICAL_COLOR} 🚨 HIGH CONFIDENCE MALICIOUS${NC}"
|
|
elif [ "${abuse_conf:-0}" -ge 50 ]; then
|
|
echo -e "${HIGH_COLOR} ⚠️ MEDIUM CONFIDENCE THREAT${NC}"
|
|
fi
|
|
echo ""
|
|
read -p "Generate full incident report? (y/n): " gen_report
|
|
if [[ "$gen_report" =~ ^[Yy]$ ]]; then
|
|
report_file=$(generate_incident_report "$lookup_ip")
|
|
echo ""
|
|
echo "Report generated: $report_file"
|
|
echo ""
|
|
echo "View report? (y/n): "
|
|
read -n 1 view_report
|
|
if [[ "$view_report" =~ ^[Yy]$ ]]; then
|
|
less "$report_file"
|
|
fi
|
|
fi
|
|
fi
|
|
echo ""
|
|
read -p "Press Enter to return to monitor..."
|
|
;;
|
|
p|P)
|
|
# Show performance impact
|
|
clear
|
|
print_banner "Server Performance Monitor"
|
|
echo ""
|
|
load_data=$(get_server_load)
|
|
IFS='|' read -r load1 load5 load15 cpu_count <<< "$load_data"
|
|
echo "${BOLD}Current Load:${NC}"
|
|
echo " 1 min: $load1"
|
|
echo " 5 min: $load5"
|
|
echo " 15 min: $load15"
|
|
echo " CPU cores: $cpu_count"
|
|
echo ""
|
|
if is_server_stressed; then
|
|
echo -e "${CRITICAL_COLOR} 🔥 SERVER UNDER STRESS${NC}"
|
|
echo ""
|
|
echo " Recommended Actions:"
|
|
echo " • Enable aggressive auto-blocking (higher threshold)"
|
|
echo " • Reduce CT_LIMIT temporarily"
|
|
echo " • Block high-volume attack IPs immediately"
|
|
else
|
|
echo -e "${SAFE_COLOR} ✓ Server load normal${NC}"
|
|
fi
|
|
echo ""
|
|
read -p "Press Enter to return to monitor..."
|
|
;;
|
|
q|Q)
|
|
cleanup
|
|
;;
|
|
r|R)
|
|
# Force refresh
|
|
continue
|
|
;;
|
|
s|S)
|
|
# Show stats
|
|
clear
|
|
show_ip_reputation_stats
|
|
read -p "Press Enter to continue..."
|
|
;;
|
|
h|H|\?)
|
|
# Show help
|
|
clear
|
|
print_banner "Keyboard Controls"
|
|
echo ""
|
|
echo "Available Commands:"
|
|
echo " ${BOLD}b${NC} - Open IP blocking menu (batch or individual)"
|
|
echo " ${BOLD}c${NC} - Security hardening menu (SYNFLOOD, SSH, CT_LIMIT, Port Knocking)"
|
|
echo " ${BOLD}i${NC} - Threat intelligence lookup (AbuseIPDB, geo, incident reports)"
|
|
echo " ${BOLD}p${NC} - Show performance impact monitor (server load)"
|
|
echo " ${BOLD}s${NC} - Show IP reputation database statistics"
|
|
echo " ${BOLD}r${NC} - Force refresh display"
|
|
echo " ${BOLD}h${NC} - Show this help screen"
|
|
echo " ${BOLD}q${NC} - Quit and save snapshot"
|
|
echo ""
|
|
echo "Features:"
|
|
echo " • Real-time bot classification (legit/AI/monitor/suspicious)"
|
|
echo " • Attack vector detection (SQL, XSS, RCE, etc.)"
|
|
echo " • Threat scoring (0-100 scale)"
|
|
echo " • Threat intelligence integration (AbuseIPDB, geolocation)"
|
|
echo " • Attack pattern learning & behavioral analysis"
|
|
echo " • Automated incident report generation"
|
|
echo " • Smart whitelisting (CDNs, search engines)"
|
|
echo " • IP reputation DB integration"
|
|
echo " • CSF/iptables temporary bans (1 hour default)"
|
|
echo " • Auto-mitigation at critical threshold (score ≥80)"
|
|
echo " • Memory protection (max ${MAX_TRACKED_IPS} IPs tracked)"
|
|
echo " • Auto-save every 5 minutes + on exit"
|
|
echo ""
|
|
read -p "Press Enter to continue..."
|
|
;;
|
|
esac
|
|
done
|