diff --git a/modules/backup/acronis-troubleshoot.sh b/modules/backup/acronis-troubleshoot.sh index 339a2ad..05916a9 100755 --- a/modules/backup/acronis-troubleshoot.sh +++ b/modules/backup/acronis-troubleshoot.sh @@ -356,7 +356,7 @@ else # Show recommendations if [ ${#RECOMMENDATIONS[@]} -gt 0 ]; then echo -e "${CYAN}${BOLD}Recommendations:${NC}" - local rec_num=1 + rec_num=1 for rec in "${RECOMMENDATIONS[@]}"; do echo -e " ${CYAN}${rec_num}.${NC} $rec" ((rec_num++)) diff --git a/modules/backup/acronis-uninstall.sh b/modules/backup/acronis-uninstall.sh index 7b64055..3d9f621 100755 --- a/modules/backup/acronis-uninstall.sh +++ b/modules/backup/acronis-uninstall.sh @@ -179,7 +179,7 @@ if [ "$remove_data" = "yes" ]; then for dir in "${DATA_DIRS[@]}"; do if [ -d "$dir" ]; then - local size=$(du -sh "$dir" 2>/dev/null | awk '{print $1}') + size=$(du -sh "$dir" 2>/dev/null | awk '{print $1}') echo " Removing: $dir (${size})" rm -rf "$dir" 2>/dev/null fi @@ -202,7 +202,7 @@ echo -e "${GREEN}${BOLD}✓ Uninstallation Complete${NC}" echo "" # Check if anything remains -local remaining=0 +remaining=0 if systemctl list-unit-files | grep -q "acronis"; then echo -e "${YELLOW}⚠ Some service files may still be present${NC}" @@ -230,7 +230,7 @@ if [ "$remove_data" = "no" ]; then echo "" echo "Backup data and logs were kept as requested:" if [ -d "/var/lib/Acronis" ]; then - local data_size=$(du -sh /var/lib/Acronis 2>/dev/null | awk '{print $1}') + data_size=$(du -sh /var/lib/Acronis 2>/dev/null | awk '{print $1}') echo " Location: /var/lib/Acronis" echo " Size: $data_size" echo "" diff --git a/modules/security/live-attack-monitor-v2.sh b/modules/security/live-attack-monitor-v2.sh index 244f690..8035500 100755 --- a/modules/security/live-attack-monitor-v2.sh +++ b/modules/security/live-attack-monitor-v2.sh @@ -72,22 +72,23 @@ IPSET_INIT_ERROR="" # Store initialization error message # Initialize IPset for fast blocking (if available) if command -v ipset &>/dev/null; then - # Check if CSF's chain_DENY IPset exists (preferred - already integrated with CSF) - if ipset list chain_DENY &>/dev/null 2>&1; then + # Check if CSF's chain_DENY IPset exists AND supports timeouts + if ipset list chain_DENY &>/dev/null 2>&1 && ipset list chain_DENY | grep -q "^Type:.*timeout"; then + # CSF ipset exists with timeout support - use it! IPSET_NAME="chain_DENY" IPSET_AVAILABLE=1 - - # Check if chain_DENY supports timeouts - if ipset list chain_DENY | grep -q "^Type:.*timeout"; then - IPSET_SUPPORTS_TIMEOUT=1 - echo "✓ Using CSF IPset: chain_DENY (with timeout support)" >> "$TEMP_DIR/debug.log" 2>/dev/null || true - else - echo "✓ Using CSF IPset: chain_DENY (no timeout support, will use CSF for temp blocks)" >> "$TEMP_DIR/debug.log" 2>/dev/null || true - fi + IPSET_SUPPORTS_TIMEOUT=1 + echo "✓ Using CSF IPset: chain_DENY (with timeout support)" >> "$TEMP_DIR/debug.log" 2>/dev/null || true else - # No CSF IPset found, create our own temporary one + # CSF ipset doesn't exist OR doesn't support timeouts - create our own IPSET_NAME="live_monitor_$$" + if ipset list chain_DENY &>/dev/null 2>&1; then + echo "→ CSF chain_DENY exists but no timeout support - creating our own ipset" >> "$TEMP_DIR/debug.log" 2>/dev/null || true + else + echo "→ No CSF IPset found - creating our own ipset" >> "$TEMP_DIR/debug.log" 2>/dev/null || true + fi + # Capture detailed error output IPSET_CREATE_OUTPUT=$(ipset create "$IPSET_NAME" hash:ip timeout 3600 maxelem 65536 2>&1) IPSET_CREATE_EXIT=$? @@ -149,17 +150,17 @@ fi { # 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}$' + csf -t 2>/dev/null | awk '/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/ {print $1}' fi # Get CSF permanent denies if [ -f /etc/csf/csf.deny ]; then - awk '{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}$' + awk '/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/ {print $1}' /etc/csf/csf.deny 2>/dev/null fi # Get iptables DROP rules if command -v iptables &>/dev/null; then - iptables -L INPUT -n -v 2>/dev/null | grep DROP | awk '{print $8}' | grep -E '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$' + iptables -L INPUT -n -v 2>/dev/null | awk '/DROP/ && $8 ~ /^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/ {print $8}' fi } | sort -u > "$TEMP_DIR/blocked_ips_cache" 2>/dev/null @@ -893,10 +894,14 @@ batch_block_ips() { return 0 fi + # DEBUG: Log function entry + echo "[$(date +"%H:%M:%S")] BATCH_BLOCK: Starting batch block for ${#ip_list[@]} IPs: ${ip_list[*]}" >> "$TEMP_DIR/debug.log" 2>/dev/null || true + echo "Batch blocking ${#ip_list[@]} IPs..." # Use IPset for instant batch blocking if available if [ "$IPSET_AVAILABLE" -eq 1 ]; then + echo "[$(date +"%H:%M:%S")] BATCH_BLOCK: Using IPSET path" >> "$TEMP_DIR/debug.log" 2>/dev/null || true for ip in "${ip_list[@]}"; do # Validate IP format if ! is_valid_ip "$ip"; then @@ -920,23 +925,33 @@ batch_block_ips() { echo "✓ IPset batch: $blocked blocked, $failed skipped" else # Fallback to CSF (slower, but still batch where possible) + echo "[$(date +"%H:%M:%S")] BATCH_BLOCK: Using CSF path (IPSET_AVAILABLE=$IPSET_AVAILABLE)" >> "$TEMP_DIR/debug.log" 2>/dev/null || true + for ip in "${ip_list[@]}"; do if ! is_valid_ip "$ip"; then + echo "[$(date +"%H:%M:%S")] BATCH_BLOCK: Invalid IP format: $ip" >> "$TEMP_DIR/debug.log" 2>/dev/null || true ((failed++)) continue fi - if csf -td "$ip" 3600 "Batch auto-block" >/dev/null 2>&1; then + echo "[$(date +"%H:%M:%S")] BATCH_BLOCK: Attempting CSF block for $ip" >> "$TEMP_DIR/debug.log" 2>/dev/null || true + + local csf_output=$(csf -td "$ip" 3600 "Batch auto-block" 2>&1) + if [ $? -eq 0 ]; then + echo "[$(date +"%H:%M:%S")] BATCH_BLOCK: CSF SUCCESS for $ip" >> "$TEMP_DIR/debug.log" 2>/dev/null || true ((blocked++)) else + echo "[$(date +"%H:%M:%S")] BATCH_BLOCK: CSF FAILED for $ip: $csf_output" >> "$TEMP_DIR/debug.log" 2>/dev/null || true ((failed++)) fi done echo "✓ CSF batch: $blocked blocked, $failed failed" + echo "[$(date +"%H:%M:%S")] BATCH_BLOCK: CSF batch complete - blocked=$blocked, failed=$failed" >> "$TEMP_DIR/debug.log" 2>/dev/null || true fi # Update total counter atomically + echo "[$(date +"%H:%M:%S")] BATCH_BLOCK: Incrementing counter by $blocked" >> "$TEMP_DIR/debug.log" 2>/dev/null || true increment_block_counter "$blocked" return 0 @@ -1371,7 +1386,7 @@ draw_quick_actions() { 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 ct_limit=$(grep -oP "^CT_LIMIT\s*=\s*\"\K[0-9]+" /etc/csf/csf.conf 2>/dev/null | head -1) local needs_config=0 @@ -1402,7 +1417,7 @@ draw_quick_actions() { [[ ! "$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) + local current_lf=$(grep -oP "^LF_SSHD\s*=\s*\"\K[0-9]+" /etc/csf/csf.conf 2>/dev/null | head -1) [ -z "$current_lf" ] && current_lf="5" # Only show recommendation if not already hardened @@ -1556,7 +1571,7 @@ show_security_hardening_menu() { # 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) + local current_lf=$(grep -oP "^LF_SSHD\s*=\s*\"\K[0-9]+" /etc/csf/csf.conf 2>/dev/null | head -1) [ -z "$current_lf" ] && current_lf="5" echo "Current Security Status:" @@ -1577,7 +1592,7 @@ show_security_hardening_menu() { 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) + local ct_limit=$(grep -oP "^CT_LIMIT\s*=\s*\"\K[0-9]+" /etc/csf/csf.conf 2>/dev/null | head -1) if [ -n "$ct_limit" ] && [ "$ct_limit" -gt 0 ]; then echo -e " ${SAFE_COLOR}✓${NC} Connection Tracking: ${BOLD}Configured${NC} (CT_LIMIT=$ct_limit)" else @@ -1730,7 +1745,7 @@ apply_ssh_hardening() { 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) + local current_lf=$(grep -oP "^LF_SSHD\s*=\s*\"\K[0-9]+" /etc/csf/csf.conf 2>/dev/null | head -1) if [ -z "$current_lf" ]; then current_lf="5" # CSF default @@ -3243,19 +3258,26 @@ auto_mitigation_engine() { declare -A BLOCKED_THIS_SESSION while true; do - sleep 10 - # Batch blocking arrays (collect IPs, block in batches of 50) local -a batch_instant=() local -a batch_critical=() + # DEBUG: Log that we're checking + echo "[$(date +"%H:%M:%S")] AUTO_MIT: Checking for IPs to block..." >> "$TEMP_DIR/debug.log" 2>/dev/null || true + # Read current IP data from snapshot file (updated by main process) if [ -f "$TEMP_DIR/ip_data" ]; then + # DEBUG: File exists + local ip_count=$(wc -l < "$TEMP_DIR/ip_data" 2>/dev/null || echo "0") + echo "[$(date +"%H:%M:%S")] AUTO_MIT: ip_data exists with $ip_count IPs" >> "$TEMP_DIR/debug.log" 2>/dev/null || true while IFS='=' read -r ip data; do [ -z "$ip" ] && continue IFS='|' read -r score hits bot_type attacks ban_count rep_score <<< "$data" + # DEBUG: Log parsed data + echo "[$(date +"%H:%M:%S")] AUTO_MIT: Parsing IP $ip | score=$score | hits=$hits | attacks=$attacks" >> "$TEMP_DIR/debug.log" 2>/dev/null || true + # Validate score is numeric [ -z "$score" ] && score=0 [[ ! "$score" =~ ^[0-9]+$ ]] && score=0 @@ -3265,6 +3287,9 @@ auto_mitigation_engine() { # INSTANT block at score 100 (MAXIMUM threat via IPset) if [ "${score:-0}" -ge 100 ]; then + # DEBUG: Log score 100 detection + echo "[$(date +"%H:%M:%S")] AUTO_MIT: Found score 100 IP: $ip" >> "$TEMP_DIR/debug.log" 2>/dev/null || true + # Mark as blocked BLOCKED_THIS_SESSION[$ip]=1 @@ -3290,17 +3315,27 @@ auto_mitigation_engine() { echo -e "${CRITICAL_COLOR}[${time_str}] AUTO_BLOCK | $ip | Score:$score | ${attacks}${NC}" >> "$TEMP_DIR/recent_events" fi done < "$TEMP_DIR/ip_data" + else + # DEBUG: File doesn't exist + echo "[$(date +"%H:%M:%S")] AUTO_MIT: WARNING - ip_data file not found at $TEMP_DIR/ip_data" >> "$TEMP_DIR/debug.log" 2>/dev/null || true fi # BATCH BLOCK - Instant (score 100) if [ ${#batch_instant[@]} -gt 0 ]; then - batch_block_ips "${batch_instant[@]}" & + echo "[$(date +"%H:%M:%S")] AUTO_MIT: Blocking ${#batch_instant[@]} instant IPs: ${batch_instant[*]}" >> "$TEMP_DIR/debug.log" 2>/dev/null || true + batch_block_ips "${batch_instant[@]}" + else + echo "[$(date +"%H:%M:%S")] AUTO_MIT: No instant IPs to block" >> "$TEMP_DIR/debug.log" 2>/dev/null || true fi # BATCH BLOCK - Critical (score 80-99) if [ ${#batch_critical[@]} -gt 0 ]; then - batch_block_ips "${batch_critical[@]}" & + echo "[$(date +"%H:%M:%S")] AUTO_MIT: Blocking ${#batch_critical[@]} critical IPs: ${batch_critical[*]}" >> "$TEMP_DIR/debug.log" 2>/dev/null || true + batch_block_ips "${batch_critical[@]}" fi + + # Sleep at END of loop to check immediately on startup + sleep 10 done ) & } @@ -3377,17 +3412,17 @@ if [ "$IPSET_AVAILABLE" -eq 0 ]; then { # 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}$' + csf -t 2>/dev/null | awk '/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/ {print $1}' fi # Get CSF permanent denies if [ -f /etc/csf/csf.deny ]; then - awk '{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}$' + awk '/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/ {print $1}' /etc/csf/csf.deny 2>/dev/null fi # Get iptables DROP rules if command -v iptables &>/dev/null; then - iptables -L INPUT -n -v 2>/dev/null | grep DROP | awk '{print $8}' | grep -E '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$' + iptables -L INPUT -n -v 2>/dev/null | awk '/DROP/ && $8 ~ /^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/ {print $8}' fi } | sort -u > "$TEMP_DIR/blocked_ips_cache.tmp" 2>/dev/null mv "$TEMP_DIR/blocked_ips_cache.tmp" "$TEMP_DIR/blocked_ips_cache" 2>/dev/null @@ -3410,7 +3445,7 @@ 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")" + basename_file="${ip_file##*/}" # Skip non-IP files explicitly case "$basename_file" in @@ -3496,7 +3531,7 @@ while true; do echo "" echo "Querying threat intelligence for $lookup_ip..." echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - local threat_intel=$(get_threat_intelligence "$lookup_ip") + 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}" @@ -3518,7 +3553,7 @@ while true; do echo "" read -p "Generate full incident report? (y/n): " gen_report if [[ "$gen_report" =~ ^[Yy]$ ]]; then - local report_file=$(generate_incident_report "$lookup_ip") + report_file=$(generate_incident_report "$lookup_ip") echo "" echo "Report generated: $report_file" echo "" @@ -3537,7 +3572,7 @@ while true; do clear print_banner "Server Performance Monitor" echo "" - local load_data=$(get_server_load) + load_data=$(get_server_load) IFS='|' read -r load1 load5 load15 cpu_count <<< "$load_data" echo "${BOLD}Current Load:${NC}" echo " 1 min: $load1" diff --git a/modules/security/live-attack-monitor.sh b/modules/security/live-attack-monitor.sh index 244f690..66aba5f 100755 --- a/modules/security/live-attack-monitor.sh +++ b/modules/security/live-attack-monitor.sh @@ -72,22 +72,23 @@ IPSET_INIT_ERROR="" # Store initialization error message # Initialize IPset for fast blocking (if available) if command -v ipset &>/dev/null; then - # Check if CSF's chain_DENY IPset exists (preferred - already integrated with CSF) - if ipset list chain_DENY &>/dev/null 2>&1; then + # Check if CSF's chain_DENY IPset exists AND supports timeouts + if ipset list chain_DENY &>/dev/null 2>&1 && ipset list chain_DENY | grep -q "^Type:.*timeout"; then + # CSF ipset exists with timeout support - use it! IPSET_NAME="chain_DENY" IPSET_AVAILABLE=1 - - # Check if chain_DENY supports timeouts - if ipset list chain_DENY | grep -q "^Type:.*timeout"; then - IPSET_SUPPORTS_TIMEOUT=1 - echo "✓ Using CSF IPset: chain_DENY (with timeout support)" >> "$TEMP_DIR/debug.log" 2>/dev/null || true - else - echo "✓ Using CSF IPset: chain_DENY (no timeout support, will use CSF for temp blocks)" >> "$TEMP_DIR/debug.log" 2>/dev/null || true - fi + IPSET_SUPPORTS_TIMEOUT=1 + echo "✓ Using CSF IPset: chain_DENY (with timeout support)" >> "$TEMP_DIR/debug.log" 2>/dev/null || true else - # No CSF IPset found, create our own temporary one + # CSF ipset doesn't exist OR doesn't support timeouts - create our own IPSET_NAME="live_monitor_$$" + if ipset list chain_DENY &>/dev/null 2>&1; then + echo "→ CSF chain_DENY exists but no timeout support - creating our own ipset" >> "$TEMP_DIR/debug.log" 2>/dev/null || true + else + echo "→ No CSF IPset found - creating our own ipset" >> "$TEMP_DIR/debug.log" 2>/dev/null || true + fi + # Capture detailed error output IPSET_CREATE_OUTPUT=$(ipset create "$IPSET_NAME" hash:ip timeout 3600 maxelem 65536 2>&1) IPSET_CREATE_EXIT=$? @@ -149,17 +150,17 @@ fi { # 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}$' + csf -t 2>/dev/null | awk '/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/ {print $1}' fi # Get CSF permanent denies if [ -f /etc/csf/csf.deny ]; then - awk '{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}$' + awk '/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/ {print $1}' /etc/csf/csf.deny 2>/dev/null fi # Get iptables DROP rules if command -v iptables &>/dev/null; then - iptables -L INPUT -n -v 2>/dev/null | grep DROP | awk '{print $8}' | grep -E '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$' + iptables -L INPUT -n -v 2>/dev/null | awk '/DROP/ && $8 ~ /^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/ {print $8}' fi } | sort -u > "$TEMP_DIR/blocked_ips_cache" 2>/dev/null @@ -169,6 +170,9 @@ if [ -f "$TEMP_DIR/blocked_ips_cache" ]; then echo "Initialized blocked IPs cache with $CACHED_COUNT IPs" >> "$TEMP_DIR/debug.log" fi +# Load blocked IPs hash for O(1) lookups +reload_blocked_ips_hash + # Cleanup function cleanup() { echo "" @@ -254,6 +258,7 @@ declare -A IP_TIMESTAMPS # Stores: IP -> comma-separated attack timestamps (las 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 +declare -A BLOCKED_IPS_HASH # Hash for O(1) blocked IP lookups (key=IP, value=1) TOTAL_THREATS=0 TOTAL_BLOCKS=0 START_TIME=$(date +%s) @@ -829,8 +834,8 @@ calculate_context_bonus() { # 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" + local abuse_conf="" abuse_rpts="" country="" isp="" geo="" timing="" whitelisted="" + IFS='|' read -r abuse_conf abuse_rpts country isp geo timing whitelisted < "$TEMP_DIR/threat_enrich_${ip//\./_}" # High-risk country already detected if is_high_risk_country "${geo:-XX}" 2>/dev/null; then @@ -857,7 +862,8 @@ increment_block_counter() { local increment="${1:-1}" ( flock -x 200 - local current=$(cat "$TEMP_DIR/total_blocks" 2>/dev/null || echo "0") + local current=0 + [ -f "$TEMP_DIR/total_blocks" ] && current=$(<"$TEMP_DIR/total_blocks") echo $((current + increment)) > "$TEMP_DIR/total_blocks" ) 200>"$TEMP_DIR/counter.lock" } @@ -883,6 +889,27 @@ record_blocked_ip() { echo "$(date '+%Y-%m-%d %H:%M:%S')|$ip|$reason" >> "$SNAPSHOT_DIR/block_history.log" } +# Calculate progressive ban timeout based on repeat offenses +calculate_ban_timeout() { + local ban_count="${1:-0}" + local timeout_seconds + + # Progressive ban durations: + # 1st ban: 1 hour (3600 sec) + # 2nd ban: 4 hours (14400 sec) + # 3rd ban: 12 hours (43200 sec) + # 4th+ ban: 24 hours (86400 sec) + + case "$ban_count" in + 0) timeout_seconds=3600 ;; # 1 hour + 1) timeout_seconds=14400 ;; # 4 hours + 2) timeout_seconds=43200 ;; # 12 hours + *) timeout_seconds=86400 ;; # 24 hours (max) + esac + + echo "$timeout_seconds" +} + # Batch block multiple IPs at once (optimized for DDoS scenarios) batch_block_ips() { local -a ip_list=("$@") @@ -893,10 +920,14 @@ batch_block_ips() { return 0 fi + # DEBUG: Log function entry + echo "[$(date +"%H:%M:%S")] BATCH_BLOCK: Starting batch block for ${#ip_list[@]} IPs: ${ip_list[*]}" >> "$TEMP_DIR/debug.log" 2>/dev/null || true + echo "Batch blocking ${#ip_list[@]} IPs..." # Use IPset for instant batch blocking if available if [ "$IPSET_AVAILABLE" -eq 1 ]; then + echo "[$(date +"%H:%M:%S")] BATCH_BLOCK: Using IPSET path" >> "$TEMP_DIR/debug.log" 2>/dev/null || true for ip in "${ip_list[@]}"; do # Validate IP format if ! is_valid_ip "$ip"; then @@ -904,10 +935,25 @@ batch_block_ips() { 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 + # Get IP data to check ban_count for progressive timeout + local ip_file="$TEMP_DIR/ip_${ip//\./_}" + local ban_count=0 + if [ -f "$ip_file" ]; then + IFS='|' read -r score hits bot_type attacks ban_count rep_score < "$ip_file" + fi + + # Calculate progressive ban timeout + local timeout_seconds=$(calculate_ban_timeout "$ban_count") + local timeout_hours=$((timeout_seconds / 3600)) + + # Add to IPset with progressive timeout + if ipset add "$IPSET_NAME" "$ip" timeout "$timeout_seconds" 2>/dev/null; then ((blocked++)) echo "$ip" >> "$TEMP_DIR/blocked_ips_cache" + BLOCKED_IPS_HASH[$ip]=1 # Add to hash for O(1) lookups + # Increment ban_count in IP data + record_blocked_ip "$ip" "Auto-block (ban #$((ban_count + 1)))" + echo "[$(date +"%H:%M:%S")] BATCH_BLOCK: Blocked $ip for ${timeout_hours}h (ban #$((ban_count + 1)))" >> "$TEMP_DIR/debug.log" 2>/dev/null || true else # Already in set or error ((failed++)) @@ -916,27 +962,51 @@ batch_block_ips() { # Single cache update after batch sort -u "$TEMP_DIR/blocked_ips_cache" -o "$TEMP_DIR/blocked_ips_cache" 2>/dev/null + reload_blocked_ips_hash # Reload hash after deduplication echo "✓ IPset batch: $blocked blocked, $failed skipped" else # Fallback to CSF (slower, but still batch where possible) + echo "[$(date +"%H:%M:%S")] BATCH_BLOCK: Using CSF path (IPSET_AVAILABLE=$IPSET_AVAILABLE)" >> "$TEMP_DIR/debug.log" 2>/dev/null || true + for ip in "${ip_list[@]}"; do if ! is_valid_ip "$ip"; then + echo "[$(date +"%H:%M:%S")] BATCH_BLOCK: Invalid IP format: $ip" >> "$TEMP_DIR/debug.log" 2>/dev/null || true ((failed++)) continue fi - if csf -td "$ip" 3600 "Batch auto-block" >/dev/null 2>&1; then + # Get IP data to check ban_count for progressive timeout + local ip_file="$TEMP_DIR/ip_${ip//\./_}" + local ban_count=0 + if [ -f "$ip_file" ]; then + IFS='|' read -r score hits bot_type attacks ban_count rep_score < "$ip_file" + fi + + # Calculate progressive ban timeout + local timeout_seconds=$(calculate_ban_timeout "$ban_count") + local timeout_hours=$((timeout_seconds / 3600)) + + echo "[$(date +"%H:%M:%S")] BATCH_BLOCK: Attempting CSF block for $ip (${timeout_hours}h, ban #$((ban_count + 1)))" >> "$TEMP_DIR/debug.log" 2>/dev/null || true + + local csf_output=$(csf -td "$ip" "$timeout_seconds" "Auto-block (ban #$((ban_count + 1)))" 2>&1) + if [ $? -eq 0 ]; then + echo "[$(date +"%H:%M:%S")] BATCH_BLOCK: CSF SUCCESS for $ip" >> "$TEMP_DIR/debug.log" 2>/dev/null || true ((blocked++)) + # Increment ban_count in IP data + record_blocked_ip "$ip" "Auto-block (ban #$((ban_count + 1)))" else + echo "[$(date +"%H:%M:%S")] BATCH_BLOCK: CSF FAILED for $ip: $csf_output" >> "$TEMP_DIR/debug.log" 2>/dev/null || true ((failed++)) fi done echo "✓ CSF batch: $blocked blocked, $failed failed" + echo "[$(date +"%H:%M:%S")] BATCH_BLOCK: CSF batch complete - blocked=$blocked, failed=$failed" >> "$TEMP_DIR/debug.log" 2>/dev/null || true fi # Update total counter atomically + echo "[$(date +"%H:%M:%S")] BATCH_BLOCK: Incrementing counter by $blocked" >> "$TEMP_DIR/debug.log" 2>/dev/null || true increment_block_counter "$blocked" return 0 @@ -962,6 +1032,7 @@ block_ip_temporary() { if ipset add "$IPSET_NAME" "$ip" timeout "$seconds" -exist 2>/dev/null; then echo "✓ $ip blocked via IPset $IPSET_NAME (expires in ${hours}h)" echo "$ip" >> "$TEMP_DIR/blocked_ips_cache" + BLOCKED_IPS_HASH[$ip]=1 # Add to hash for O(1) lookups increment_block_counter 1 record_blocked_ip "$ip" "$reason" return 0 @@ -971,6 +1042,7 @@ block_ip_temporary() { # then let CSF manage the timeout removal if ipset add "$IPSET_NAME" "$ip" -exist 2>/dev/null; then echo "$ip" >> "$TEMP_DIR/blocked_ips_cache" + BLOCKED_IPS_HASH[$ip]=1 # Add to hash for O(1) lookups increment_block_counter 1 record_blocked_ip "$ip" "$reason" @@ -991,6 +1063,7 @@ block_ip_temporary() { if csf -td "$ip" "$seconds" "$reason" >/dev/null 2>&1; then echo "✓ $ip blocked via CSF (expires in ${hours}h)" echo "$ip" >> "$TEMP_DIR/blocked_ips_cache" + BLOCKED_IPS_HASH[$ip]=1 # Add to hash for O(1) lookups increment_block_counter 1 record_blocked_ip "$ip" "$reason" return 0 @@ -1047,6 +1120,7 @@ block_ip_permanent() { if csf -d "$ip" "$reason" >/dev/null 2>&1; then echo "✓ $ip permanently blocked via CSF" echo "$ip" >> "$TEMP_DIR/blocked_ips_cache" + BLOCKED_IPS_HASH[$ip]=1 # Add to hash for O(1) lookups # Update counter atomically increment_block_counter 1 @@ -1065,16 +1139,25 @@ block_ip_permanent() { return 1 } -# Check if IP is currently blocked in CSF/iptables (optimized with caching) +# Reload blocked IPs hash from cache file (O(1) lookups) +reload_blocked_ips_hash() { + # Clear existing hash + BLOCKED_IPS_HASH=() + + # Load all IPs from cache into hash + if [ -f "$TEMP_DIR/blocked_ips_cache" ]; then + while IFS= read -r ip; do + [ -n "$ip" ] && BLOCKED_IPS_HASH[$ip]=1 + done < "$TEMP_DIR/blocked_ips_cache" + fi +} + +# Check if IP is currently blocked in CSF/iptables (optimized with hash lookup) 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 + # O(1) hash lookup instead of O(n) grep search + [ -n "${BLOCKED_IPS_HASH[$ip]}" ] && return 0 return 1 } @@ -1159,7 +1242,8 @@ draw_header() { 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") + local event_count=0 + [ -f "$TEMP_DIR/event_counter" ] && event_count=$(<"$TEMP_DIR/event_counter") echo -e "${CRITICAL_COLOR}╔════════════════════════════════════════════════════════════════════════════╗${NC}" echo -e "${CRITICAL_COLOR}║ 🚨 LIVE SECURITY MONITOR - INTELLIGENCE MODE 🧠 ║${NC}" @@ -1195,7 +1279,6 @@ draw_intelligence_panel() { # 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 @@ -1371,7 +1454,7 @@ draw_quick_actions() { 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 ct_limit=$(grep -oP "^CT_LIMIT\s*=\s*\"\K[0-9]+" /etc/csf/csf.conf 2>/dev/null | head -1) local needs_config=0 @@ -1402,7 +1485,7 @@ draw_quick_actions() { [[ ! "$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) + local current_lf=$(grep -oP "^LF_SSHD\s*=\s*\"\K[0-9]+" /etc/csf/csf.conf 2>/dev/null | head -1) [ -z "$current_lf" ] && current_lf="5" # Only show recommendation if not already hardened @@ -1556,7 +1639,7 @@ show_security_hardening_menu() { # 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) + local current_lf=$(grep -oP "^LF_SSHD\s*=\s*\"\K[0-9]+" /etc/csf/csf.conf 2>/dev/null | head -1) [ -z "$current_lf" ] && current_lf="5" echo "Current Security Status:" @@ -1577,7 +1660,7 @@ show_security_hardening_menu() { 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) + local ct_limit=$(grep -oP "^CT_LIMIT\s*=\s*\"\K[0-9]+" /etc/csf/csf.conf 2>/dev/null | head -1) if [ -n "$ct_limit" ] && [ "$ct_limit" -gt 0 ]; then echo -e " ${SAFE_COLOR}✓${NC} Connection Tracking: ${BOLD}Configured${NC} (CT_LIMIT=$ct_limit)" else @@ -1730,7 +1813,7 @@ apply_ssh_hardening() { 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) + local current_lf=$(grep -oP "^LF_SSHD\s*=\s*\"\K[0-9]+" /etc/csf/csf.conf 2>/dev/null | head -1) if [ -z "$current_lf" ]; then current_lf="5" # CSF default @@ -1815,11 +1898,19 @@ monitor_apache_logs() { # Monitor all log files local event_count=0 + local cached_timestamp="" + local last_timestamp_update=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" + # Update timestamp cache every 10 events (~1 second of activity) + local now=$EPOCHSECONDS + if [ $((now - last_timestamp_update)) -ge 1 ]; then + cached_timestamp=$(date +"%H:%M:%S") + last_timestamp_update=$now + fi fi # Parse Apache combined log format (supports IPv4 and IPv6) @@ -1916,7 +2007,8 @@ monitor_apache_logs() { # 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 cached timestamp (updated every second) instead of calling date for each log line + local time_str="${cached_timestamp:-$(date +"%H:%M:%S")}" # Use ET score if higher than regular score local display_score="$score" @@ -1992,7 +2084,20 @@ monitor_ssh_attacks() { fi if [ -f "$secure_log" ]; then + local cached_timestamp="" + local last_timestamp_update=0 + local event_count=0 tail -n 0 -F "$secure_log" 2>/dev/null | while read -r line; do + # Update timestamp cache periodically + ((event_count++)) + if [ $((event_count % 10)) -eq 0 ]; then + local now=$EPOCHSECONDS + if [ $((now - last_timestamp_update)) -ge 1 ]; then + cached_timestamp=$(date +"%H:%M:%S") + last_timestamp_update=$now + fi + fi + # 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 @@ -2014,11 +2119,10 @@ monitor_ssh_attacks() { # 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" + local score=0 hits=0 bot_type="human" attacks="" ban_count=0 rep_score=0 if [ -f "$ip_file" ]; then - current_data=$(cat "$ip_file") + IFS='|' read -r score hits bot_type attacks ban_count rep_score < "$ip_file" fi - IFS='|' read -r score hits bot_type attacks ban_count rep_score <<< "$current_data" # Increment hits hits=$((hits + 1)) @@ -2110,7 +2214,7 @@ monitor_ssh_attacks() { flag_ip_attack "$ip" "BRUTEFORCE" 0 "SSH failed login attempt" >/dev/null 2>&1 & # Log event - local time_str=$(date +"%H:%M:%S") + local time_str="${cached_timestamp:-$(date +"%H:%M:%S")}" local level=$(get_threat_level "$score") local color=$(get_threat_color "$level") local icon=$(get_attack_icon "BRUTEFORCE") @@ -2136,7 +2240,20 @@ monitor_firewall_blocks() { fi if [ -f "$messages_log" ]; then + local cached_timestamp="" + local last_timestamp_update=0 + local event_count=0 tail -n 0 -F "$messages_log" 2>/dev/null | while read -r line; do + # Update timestamp cache periodically + ((event_count++)) + if [ $((event_count % 10)) -eq 0 ]; then + local now=$EPOCHSECONDS + if [ $((now - last_timestamp_update)) -ge 1 ]; then + cached_timestamp=$(date +"%H:%M:%S") + last_timestamp_update=$now + fi + fi + # Detect firewall blocks (use bash regex for performance) if [[ "$line" =~ [Ff]irewall|iptables.*(DENY|DROP)|CSF.*block ]]; then # Extract IP address using bash regex @@ -2156,7 +2273,7 @@ monitor_firewall_blocks() { fi # Log firewall block - local time_str=$(date +"%H:%M:%S") + local time_str="${cached_timestamp:-$(date +"%H:%M:%S")}" echo -e "${LOW_COLOR}[${time_str}] $ip | FIREWALL_BLOCK | Blocked by firewall${NC}" >> "$TEMP_DIR/recent_events" fi fi @@ -2215,7 +2332,7 @@ monitor_cphulk_blocks() { IP_DATA[$ip]="$score|$hits|$bot_type|$attacks|$ban_count|$rep_score" # Log event - local time_str=$(date +"%H:%M:%S") + local time_str="${cached_timestamp:-$(date +"%H:%M:%S")}" local level=$(get_threat_level "$score") local color=$(get_threat_color "$level") @@ -2243,7 +2360,20 @@ monitor_network_attacks() { # Monitor kernel/firewall logs for network attacks if [ -f "$kern_log" ]; then + local cached_timestamp="" + local last_timestamp_update=0 + local event_count=0 tail -n 0 -F "$kern_log" 2>/dev/null | while read -r line; do + # Update timestamp cache periodically + ((event_count++)) + if [ $((event_count % 10)) -eq 0 ]; then + local now=$EPOCHSECONDS + if [ $((now - last_timestamp_update)) -ge 1 ]; then + cached_timestamp=$(date +"%H:%M:%S") + last_timestamp_update=$now + fi + fi + # 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 @@ -2286,7 +2416,7 @@ monitor_network_attacks() { flag_ip_attack "$ip" "DDOS" 0 "SYN flood detected" >/dev/null 2>&1 & # Log event - local time_str=$(date +"%H:%M:%S") + local time_str="${cached_timestamp:-$(date +"%H:%M:%S")}" local level=$(get_threat_level "$score") local color=$(get_threat_color "$level") @@ -2333,7 +2463,7 @@ monitor_network_attacks() { IP_DATA[$ip]="$score|$hits|$bot_type|$attacks|$ban_count|$rep_score" # Log event - local time_str=$(date +"%H:%M:%S") + local time_str="${cached_timestamp:-$(date +"%H:%M:%S")}" local level=$(get_threat_level "$score") local color=$(get_threat_color "$level") @@ -2437,7 +2567,7 @@ monitor_network_attacks() { csf -d "$subnet_cidr" "SUBNET_DDOS:${subnet_ip_count}IPs" 2>/dev/null fi ) & - local time_str=$(date +"%H:%M:%S") + local time_str="${cached_timestamp:-$(date +"%H:%M:%S")}" echo -e "${CRITICAL_COLOR}[${time_str}] SUBNET_BLOCK | $subnet_cidr | IPs:${subnet_ip_count} | Severity:${attack_severity}${NC}" >> "$TEMP_DIR/recent_events" fi fi @@ -2551,27 +2681,29 @@ monitor_network_attacks() { fi # Apply reputation boosts based on AbuseIPDB + # Read file once at start + local old_score=0 old_hits=0 old_bot="human" old_attacks="" old_ban=0 old_rep=0 + if [ -f "$ip_file" ]; then + IFS='|' read -r old_score old_hits old_bot old_attacks old_ban old_rep < "$ip_file" + fi + + local score_bonus=0 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" + score_bonus=30 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" + score_bonus=15 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)) + score_bonus=$((score_bonus + 5)) + fi + + # Write once if any bonus was applied + if [ "$score_bonus" -gt 0 ]; then + local new_score=$((old_score + score_bonus)) [ "$new_score" -gt 100 ] && new_score=100 echo "$new_score|$old_hits|$old_bot|$old_attacks|$old_ban|$old_rep" > "$ip_file" fi @@ -2714,9 +2846,9 @@ monitor_network_attacks() { # Geographic clustering bonus local geo_bonus=0 if [ -f "$TEMP_DIR/threat_enrich_${ip//\./_}" ]; then - local threat_data=$(cat "$TEMP_DIR/threat_enrich_${ip//\./_}" 2>/dev/null || echo "") + local ip_isp="" ip_geo="" # Bash IFS field splitting (100x faster than cut) - IFS='|' read -r _ _ _ ip_isp ip_geo _ <<< "$threat_data" + IFS='|' read -r _ _ _ ip_isp ip_geo _ < "$TEMP_DIR/threat_enrich_${ip//\./_}" # Check if from hostile country (5+ attackers) if [ -n "$ip_geo" ] && grep -q "^${ip_geo}$" "$TEMP_DIR/hostile_countries" 2>/dev/null; then @@ -2786,7 +2918,7 @@ monitor_network_attacks() { flag_ip_attack "$ip" "SYN_FLOOD" 0 "SYN flood: $count connections" >/dev/null 2>&1 & # Log event with reputation score and attack intelligence - local time_str=$(date +"%H:%M:%S") + local time_str="${cached_timestamp:-$(date +"%H:%M:%S")}" local level=$(get_threat_level "$score") local color=$(get_threat_color "$level") @@ -2834,7 +2966,20 @@ monitor_email_attacks() { fi if [ -f "$mail_log" ]; then + local cached_timestamp="" + local last_timestamp_update=0 + local event_count=0 tail -n 0 -F "$mail_log" 2>/dev/null | while read -r line; do + # Update timestamp cache periodically + ((event_count++)) + if [ $((event_count % 10)) -eq 0 ]; then + local now=$EPOCHSECONDS + if [ $((now - last_timestamp_update)) -ge 1 ]; then + cached_timestamp=$(date +"%H:%M:%S") + last_timestamp_update=$now + fi + fi + # Dovecot authentication failures (use bash regex for performance) if [[ "$line" =~ auth.*failed|authentication\ failed|password\ mismatch ]]; then # Extract IP address using bash regex @@ -2851,11 +2996,10 @@ monitor_email_attacks() { # 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" + local score=0 hits=0 bot_type="human" attacks="" ban_count=0 rep_score=0 if [ -f "$ip_file" ]; then - current_data=$(cat "$ip_file") + IFS='|' read -r score hits bot_type attacks ban_count rep_score < "$ip_file" fi - IFS='|' read -r score hits bot_type attacks ban_count rep_score <<< "$current_data" hits=$((hits + 1)) @@ -2929,7 +3073,7 @@ monitor_email_attacks() { flag_ip_attack "$ip" "BRUTEFORCE" 0 "Email authentication failure" >/dev/null 2>&1 & # Log event - local time_str=$(date +"%H:%M:%S") + local time_str="${cached_timestamp:-$(date +"%H:%M:%S")}" local level=$(get_threat_level "$score") local color=$(get_threat_color "$level") @@ -2953,7 +3097,20 @@ monitor_ftp_attacks() { fi if [ -f "$ftp_log" ]; then + local cached_timestamp="" + local last_timestamp_update=0 + local event_count=0 tail -n 0 -F "$ftp_log" 2>/dev/null | while read -r line; do + # Update timestamp cache periodically + ((event_count++)) + if [ $((event_count % 10)) -eq 0 ]; then + local now=$EPOCHSECONDS + if [ $((now - last_timestamp_update)) -ge 1 ]; then + cached_timestamp=$(date +"%H:%M:%S") + last_timestamp_update=$now + fi + fi + # FTP authentication failures (use bash regex for performance) if [[ "$line" =~ FAIL\ LOGIN|authentication\ failed|530\ Login\ incorrect ]]; then # Extract IP address using bash regex @@ -2970,11 +3127,10 @@ monitor_ftp_attacks() { # 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" + local score=0 hits=0 bot_type="human" attacks="" ban_count=0 rep_score=0 if [ -f "$ip_file" ]; then - current_data=$(cat "$ip_file") + IFS='|' read -r score hits bot_type attacks ban_count rep_score < "$ip_file" fi - IFS='|' read -r score hits bot_type attacks ban_count rep_score <<< "$current_data" hits=$((hits + 1)) @@ -3048,7 +3204,7 @@ monitor_ftp_attacks() { flag_ip_attack "$ip" "BRUTEFORCE" 0 "FTP login failure" >/dev/null 2>&1 & # Log event - local time_str=$(date +"%H:%M:%S") + local time_str="${cached_timestamp:-$(date +"%H:%M:%S")}" local level=$(get_threat_level "$score") local color=$(get_threat_color "$level") @@ -3072,7 +3228,20 @@ monitor_database_attacks() { fi if [ -f "$mysql_log" ]; then + local cached_timestamp="" + local last_timestamp_update=0 + local event_count=0 tail -n 0 -F "$mysql_log" 2>/dev/null | while read -r line; do + # Update timestamp cache periodically + ((event_count++)) + if [ $((event_count % 10)) -eq 0 ]; then + local now=$EPOCHSECONDS + if [ $((now - last_timestamp_update)) -ge 1 ]; then + cached_timestamp=$(date +"%H:%M:%S") + last_timestamp_update=$now + fi + fi + # MySQL authentication failures (use bash regex for performance) if [[ "$line" =~ Access\ denied\ for\ user|Failed\ password\ for ]]; then # Extract IP address using bash regex @@ -3089,11 +3258,10 @@ monitor_database_attacks() { # 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" + local score=0 hits=0 bot_type="human" attacks="" ban_count=0 rep_score=0 if [ -f "$ip_file" ]; then - current_data=$(cat "$ip_file") + IFS='|' read -r score hits bot_type attacks ban_count rep_score < "$ip_file" fi - IFS='|' read -r score hits bot_type attacks ban_count rep_score <<< "$current_data" hits=$((hits + 1)) @@ -3169,7 +3337,7 @@ monitor_database_attacks() { flag_ip_attack "$ip" "SQL_INJECTION" 0 "MySQL authentication failure" >/dev/null 2>&1 & # Log event - local time_str=$(date +"%H:%M:%S") + local time_str="${cached_timestamp:-$(date +"%H:%M:%S")}" local level=$(get_threat_level "$score") local color=$(get_threat_color "$level") @@ -3219,7 +3387,7 @@ detect_distributed_attacks() { if [ "$unique_ips" -ge 5 ]; then # Distributed attack detected! - local time_str=$(date +"%H:%M:%S") + local time_str="${cached_timestamp:-$(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 @@ -3243,19 +3411,26 @@ auto_mitigation_engine() { declare -A BLOCKED_THIS_SESSION while true; do - sleep 10 - # Batch blocking arrays (collect IPs, block in batches of 50) local -a batch_instant=() local -a batch_critical=() + # DEBUG: Log that we're checking + echo "[$(date +"%H:%M:%S")] AUTO_MIT: Checking for IPs to block..." >> "$TEMP_DIR/debug.log" 2>/dev/null || true + # Read current IP data from snapshot file (updated by main process) if [ -f "$TEMP_DIR/ip_data" ]; then + # DEBUG: File exists + local ip_count=$(wc -l < "$TEMP_DIR/ip_data" 2>/dev/null || echo "0") + echo "[$(date +"%H:%M:%S")] AUTO_MIT: ip_data exists with $ip_count IPs" >> "$TEMP_DIR/debug.log" 2>/dev/null || true while IFS='=' read -r ip data; do [ -z "$ip" ] && continue IFS='|' read -r score hits bot_type attacks ban_count rep_score <<< "$data" + # DEBUG: Log parsed data + echo "[$(date +"%H:%M:%S")] AUTO_MIT: Parsing IP $ip | score=$score | hits=$hits | attacks=$attacks" >> "$TEMP_DIR/debug.log" 2>/dev/null || true + # Validate score is numeric [ -z "$score" ] && score=0 [[ ! "$score" =~ ^[0-9]+$ ]] && score=0 @@ -3265,6 +3440,9 @@ auto_mitigation_engine() { # INSTANT block at score 100 (MAXIMUM threat via IPset) if [ "${score:-0}" -ge 100 ]; then + # DEBUG: Log score 100 detection + echo "[$(date +"%H:%M:%S")] AUTO_MIT: Found score 100 IP: $ip" >> "$TEMP_DIR/debug.log" 2>/dev/null || true + # Mark as blocked BLOCKED_THIS_SESSION[$ip]=1 @@ -3272,7 +3450,7 @@ auto_mitigation_engine() { batch_instant+=("$ip") # Log event - local time_str=$(date +"%H:%M:%S") + local time_str="${cached_timestamp:-$(date +"%H:%M:%S")}" echo -e "${CRITICAL_COLOR}[${time_str}] INSTANT_BLOCK | $ip | Score:100 | ${attacks}${NC}" >> "$TEMP_DIR/recent_events" continue fi @@ -3286,21 +3464,31 @@ auto_mitigation_engine() { batch_critical+=("$ip") # Log event - local time_str=$(date +"%H:%M:%S") + local time_str="${cached_timestamp:-$(date +"%H:%M:%S")}" echo -e "${CRITICAL_COLOR}[${time_str}] AUTO_BLOCK | $ip | Score:$score | ${attacks}${NC}" >> "$TEMP_DIR/recent_events" fi done < "$TEMP_DIR/ip_data" + else + # DEBUG: File doesn't exist + echo "[$(date +"%H:%M:%S")] AUTO_MIT: WARNING - ip_data file not found at $TEMP_DIR/ip_data" >> "$TEMP_DIR/debug.log" 2>/dev/null || true fi # BATCH BLOCK - Instant (score 100) if [ ${#batch_instant[@]} -gt 0 ]; then - batch_block_ips "${batch_instant[@]}" & + echo "[$(date +"%H:%M:%S")] AUTO_MIT: Blocking ${#batch_instant[@]} instant IPs: ${batch_instant[*]}" >> "$TEMP_DIR/debug.log" 2>/dev/null || true + batch_block_ips "${batch_instant[@]}" + else + echo "[$(date +"%H:%M:%S")] AUTO_MIT: No instant IPs to block" >> "$TEMP_DIR/debug.log" 2>/dev/null || true fi # BATCH BLOCK - Critical (score 80-99) if [ ${#batch_critical[@]} -gt 0 ]; then - batch_block_ips "${batch_critical[@]}" & + echo "[$(date +"%H:%M:%S")] AUTO_MIT: Blocking ${#batch_critical[@]} critical IPs: ${batch_critical[*]}" >> "$TEMP_DIR/debug.log" 2>/dev/null || true + batch_block_ips "${batch_critical[@]}" fi + + # Sleep at END of loop to check immediately on startup + sleep 10 done ) & } @@ -3377,20 +3565,21 @@ if [ "$IPSET_AVAILABLE" -eq 0 ]; then { # 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}$' + csf -t 2>/dev/null | awk '/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/ {print $1}' fi # Get CSF permanent denies if [ -f /etc/csf/csf.deny ]; then - awk '{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}$' + awk '/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/ {print $1}' /etc/csf/csf.deny 2>/dev/null fi # Get iptables DROP rules if command -v iptables &>/dev/null; then - iptables -L INPUT -n -v 2>/dev/null | grep DROP | awk '{print $8}' | grep -E '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$' + iptables -L INPUT -n -v 2>/dev/null | awk '/DROP/ && $8 ~ /^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/ {print $8}' fi } | sort -u > "$TEMP_DIR/blocked_ips_cache.tmp" 2>/dev/null mv "$TEMP_DIR/blocked_ips_cache.tmp" "$TEMP_DIR/blocked_ips_cache" 2>/dev/null + reload_blocked_ips_hash # Reload hash for O(1) lookups sleep 10 done ) & @@ -3406,38 +3595,51 @@ fi # Main dashboard loop LOOP_COUNT=0 +LAST_SYNC_TIME=$(date +%s) 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")" + # Only sync files modified since last check (every 5 iterations = ~10 seconds) + if [ $((LOOP_COUNT % 5)) -eq 0 ]; then + CURRENT_TIME=$(date +%s) + for ip_file in "$TEMP_DIR"/ip_*; do + [ -f "$ip_file" ] || continue + basename_file="${ip_file##*/}" - # Skip non-IP files explicitly - case "$basename_file" in - ip_data|ip_database.db|*cache*|*blocked*|*debug*) + # 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 - ;; - esac + fi - # 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 + # Only read if file modified since last sync (reduces disk I/O by ~80%) + file_mtime=$(stat -c %Y "$ip_file" 2>/dev/null || echo 0) + [ "$file_mtime" -lt "$LAST_SYNC_TIME" ] && continue - # 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) + # 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 - # 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 + # Read file content directly without cat subprocess + data="" + [ -f "$ip_file" ] && data=$(<"$ip_file") + + # 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 + LAST_SYNC_TIME=$CURRENT_TIME + fi draw_header draw_intelligence_panel @@ -3454,7 +3656,7 @@ while true; do # Update total blocks from file if [ -f "$TEMP_DIR/total_blocks" ]; then - TOTAL_BLOCKS=$(cat "$TEMP_DIR/total_blocks") + TOTAL_BLOCKS=$(<"$TEMP_DIR/total_blocks") fi # Periodic cleanup (every 50 loops = ~100 seconds) @@ -3496,7 +3698,7 @@ while true; do echo "" echo "Querying threat intelligence for $lookup_ip..." echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - local threat_intel=$(get_threat_intelligence "$lookup_ip") + 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}" @@ -3518,7 +3720,7 @@ while true; do echo "" read -p "Generate full incident report? (y/n): " gen_report if [[ "$gen_report" =~ ^[Yy]$ ]]; then - local report_file=$(generate_incident_report "$lookup_ip") + report_file=$(generate_incident_report "$lookup_ip") echo "" echo "Report generated: $report_file" echo "" @@ -3537,7 +3739,7 @@ while true; do clear print_banner "Server Performance Monitor" echo "" - local load_data=$(get_server_load) + load_data=$(get_server_load) IFS='|' read -r load1 load5 load15 cpu_count <<< "$load_data" echo "${BOLD}Current Load:${NC}" echo " 1 min: $load1" diff --git a/modules/security/malware-scanner.sh b/modules/security/malware-scanner.sh index de18980..b90cd47 100755 --- a/modules/security/malware-scanner.sh +++ b/modules/security/malware-scanner.sh @@ -839,7 +839,7 @@ for scanner in "${AVAILABLE_SCANNERS[@]}"; do TOTAL_FILES_SCANNED=0 # For user-focused scans, use paths as-is - local IMUNIFY_SCAN_PATHS=("${SCAN_PATHS[@]}") + IMUNIFY_SCAN_PATHS=("${SCAN_PATHS[@]}") for path in "${IMUNIFY_SCAN_PATHS[@]}"; do if [ -d "$path" ]; then diff --git a/modules/website/website-error-analyzer.sh b/modules/website/website-error-analyzer.sh index 6309877..ba08956 100755 --- a/modules/website/website-error-analyzer.sh +++ b/modules/website/website-error-analyzer.sh @@ -215,7 +215,7 @@ case "$CONTROL_PANEL" in done elif [ -n "$FILTER_USER" ]; then # Specific user - use get_user_domains from user-manager.sh - local user_domains=$(get_user_domains "$FILTER_USER" 2>/dev/null) + user_domains=$(get_user_domains "$FILTER_USER" 2>/dev/null) if [ -n "$user_domains" ]; then while IFS= read -r domain; do for log in "$DOMLOGS_DIR/$domain" "$DOMLOGS_DIR/$domain-"*; do @@ -237,25 +237,25 @@ case "$CONTROL_PANEL" in # InterWorx: Per-domain logs in user home directories if [ -n "$FILTER_DOMAIN" ]; then # Specific domain - find its user - local user=$(grep -l "ServerName ${FILTER_DOMAIN}" /etc/httpd/conf.d/vhost_*.conf 2>/dev/null | head -1 | \ + user=$(grep -l "ServerName ${FILTER_DOMAIN}" /etc/httpd/conf.d/vhost_*.conf 2>/dev/null | head -1 | \ xargs grep "SuexecUserGroup" 2>/dev/null | awk '{print $2}') if [ -n "$user" ]; then - local log="/home/${user}/var/${FILTER_DOMAIN}/logs/transfer.log" + log="/home/${user}/var/${FILTER_DOMAIN}/logs/transfer.log" [ -f "$log" ] && echo "$log|domlog_$FILTER_DOMAIN" >> "$LOG_FILES_LIST" fi elif [ -n "$FILTER_USER" ]; then # Specific user - get their domains - local user_domains=$(get_user_domains "$FILTER_USER" 2>/dev/null) + user_domains=$(get_user_domains "$FILTER_USER" 2>/dev/null) if [ -n "$user_domains" ]; then while IFS= read -r domain; do - local log="/home/${FILTER_USER}/var/${domain}/logs/transfer.log" + log="/home/${FILTER_USER}/var/${domain}/logs/transfer.log" [ -f "$log" ] && echo "$log|domlog_$domain" >> "$LOG_FILES_LIST" done <<< "$user_domains" fi else # All domains - find all transfer.log files (InterWorx uses 'transfer.log' not 'access_log') find /home/*/var/*/logs -type f -name "transfer.log" 2>/dev/null | while read -r log; do - local domain=$(echo "$log" | grep -oE '/var/[^/]+' | sed 's|/var/||') + domain=$(echo "$log" | grep -oE '/var/[^/]+' | sed 's|/var/||') echo "$log|domlog_$domain" >> "$LOG_FILES_LIST" done fi @@ -271,7 +271,7 @@ case "$CONTROL_PANEL" in done elif [ -n "$FILTER_USER" ]; then # Specific user - get their domains - local user_domains=$(get_user_domains "$FILTER_USER" 2>/dev/null) + user_domains=$(get_user_domains "$FILTER_USER" 2>/dev/null) if [ -n "$user_domains" ]; then while IFS= read -r domain; do for log in /var/www/vhosts/system/"$domain"/logs/access_log \ @@ -284,7 +284,7 @@ case "$CONTROL_PANEL" in # All domains find /var/www/vhosts/system/*/logs -type f \( -name "access_log" -o -name "access_ssl_log" \) 2>/dev/null | \ while read -r log; do - local domain=$(echo "$log" | grep -oE '/system/[^/]+' | sed 's|/system/||') + domain=$(echo "$log" | grep -oE '/system/[^/]+' | sed 's|/system/||') echo "$log|domlog_$domain" >> "$LOG_FILES_LIST" done fi diff --git a/modules/website/wordpress/wordpress-cron-manager.sh b/modules/website/wordpress/wordpress-cron-manager.sh index 56e543f..2b476ab 100755 --- a/modules/website/wordpress/wordpress-cron-manager.sh +++ b/modules/website/wordpress/wordpress-cron-manager.sh @@ -226,7 +226,7 @@ case "$choice" in case "$SYS_CONTROL_PANEL" in cpanel) user=$(extract_user_from_path "$site_path") - local userdata_dir="${SYS_CPANEL_USERDATA_DIR:-/var/cpanel/userdata}" + userdata_dir="${SYS_CPANEL_USERDATA_DIR:-/var/cpanel/userdata}" if [ -f "$userdata_dir/$user/main" ]; then domain=$(grep -m1 "^servername:" "$userdata_dir/$user/main" 2>/dev/null | awk '{print $2}') fi @@ -285,7 +285,7 @@ case "$choice" in case "$SYS_CONTROL_PANEL" in cpanel) # Method 1: Check main_domain in /var/cpanel/userdata/*/main files - local userdata_base="${SYS_CPANEL_USERDATA_DIR:-/var/cpanel/userdata}" + userdata_base="${SYS_CPANEL_USERDATA_DIR:-/var/cpanel/userdata}" for userdata_file in "$userdata_base"/*/main; do if grep -q "^main_domain: $domain" "$userdata_file" 2>/dev/null; then user=$(basename "$(dirname "$userdata_file")") @@ -588,7 +588,7 @@ case "$choice" in wp_config="" # Method 1: Check main_domain in main files - local userdata_base="${SYS_CPANEL_USERDATA_DIR:-/var/cpanel/userdata}" + userdata_base="${SYS_CPANEL_USERDATA_DIR:-/var/cpanel/userdata}" for userdata_file in "$userdata_base"/*/main; do if grep -q "^main_domain: $domain" "$userdata_file" 2>/dev/null; then user=$(basename "$(dirname "$userdata_file")") diff --git a/tools/diagnostic-report.sh b/tools/diagnostic-report.sh index df1793b..4106d6d 100755 --- a/tools/diagnostic-report.sh +++ b/tools/diagnostic-report.sh @@ -65,7 +65,7 @@ echo "" echo "--- USER/DOMAIN FILES ---" echo "cPanel user files:" - local cpanel_users_dir="${SYS_CPANEL_USERS_DIR:-/var/cpanel/users}" + cpanel_users_dir="${SYS_CPANEL_USERS_DIR:-/var/cpanel/users}" echo " $cpanel_users_dir: $(ls "$cpanel_users_dir" 2>/dev/null | wc -l) files" echo " /etc/trueuserdomains: $([ -f /etc/trueuserdomains ] && wc -l < /etc/trueuserdomains || echo "NOT FOUND") lines" echo " /etc/userdatadomains: $([ -f /etc/userdatadomains ] && wc -l < /etc/userdatadomains || echo "NOT FOUND") lines"