Compare commits

...

3 Commits

Author SHA1 Message Date
cschantz cd5c0a7e8c Add full reputation tracking and auto-blocking for SYN flood attacks
CRITICAL FIX for active SYN flood attacks

PROBLEM:
- SYN_RECV connection monitoring only logged events
- NO reputation scoring for active SYN flood attackers
- NO auto-blocking even with 100+ simultaneous connections
- User report: "server with active attack cant auto block ips"

ROOT CAUSE:
SYN_RECV monitoring (lines 2225-2247) only logged to recent_events
without updating IP_DATA or reputation database. This meant:
- IPs with massive connection counts got no reputation score
- Auto-mitigation engine never saw these IPs
- Manual blocking was the only option

SOLUTION IMPLEMENTED:

1. Full IP Reputation Tracking (lines 2243-2317)
   - Reads/updates IP reputation file (ip_X_X_X_X)
   - Increments hit counter for each detection
   - Adds "SYN_FLOOD" to attack list

2. Progressive Scoring by Connection Count
   - 20-50 connections: +15 points
   - 50-100 connections: +25 points
   - 100+ connections: +40 points per hit
   - Can quickly reach score 100 for instant blocking

3. Advanced Intelligence Integration
   - Attack velocity tracking (rapid successive hits)
   - Attack diversity bonuses (multiple attack vectors)
   - Subnet attack detection (coordinated DDoS)
   - Timing pattern analysis (botnet identification)

4. Reputation Database Logging (line 2325)
   - Logs to IP reputation DB: flag_ip_attack()
   - Persistent tracking across sessions
   - Historical attack data preserved

5. Auto-Mitigation Integration (line 2317)
   - Writes IP data to file for auto_mitigation_engine()
   - Stores block reasons for detailed logging
   - Enables automatic blocking when score >= 80
   - INSTANT blocking when score = 100

ATTACK PROGRESSION EXAMPLE:
- Detection 1 (50 conns): Score 25
- Detection 2 (75 conns): Score 25 + 25 + bonuses = ~60
- Detection 3 (100 conns): Score 60 + 40 + bonuses = 100
- RESULT: INSTANT_BLOCK triggered automatically

This restores full auto-blocking for network-layer attacks.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-24 19:24:35 -05:00
cschantz 50ee27fef1 Fix auto-blocking: Add missing quick_block_ip() + instant block for score 100
USER REPORT:
- IPs hitting reputation 100 not being auto-blocked
- Auto-blocking appears completely broken

ROOT CAUSE ANALYSIS:
1. Missing quick_block_ip() function (called at line 1758 but never defined)
2. Auto-mitigation engine lacked score validation (empty/non-numeric scores failed silently)
3. No differentiation between score 80-99 vs 100 (instant block)

FIXES APPLIED:

1. Added quick_block_ip() function (lines 888-901)
   - Wrapper around block_ip_temporary()
   - Used by ET detection and auto-mitigation engine
   - Background-compatible, IPset-optimized

2. Added score validation in auto_mitigation_engine() (lines 2687-2689)
   - Validates score is not empty
   - Validates score is numeric
   - Defaults to 0 if invalid
   - Prevents silent failures in integer comparison

3. Added INSTANT blocking for score 100 (lines 2694-2713)
   - Score 100 = immediate IPset block
   - Labeled as "INSTANT_BLOCK" in logs
   - Uses quick_block_ip() for speed
   - Separate from regular auto-block (score 80-99)

4. Maintained existing auto-block for score >= 80 (lines 2715-2734)
   - Regular 1-hour temporary block
   - Labeled as "AUTO_BLOCK" in logs
   - Uses block_ip_temporary()

BLOCKING TIERS NOW:
- Score 100: INSTANT_BLOCK (immediate IPset, highest priority)
- Score 80-99: AUTO_BLOCK (1-hour temp block)
- Score 60-79: Manual blocking recommended (user presses 'b')
- Score < 60: Monitoring only

This restores the original auto-blocking behavior that was broken.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-24 19:21:55 -05:00
cschantz 734e26d45c Add Plesk MySQL authentication support to database discovery
Problem: Plesk MySQL requires password authentication
  User report: "ERROR 1045 (28000): Access denied for user 'root'@'localhost'"
  Result: 0 databases detected on Plesk servers

Root Cause:
  Plesk stores MySQL admin password in /etc/psa/.psa.shadow
  All MySQL queries were using passwordless 'mysql' command
  This works on cPanel (uses ~/.my.cnf) but fails on Plesk

Solution: build_databases_section() in lib/reference-db.sh
  1. Check if running on Plesk and /etc/psa/.psa.shadow exists
  2. Read admin password from file
  3. Build mysql_cmd variable with credentials
  4. Use $mysql_cmd for all database queries

Changes (lib/reference-db.sh):
  Lines 161-166: Added Plesk credential detection
  Line 168: Use $mysql_cmd for SHOW DATABASES
  Line 179: Use $mysql_cmd for size calculation
  Line 184: Use $mysql_cmd for table count

Impact:
   Database discovery now works on Plesk
   Backwards compatible with cPanel/InterWorx/Standalone
   No performance impact (password read once)

