#!/bin/bash # # Suspicious Login Monitor - Integrated Security Analysis & Compromise Detection # Detects suspicious login patterns, correlates with web attack activity, # and checks for actual system compromise indicators # Supports: cPanel, Plesk, InterWorx, Standalone # # Get script directory SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" TOOLKIT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" # Configuration SUSPICIOUS_LOGIN_AUTO_BLOCK="${SUSPICIOUS_LOGIN_AUTO_BLOCK:-yes}" SUSPICIOUS_LOGIN_AUTO_SCAN="${SUSPICIOUS_LOGIN_AUTO_SCAN:-yes}" SUSPICIOUS_LOGIN_EMAIL_ALERTS="${SUSPICIOUS_LOGIN_EMAIL_ALERTS:-no}" # Risk thresholds RISK_CRITICAL=85 RISK_HIGH=70 RISK_MEDIUM=50 # False positive reduction settings FP_CHECK_PACKAGE_LOGS="${FP_CHECK_PACKAGE_LOGS:-yes}" # Check if changes from package updates FP_REQUIRE_MULTIPLE_INDICATORS="${FP_REQUIRE_MULTIPLE_INDICATORS:-yes}" # Lower risk if only 1 indicator FP_IGNORE_BUSINESS_HOURS="${FP_IGNORE_BUSINESS_HOURS:-no}" # Lower risk during business hours (9am-5pm) FP_SSH_KEY_THRESHOLD="${FP_SSH_KEY_THRESHOLD:-10}" # Number of SSH keys before flagging (default: 10) FP_PASSWORD_CHANGE_THRESHOLD="${FP_PASSWORD_CHANGE_THRESHOLD:-5}" # Number of accounts before flagging mass change # Whitelist/Ignore settings (can be comma-separated lists) FP_WHITELIST_USERS="${FP_WHITELIST_USERS:-}" # Trusted users (e.g., "admin,bob,alice") FP_WHITELIST_IPS="${FP_WHITELIST_IPS:-}" # Trusted IPs (e.g., "192.168.1.100,10.0.0.50") FP_IGNORE_USERS="${FP_IGNORE_USERS:-}" # Users to ignore (e.g., "deploy,backup") FP_SAFE_TIME_WINDOWS="${FP_SAFE_TIME_WINDOWS:-}" # Safe maintenance windows (e.g., "Sun:02-04,*:03-03:30") FP_MIN_ACCOUNT_AGE_DAYS="${FP_MIN_ACCOUNT_AGE_DAYS:-30}" # Days before account considered "established" # Integration paths BOT_ANALYZER="$TOOLKIT_ROOT/modules/security/bot-analyzer.sh" MALWARE_SCANNER="$TOOLKIT_ROOT/modules/security/malware-scanner.sh" IP_REPUTATION_LIB="$TOOLKIT_ROOT/lib/ip-reputation.sh" THREAT_INTEL_LIB="$TOOLKIT_ROOT/lib/threat-intelligence.sh" # Temp files TMP_DIR="/tmp" REPORT_FILE="$TMP_DIR/suspicious_login_report_$(date +%Y%m%d_%H%M%S).txt" SSH_EVENTS="$TMP_DIR/ssh_events_$$.txt" PANEL_EVENTS="$TMP_DIR/panel_events_$$.txt" SUDO_EVENTS="$TMP_DIR/sudo_events_$$.txt" SUSPICIOUS_IPS="$TMP_DIR/suspicious_ips_$$.txt" # Analysis period (default: last 24 hours) HOURS="${1:-24}" # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' NC='\033[0m' # Cleanup on exit trap cleanup EXIT cleanup() { rm -f "$SSH_EVENTS" "$PANEL_EVENTS" "$SUDO_EVENTS" "$SUSPICIOUS_IPS" 2>/dev/null } # Detect control panel detect_panel() { if [ -d /usr/local/cpanel ]; then echo "cpanel" elif [ -d /usr/local/psa ]; then echo "plesk" elif [ -d /home/interworx ]; then echo "interworx" else echo "standalone" fi } # Get server IPs to exclude get_server_ips() { { echo "127.0.0.1" echo "::1" ip addr | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | grep -v "^127\." ip addr | grep -oP '(?<=inet6\s)[0-9a-f:]+' | grep -v "^::1" } | sort -u } # # SSH LOG PARSING # parse_ssh_logins() { local hours=$1 local secure_log="/var/log/secure" # Use auth.log on Debian-based systems if [ ! -f "$secure_log" ] && [ -f "/var/log/auth.log" ]; then secure_log="/var/log/auth.log" fi if [ ! -f "$secure_log" ]; then return 0 fi echo " Parsing SSH login attempts..." >&2 # Parse successful SSH logins awk -v hours="$hours" ' BEGIN { # Calculate time threshold cmd = "date -d \"" hours " hours ago\" +%s" cmd | getline threshold close(cmd) } /sshd.*Accepted/ { # Parse timestamp timestamp = $1 " " $2 " " $3 cmd = "date -d \"" timestamp "\" +%s 2>/dev/null" cmd | getline epoch close(cmd) if (epoch < threshold) next # Extract details user = "" ip = "" method = "" # Accepted password for user from ip if (match($0, /Accepted (password|publickey) for ([^ ]+) from ([^ ]+)/)) { method = (index($0, "publickey") > 0) ? "key" : "password" for (i=1; i<=NF; i++) { if ($i == "for") user = $(i+1) if ($i == "from") ip = $(i+1) } } if (user != "" && ip != "") { print timestamp "|" user "|" ip "|ssh|" method "|success" } } /sshd.*Failed/ { # Parse timestamp timestamp = $1 " " $2 " " $3 cmd = "date -d \"" timestamp "\" +%s 2>/dev/null" cmd | getline epoch close(cmd) if (epoch < threshold) next # Extract details user = "" ip = "" # Failed password for user from ip if (match($0, /Failed password for/)) { for (i=1; i<=NF; i++) { if ($i == "for") user = $(i+1) if ($i == "from") ip = $(i+1) } } if (user != "" && ip != "") { print timestamp "|" user "|" ip "|ssh|password|failed" } } ' "$secure_log" > "$SSH_EVENTS" } # # WTMP LOG PARSING (Universal - All Panels) # parse_wtmp_logins() { local hours=$1 if [ ! -f /var/log/wtmp ]; then return 0 fi echo " Parsing session history (wtmp)..." >&2 # Calculate cutoff date for last command local cutoff_date=$(date -d "$hours hours ago" "+%Y-%m-%d %H:%M:%S") # Use last command to parse binary wtmp log # Format: user tty ip datetime - datetime (duration) last -F 2>/dev/null | awk -v cutoff="$cutoff_date" ' BEGIN { # Convert cutoff to epoch cmd = "date -d \"" cutoff "\" +%s" cmd | getline cutoff_epoch close(cmd) } /^[a-zA-Z0-9]/ && !/^reboot/ && !/^wtmp/ { # Skip header lines if ($1 == "USER" || $1 == "wtmp") next user = $1 tty = $2 ip = $3 # Parse login time: "Mon Feb 2 18:59:03 2026" month = $4 day = $5 time = $6 year = $7 # Check if still logged in or logged out if ($9 == "still") { status = "active" } else { status = "success" } # Build timestamp for comparison timestamp = month " " day " " time " " year cmd = "date -d \"" timestamp "\" +%s 2>/dev/null" cmd | getline login_epoch close(cmd) # Only include logins within time window if (login_epoch >= cutoff_epoch) { # Normalize timestamp format simple_timestamp = month " " day " " time # Determine auth method (if from pts/pty, assume key; if no IP, skip) method = "key" if (ip == "" || ip == "-" || ip == ":0" || ip == "localhost") { next # Skip local logins } print simple_timestamp "|" user "|" ip "|ssh|" method "|" status } } ' >> "$SSH_EVENTS" } # # BTMP LOG PARSING (Universal - All Panels) # parse_btmp_logins() { local hours=$1 if [ ! -f /var/log/btmp ]; then return 0 fi echo " Parsing failed logins (btmp)..." >&2 # Calculate cutoff date local cutoff_date=$(date -d "$hours hours ago" "+%Y-%m-%d %H:%M:%S") # Use lastb command to parse binary btmp log lastb -F 2>/dev/null | awk -v cutoff="$cutoff_date" ' BEGIN { cmd = "date -d \"" cutoff "\" +%s" cmd | getline cutoff_epoch close(cmd) } /^[a-zA-Z0-9]/ && !/^btmp/ { # Skip header if ($1 == "USERNAME" || $1 == "btmp") next user = $1 tty = $2 ip = $3 # Parse timestamp: "Mon Feb 2 19:18:10 2026" month = $4 day = $5 time = $6 year = $7 timestamp = month " " day " " time " " year cmd = "date -d \"" timestamp "\" +%s 2>/dev/null" cmd | getline login_epoch close(cmd) # Only include failed attempts within time window if (login_epoch >= cutoff_epoch) { simple_timestamp = month " " day " " time # Skip if no IP if (ip == "" || ip == "-") next print simple_timestamp "|" user "|" ip "|ssh|password|failed" } } ' >> "$SSH_EVENTS" } # # SUDO/PRIVILEGE ESCALATION DETECTION (Universal - All Panels) # parse_sudo_escalation() { local hours=$1 local secure_log="/var/log/secure" # Use auth.log on Debian-based systems if [ ! -f "$secure_log" ] && [ -f "/var/log/auth.log" ]; then secure_log="/var/log/auth.log" fi if [ ! -f "$secure_log" ]; then return 0 fi echo " Detecting sudo/privilege escalation..." >&2 # Parse sudo commands from secure log awk -v hours="$hours" ' BEGIN { cmd = "date -d \"" hours " hours ago\" +%s" cmd | getline threshold close(cmd) } /sudo:/ { # Parse timestamp timestamp = $1 " " $2 " " $3 cmd = "date -d \"" timestamp "\" +%s 2>/dev/null" cmd | getline epoch close(cmd) if (epoch < threshold) next # Extract user and command # Format: "user : TTY=pts/0 ; PWD=/root ; USER=root ; COMMAND=/bin/bash" user = "" target_user = "" sudo_cmd = "" # Find the username before the colon for (i=1; i<=NF; i++) { if ($i == "sudo:") { user = $(i+1) break } } # Extract TARGET user (USER=root) if (match($0, /USER=([^ ;]+)/)) { target_user_match = substr($0, RSTART+5) split(target_user_match, arr, " ") target_user = arr[1] gsub(/;/, "", target_user) } # Extract COMMAND if (match($0, /COMMAND=(.+)$/)) { sudo_cmd = substr($0, RSTART+8) } # Determine if this is a privilege escalation to root if (target_user == "root" && user != "root") { # Non-root user executing sudo to root print timestamp "|" user "|localhost|sudo|escalation|" target_user "|" sudo_cmd } } ' "$secure_log" > "$TMP_DIR/sudo_events_$$.txt" } # # CPANEL LOG PARSING # parse_cpanel_logins() { local hours=$1 local whm_log="/usr/local/cpanel/logs/access_log" local cpanel_log="/usr/local/cpanel/logs/login_log" echo " Parsing cPanel/WHM logins..." >&2 # WHM access log if [ -f "$whm_log" ]; then awk -v hours="$hours" ' BEGIN { cmd = "date -d \"" hours " hours ago\" +%s" cmd | getline threshold close(cmd) } { # Parse timestamp [01/Feb/2026:19:30:00] if (match($4, /\[([0-9]{2})\/([A-Za-z]{3})\/([0-9]{4}):([0-9:]+)\]/)) { timestamp_str = substr($4, 2) cmd = "date -d \"" timestamp_str "\" +\"%b %d %H:%M:%S\" 2>/dev/null" cmd | getline timestamp close(cmd) cmd = "date -d \"" timestamp_str "\" +%s 2>/dev/null" cmd | getline epoch close(cmd) if (epoch < threshold) next # Extract IP (first field) ip = $1 # Check for WHM login if (match($0, /POST \/login\//) || match($0, /GET \/cpsess/)) { # Extract user from URL if possible user = "root" # WHM is root access status = "success" if ($9 ~ /^(40|50)/) status = "failed" print timestamp "|" user "|" ip "|whm|web|" status } } } ' "$whm_log" >> "$PANEL_EVENTS" fi # cPanel login log if [ -f "$cpanel_log" ]; then awk -v hours="$hours" ' BEGIN { cmd = "date -d \"" hours " hours ago\" +%s" cmd | getline threshold close(cmd) } { # Parse timestamp timestamp = $1 " " $2 " " $3 cmd = "date -d \"" timestamp "\" +%s 2>/dev/null" cmd | getline epoch close(cmd) if (epoch < threshold) next # Extract user and IP if (match($0, /user: ([^ ]+).*ip: ([^ ]+)/)) { for (i=1; i<=NF; i++) { if ($i == "user:") user = $(i+1) if ($i == "ip:") ip = $(i+1) } status = (match($0, /FAILED/) || match($0, /failed/)) ? "failed" : "success" if (user != "" && ip != "") { print timestamp "|" user "|" ip "|cpanel|web|" status } } } ' "$cpanel_log" >> "$PANEL_EVENTS" fi # cPanel session log (WHM Terminal access) local session_log="/usr/local/cpanel/logs/session_log" if [ -f "$session_log" ]; then awk -v hours="$hours" ' BEGIN { cmd = "date -d \"" hours " hours ago\" +%s" cmd | getline threshold close(cmd) } /NEW/ && /whostmgrd/ { # Parse timestamp: [2026-01-05 19:40:56 -0500] if (match($0, /\[([0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2})/)) { timestamp_str = substr($0, RSTART+1, RLENGTH-1) # Convert to epoch for comparison cmd = "date -d \"" timestamp_str "\" +%s 2>/dev/null" cmd | getline epoch close(cmd) if (epoch < threshold) next # Convert to simple format for output cmd = "date -d \"" timestamp_str "\" \"+%b %d %H:%M:%S\" 2>/dev/null" cmd | getline timestamp close(cmd) # Extract IP from "address=IP" ip = "" if (match($0, /address=([0-9.]+)/)) { ip_match = substr($0, RSTART+8) split(ip_match, arr, ",") ip = arr[1] } # Extract user (usually root for WHM terminal) user = "root" if (match($0, /creator=([^ ,]+)/)) { user_match = substr($0, RSTART+8) split(user_match, arr, ",") user = arr[1] } if (ip != "" && ip != "internal") { print timestamp "|" user "|" ip "|whm-terminal|web|success" } } } ' "$session_log" >> "$PANEL_EVENTS" fi } # # PLESK LOG PARSING # parse_plesk_logins() { local hours=$1 local plesk_log="/var/log/plesk/panel.log" # Try alternative location if [ ! -f "$plesk_log" ]; then plesk_log="/usr/local/psa/var/log/panel.log" fi if [ ! -f "$plesk_log" ]; then return 0 fi echo " Parsing Plesk panel logins..." >&2 awk -v hours="$hours" ' BEGIN { cmd = "date -d \"" hours " hours ago\" +%s" cmd | getline threshold close(cmd) } /login/ { # Parse timestamp (ISO format: 2026-02-02 19:30:00) timestamp = $1 " " $2 cmd = "date -d \"" timestamp "\" +%s 2>/dev/null" cmd | getline epoch close(cmd) if (epoch < threshold) next # Extract user and IP user = "" ip = "" status = "success" if (match($0, /user=([^ ]+)/)) { for (i=1; i<=NF; i++) { if (match($i, /user=/)) { gsub(/user=/, "", $i) user = $i } if (match($i, /ip=/)) { gsub(/ip=/, "", $i) ip = $i } } } if (match($0, /failed/)) status = "failed" if (user != "" && ip != "") { print timestamp "|" user "|" ip "|plesk|web|" status } } ' "$plesk_log" >> "$PANEL_EVENTS" } # # INTERWORX LOG PARSING # parse_interworx_logins() { local hours=$1 local iworx_log="/home/interworx/var/log/iworx.log" local siteworx_log="/home/interworx/var/log/siteworx.log" if [ ! -f "$iworx_log" ]; then return 0 fi echo " Parsing InterWorx logins..." >&2 # Parse NodeWorx (admin) logins awk -v hours="$hours" ' BEGIN { cmd = "date -d \"" hours " hours ago\" +%s" cmd | getline threshold close(cmd) } /login/ { timestamp = $1 " " $2 cmd = "date -d \"" timestamp "\" +%s 2>/dev/null" cmd | getline epoch close(cmd) if (epoch < threshold) next # Extract details user = "" ip = "" status = "success" for (i=1; i<=NF; i++) { if (match($i, /user=/)) { gsub(/user=/, "", $i) user = $i } if (match($i, /ip=/)) { gsub(/ip=/, "", $i) ip = $i } } if (match($0, /failed/)) status = "failed" if (user != "" && ip != "") { print timestamp "|" user "|" ip "|interworx|web|" status } } ' "$iworx_log" >> "$PANEL_EVENTS" # Parse SiteWorx (user/site owner) logins if [ -f "$siteworx_log" ]; then awk -v hours="$hours" ' BEGIN { cmd = "date -d \"" hours " hours ago\" +%s" cmd | getline threshold close(cmd) } /login/ { timestamp = $1 " " $2 cmd = "date -d \"" timestamp "\" +%s 2>/dev/null" cmd | getline epoch close(cmd) if (epoch < threshold) next # Extract details user = "" ip = "" status = "success" for (i=1; i<=NF; i++) { if (match($i, /user=/)) { gsub(/user=/, "", $i) user = $i } if (match($i, /ip=/)) { gsub(/ip=/, "", $i) ip = $i } } if (match($0, /failed/)) status = "failed" if (user != "" && ip != "") { print timestamp "|" user "|" ip "|siteworx|web|" status } } ' "$siteworx_log" >> "$PANEL_EVENTS" fi } # # ANOMALY DETECTION # detect_anomalies() { echo " Analyzing login patterns..." >&2 # Combine all events (including sudo) cat "$SSH_EVENTS" "$PANEL_EVENTS" "$SUDO_EVENTS" 2>/dev/null | \ awk -F'|' -v server_ips="$(get_server_ips | tr '\n' '|')" ' { timestamp = $1 user = $2 ip = $3 service = $4 method = $5 status = $6 # Skip server IPs (but not localhost for sudo events) if (service != "sudo" && index(server_ips, ip "|") > 0) next # Skip cPanel internal IPs if (match(ip, /^(208\.74\.123\.|184\.94\.197\.)/)) next # Track all events by IP (use "local" for sudo events) event_key = (ip == "localhost") ? "LOCAL_SUDO" : ip ip_events[event_key]++ ip_users[event_key] = ip_users[event_key] (ip_users[event_key] ? "," : "") user # Track sudo escalation events (method == "escalation") if (method == "escalation") { sudo_escalations[event_key]++ sudo_users[event_key] = sudo_users[event_key] (sudo_users[event_key] ? "," : "") user # status field contains target_user for sudo sudo_targets[event_key] = sudo_targets[event_key] (sudo_targets[event_key] ? "," : "") status } # Track failed attempts if (status == "failed") { failed[event_key]++ failed_users[event_key] = failed_users[event_key] (failed_users[event_key] ? "," : "") user } # Track successful logins if (status == "success" || status == "active") { successful[event_key]++ successful_users[event_key] = successful_users[event_key] (successful_users[event_key] ? "," : "") user successful_services[event_key] = successful_services[event_key] (successful_services[event_key] ? "," : "") service # Track root access if (user == "root") { root_logins[event_key]++ root_methods[event_key] = root_methods[event_key] (root_methods[event_key] ? "," : "") method } # Track password vs key auth if (method == "password") { password_auth[event_key]++ } else if (method == "key") { key_auth[event_key]++ } } # Store first and last seen if (first_seen[event_key] == "") { first_seen[event_key] = timestamp } last_seen[event_key] = timestamp } END { for (ip in ip_events) { risk = 0 reasons = "" # Root access risk if (root_logins[ip] > 0) { risk += 20 reasons = reasons "Root-Access " # Password auth for root (higher risk) if (index(root_methods[ip], "password") > 0) { risk += 10 reasons = reasons "Root-Password-Auth " } } # Failed login risk if (failed[ip] > 0) { fail_score = failed[ip] * 5 if (fail_score > 25) fail_score = 25 risk += fail_score reasons = reasons "Failed-Attempts(" failed[ip] ") " # Failed root attempts (very suspicious) if (index(failed_users[ip], "root") > 0) { risk += 15 reasons = reasons "Failed-Root " } } # Brute force detection if (failed[ip] >= 5) { risk += 20 reasons = reasons "Brute-Force " } # Multiple users from same IP (potential attack) split(ip_users[ip], user_arr, ",") unique_users = 0 for (u in user_arr) unique_users++ if (unique_users > 3) { risk += 15 reasons = reasons "Multiple-Users(" unique_users ") " } # Password auth risk (should use keys) if (password_auth[ip] > 0 && key_auth[ip] == 0) { risk += 5 reasons = reasons "Password-Only " } # Sudo escalation risk if (sudo_escalations[ip] > 0) { risk += 15 reasons = reasons "Sudo-Escalation(" sudo_escalations[ip] ") " } # Cap at 100 if (risk > 100) risk = 100 # Only report suspicious IPs (risk >= 30) if (risk >= 30) { print ip "|" risk "|" reasons "|" successful[ip] "|" failed[ip] "|" root_logins[ip] "|" ip_users[ip] "|" successful_services[ip] } } } ' | sort -t'|' -k2 -nr > "$SUSPICIOUS_IPS" } # # BOT ANALYZER INTEGRATION # correlate_with_access_logs() { local ip=$1 local risk_score=$2 # Find most recent bot analyzer report local latest_report=$(ls -t "$TOOLKIT_ROOT"/tmp/bot_analysis_report_*.txt /tmp/bot_analysis_report_*.txt 2>/dev/null | head -n1) if [ -z "$latest_report" ]; then echo "0|No bot analyzer data available" return 0 fi # Check if this IP appears in bot analyzer results local ip_data=$(grep -w "$ip" "$latest_report" 2>/dev/null || echo "") if [ -z "$ip_data" ]; then echo "0|No access log activity" return 0 fi # Extract attack vectors from bot analyzer report local attack_vectors="" local additional_risk=0 # Look for attack patterns in the report around this IP local context=$(grep -w -A 5 -B 5 "$ip" "$latest_report" 2>/dev/null) # Check for specific attack types if echo "$context" | grep -qi "RCE/Upload"; then attack_vectors="$attack_vectors RCE/Upload" additional_risk=$((additional_risk + 25)) fi if echo "$context" | grep -qi "SQLi\|SQL injection"; then attack_vectors="$attack_vectors SQLi" additional_risk=$((additional_risk + 20)) fi if echo "$context" | grep -qi "Admin.*prob\|wp-login\|admin login"; then attack_vectors="$attack_vectors Admin-Probe" additional_risk=$((additional_risk + 15)) fi if echo "$context" | grep -qi "Login.*Brute\|brute.*force"; then attack_vectors="$attack_vectors Login-Bruteforce" additional_risk=$((additional_risk + 15)) fi if echo "$context" | grep -qi "Info.*Disclosure\|info disclosure"; then attack_vectors="$attack_vectors Info-Disclosure" additional_risk=$((additional_risk + 10)) fi if echo "$context" | grep -qi "XSS"; then attack_vectors="$attack_vectors XSS" additional_risk=$((additional_risk + 10)) fi # Cap at 100 local new_risk=$((risk_score + additional_risk)) [ $new_risk -gt 100 ] && new_risk=100 echo "$additional_risk|$attack_vectors" } # # IP REPUTATION CHECK # check_ip_reputation() { local ip=$1 local risk_score=$2 # Source IP reputation library if available if [ ! -f "$IP_REPUTATION_LIB" ]; then echo "0|IP reputation library not available" return 0 fi source "$IP_REPUTATION_LIB" local additional_risk=0 local notes="" # Check whitelist if is_whitelisted "$ip" 2>/dev/null; then echo "-30|Whitelisted IP" return 0 fi # Check blacklist if is_blacklisted "$ip" 2>/dev/null; then additional_risk=$((additional_risk + 20)) notes="$notes Previously-Blacklisted" fi # Check reputation score local reputation=$(get_ip_reputation "$ip" 2>/dev/null) if [ -n "$reputation" ] && [ "$reputation" -lt 40 ]; then additional_risk=$((additional_risk + 15)) notes="$notes Poor-Reputation($reputation)" fi echo "$additional_risk|$notes" } # # THREAT INTELLIGENCE CORRELATION # correlate_with_threat_intel() { local ip=$1 # Source threat intelligence library if available if [ ! -f "$THREAT_INTEL_LIB" ]; then echo "0|Threat intelligence not available" return 0 fi source "$THREAT_INTEL_LIB" local additional_risk=0 local notes="" # Check known botnets if is_known_botnet "$ip" 2>/dev/null; then additional_risk=$((additional_risk + 30)) notes="$notes Known-Botnet" fi # GeoIP check (if available) if command -v geoiplookup &>/dev/null; then local country=$(geoiplookup "$ip" 2>/dev/null | awk -F': ' '{print $2}' | head -n1) if [ -n "$country" ]; then notes="$notes Country:$country" # High-risk countries case "$country" in *China*|*Russia*|*North\ Korea*) additional_risk=$((additional_risk + 10)) notes="$notes High-Risk-Geo" ;; esac fi fi echo "$additional_risk|$notes" } # # FALSE POSITIVE REDUCTION - Context checking functions # check_package_manager_activity() { local hours_ago=${1:-24} # Check YUM/DNF logs if [ -f /var/log/yum.log ]; then local yum_activity=$(find /var/log/yum.log -mmin -$((hours_ago * 60)) 2>/dev/null) if [ -n "$yum_activity" ]; then local recent_installs=$(grep -E "Installed|Updated" /var/log/yum.log 2>/dev/null | tail -5 | wc -l) if [ "$recent_installs" -gt 0 ]; then echo "yum_activity" return 0 fi fi fi # Check APT logs if [ -f /var/log/apt/history.log ]; then local apt_activity=$(find /var/log/apt/history.log -mmin -$((hours_ago * 60)) 2>/dev/null) if [ -n "$apt_activity" ]; then echo "apt_activity" return 0 fi fi # Check cPanel update logs if [ -d /var/cpanel/updatelogs ]; then local cpanel_update=$(find /var/cpanel/updatelogs/ -name "update.*.log" -mmin -$((hours_ago * 60)) 2>/dev/null | head -1) if [ -n "$cpanel_update" ]; then echo "cpanel_update" return 0 fi fi echo "none" return 1 } is_business_hours() { local hour=$(date +%H) local day=$(date +%u) # 1=Monday, 7=Sunday # Monday-Friday, 9am-5pm if [ "$day" -le 5 ] && [ "$hour" -ge 9 ] && [ "$hour" -lt 17 ]; then return 0 fi return 1 } check_cpanel_account_creation() { local hours_ago=${1:-24} # Check cPanel access log for account creation if [ -f /usr/local/cpanel/logs/access_log ]; then local account_creation=$(grep -E "createacct|\/json-api\/cpanel\?cpanel_jsonapi_module=Accounts" /usr/local/cpanel/logs/access_log 2>/dev/null | tail -1) if [ -n "$account_creation" ]; then echo "cpanel_account_creation" return 0 fi fi echo "none" return 1 } get_process_parent() { local pid=$1 ps -o ppid= -p "$pid" 2>/dev/null | tr -d ' ' } is_legitimate_parent() { local ppid=$1 local parent_name=$(ps -o comm= -p "$ppid" 2>/dev/null) # Legitimate parent processes case "$parent_name" in yum|dnf|apt|apt-get|dpkg|cpanelsync|upcp|ea-update|sshd|systemd) return 0 ;; esac return 1 } is_whitelisted_user() { local user=$1 if [ -z "$FP_WHITELIST_USERS" ]; then return 1 fi # Check if user is in whitelist echo "$FP_WHITELIST_USERS" | grep -q "\b$user\b" return $? } is_whitelisted_ip() { local ip=$1 if [ -z "$FP_WHITELIST_IPS" ]; then return 1 fi # Check if IP is in whitelist echo "$FP_WHITELIST_IPS" | grep -q "\b$ip\b" return $? } is_ignored_user() { local user=$1 if [ -z "$FP_IGNORE_USERS" ]; then return 1 fi # Check if user is in ignore list echo "$FP_IGNORE_USERS" | grep -q "\b$user\b" return $? } is_safe_time_window() { local current_day=$(date +%a) # Mon, Tue, Wed, etc. local current_hour=$(date +%H) local current_min=$(date +%M) local current_time="$current_hour:$current_min" if [ -z "$FP_SAFE_TIME_WINDOWS" ]; then return 1 fi # Parse safe time windows: "Sun:02-04,*:03-03:30" # Format: Day:StartHour-EndHour or *:StartTime-EndTime IFS=',' read -ra WINDOWS <<< "$FP_SAFE_TIME_WINDOWS" for window in "${WINDOWS[@]}"; do local day_part=$(echo "$window" | cut -d: -f1) local time_part=$(echo "$window" | cut -d: -f2) # Check day match (* = any day) if [ "$day_part" = "*" ] || [ "$day_part" = "$current_day" ]; then local start_time=$(echo "$time_part" | cut -d- -f1) local end_time=$(echo "$time_part" | cut -d- -f2) # Simple hour comparison (could be enhanced for minutes) local start_hour=$(echo "$start_time" | cut -d: -f1) local end_hour=$(echo "$end_time" | cut -d: -f1) if [ "$current_hour" -ge "$start_hour" ] && [ "$current_hour" -le "$end_hour" ]; then return 0 fi fi done return 1 } check_active_admin_session() { local hours_ago=${1:-1} # Check if any admin is currently logged in via SSH local active_sessions=$(w -h 2>/dev/null | awk '{print $1}' | sort -u) if [ -n "$active_sessions" ]; then # Check if any active session is a whitelisted user or root for session_user in $active_sessions; do if [ "$session_user" = "root" ] || is_whitelisted_user "$session_user"; then echo "active_admin_session:$session_user" return 0 fi done fi # Check recent SSH logins in /var/log/secure if [ -f /var/log/secure ]; then local recent_admin=$(awk -v hours="$hours_ago" ' BEGIN { cmd = "date -d \"" hours " hours ago\" +%s" cmd | getline threshold close(cmd) } /sshd.*Accepted/ && /root/ { timestamp = $1 " " $2 " " $3 cmd = "date -d \"" timestamp "\" +%s 2>/dev/null" cmd | getline epoch close(cmd) if (epoch >= threshold) { print "recent_admin_login:root" exit } } ' /var/log/secure 2>/dev/null) if [ -n "$recent_admin" ]; then echo "$recent_admin" return 0 fi fi echo "none" return 1 } get_account_age_days() { local user=$1 # Check home directory creation date as proxy for account age if [ -d "/home/$user" ]; then local home_created=$(stat -c %Y "/home/$user" 2>/dev/null) if [ -n "$home_created" ]; then local age_seconds=$(( $(date +%s) - home_created )) local age_days=$(( age_seconds / 86400 )) echo "$age_days" return 0 fi fi # Fallback: Check /etc/passwd modification (less accurate) local passwd_age=$(( $(date +%s) - $(stat -c %Y /etc/passwd 2>/dev/null) )) local passwd_days=$(( passwd_age / 86400 )) echo "$passwd_days" return 0 } check_who_made_change() { local file=$1 local hours_ago=${2:-24} # Check audit logs for who modified the file if [ -f /var/log/audit/audit.log ]; then local audit_user=$(ausearch -f "$file" -ts recent 2>/dev/null | grep -oP 'acct="\K[^"]+' | head -1) if [ -n "$audit_user" ]; then echo "$audit_user" return 0 fi fi # Fallback: Check /var/log/secure for context local secure_context=$(tail -1000 /var/log/secure 2>/dev/null | grep -E "useradd|usermod|passwd|chage" | tail -1 | awk '{print $5}' | cut -d'[' -f1) if [ -n "$secure_context" ]; then echo "$secure_context" return 0 fi echo "unknown" return 1 } # # COMPROMISE DETECTION - Check for actual root compromise indicators # check_recent_password_changes() { echo " Checking for recent password changes..." >&2 local findings="" local risk=0 local details="" # Check for password changes in last 7 days (using chage) # Password change date is stored in /etc/shadow field 3 (days since epoch) local recent_pw_changes=$(awk -F: -v cutoff=$(( $(date +%s) / 86400 - 7 )) ' $3 != "" && $3 !~ /^!/ && $3 > cutoff { # Field 3 = last password change (days since 1970-01-01) print $1 ":" $3 } ' /etc/shadow 2>/dev/null) if [ -n "$recent_pw_changes" ]; then local pw_count=$(echo "$recent_pw_changes" | wc -l) local pw_users=$(echo "$recent_pw_changes" | cut -d: -f1 | tr '\n' ',' | sed 's/,$//') # FALSE POSITIVE REDUCTION: Filter out whitelisted users local filtered_users="" local filtered_count=0 for user in $(echo "$pw_users" | tr ',' ' '); do if ! is_whitelisted_user "$user" && ! is_ignored_user "$user"; then filtered_users="${filtered_users}${user}," filtered_count=$((filtered_count + 1)) fi done filtered_users=${filtered_users%,} # Remove trailing comma # If all users are whitelisted, skip if [ "$filtered_count" -eq 0 ]; then details="${details}(all-whitelisted) " echo "$risk|$findings$details" return 0 fi # Check if admin is actively logged in (reduce risk) local admin_session=$(check_active_admin_session 1) local admin_active=0 if [ "$admin_session" != "none" ]; then admin_active=1 details="${details}[$admin_session] " fi # Check if in safe time window (reduce risk) local safe_window=0 if is_safe_time_window; then safe_window=1 details="${details}[safe-window] " fi # FALSE POSITIVE REDUCTION: Check if this is mass change (more suspicious) if [ "$filtered_count" -lt "$FP_PASSWORD_CHANGE_THRESHOLD" ]; then # Small number of password changes - likely legitimate # Only flag if root is included OR if outside business hours if echo "$filtered_users" | grep -q "root"; then findings="${findings}Root-Password-Changed " details="${details}(root) " # Calculate risk with context local base_risk=35 # Reduce if admin active [ "$admin_active" -eq 1 ] && base_risk=$((base_risk - 15)) # Reduce if business hours [ "$FP_IGNORE_BUSINESS_HOURS" = "yes" ] && is_business_hours && base_risk=$((base_risk - 10)) # Reduce if safe window [ "$safe_window" -eq 1 ] && base_risk=$((base_risk - 10)) risk=$((risk + base_risk)) elif [ "$filtered_count" -eq 1 ]; then # Single non-root password change local base_risk=5 [ "$admin_active" -eq 1 ] && base_risk=2 [ "$safe_window" -eq 1 ] && base_risk=2 risk=$((risk + base_risk)) details="${details}Single-user:$filtered_users " else # 2-4 password changes, no root local base_risk=10 [ "$admin_active" -eq 1 ] && base_risk=5 [ "$safe_window" -eq 1 ] && base_risk=5 risk=$((risk + base_risk)) details="${details}$filtered_count-users:$filtered_users " fi else # Mass password change (5+ accounts) - VERY suspicious findings="${findings}Mass-Password-Changes:$filtered_count-accounts " details="${details}Users:$filtered_users " # Even with admin active, mass changes are suspicious local base_risk=45 [ "$admin_active" -eq 1 ] && base_risk=$((base_risk - 10)) risk=$((risk + base_risk)) fi fi # Check for locked accounts that were recently unlocked local recently_unlocked=$(awk -F: -v cutoff=$(( $(date +%s) / 86400 - 7 )) ' # Field 2 starts with ! or !! = locked # If field 3 (last change) is recent and field 2 does NOT start with !, might have been unlocked $3 > cutoff && $2 !~ /^!/ && $2 != "" && $2 != "*" && $3 != "" { print $1 } ' /etc/shadow 2>/dev/null | while read user; do # Check if account was previously locked (this is imperfect without history) if grep "^$user:" /etc/passwd | grep -q "/sbin/nologin\|/bin/false"; then echo "$user" fi done) if [ -n "$recently_unlocked" ]; then findings="${findings}Recently-Unlocked-Accounts:$(echo $recently_unlocked | tr '\n' ',') " risk=$((risk + 30)) fi echo "$risk|$findings$details" } check_recent_user_changes() { echo " Checking for recent user/group changes..." >&2 local findings="" local risk=0 # Check for users created in last 7 days (based on UID sequence) # In most systems, UIDs are assigned sequentially local max_uid=$(awk -F: '$3 >= 1000 && $3 < 60000 {print $3}' /etc/passwd | sort -n | tail -1) local recent_users=$(awk -F: -v max_uid="$max_uid" ' $3 >= 1000 && $3 < 60000 && $3 >= (max_uid - 10) { print $1 } ' /etc/passwd) if [ -n "$recent_users" ]; then # Check if these accounts are actually new (home dir creation date) local new_users="" local new_count=0 for user in $recent_users; do if [ -d "/home/$user" ]; then local home_age=$(($(date +%s) - $(stat -c %Y /home/$user 2>/dev/null))) if [ "$home_age" -lt 604800 ]; then # 7 days local days=$((home_age / 86400)) new_users="${new_users}${user}(${days}d) " new_count=$((new_count + 1)) fi fi done if [ -n "$new_users" ]; then # FALSE POSITIVE REDUCTION: Filter out whitelisted/ignored users local filtered_new_users="" local filtered_new_count=0 for user_entry in $new_users; do local username=$(echo "$user_entry" | cut -d'(' -f1) if ! is_whitelisted_user "$username" && ! is_ignored_user "$username"; then filtered_new_users="${filtered_new_users}${user_entry} " filtered_new_count=$((filtered_new_count + 1)) fi done # If all users are whitelisted, skip if [ "$filtered_new_count" -eq 0 ]; then return 0 fi # FALSE POSITIVE REDUCTION: Check if cPanel account creation local cpanel_activity=$(check_cpanel_account_creation 168) # 7 days # Check if admin is actively logged in local admin_session=$(check_active_admin_session 24) local admin_active=0 if [ "$admin_session" != "none" ]; then admin_active=1 fi if [ "$cpanel_activity" = "cpanel_account_creation" ] && [ "$filtered_new_count" -le 3 ]; then # Likely legitimate cPanel hosting account findings="${findings}New-Users:$filtered_new_users[cpanel] " risk=$((risk + 5)) # Very low risk else # Not from cPanel or too many accounts findings="${findings}Recently-Created-Users:$filtered_new_users " if [ "$filtered_new_count" -eq 1 ]; then # Single user creation local base_risk=15 [ "$admin_active" -eq 1 ] && base_risk=8 is_business_hours && base_risk=$((base_risk - 3)) risk=$((risk + base_risk)) else # Multiple users local base_risk=25 [ "$admin_active" -eq 1 ] && base_risk=15 risk=$((risk + base_risk)) fi fi fi fi # Check for users added to sudo/wheel group recently local sudo_group=$(getent group sudo wheel 2>/dev/null | head -1) if [ -n "$sudo_group" ]; then local sudo_members=$(echo "$sudo_group" | cut -d: -f4) if [ -n "$sudo_members" ]; then # Check if group file was modified recently local group_age=$(($(date +%s) - $(stat -c %Y /etc/group 2>/dev/null))) if [ "$group_age" -lt 86400 ]; then findings="${findings}Sudo-Group-Modified-Recently:members=$sudo_members " risk=$((risk + 30)) fi fi fi echo "$risk|$findings" } check_backdoor_accounts() { echo " Checking for backdoor user accounts..." >&2 local findings="" local risk=0 # Check for multiple UID 0 accounts (besides root) local uid0_accounts=$(awk -F: '$3 == 0 && $1 != "root" {print $1}' /etc/passwd) if [ -n "$uid0_accounts" ]; then findings="${findings}Unauthorized-UID-0-Accounts:$(echo $uid0_accounts | tr '\n' ',') " risk=$((risk + 50)) fi # Check for accounts with no password (empty password field, not locked) local no_pass=$(awk -F: '$2 == "" || $2 == " " {print $1}' /etc/shadow 2>/dev/null | head -10) if [ -n "$no_pass" ]; then findings="${findings}No-Password-Accounts:$(echo $no_pass | tr '\n' ',' | head -c 100) " risk=$((risk + 30)) fi # Check for suspicious usernames (common backdoor names) local suspicious_users=$(awk -F: ' $1 ~ /^(test|temp|backup|hacker|admin|ftpuser|nobody2|bin2|daemon2)$/ && $3 >= 1000 { print $1 } ' /etc/passwd) if [ -n "$suspicious_users" ]; then # Filter out whitelisted users local filtered_suspicious="" for user in $suspicious_users; do if ! is_whitelisted_user "$user" && ! is_ignored_user "$user"; then # Check account age - old accounts less suspicious local account_age=$(get_account_age_days "$user") if [ "$account_age" -lt "$FP_MIN_ACCOUNT_AGE_DAYS" ]; then # New account with suspicious name - HIGH risk filtered_suspicious="${filtered_suspicious}${user}(${account_age}d)," elif [ "$account_age" -lt 365 ]; then # Moderately old account - MEDIUM risk filtered_suspicious="${filtered_suspicious}${user}(${account_age}d)," # Lower risk for older accounts fi # Very old accounts (1+ year) with suspicious names ignored - likely legitimate fi done if [ -n "$filtered_suspicious" ]; then findings="${findings}Suspicious-Usernames:${filtered_suspicious%,} " # Risk based on newest account local min_age=999 for user in $suspicious_users; do local age=$(get_account_age_days "$user") [ "$age" -lt "$min_age" ] && min_age=$age done if [ "$min_age" -lt 7 ]; then risk=$((risk + 30)) # Very new suspicious account elif [ "$min_age" -lt 30 ]; then risk=$((risk + 20)) # Recent suspicious account else risk=$((risk + 10)) # Older but still flagged fi fi fi echo "$risk|$findings" } check_unauthorized_ssh_keys() { echo " Checking for unauthorized SSH keys..." >&2 local findings="" local risk=0 # Check root's authorized_keys if [ -f /root/.ssh/authorized_keys ]; then # FALSE POSITIVE REDUCTION: Only count active keys (not commented/disabled) local key_count=$(grep -v "^#" /root/.ssh/authorized_keys 2>/dev/null | grep -v "^$" | grep -c "ssh-") # Use configurable threshold if [ "$key_count" -gt "$FP_SSH_KEY_THRESHOLD" ]; then findings="${findings}Excessive-Root-SSH-Keys:$key_count " risk=$((risk + 20)) fi # Check for keys with suspicious comments local suspicious_keys=$(grep -v "^#" /root/.ssh/authorized_keys 2>/dev/null | grep -i "hacker\|backdoor\|pwned\|rooted") if [ -n "$suspicious_keys" ]; then findings="${findings}Suspicious-SSH-Key-Comments " risk=$((risk + 40)) fi # Check file permissions (should be 600) local perms=$(stat -c %a /root/.ssh/authorized_keys 2>/dev/null) if [ "$perms" != "600" ] && [ "$perms" != "400" ]; then findings="${findings}Incorrect-SSH-Key-Permissions:$perms " risk=$((risk + 15)) fi fi # Check for authorized_keys in unusual locations local unusual_keys=$(find /tmp /var/tmp /dev/shm -name "authorized_keys" 2>/dev/null) if [ -n "$unusual_keys" ]; then findings="${findings}SSH-Keys-In-Unusual-Locations " risk=$((risk + 35)) fi echo "$risk|$findings" } check_system_file_tampering() { echo " Checking for system file tampering..." >&2 local findings="" local risk=0 # FALSE POSITIVE REDUCTION: Check if package manager was active local pkg_activity="" if [ "$FP_CHECK_PACKAGE_LOGS" = "yes" ]; then pkg_activity=$(check_package_manager_activity 24) fi # Check if admin was active (reduce suspicion) local admin_session=$(check_active_admin_session 24) local admin_active=0 if [ "$admin_session" != "none" ]; then admin_active=1 fi # Check if in safe time window local safe_window=0 if is_safe_time_window; then safe_window=1 fi # Check /etc/passwd modification time (recent changes suspicious) local passwd_age=$(($(date +%s) - $(stat -c %Y /etc/passwd 2>/dev/null))) if [ "$passwd_age" -lt 86400 ]; then # Modified in last 24 hours local passwd_hours=$((passwd_age / 3600)) # Build context string local context="" [ "$pkg_activity" != "none" ] && context="${context}$pkg_activity," [ "$admin_active" -eq 1 ] && context="${context}admin-active," [ "$safe_window" -eq 1 ] && context="${context}safe-window," context=${context%,} # Remove trailing comma # Calculate risk local base_risk=25 if [ "$pkg_activity" != "none" ]; then base_risk=5 # Package manager activity elif [ "$admin_active" -eq 1 ]; then base_risk=12 # Admin was logged in fi [ "$safe_window" -eq 1 ] && base_risk=$((base_risk / 2)) if [ -n "$context" ]; then findings="${findings}/etc/passwd-Modified-${passwd_hours}h-ago[$context] " else findings="${findings}/etc/passwd-Modified-${passwd_hours}h-ago " fi risk=$((risk + base_risk)) fi # Check /etc/shadow modification time local shadow_age=$(($(date +%s) - $(stat -c %Y /etc/shadow 2>/dev/null))) if [ "$shadow_age" -lt 86400 ]; then local shadow_hours=$((shadow_age / 3600)) local base_risk=25 if [ "$pkg_activity" != "none" ]; then base_risk=5 elif [ "$admin_active" -eq 1 ]; then base_risk=12 fi [ "$safe_window" -eq 1 ] && base_risk=$((base_risk / 2)) if [ -n "$context" ]; then findings="${findings}/etc/shadow-Modified-${shadow_hours}h-ago[$context] " else findings="${findings}/etc/shadow-Modified-${shadow_hours}h-ago " fi risk=$((risk + base_risk)) fi # Check /etc/group modification time local group_age=$(($(date +%s) - $(stat -c %Y /etc/group 2>/dev/null))) if [ "$group_age" -lt 86400 ]; then local group_hours=$((group_age / 3600)) local base_risk=20 if [ "$pkg_activity" != "none" ]; then base_risk=3 elif [ "$admin_active" -eq 1 ]; then base_risk=10 fi [ "$safe_window" -eq 1 ] && base_risk=$((base_risk / 2)) if [ -n "$context" ]; then findings="${findings}/etc/group-Modified-${group_hours}h-ago[$context] " else findings="${findings}/etc/group-Modified-${group_hours}h-ago " fi risk=$((risk + base_risk)) fi # Check /etc/gshadow modification time if [ -f /etc/gshadow ]; then local gshadow_age=$(($(date +%s) - $(stat -c %Y /etc/gshadow 2>/dev/null))) if [ "$gshadow_age" -lt 86400 ]; then local gshadow_hours=$((gshadow_age / 3600)) local base_risk=20 if [ "$pkg_activity" != "none" ]; then base_risk=3 elif [ "$admin_active" -eq 1 ]; then base_risk=10 fi [ "$safe_window" -eq 1 ] && base_risk=$((base_risk / 2)) if [ -n "$context" ]; then findings="${findings}/etc/gshadow-Modified-${gshadow_hours}h-ago[$context] " else findings="${findings}/etc/gshadow-Modified-${gshadow_hours}h-ago " fi risk=$((risk + base_risk)) fi fi # Check for suspicious entries in /etc/passwd (exclude system accounts) # Look for non-standard shells on user accounts (UID >= 1000) local backdoor_shells=$(awk -F: '$3 >= 1000 && $7 != "" { shell = $7 # Standard shells if (shell ~ /\/bash$/ || shell ~ /\/sh$/ || shell ~ /nologin$/ || shell ~ /false$/ || shell ~ /true$/) next # System accounts if ($1 == "sync" || $1 == "shutdown" || $1 == "halt" || $1 == "operator") next # cPanel shells if (shell ~ /\/noshell$/) next # If we get here, shell is suspicious print $1":"shell }' /etc/passwd 2>/dev/null) if [ -n "$backdoor_shells" ]; then findings="${findings}Suspicious-Login-Shells:$backdoor_shells " risk=$((risk + 30)) fi # Check sudoers for unauthorized entries if [ -f /etc/sudoers ]; then local suspicious_sudo=$(grep -v "^#" /etc/sudoers 2>/dev/null | grep "NOPASSWD" | grep -v "root") if [ -n "$suspicious_sudo" ]; then findings="${findings}Suspicious-Sudoers-Entries " risk=$((risk + 35)) fi fi echo "$risk|$findings" } check_suspicious_processes() { echo " Checking for suspicious processes..." >&2 local findings="" local risk=0 # Check for processes with suspicious names local suspicious_procs=$(ps aux | grep -E "nc -l|ncat -l|/dev/tcp|bash -i|perl.*socket|python.*socket" | grep -v grep) if [ -n "$suspicious_procs" ]; then findings="${findings}Reverse-Shell-Processes " risk=$((risk + 50)) fi # Check for hidden processes (spaces in name) local hidden_procs=$(ps aux | awk '$11 ~ /^[ ]+$/ {print $2}') if [ -n "$hidden_procs" ]; then findings="${findings}Hidden-Processes " risk=$((risk + 40)) fi # Check for processes running from /tmp or /dev/shm local suspicious_tmp_procs=0 local tmp_proc_pids=$(ps aux | awk '$11 ~ /\/(tmp|dev\/shm)/ {print $2}' 2>/dev/null) # FALSE POSITIVE REDUCTION: Check parent process for pid in $tmp_proc_pids; do local ppid=$(get_process_parent "$pid") if [ -n "$ppid" ] && ! is_legitimate_parent "$ppid"; then suspicious_tmp_procs=$((suspicious_tmp_procs + 1)) fi done if [ "$suspicious_tmp_procs" -gt 0 ]; then findings="${findings}Suspicious-Processes-From-Tmp:$suspicious_tmp_procs " risk=$((risk + 30)) fi # Check for unusual network connections local suspicious_conns=$(netstat -antp 2>/dev/null | grep ESTABLISHED | awk '{print $5}' | cut -d: -f1 | sort -u | wc -l) if [ "$suspicious_conns" -gt 50 ]; then findings="${findings}Excessive-Network-Connections:$suspicious_conns " risk=$((risk + 20)) fi echo "$risk|$findings" } check_backdoor_cron_jobs() { echo " Checking for backdoor cron jobs..." >&2 local findings="" local risk=0 # Check root crontab local suspicious_cron=$(crontab -l 2>/dev/null | grep -v "^#" | grep -E "wget|curl|nc |bash -i|/tmp/|/dev/shm|base64") if [ -n "$suspicious_cron" ]; then findings="${findings}Suspicious-Root-Cron-Jobs " risk=$((risk + 45)) fi # Check /etc/cron.d for suspicious entries (exclude false positives) local suspicious_cron_d=$(grep -rE "wget.*\||curl.*\||bash -i|/dev/tcp" /etc/cron.d/ 2>/dev/null | grep -v "^#" | grep -v "cpanel\|license_sync") if [ -n "$suspicious_cron_d" ]; then findings="${findings}Suspicious-Cron.d-Entries " risk=$((risk + 45)) fi # Check for cron jobs in unusual locations local unusual_crons=$(find /tmp /var/tmp /dev/shm -name "cron*" -o -name "*.cron" 2>/dev/null) if [ -n "$unusual_crons" ]; then findings="${findings}Cron-Jobs-In-Unusual-Locations " risk=$((risk + 40)) fi echo "$risk|$findings" } check_bash_history_malicious_commands() { echo " Analyzing bash history for malicious commands..." >&2 local findings="" local risk=0 if [ -f /root/.bash_history ]; then # Check for common attack commands (exclude legitimate installers) local malicious_cmds=$(grep -E "wget.*\/tmp\/.*sh|curl.*(base64|eval)|nc -l|bash -i.*\/dev\/tcp|chmod \+s \/|chattr \+i" /root/.bash_history 2>/dev/null | grep -v "claude.ai\|github.com\|githubusercontent") if [ -n "$malicious_cmds" ]; then findings="${findings}Malicious-Commands-In-History " risk=$((risk + 40)) fi # Check for history clearing attempts local history_tampering=$(grep -E "history -c|rm.*bash_history|unset HISTFILE" /root/.bash_history 2>/dev/null) if [ -n "$history_tampering" ]; then findings="${findings}History-Tampering-Detected " risk=$((risk + 35)) fi # Check for password hash modifications local passwd_mods=$(grep -E "echo.*\/etc\/passwd|echo.*\/etc\/shadow|vipw|usermod.*-p" /root/.bash_history 2>/dev/null) if [ -n "$passwd_mods" ]; then findings="${findings}Password-File-Manipulation " risk=$((risk + 45)) fi fi # Check if history is disabled if ! grep -q "^HISTFILE=" /root/.bashrc 2>/dev/null && [ ! -f /root/.bash_history ]; then findings="${findings}Bash-History-Disabled " risk=$((risk + 25)) fi echo "$risk|$findings" } check_web_shells() { echo " Scanning for web shells..." >&2 local findings="" local risk=0 local web_roots="" # Determine web roots based on panel if [ -d /home ]; then web_roots="/home/*/public_html /var/www/html /usr/local/apache/htdocs" fi # FALSE POSITIVE REDUCTION: Only scan very recent files (last 7 days) with suspicious patterns # Look for multiple suspicious indicators, not just one function local suspicious_files=$(find $web_roots -type f -name "*.php" -mtime -7 2>/dev/null | head -50 | xargs grep -l "eval.*base64\|system.*base64\|exec.*\$_\|shell_exec.*POST\|assert.*base64" 2>/dev/null | head -10) if [ -n "$suspicious_files" ]; then local file_count=0 # Check each file more carefully for file in $suspicious_files; do local file_age=$(($(date +%s) - $(stat -c %Y "$file" 2>/dev/null))) local file_days=$((file_age / 86400)) # Very recent files (< 24 hours) are more suspicious if [ "$file_days" -lt 1 ]; then file_count=$((file_count + 1)) elif [ "$file_days" -lt 3 ]; then # 1-3 days old, check for obfuscation if grep -q "base64_decode.*base64_decode\|eval.*eval\|gzinflate" "$file" 2>/dev/null; then file_count=$((file_count + 1)) fi fi done if [ "$file_count" -gt 0 ]; then findings="${findings}Potential-Web-Shells:$file_count " risk=$((risk + 35)) fi fi # Check for suspicious PHP files in unusual locations local unusual_php=$(find /tmp /var/tmp /dev/shm -name "*.php" 2>/dev/null) if [ -n "$unusual_php" ]; then findings="${findings}PHP-Files-In-Tmp " risk=$((risk + 40)) fi echo "$risk|$findings" } check_rootkit_indicators() { echo " Checking for rootkit indicators..." >&2 local findings="" local risk=0 # Check for common rootkit files local rootkit_files="/dev/.hid /usr/bin/.sniffer /usr/local/hide /lib/libproc.a" for file in $rootkit_files; do if [ -e "$file" ]; then findings="${findings}Rootkit-File:$file " risk=$((risk + 50)) fi done # Check for suspicious kernel modules local suspicious_modules=$(lsmod | grep -E "^(diamond|phalanx|beastkit)") if [ -n "$suspicious_modules" ]; then findings="${findings}Suspicious-Kernel-Modules " risk=$((risk + 50)) fi # Check for modified binaries (ls, ps, netstat) - look for backdoor/rootkit strings only for cmd in /bin/ls /bin/ps /bin/netstat; do if [ -f "$cmd" ]; then local strings_check=$(strings "$cmd" 2>/dev/null | grep -iE "backdoor|rootkit|\bhidden\b.*process|\bhide\b.*file") if [ -n "$strings_check" ]; then findings="${findings}Modified-Binary:$cmd " risk=$((risk + 50)) fi fi done # Check for hidden directories (... or . or . ) local hidden_dirs=$(find / -maxdepth 3 -type d -name "..." -o -name ". " -o -name ". " 2>/dev/null) if [ -n "$hidden_dirs" ]; then findings="${findings}Hidden-Directories " risk=$((risk + 35)) fi echo "$risk|$findings" } check_suspicious_network_activity() { echo " Analyzing network connections..." >&2 local findings="" local risk=0 # Check for connections to unusual ports (reverse shells often use 4444, 5555, 6666, 1337) local suspicious_ports=$(netstat -antp 2>/dev/null | grep ESTABLISHED | awk '{print $4}' | cut -d: -f2 | grep -E "^(4444|5555|6666|1337|31337)$") if [ -n "$suspicious_ports" ]; then findings="${findings}Suspicious-Port-Connections " risk=$((risk + 45)) fi # Check for IRC connections (common in botnets) local irc_conns=$(netstat -antp 2>/dev/null | grep ESTABLISHED | grep ":6667\|:6666\|:7000") if [ -n "$irc_conns" ]; then findings="${findings}IRC-Connections-Detected " risk=$((risk + 30)) fi # Check for excessive outbound connections from web server if command -v pgrep &>/dev/null; then local httpd_conns=$(lsof -p $(pgrep -d, httpd 2>/dev/null) 2>/dev/null | grep -c ESTABLISHED) if [ "$httpd_conns" -gt 100 ]; then findings="${findings}Excessive-Web-Server-Connections:$httpd_conns " risk=$((risk + 25)) fi fi echo "$risk|$findings" } perform_compromise_detection() { local ip=$1 echo " ${YELLOW}Running comprehensive compromise detection...${NC}" >&2 echo "" >&2 local total_risk=0 local all_findings="" # Run all compromise checks (11 total checks now) local result=$(check_recent_password_changes) local check_risk=$(echo "$result" | cut -d'|' -f1) local check_findings=$(echo "$result" | cut -d'|' -f2-) total_risk=$((total_risk + check_risk)) [ -n "$check_findings" ] && all_findings="${all_findings}${check_findings}" result=$(check_recent_user_changes) check_risk=$(echo "$result" | cut -d'|' -f1) check_findings=$(echo "$result" | cut -d'|' -f2-) total_risk=$((total_risk + check_risk)) [ -n "$check_findings" ] && all_findings="${all_findings}${check_findings}" result=$(check_backdoor_accounts) check_risk=$(echo "$result" | cut -d'|' -f1) check_findings=$(echo "$result" | cut -d'|' -f2-) total_risk=$((total_risk + check_risk)) [ -n "$check_findings" ] && all_findings="${all_findings}${check_findings}" result=$(check_unauthorized_ssh_keys) check_risk=$(echo "$result" | cut -d'|' -f1) check_findings=$(echo "$result" | cut -d'|' -f2-) total_risk=$((total_risk + check_risk)) [ -n "$check_findings" ] && all_findings="${all_findings}${check_findings}" result=$(check_system_file_tampering) check_risk=$(echo "$result" | cut -d'|' -f1) check_findings=$(echo "$result" | cut -d'|' -f2-) total_risk=$((total_risk + check_risk)) [ -n "$check_findings" ] && all_findings="${all_findings}${check_findings}" result=$(check_suspicious_processes) check_risk=$(echo "$result" | cut -d'|' -f1) check_findings=$(echo "$result" | cut -d'|' -f2-) total_risk=$((total_risk + check_risk)) [ -n "$check_findings" ] && all_findings="${all_findings}${check_findings}" result=$(check_backdoor_cron_jobs) check_risk=$(echo "$result" | cut -d'|' -f1) check_findings=$(echo "$result" | cut -d'|' -f2-) total_risk=$((total_risk + check_risk)) [ -n "$check_findings" ] && all_findings="${all_findings}${check_findings}" result=$(check_bash_history_malicious_commands) check_risk=$(echo "$result" | cut -d'|' -f1) check_findings=$(echo "$result" | cut -d'|' -f2-) total_risk=$((total_risk + check_risk)) [ -n "$check_findings" ] && all_findings="${all_findings}${check_findings}" result=$(check_web_shells) check_risk=$(echo "$result" | cut -d'|' -f1) check_findings=$(echo "$result" | cut -d'|' -f2-) total_risk=$((total_risk + check_risk)) [ -n "$check_findings" ] && all_findings="${all_findings}${check_findings}" result=$(check_rootkit_indicators) check_risk=$(echo "$result" | cut -d'|' -f1) check_findings=$(echo "$result" | cut -d'|' -f2-) total_risk=$((total_risk + check_risk)) [ -n "$check_findings" ] && all_findings="${all_findings}${check_findings}" result=$(check_suspicious_network_activity) check_risk=$(echo "$result" | cut -d'|' -f1) check_findings=$(echo "$result" | cut -d'|' -f2-) total_risk=$((total_risk + check_risk)) [ -n "$check_findings" ] && all_findings="${all_findings}${check_findings}" # FALSE POSITIVE REDUCTION: Adjust risk based on indicator count if [ "$FP_REQUIRE_MULTIPLE_INDICATORS" = "yes" ]; then # Count number of distinct indicators local indicator_count=0 echo "$all_findings" | grep -q "Password" && indicator_count=$((indicator_count + 1)) echo "$all_findings" | grep -q "User" && indicator_count=$((indicator_count + 1)) echo "$all_findings" | grep -q "UID-0" && indicator_count=$((indicator_count + 1)) echo "$all_findings" | grep -q "SSH" && indicator_count=$((indicator_count + 1)) echo "$all_findings" | grep -q "Modified" && indicator_count=$((indicator_count + 1)) echo "$all_findings" | grep -q "Process" && indicator_count=$((indicator_count + 1)) echo "$all_findings" | grep -q "Cron" && indicator_count=$((indicator_count + 1)) echo "$all_findings" | grep -q "History" && indicator_count=$((indicator_count + 1)) echo "$all_findings" | grep -q "Shell" && indicator_count=$((indicator_count + 1)) echo "$all_findings" | grep -q "Rootkit" && indicator_count=$((indicator_count + 1)) echo "$all_findings" | grep -q "Network" && indicator_count=$((indicator_count + 1)) # If only 1 indicator and low risk, reduce further if [ "$indicator_count" -eq 1 ] && [ "$total_risk" -lt 50 ]; then total_risk=$((total_risk / 2)) all_findings="${all_findings}[single-indicator:lowered-risk] " # If multiple indicators, this is more confidence - increase risk slightly elif [ "$indicator_count" -ge 3 ]; then total_risk=$((total_risk + 15)) all_findings="${all_findings}[multiple-indicators:$indicator_count] " fi fi # Cap at 100 [ $total_risk -gt 100 ] && total_risk=100 echo "$total_risk|$all_findings" } # # AUTOMATED RESPONSE # trigger_automated_response() { local ip=$1 local risk_score=$2 local username=$3 local panel=$4 # CRITICAL: 85-100 if [ $risk_score -ge $RISK_CRITICAL ] && [ "$SUSPICIOUS_LOGIN_AUTO_BLOCK" = "yes" ]; then echo -e "\n${RED}🚨 CRITICAL RISK: Triggering automated response${NC}" # 1. Block IP if command -v csf &>/dev/null; then echo " [1/3] Blocking IP via CSF..." if csf -d "$ip" "Suspicious login (risk: $risk_score)" 2>/dev/null; then echo -e " ${GREEN}✓ IP blocked via CSF${NC}" else echo -e " ${RED}✗ CSF block failed${NC}" fi else echo " [1/3] Blocking IP via iptables..." if iptables -I INPUT -s "$ip" -j DROP 2>/dev/null; then echo -e " ${GREEN}✓ IP blocked via iptables${NC}" else echo -e " ${RED}✗ iptables block failed${NC}" fi fi # 2. Trigger rkhunter scan if [ "$SUSPICIOUS_LOGIN_AUTO_SCAN" = "yes" ] && [ -f "$MALWARE_SCANNER" ]; then echo " [2/3] Triggering rootkit scan..." # Run in background with timeout timeout 300 bash "$MALWARE_SCANNER" --scanner rkhunter --quick &>/dev/null & local scan_pid=$! echo -e " ${GREEN}✓ Rootkit scan initiated (PID: $scan_pid)${NC}" else echo " [2/3] Rootkit scan (skipped - not configured)" fi # 3. CSI recommendation (cPanel only) if [ "$panel" = "cpanel" ]; then echo " [3/3] CSI scan recommended" echo "" echo " Run comprehensive security scan:" echo " wget https://raw.githubusercontent.com/CpanelInc/tech-CSI/master/csi.pl" echo " perl csi.pl --full" fi # HIGH: 70-84 elif [ $risk_score -ge $RISK_HIGH ]; then echo -e "\n${YELLOW}âš ī¸ HIGH RISK: Manual review recommended${NC}" if [ "$SUSPICIOUS_LOGIN_AUTO_BLOCK" = "yes" ] && command -v csf &>/dev/null; then echo " [1/2] Adding temporary rate limit..." if csf -tr "$ip" 300 "Rate limit: suspicious login" 2>/dev/null; then echo -e " ${GREEN}✓ Rate limited for 5 minutes${NC}" fi fi echo " [2/2] Schedule security scan for review" # MEDIUM: 50-69 elif [ $risk_score -ge $RISK_MEDIUM ]; then echo -e "\n${BLUE}â„šī¸ MEDIUM RISK: Monitoring recommended${NC}" # LOW: <50 else echo -e "\n${GREEN}✓ LOW RISK: Logged for analysis${NC}" fi } # # REPORTING # generate_report() { local panel=$1 echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" echo -e "${CYAN} SUSPICIOUS LOGIN MONITOR - Integrated Security Report${NC}" echo -e "${CYAN} Generated: $(date '+%Y-%m-%d %H:%M:%S')${NC}" echo -e "${CYAN} Scanning: Last $HOURS hours${NC}" echo -e "${CYAN} Panel: $panel${NC}" echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" echo "" # Count total events local total_events=$(cat "$SSH_EVENTS" "$PANEL_EVENTS" 2>/dev/null | wc -l) local successful=$(cat "$SSH_EVENTS" "$PANEL_EVENTS" 2>/dev/null | grep -c "|success$") local failed=$(cat "$SSH_EVENTS" "$PANEL_EVENTS" 2>/dev/null | grep -c "|failed$") local root_count=$(cat "$SSH_EVENTS" "$PANEL_EVENTS" 2>/dev/null | grep -c "|root|") if [ ! -s "$SUSPICIOUS_IPS" ]; then echo -e "${GREEN}✓ No suspicious login activity detected${NC}" echo "" echo "SUMMARY:" echo " Total Login Events: $total_events" echo " Successful: $successful" echo " Failed: $failed" echo " Root Logins: $root_count" echo "" return 0 fi local critical_count=$(awk -F'|' -v thresh=$RISK_CRITICAL '$2 >= thresh' "$SUSPICIOUS_IPS" | wc -l) local high_count=$(awk -F'|' -v crit=$RISK_CRITICAL -v high=$RISK_HIGH '$2 >= high && $2 < crit' "$SUSPICIOUS_IPS" | wc -l) if [ $critical_count -gt 0 ]; then echo -e "${RED}🚨 CRITICAL ALERTS ($critical_count):${NC}" echo "" awk -F'|' -v thresh=$RISK_CRITICAL '$2 >= thresh' "$SUSPICIOUS_IPS" | head -n 5 | while IFS='|' read -r ip risk reasons successful failed root users services; do echo -e " ${RED}[CRITICAL] $ip - Risk: $risk/100${NC}" echo " ┌─────────────────────────────────────────────────────────" echo " │ LOGIN EVENT" echo " ├─────────────────────────────────────────────────────────" echo " │ IP: $ip" echo " │ Successful logins: ${successful:-0}" echo " │ Failed attempts: ${failed:-0}" echo " │ Root logins: ${root:-0}" echo " │ Users: $users" echo " │ Services: $services" echo " │ Initial Risk Factors: $reasons" echo " │ Initial Risk: $risk/100" echo " │" # Cross-reference with bot analyzer echo " ├─────────────────────────────────────────────────────────" echo " │ ACCESS LOG CORRELATION (bot-analyzer.sh)" echo " ├─────────────────────────────────────────────────────────" local correlation=$(correlate_with_access_logs "$ip" "$risk") local corr_risk=$(echo "$correlation" | cut -d'|' -f1) local corr_attacks=$(echo "$correlation" | cut -d'|' -f2-) if [ "$corr_risk" != "0" ] && [ -n "$corr_attacks" ]; then echo " │ âš ī¸ Web attack activity detected:" for attack in $corr_attacks; do echo " │ - $attack" done risk=$((risk + corr_risk)) [ $risk -gt 100 ] && risk=100 else echo " │ $corr_attacks" fi echo " │" # Check IP reputation echo " ├─────────────────────────────────────────────────────────" echo " │ IP REPUTATION" echo " ├─────────────────────────────────────────────────────────" local rep_result=$(check_ip_reputation "$ip" "$risk") local rep_risk=$(echo "$rep_result" | cut -d'|' -f1) local rep_notes=$(echo "$rep_result" | cut -d'|' -f2-) if [ "$rep_risk" != "0" ]; then echo " │ $rep_notes" risk=$((risk + rep_risk)) [ $risk -gt 100 ] && risk=100 else echo " │ $rep_notes" fi echo " │" # Threat intelligence echo " ├─────────────────────────────────────────────────────────" echo " │ THREAT INTELLIGENCE" echo " ├─────────────────────────────────────────────────────────" local threat_result=$(correlate_with_threat_intel "$ip") local threat_risk=$(echo "$threat_result" | cut -d'|' -f1) local threat_notes=$(echo "$threat_result" | cut -d'|' -f2-) if [ "$threat_risk" != "0" ]; then echo " │ âš ī¸ $threat_notes" risk=$((risk + threat_risk)) [ $risk -gt 100 ] && risk=100 else echo " │ $threat_notes" fi echo " │" # COMPROMISE DETECTION - Check if server is actually rooted echo " ├─────────────────────────────────────────────────────────" echo -e " │ ${RED}COMPROMISE DETECTION - System Integrity Check${NC}" echo " ├─────────────────────────────────────────────────────────" local compromise_result=$(perform_compromise_detection "$ip") local compromise_risk=$(echo "$compromise_result" | cut -d'|' -f1) local compromise_findings=$(echo "$compromise_result" | cut -d'|' -f2-) if [ "$compromise_risk" -ge 50 ]; then echo -e " │ ${RED}🚨 COMPROMISE CONFIRMED - $compromise_risk risk points${NC}" echo " │" echo " │ Indicators of compromise found:" # Parse and display findings for finding in $(echo "$compromise_findings" | tr ' ' '\n'); do echo " │ â€ĸ $finding" done risk=$((risk + compromise_risk)) [ $risk -gt 100 ] && risk=100 elif [ "$compromise_risk" -gt 0 ]; then echo -e " │ ${YELLOW}âš ī¸ Suspicious indicators found - $compromise_risk risk points${NC}" echo " │" for finding in $(echo "$compromise_findings" | tr ' ' '\n'); do echo " │ â€ĸ $finding" done risk=$((risk + compromise_risk)) [ $risk -gt 100 ] && risk=100 else echo -e " │ ${GREEN}✓ No compromise indicators detected${NC}" echo " │ System integrity checks passed" fi echo " │" echo " ├─────────────────────────────────────────────────────────" if [ "$compromise_risk" -ge 50 ]; then echo -e " │ ${RED}FINAL RISK SCORE: $risk/100 - SERVER LIKELY COMPROMISED${NC}" else echo -e " │ ${RED}FINAL RISK SCORE: $risk/100 - CRITICAL${NC}" fi echo " └─────────────────────────────────────────────────────────" # Trigger automated response trigger_automated_response "$ip" "$risk" "$(echo "$users" | cut -d',' -f1)" "$panel" echo "" done fi if [ $high_count -gt 0 ]; then echo -e "${YELLOW}âš ī¸ HIGH ALERTS ($high_count):${NC}" echo "" awk -F'|' -v crit=$RISK_CRITICAL -v high=$RISK_HIGH '$2 >= high && $2 < crit' "$SUSPICIOUS_IPS" | head -n 5 | while IFS='|' read -r ip risk reasons successful failed root users services; do echo -e " ${YELLOW}[HIGH] $ip - Risk: $risk/100${NC}" echo " Successful: ${successful:-0} | Failed: ${failed:-0} | Root: ${root:-0}" echo " Reasons: $reasons" # Quick correlation local correlation=$(correlate_with_access_logs "$ip" "$risk") local corr_attacks=$(echo "$correlation" | cut -d'|' -f2-) if [ -n "$corr_attacks" ] && [ "$corr_attacks" != "No access log activity" ]; then echo " Web attacks: $corr_attacks" fi echo "" done fi echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" echo "SUMMARY:" echo "" echo " Total Login Events: $total_events" echo " Successful: $successful" echo " Failed: $failed" echo " Root Logins: $root_count" echo "" echo " Suspicious IPs: $(wc -l < "$SUSPICIOUS_IPS")" echo " Critical Risk: $critical_count" echo " High Risk: $high_count" echo "" # Integration status echo " Integration Status:" local bot_report=$(ls -t "$TOOLKIT_ROOT"/tmp/bot_analysis_report_*.txt /tmp/bot_analysis_report_*.txt 2>/dev/null | head -n1) [ -n "$bot_report" ] && echo " ✓ Bot Analyzer: Available" || echo " ✗ Bot Analyzer: No recent data" [ -f "$IP_REPUTATION_LIB" ] && echo " ✓ IP Reputation: Available" || echo " ✗ IP Reputation: Not available" [ -f "$THREAT_INTEL_LIB" ] && echo " ✓ Threat Intelligence: Available" || echo " ✗ Threat Intelligence: Not available" echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" } # # MAIN # main() { echo -e "${CYAN}Starting Suspicious Login Monitor...${NC}" echo "" # Detect panel local panel=$(detect_panel) echo "Detected panel: $panel" echo "" # Parse logs (universal parsers first) parse_ssh_logins "$HOURS" parse_wtmp_logins "$HOURS" parse_btmp_logins "$HOURS" parse_sudo_escalation "$HOURS" # Parse panel-specific logs case "$panel" in cpanel) parse_cpanel_logins "$HOURS" ;; plesk) parse_plesk_logins "$HOURS" ;; interworx) parse_interworx_logins "$HOURS" ;; esac # Analyze detect_anomalies # Generate report generate_report "$panel" | tee "$REPORT_FILE" echo "" echo "Report saved to: $REPORT_FILE" # ALWAYS run system-wide compromise detection (regardless of login activity) echo "" echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" echo -e "${CYAN} SYSTEM COMPROMISE DETECTION - Integrity Check${NC}" echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" echo "" # Show root password change date (always display) echo -e "${BLUE}Root Account Status:${NC}" if [ -f /etc/shadow ]; then local root_pw_date=$(awk -F: '$1 == "root" {print $3}' /etc/shadow 2>/dev/null) if [ -n "$root_pw_date" ] && [ "$root_pw_date" != "0" ]; then # Convert days since epoch to actual date local pw_date=$(date -d "1970-01-01 + $root_pw_date days" "+%Y-%m-%d" 2>/dev/null) local pw_age_days=$(( $(date +%s) / 86400 - root_pw_date )) echo " Last password change: $pw_date ($pw_age_days days ago)" # Warn if password hasn't been changed in a long time or changed very recently if [ "$pw_age_days" -lt 1 ]; then echo -e " ${RED}âš ī¸ Password changed TODAY - verify this was authorized${NC}" elif [ "$pw_age_days" -lt 7 ]; then echo -e " ${YELLOW}âš ī¸ Password changed within last 7 days${NC}" elif [ "$pw_age_days" -gt 365 ]; then echo -e " ${YELLOW}âš ī¸ Password not changed in over a year (consider rotating)${NC}" else echo " Status: Normal" fi else echo -e " ${RED}âš ī¸ Unable to determine password age${NC}" fi fi echo "" local compromise_result=$(perform_compromise_detection "system-wide") local compromise_risk=$(echo "$compromise_result" | cut -d'|' -f1) local compromise_findings=$(echo "$compromise_result" | cut -d'|' -f2-) if [ "$compromise_risk" -ge 100 ]; then echo -e "${RED}🚨 CRITICAL: Server shows strong indicators of compromise${NC}" echo -e "${RED} Risk Score: $compromise_risk/100${NC}" echo "" echo "Indicators found:" for finding in $(echo "$compromise_findings" | tr ' ' '\n'); do echo -e " ${RED}â€ĸ${NC} $(echo $finding | tr '-' ' ')" done echo "" echo -e "${RED}RECOMMENDED ACTIONS:${NC}" echo " 1. Investigate all findings immediately" echo " 2. Run full rootkit scan: rkhunter --check" if [ "$panel" = "cpanel" ]; then echo " 3. Run cPanel CSI: perl csi.pl --full" fi echo " 4. Consider full system reinstall if compromised" elif [ "$compromise_risk" -ge 50 ]; then echo -e "${RED}âš ī¸ WARNING: Suspicious indicators detected${NC}" echo -e "${YELLOW} Risk Score: $compromise_risk/100${NC}" echo "" echo "Indicators found:" for finding in $(echo "$compromise_findings" | tr ' ' '\n'); do echo -e " ${YELLOW}â€ĸ${NC} $(echo $finding | tr '-' ' ')" done echo "" echo -e "${YELLOW}RECOMMENDED ACTIONS:${NC}" echo " 1. Review all findings carefully" echo " 2. Run rootkit scan: rkhunter --check" echo " 3. Investigate recent account/file changes" elif [ "$compromise_risk" -gt 0 ]; then echo -e "${BLUE}â„šī¸ NOTICE: Minor security concerns detected${NC}" echo -e "${BLUE} Risk Score: $compromise_risk/100${NC}" echo "" echo "Issues found:" for finding in $(echo "$compromise_findings" | tr ' ' '\n'); do echo -e " ${BLUE}â€ĸ${NC} $(echo $finding | tr '-' ' ')" done echo "" echo "Review these items when convenient." else echo -e "${GREEN}✓ No compromise indicators detected${NC}" echo "" echo "System integrity checks:" echo " ✓ No suspicious password changes detected" echo " ✓ No suspicious user/group changes" echo " ✓ No unauthorized UID 0 accounts" echo " ✓ No suspicious SSH keys" echo " ✓ No system file tampering detected" echo " ✓ No suspicious processes found" echo " ✓ No backdoor cron jobs" echo " ✓ No malicious commands in bash history" echo " ✓ No web shells detected" echo " ✓ No rootkit indicators" echo " ✓ No suspicious network activity" fi echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" } # Run main function main exit 0