Files
Linux-Server-Management-Too…/modules/security/live-attack-monitor-v2.sh
T
cschantz 073890f062 BUG FIX #10: Variable scope issue with multi_vector and geo_bonus
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>
2026-03-06 23:49:29 -05:00

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