diff --git a/modules/security/suspicious-login-monitor.sh b/modules/security/suspicious-login-monitor.sh index 985b884..3554f62 100755 --- a/modules/security/suspicious-login-monitor.sh +++ b/modules/security/suspicious-login-monitor.sh @@ -21,6 +21,13 @@ 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 + # Integration paths BOT_ANALYZER="$TOOLKIT_ROOT/modules/security/bot-analyzer.sh" MALWARE_SCANNER="$TOOLKIT_ROOT/modules/security/malware-scanner.sh" @@ -946,6 +953,92 @@ correlate_with_threat_intel() { 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 +} + # # COMPROMISE DETECTION - Check for actual root compromise indicators # @@ -969,14 +1062,36 @@ check_recent_password_changes() { 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/,$//') - findings="${findings}Recent-Password-Changes:$pw_count-accounts " - details="${details}Changed-passwords:$pw_users " - # Higher risk if root password was changed - if echo "$recent_pw_changes" | grep -q "^root:"; then - risk=$((risk + 35)) + # FALSE POSITIVE REDUCTION: Check if this is mass change (more suspicious) + if [ "$pw_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 "$recent_pw_changes" | grep -q "^root:"; then + findings="${findings}Root-Password-Changed " + details="${details}(root) " + + # Check if during business hours (less suspicious) + if [ "$FP_IGNORE_BUSINESS_HOURS" = "yes" ] && is_business_hours; then + risk=$((risk + 20)) # Lower risk during business hours + details="${details}[business-hours] " + else + risk=$((risk + 35)) + fi + elif [ "$pw_count" -eq 1 ]; then + # Single non-root password change - very low risk + risk=$((risk + 5)) + details="${details}Single-user:$pw_users " + else + # 2-4 password changes, no root - medium-low risk + risk=$((risk + 10)) + details="${details}$pw_count-users:$pw_users " + fi else - risk=$((risk + 15)) + # Mass password change (5+ accounts) - VERY suspicious + findings="${findings}Mass-Password-Changes:$pw_count-accounts " + details="${details}Users:$pw_users " + risk=$((risk + 45)) # High risk for mass changes fi fi @@ -1020,19 +1135,35 @@ check_recent_user_changes() { 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 - findings="${findings}Recently-Created-Users:$new_users " - risk=$((risk + 25)) + # FALSE POSITIVE REDUCTION: Check if cPanel account creation + local cpanel_activity=$(check_cpanel_account_creation 168) # 7 days + + if [ "$cpanel_activity" = "cpanel_account_creation" ] && [ "$new_count" -le 3 ]; then + # Likely legitimate cPanel hosting account + findings="${findings}New-Users:$new_users[cpanel] " + risk=$((risk + 5)) # Very low risk + else + # Not from cPanel or too many accounts + findings="${findings}Recently-Created-Users:$new_users " + if [ "$new_count" -eq 1 ]; then + risk=$((risk + 15)) # Single user, moderate risk + else + risk=$((risk + 25)) # Multiple users, higher risk + fi + fi fi fi @@ -1096,8 +1227,11 @@ check_unauthorized_ssh_keys() { # Check root's authorized_keys if [ -f /root/.ssh/authorized_keys ]; then - local key_count=$(grep -v "^#" /root/.ssh/authorized_keys 2>/dev/null | grep -c "ssh-") - if [ "$key_count" -gt 5 ]; 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 @@ -1133,28 +1267,54 @@ check_system_file_tampering() { 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 /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)) - findings="${findings}/etc/passwd-Modified-${passwd_hours}h-ago " - risk=$((risk + 25)) + + if [ "$pkg_activity" != "none" ]; then + # Package manager was active - likely legitimate + findings="${findings}/etc/passwd-Modified-${passwd_hours}h-ago[$pkg_activity] " + risk=$((risk + 5)) # Very low risk + else + # No package activity - more suspicious + findings="${findings}/etc/passwd-Modified-${passwd_hours}h-ago " + risk=$((risk + 25)) + fi 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)) - findings="${findings}/etc/shadow-Modified-${shadow_hours}h-ago " - risk=$((risk + 25)) + + if [ "$pkg_activity" != "none" ]; then + findings="${findings}/etc/shadow-Modified-${shadow_hours}h-ago[$pkg_activity] " + risk=$((risk + 5)) + else + findings="${findings}/etc/shadow-Modified-${shadow_hours}h-ago " + risk=$((risk + 25)) + fi 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)) - findings="${findings}/etc/group-Modified-${group_hours}h-ago " - risk=$((risk + 20)) + + if [ "$pkg_activity" != "none" ]; then + findings="${findings}/etc/group-Modified-${group_hours}h-ago[$pkg_activity] " + risk=$((risk + 3)) + else + findings="${findings}/etc/group-Modified-${group_hours}h-ago " + risk=$((risk + 20)) + fi fi # Check /etc/gshadow modification time @@ -1162,8 +1322,14 @@ check_system_file_tampering() { 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)) - findings="${findings}/etc/gshadow-Modified-${gshadow_hours}h-ago " - risk=$((risk + 20)) + + if [ "$pkg_activity" != "none" ]; then + findings="${findings}/etc/gshadow-Modified-${gshadow_hours}h-ago[$pkg_activity] " + risk=$((risk + 3)) + else + findings="${findings}/etc/gshadow-Modified-${gshadow_hours}h-ago " + risk=$((risk + 20)) + fi fi fi @@ -1218,9 +1384,19 @@ check_suspicious_processes() { fi # Check for processes running from /tmp or /dev/shm - local tmp_procs=$(lsof -p $(ps aux | awk '$11 ~ /\/(tmp|dev\/shm)/ {print $2}' | tr '\n' ',') 2>/dev/null | grep -c "^COMMAND") - if [ "$tmp_procs" -gt 0 ]; then - findings="${findings}Processes-From-Tmp:$tmp_procs " + 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 @@ -1314,13 +1490,33 @@ check_web_shells() { web_roots="/home/*/public_html /var/www/html /usr/local/apache/htdocs" fi - # Scan for common web shell patterns (limit to recent files for performance) - local suspicious_files=$(find $web_roots -type f -name "*.php" -mtime -7 2>/dev/null | head -50 | xargs grep -l "eval(\|base64_decode(\|system(\|exec(\|passthru(\|shell_exec(" 2>/dev/null | head -10) + # 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=$(echo "$suspicious_files" | wc -l) - findings="${findings}Potential-Web-Shells:$file_count " - risk=$((risk + 35)) + 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 @@ -1484,6 +1680,33 @@ perform_compromise_detection() { 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