7719cfecd1
CRITICAL FIX for botnet-style attacks USER REPORT: "512 SYN_RECV connections but live monitor only shows 2 IPs" ROOT CAUSE: Threshold was hardcoded at >20 connections per IP. This works for focused attacks (one IP, many connections) but FAILS for distributed DDoS where 50+ IPs each send 5-15 connections. Example from user's attack: - 512 total SYN_RECV connections - Spread across 40+ attacker IPs - Top attacker: 107 packets (likely <20 active connections) - Result: NONE detected, server getting hammered SOLUTION - Dynamic Threshold: 1. Total SYN_RECV Detection (line 2226) Count total SYN_RECV across all IPs If > 100 total → distributed_attack mode activated 2. Adaptive Thresholds (lines 2247-2253) NORMAL MODE: threshold = 20 connections - Focused attack (1-2 IPs) - High bar to avoid false positives DISTRIBUTED MODE: threshold = 5 connections - Botnet attack (many IPs) - Catches participants in coordinated attack - Triggers when total > 100 DETECTION EXAMPLES: Focused Attack (unchanged behavior): - 1 IP with 150 SYN_RECV - Total: 150, threshold: 20 - Result: 1 IP detected, blocked Distributed Botnet (NEW): - 50 IPs each with 10 SYN_RECV - Total: 500, threshold: 5 (distributed mode) - Result: ALL 50 IPs detected, reputation tracked - Progressive blocking as scores accumulate User's Attack (512 total): - distributed_attack = 1 (512 > 100) - threshold = 5 - All IPs with >5 connections now tracked - Likely catches 30-40 of the attackers This allows catching both attack patterns without flooding the system with false positives during normal traffic.
3167 lines
124 KiB
Bash
Executable File
3167 lines
124 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="/var/lib/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="live_monitor_$$"
|
|
IPSET_AVAILABLE=0
|
|
|
|
# Initialize IPset for fast blocking (if available)
|
|
if command -v ipset &>/dev/null; then
|
|
# Create temporary IPset with 1-hour default timeout
|
|
if ipset create "$IPSET_NAME" hash:ip timeout 3600 maxelem 65536 2>/dev/null; then
|
|
IPSET_AVAILABLE=1
|
|
|
|
# Add iptables rule to block IPs in the set
|
|
iptables -I INPUT -m set --match-set "$IPSET_NAME" src -j DROP 2>/dev/null
|
|
|
|
echo "✓ IPset initialized: $IPSET_NAME (fast blocking enabled)" >> "$TEMP_DIR/debug.log"
|
|
else
|
|
echo "✗ IPset creation failed - falling back to CSF" >> "$TEMP_DIR/debug.log"
|
|
fi
|
|
else
|
|
echo "✗ IPset not available - using CSF for blocking" >> "$TEMP_DIR/debug.log"
|
|
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 '{print $1}' | grep -E '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$'
|
|
fi
|
|
|
|
# Get CSF permanent denies
|
|
if [ -f /etc/csf/csf.deny ]; then
|
|
awk '{print $1}' /etc/csf/csf.deny 2>/dev/null | grep -E '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$'
|
|
fi
|
|
|
|
# Get iptables DROP rules
|
|
if command -v iptables &>/dev/null; then
|
|
iptables -L INPUT -n -v 2>/dev/null | grep DROP | awk '{print $8}' | grep -E '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$'
|
|
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
|
|
|
|
# Clean up IPset and iptables rule if we created them
|
|
if [ "$IPSET_AVAILABLE" -eq 1 ]; 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
|
|
rm -rf "$TEMP_DIR" 2>/dev/null
|
|
|
|
# Restore cursor
|
|
command -v tput &>/dev/null && tput cnorm
|
|
|
|
echo "✓ Cleanup complete (snapshot saved)"
|
|
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 file
|
|
local snapshot_file="$TEMP_DIR/snapshot.dat"
|
|
|
|
# Write IP data
|
|
{
|
|
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
|
|
}
|
|
|
|
# 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
|
|
}
|
|
|
|
# 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"
|
|
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"
|
|
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"
|
|
fi
|
|
) &
|
|
fi
|
|
fi
|
|
|
|
# Classify bot if unknown
|
|
if [ "$bot_type" = "unknown" ] && [ -n "$user_agent" ]; then
|
|
bot_type=$(classify_bot_type "$user_agent")
|
|
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
|
|
score=$((score - 5))
|
|
[ "${score:-0}" -lt 0 ] && score=0
|
|
;;
|
|
suspicious)
|
|
# Suspicious bots - increase score
|
|
score=$((score + 10))
|
|
;;
|
|
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"
|
|
|
|
# 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
|
|
}
|
|
|
|
################################################################################
|
|
# 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' | wc -l)
|
|
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
|
|
local subnet=$(echo "$ip" | cut -d. -f1-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=$(echo "$ip" | cut -d. -f1-3)
|
|
|
|
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' | tail -1)
|
|
local time_since_attack=$((now - last_attack))
|
|
|
|
# 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)
|
|
if echo "$isp" | grep -qiE "(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"
|
|
}
|
|
|
|
# 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
|
|
|
|
echo "Batch blocking ${#ip_list[@]} IPs..."
|
|
|
|
# Use IPset for instant batch blocking if available
|
|
if [ "$IPSET_AVAILABLE" -eq 1 ]; then
|
|
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 (slower, but still batch where possible)
|
|
for ip in "${ip_list[@]}"; do
|
|
if ! is_valid_ip "$ip"; then
|
|
((failed++))
|
|
continue
|
|
fi
|
|
|
|
if csf -td "$ip" 3600 "Batch auto-block" >/dev/null 2>&1; then
|
|
((blocked++))
|
|
else
|
|
((failed++))
|
|
fi
|
|
done
|
|
|
|
echo "✓ CSF batch: $blocked blocked, $failed failed"
|
|
fi
|
|
|
|
# Update total counter atomically
|
|
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
|
|
|
|
# Use IPset for instant blocking if available
|
|
if [ "$IPSET_AVAILABLE" -eq 1 ]; then
|
|
echo "Blocking $ip for ${hours}h: $reason"
|
|
if ipset add "$IPSET_NAME" "$ip" timeout "$seconds" 2>/dev/null; then
|
|
echo "✓ $ip blocked via IPset (auto-expires in ${hours}h)"
|
|
echo "$ip" >> "$TEMP_DIR/blocked_ips_cache"
|
|
|
|
# Update counter atomically
|
|
increment_block_counter 1
|
|
|
|
return 0
|
|
else
|
|
echo "✗ Warning: IPset add failed (IP may already be blocked)"
|
|
return 1
|
|
fi
|
|
fi
|
|
|
|
# Fallback to CSF if IPset not available
|
|
if command -v csf &>/dev/null; then
|
|
echo "Blocking $ip for ${hours}h: $reason"
|
|
if csf -td "$ip" "$seconds" "$reason" >/dev/null 2>&1; then
|
|
echo "✓ $ip blocked via CSF"
|
|
echo "$ip" >> "$TEMP_DIR/blocked_ips_cache"
|
|
|
|
# Update counter atomically
|
|
increment_block_counter 1
|
|
|
|
return 0
|
|
else
|
|
echo "✗ Warning: CSF block failed for $ip"
|
|
return 1
|
|
fi
|
|
fi
|
|
|
|
echo "✗ Error: CSF not 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
|
|
|
|
# Permanent blocks always use CSF (IPset is temp-only for this script)
|
|
# But we can add to IPset with max timeout as well for immediate effect
|
|
if [ "$IPSET_AVAILABLE" -eq 1 ]; then
|
|
# Add to IPset with 24-hour timeout for immediate blocking
|
|
ipset add "$IPSET_NAME" "$ip" timeout 86400 2>/dev/null
|
|
fi
|
|
|
|
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
|
|
|
|
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
|
|
if csf -t 2>/dev/null | grep -q "$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
|
|
if iptables -L INPUT -n 2>/dev/null | grep -q "$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
|
|
}
|
|
|
|
# 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
|
|
}
|
|
|
|
# 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 "^CT_LIMIT\s*=" /etc/csf/csf.conf 2>/dev/null | grep -oE '[0-9]+' | 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 "^LF_SSHD\s*=" /etc/csf/csf.conf 2>/dev/null | grep -oE '[0-9]+' | 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 "^LF_SSHD\s*=" /etc/csf/csf.conf 2>/dev/null | grep -oE '[0-9]+' | 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 "^CT_LIMIT\s*=" /etc/csf/csf.conf 2>/dev/null | grep -oE '[0-9]+' | 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 "^LF_SSHD\s*=" /etc/csf/csf.conf 2>/dev/null | grep -oE '[0-9]+' | 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
|
|
echo "ERROR: No accessible Apache log files found" >> "$TEMP_DIR/recent_events"
|
|
echo "Control panel: ${SYS_CONTROL_PANEL}, Log dir: ${LOG_DIR}" >> "$TEMP_DIR/recent_events"
|
|
return 1
|
|
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"
|
|
|
|
# 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)
|
|
if [ -z "$primary_type" ]; then
|
|
primary_type=$(echo "$et_attack_types" | cut -d',' -f1)
|
|
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
|
|
local first_attack=$(echo "$attacks" | cut -d',' -f1)
|
|
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"
|
|
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"
|
|
|
|
# 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"
|
|
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"
|
|
|
|
# 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"
|
|
|
|
# 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
|
|
while true; do
|
|
# Use ss if available (faster), otherwise netstat
|
|
if command -v ss &>/dev/null; then
|
|
# Get total SYN_RECV count for distributed attack detection
|
|
local total_syn=$(ss -tn state syn-recv 2>/dev/null | wc -l)
|
|
local distributed_attack=0
|
|
|
|
# Distributed DDoS detection: Many IPs with small counts
|
|
if [ "$total_syn" -gt 100 ]; then
|
|
distributed_attack=1
|
|
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
|
|
|
|
# Dynamic threshold based on attack type:
|
|
# - Normal: >20 connections (focused attack)
|
|
# - Distributed DDoS: >5 connections (botnet)
|
|
local threshold=20
|
|
if [ "$distributed_attack" -eq 1 ]; then
|
|
threshold=5 # Lower threshold during distributed attacks
|
|
fi
|
|
|
|
if [ "$count" -gt "$threshold" ]; then
|
|
# Only process once per detection window
|
|
if [ -z "${ALERT_SENT[$ip]}" ]; then
|
|
ALERT_SENT[$ip]=1
|
|
|
|
# Update IP reputation via file (subshell 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))
|
|
|
|
# Enhanced threat intelligence on first detection
|
|
if [ "${hits:-0}" -eq 1 ]; then
|
|
# Check if whitelisted service first
|
|
if is_whitelisted_service "$ip" 2>/dev/null; then
|
|
continue # Skip whitelisted IPs
|
|
fi
|
|
|
|
# Get threat intelligence in background to avoid slowdown
|
|
(
|
|
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//\./_}"
|
|
|
|
# Apply reputation boosts based on AbuseIPDB
|
|
if [ "${abuse_conf:-0}" -ge 75 ]; then
|
|
# High confidence malicious - add 30 points
|
|
local curr_data=$(cat "$ip_file" 2>/dev/null || echo "0|0|human||0|0")
|
|
IFS='|' read -r old_score old_hits old_bot old_attacks old_ban old_rep <<< "$curr_data"
|
|
local new_score=$((old_score + 30))
|
|
[ "$new_score" -gt 100 ] && new_score=100
|
|
echo "$new_score|$old_hits|$old_bot|$old_attacks|$old_ban|$old_rep" > "$ip_file"
|
|
elif [ "${abuse_conf:-0}" -ge 50 ]; then
|
|
# Medium confidence - add 15 points
|
|
local curr_data=$(cat "$ip_file" 2>/dev/null || echo "0|0|human||0|0")
|
|
IFS='|' read -r old_score old_hits old_bot old_attacks old_ban old_rep <<< "$curr_data"
|
|
local new_score=$((old_score + 15))
|
|
[ "$new_score" -gt 100 ] && new_score=100
|
|
echo "$new_score|$old_hits|$old_bot|$old_attacks|$old_ban|$old_rep" > "$ip_file"
|
|
fi
|
|
|
|
# High-risk country adds 5 points
|
|
if is_high_risk_country "${geo:-XX}" 2>/dev/null; then
|
|
local curr_data=$(cat "$ip_file" 2>/dev/null || echo "0|0|human||0|0")
|
|
IFS='|' read -r old_score old_hits old_bot old_attacks old_ban old_rep <<< "$curr_data"
|
|
local new_score=$((old_score + 5))
|
|
[ "$new_score" -gt 100 ] && new_score=100
|
|
echo "$new_score|$old_hits|$old_bot|$old_attacks|$old_ban|$old_rep" > "$ip_file"
|
|
fi
|
|
) &
|
|
fi
|
|
|
|
# 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
|
|
|
|
# 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
|
|
|
|
# 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
|
|
|
|
# First hit or add to existing score
|
|
if [ "${hits:-0}" -eq 1 ]; then
|
|
score=$conn_bonus
|
|
else
|
|
score=$((score + conn_bonus))
|
|
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 subnet_bonus=$(calculate_subnet_bonus "$ip")
|
|
if [ "$subnet_bonus" -gt 0 ]; then
|
|
score=$((score + subnet_bonus))
|
|
local context_reason="SUBNET_ATTACK"
|
|
[ -n "$block_reasons" ] && block_reasons="${block_reasons}+" || block_reasons=""
|
|
block_reasons="${block_reasons}${context_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
|
|
|
|
# Write to file for main process
|
|
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
|
|
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 | $count SYN_RECV connections${NC}" >> "$TEMP_DIR/recent_events"
|
|
fi
|
|
else
|
|
# Reset alert if connections drop below threshold
|
|
unset ALERT_SENT[$ip]
|
|
fi
|
|
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 > 5 {print $2, $1}')
|
|
fi
|
|
|
|
sleep 15 # Check every 15 seconds
|
|
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 count attacks and unique IPs
|
|
local result=$(echo "$recent" | awk -v pattern="$attack_type" '
|
|
$0 ~ pattern {
|
|
count++
|
|
# 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}$/) {
|
|
ips[$i]=1
|
|
break
|
|
}
|
|
}
|
|
}
|
|
END {
|
|
print count "|" length(ips)
|
|
}
|
|
')
|
|
IFS='|' read -r attack_count unique_ips <<< "$result"
|
|
|
|
if [ "${attack_count:-0}" -ge 5 ]; then
|
|
|
|
if [ "$unique_ips" -ge 5 ]; then
|
|
# Distributed attack detected!
|
|
local time_str=$(date +"%H:%M:%S")
|
|
echo -e "${CRITICAL_COLOR}[${time_str}] DISTRIBUTED_ATTACK | ${attack_type} from ${unique_ips} IPs in last 2min | Possible botnet${NC}" >> "$TEMP_DIR/recent_events"
|
|
|
|
# Mark in a file for Quick Actions to see
|
|
echo "${attack_type}|${unique_ips}|$(date +%s)" >> "$TEMP_DIR/distributed_attacks"
|
|
fi
|
|
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
|
|
sleep 10
|
|
|
|
# Read current IP data from snapshot file (updated by main process)
|
|
if [ -f "$TEMP_DIR/ip_data" ]; then
|
|
while IFS='=' read -r ip data; do
|
|
[ -z "$ip" ] && continue
|
|
|
|
IFS='|' read -r score hits bot_type attacks ban_count rep_score <<< "$data"
|
|
|
|
# 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
|
|
# Mark as blocked
|
|
BLOCKED_THIS_SESSION[$ip]=1
|
|
|
|
# Instant IPset block
|
|
local time_str=$(date +"%H:%M:%S")
|
|
echo -e "${CRITICAL_COLOR}[${time_str}] INSTANT_BLOCK | $ip | Score:100 | ${attacks}${NC}" >> "$TEMP_DIR/recent_events"
|
|
|
|
# Get detailed block reason
|
|
local block_reason="INSTANT AUTO-BLOCK: Score=100 Attacks=${attacks}"
|
|
if [ -f "$TEMP_DIR/block_reason_${ip//\./_}" ]; then
|
|
local intel_reason=$(cat "$TEMP_DIR/block_reason_${ip//\./_}")
|
|
block_reason="${block_reason} Intel:${intel_reason}"
|
|
fi
|
|
|
|
# Instant block via quick_block_ip (uses IPset for speed)
|
|
quick_block_ip "$ip" "$block_reason" &
|
|
continue
|
|
fi
|
|
|
|
# Auto-block at score >= 80 (CRITICAL)
|
|
if [ "${score:-0}" -ge 80 ]; then
|
|
# Mark as blocked to prevent duplicate attempts
|
|
BLOCKED_THIS_SESSION[$ip]=1
|
|
|
|
# Auto-block
|
|
local time_str=$(date +"%H:%M:%S")
|
|
echo -e "${CRITICAL_COLOR}[${time_str}] AUTO_BLOCK | $ip | Score:$score | ${attacks}${NC}" >> "$TEMP_DIR/recent_events"
|
|
|
|
# Get detailed block reason
|
|
local block_reason="Auto-block: Score=$score Attacks=${attacks}"
|
|
if [ -f "$TEMP_DIR/block_reason_${ip//\./_}" ]; then
|
|
local intel_reason=$(cat "$TEMP_DIR/block_reason_${ip//\./_}")
|
|
block_reason="${block_reason} Intel:${intel_reason}"
|
|
fi
|
|
|
|
# Block for 1 hour with detailed reason
|
|
# Block in background and counter is updated within function
|
|
block_ip_temporary "$ip" 1 "$block_reason" &
|
|
fi
|
|
done < "$TEMP_DIR/ip_data"
|
|
fi
|
|
done
|
|
) &
|
|
}
|
|
|
|
# Start all log monitoring sources
|
|
monitor_apache_logs
|
|
monitor_ssh_attacks
|
|
monitor_email_attacks
|
|
monitor_ftp_attacks
|
|
monitor_database_attacks
|
|
monitor_firewall_blocks
|
|
monitor_cphulk_blocks
|
|
monitor_network_attacks
|
|
|
|
# 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 '{print $1}' | grep -E '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$'
|
|
fi
|
|
|
|
# Get CSF permanent denies
|
|
if [ -f /etc/csf/csf.deny ]; then
|
|
awk '{print $1}' /etc/csf/csf.deny 2>/dev/null | grep -E '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$'
|
|
fi
|
|
|
|
# Get iptables DROP rules
|
|
if command -v iptables &>/dev/null; then
|
|
iptables -L INPUT -n -v 2>/dev/null | grep DROP | awk '{print $8}' | grep -E '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$'
|
|
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="$(basename "$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
|
|
{
|
|
for ip in "${!IP_DATA[@]}"; do
|
|
echo "$ip=${IP_DATA[$ip]}"
|
|
done
|
|
} > "$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 "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
local 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
|
|
local 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 ""
|
|
local 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
|