Status: Ready for testing on Plesk server

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-24 19:15:15 -05:00
2 changed files with 154 additions and 17 deletions
+10 -3
View File
@@ -158,7 +158,14 @@ build_databases_section() {
return return
fi fi
local all_dbs=$(mysql -Ns -e "SHOW DATABASES" 2>/dev/null | grep -v "^information_schema$\|^mysql$\|^performance_schema$\|^sys$" || true) # Build MySQL command with credentials if needed
local mysql_cmd="mysql"
if [ "$SYS_CONTROL_PANEL" = "plesk" ] && [ -f /etc/psa/.psa.shadow ]; then
local plesk_mysql_pass=$(cat /etc/psa/.psa.shadow)
mysql_cmd="mysql -uadmin -p${plesk_mysql_pass}"
fi
local all_dbs=$($mysql_cmd -Ns -e "SHOW DATABASES" 2>/dev/null | grep -v "^information_schema$\|^mysql$\|^performance_schema$\|^sys$" || true)
local total_dbs=$(echo "$all_dbs" | wc -l) local total_dbs=$(echo "$all_dbs" | wc -l)
local current=0 local current=0
@@ -169,12 +176,12 @@ build_databases_section() {
local owner=$(get_database_owner "$db") local owner=$(get_database_owner "$db")
local domain=$(get_database_domain "$db") local domain=$(get_database_domain "$db")
local size_mb=$(mysql -Ns -e "SELECT ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) local size_mb=$($mysql_cmd -Ns -e "SELECT ROUND(SUM(data_length + index_length) / 1024 / 1024, 2)
FROM information_schema.TABLES FROM information_schema.TABLES
WHERE table_schema='$db'" 2>/dev/null) WHERE table_schema='$db'" 2>/dev/null)
[ -z "$size_mb" ] && size_mb=0 [ -z "$size_mb" ] && size_mb=0
local table_count=$(mysql -Ns "$db" -e "SHOW TABLES" 2>/dev/null | wc -l) local table_count=$($mysql_cmd -Ns "$db" -e "SHOW TABLES" 2>/dev/null | wc -l)
echo "DB|$db|$owner|$domain|$size_mb|$table_count" >> "$SYSREF_DB" echo "DB|$db|$owner|$domain|$size_mb|$table_count" >> "$SYSREF_DB"
done done
+140 -10
View File
@@ -885,6 +885,21 @@ block_ip_temporary() {
return 1 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 permanently with CSF
block_ip_permanent() { block_ip_permanent() {
local ip="$1" local ip="$1"
@@ -2209,11 +2224,7 @@ monitor_network_attacks() {
if command -v ss &>/dev/null; then if command -v ss &>/dev/null; then
# Count SYN_RECV connections per IP (sign of SYN flood) # Count SYN_RECV connections per IP (sign of SYN flood)
while read -r ip count; do while read -r ip count; do
if [ "$count" -gt 20 ]; then # More than 20 SYN_RECV connections # Skip local/private IPs first
if [ -z "${ALERT_SENT[$ip]}" ]; then
ALERT_SENT[$ip]=1
# Skip local/private IPs
if [[ "$ip" =~ ^127\. ]] || \ if [[ "$ip" =~ ^127\. ]] || \
[[ "$ip" =~ ^10\. ]] || \ [[ "$ip" =~ ^10\. ]] || \
[[ "$ip" =~ ^192\.168\. ]] || \ [[ "$ip" =~ ^192\.168\. ]] || \
@@ -2221,12 +2232,106 @@ monitor_network_attacks() {
continue continue
fi fi
# Log high connection count # Track connection count for this IP
CONNECTION_COUNT[$ip]=$count
if [ "$count" -gt 20 ]; then # More than 20 SYN_RECV connections = DDoS
# 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))
# 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
# 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 time_str=$(date +"%H:%M:%S")
echo -e "${HIGH_COLOR}[${time_str}] $ip | 💥HIGH_CONN_COUNT | $count SYN_RECV connections (possible DDoS)${NC}" >> "$TEMP_DIR/recent_events" 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 fi
else else
# Reset alert if connections drop # Reset alert if connections drop below threshold
unset ALERT_SENT[$ip] unset ALERT_SENT[$ip]
fi 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}') 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}')
@@ -2669,11 +2774,36 @@ auto_mitigation_engine() {
IFS='|' read -r score hits bot_type attacks ban_count rep_score <<< "$data" IFS='|' read -r score hits bot_type attacks ban_count rep_score <<< "$data"
# Auto-block at score >= 80 (CRITICAL) # Validate score is numeric
if [ "$score" -ge 80 ]; then [ -z "$score" ] && score=0
[[ ! "$score" =~ ^[0-9]+$ ]] && score=0
# Skip if already blocked in this session # Skip if already blocked in this session
[ -n "${BLOCKED_THIS_SESSION[$ip]}" ] && continue [ -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 # Mark as blocked to prevent duplicate attempts
BLOCKED_THIS_SESSION[$ip]=1 BLOCKED_THIS_SESSION[$ip]=1