diff --git a/modules/security/suspicious-login-monitor.sh b/modules/security/suspicious-login-monitor.sh index f5657e3..9de34d7 100755 --- a/modules/security/suspicious-login-monitor.sh +++ b/modules/security/suspicious-login-monitor.sh @@ -31,6 +31,7 @@ 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) @@ -47,7 +48,7 @@ NC='\033[0m' # Cleanup on exit trap cleanup EXIT cleanup() { - rm -f "$SSH_EVENTS" "$PANEL_EVENTS" "$SUSPICIOUS_IPS" 2>/dev/null + rm -f "$SSH_EVENTS" "$PANEL_EVENTS" "$SUDO_EVENTS" "$SUSPICIOUS_IPS" 2>/dev/null } # Detect control panel @@ -157,6 +158,204 @@ parse_ssh_logins() { ' "$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 # @@ -242,6 +441,57 @@ parse_cpanel_logins() { } ' "$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 } # @@ -360,6 +610,48 @@ parse_interworx_logins() { } } ' "$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 } # @@ -369,8 +661,8 @@ parse_interworx_logins() { detect_anomalies() { echo " Analyzing login patterns..." >&2 - # Combine all events - cat "$SSH_EVENTS" "$PANEL_EVENTS" 2>/dev/null | \ + # 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 @@ -380,47 +672,56 @@ detect_anomalies() { method = $5 status = $6 - # Skip server IPs - if (index(server_ips, ip "|") > 0) next + # 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 - ip_events[ip]++ - ip_users[ip] = ip_users[ip] (ip_users[ip] ? "," : "") user + # 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[ip]++ - failed_users[ip] = failed_users[ip] (failed_users[ip] ? "," : "") user + failed[event_key]++ + failed_users[event_key] = failed_users[event_key] (failed_users[event_key] ? "," : "") user } # Track successful logins - if (status == "success") { - successful[ip]++ - successful_users[ip] = successful_users[ip] (successful_users[ip] ? "," : "") user - successful_services[ip] = successful_services[ip] (successful_services[ip] ? "," : "") service + 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[ip]++ - root_methods[ip] = root_methods[ip] (root_methods[ip] ? "," : "") method + 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[ip]++ + password_auth[event_key]++ } else if (method == "key") { - key_auth[ip]++ + key_auth[event_key]++ } } # Store first and last seen - if (first_seen[ip] == "") { - first_seen[ip] = timestamp + if (first_seen[event_key] == "") { + first_seen[event_key] = timestamp } - last_seen[ip] = timestamp + last_seen[event_key] = timestamp } END { for (ip in ip_events) { @@ -474,6 +775,12 @@ detect_anomalies() { 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 @@ -888,9 +1195,13 @@ main() { echo "Detected panel: $panel" echo "" - # Parse logs + # 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"