Files
Linux-Server-Management-Too…/modules/security/suspicious-login-monitor.sh
T
cschantz bd733e919a Fix: Add -e flag to echo for ANSI color codes
Issue: Line 2536 used echo without -e flag
Result: ANSI escape codes printed literally instead of rendering colors
Example: \033[1;33mRunning...\033[0m

Fix: Changed echo to echo -e
Result: Colors now render correctly in terminal

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-05 20:00:22 -05:00

3106 lines
109 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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"
# Baseline storage (persistent across runs, within toolkit directory)
BASELINE_DIR="$TOOLKIT_ROOT/data/suspicious-login-monitor"
BASELINE_FILE="$BASELINE_DIR/baseline.dat"
mkdir -p "$BASELINE_DIR" 2>/dev/null
# 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"
}
#
# CONFIDENCE IMPROVEMENT - Baseline and pattern matching
#
load_baseline() {
# Load historical baseline data
if [ -f "$BASELINE_FILE" ]; then
source "$BASELINE_FILE"
else
# Initialize empty baseline
BASELINE_SSH_KEY_COUNT=0
BASELINE_USER_COUNT=0
BASELINE_TYPICAL_LOGIN_HOURS=""
BASELINE_PASSWORD_CHANGES_PER_WEEK=0
BASELINE_NEW_USERS_PER_WEEK=0
BASELINE_LAST_UPDATE=0
fi
}
save_baseline() {
local ssh_key_count=$1
local user_count=$2
local login_hours=$3
local pw_changes=$4
local new_users=$5
cat > "$BASELINE_FILE" << EOF
# Baseline data for suspicious login monitor
# Last updated: $(date)
BASELINE_SSH_KEY_COUNT=$ssh_key_count
BASELINE_USER_COUNT=$user_count
BASELINE_TYPICAL_LOGIN_HOURS="$login_hours"
BASELINE_PASSWORD_CHANGES_PER_WEEK=$pw_changes
BASELINE_NEW_USERS_PER_WEEK=$new_users
BASELINE_LAST_UPDATE=$(date +%s)
EOF
}
update_baseline() {
# Update baseline with current system state
local current_keys=$(grep -v "^#" /root/.ssh/authorized_keys 2>/dev/null | grep -c "ssh-" || echo 0)
local current_users=$(awk -F: '$3 >= 1000 && $3 < 60000 {print $1}' /etc/passwd | wc -l)
local typical_hours=$(who | awk '{print $4}' | cut -d: -f1 | sort | uniq -c | sort -rn | head -3 | awk '{print $2}' | tr '\n' ',' | sed 's/,$//')
# Track password changes and user creation over time
local pw_changes=0
local new_users=0
# Simple tracking - could be enhanced with historical analysis
if [ -f "$BASELINE_FILE" ]; then
source "$BASELINE_FILE"
# Use existing values as basis
fi
save_baseline "$current_keys" "$current_users" "$typical_hours" "$pw_changes" "$new_users"
}
check_deviation_from_baseline() {
local metric=$1
local current_value=$2
local baseline_value=$3
# Calculate percentage deviation
if [ "$baseline_value" -eq 0 ]; then
echo "100" # New metric, 100% deviation
return
fi
local diff=$((current_value - baseline_value))
local abs_diff=${diff#-} # Absolute value
local percent=$((abs_diff * 100 / baseline_value))
echo "$percent"
}
match_attack_patterns() {
local findings=$1
local confidence_boost=0
local matched_patterns=""
# Known attack pattern signatures
# Pattern 1: Backdoor account + SSH key + recent creation
if echo "$findings" | grep -q "UID-0" && \
echo "$findings" | grep -q "SSH-Key" && \
echo "$findings" | grep -q "Created-Users"; then
confidence_boost=$((confidence_boost + 30))
matched_patterns="${matched_patterns}Backdoor-Installation-Pattern "
fi
# Pattern 2: Mass password change + file tampering
if echo "$findings" | grep -q "Mass-Password" && \
echo "$findings" | grep -q "Modified"; then
confidence_boost=$((confidence_boost + 25))
matched_patterns="${matched_patterns}Ransomware-Pattern "
fi
# Pattern 3: Sudo escalation + suspicious process + cron
if echo "$findings" | grep -q "Sudo" && \
echo "$findings" | grep -q "Process" && \
echo "$findings" | grep -q "Cron"; then
confidence_boost=$((confidence_boost + 30))
matched_patterns="${matched_patterns}Privilege-Escalation-Pattern "
fi
# Pattern 4: Web shell + backdoor cron + network activity
if echo "$findings" | grep -q "Shell" && \
echo "$findings" | grep -q "Cron" && \
echo "$findings" | grep -q "Network"; then
confidence_boost=$((confidence_boost + 35))
matched_patterns="${matched_patterns}Persistent-Backdoor-Pattern "
fi
# Pattern 5: Rootkit indicators + modified binaries + hidden files
if echo "$findings" | grep -q "Rootkit" && \
echo "$findings" | grep -q "Binary" && \
echo "$findings" | grep -q "Hidden"; then
confidence_boost=$((confidence_boost + 40))
matched_patterns="${matched_patterns}Rootkit-Compromise-Pattern "
fi
# Pattern 6: Recently created user + suspicious name + no password age
if echo "$findings" | grep -q "Suspicious-Username" && \
echo "$findings" | grep -q "Created-Users" && \
echo "$findings" | grep -q "Password"; then
confidence_boost=$((confidence_boost + 25))
matched_patterns="${matched_patterns}Account-Takeover-Pattern "
fi
echo "$confidence_boost|$matched_patterns"
}
calculate_confidence_score() {
local risk=$1
local findings=$2
local mitigating_factors=$3
# Start with base confidence from risk level
local confidence=50 # Medium baseline
# Risk-based confidence adjustment
if [ "$risk" -ge 85 ]; then
confidence=$((confidence + 30)) # High risk = higher confidence it's real
elif [ "$risk" -ge 70 ]; then
confidence=$((confidence + 20))
elif [ "$risk" -ge 50 ]; then
confidence=$((confidence + 10))
fi
# Multiple independent indicators increase confidence
local indicator_count=$(echo "$findings" | grep -o "[A-Z][a-z]*-[A-Z]" | wc -l)
if [ "$indicator_count" -ge 5 ]; then
confidence=$((confidence + 25))
elif [ "$indicator_count" -ge 3 ]; then
confidence=$((confidence + 15))
elif [ "$indicator_count" -ge 2 ]; then
confidence=$((confidence + 5))
else
confidence=$((confidence - 20)) # Single indicator = lower confidence
fi
# Mitigating factors reduce confidence
local mitigation_count=$(echo "$mitigating_factors" | grep -o "\[" | wc -l)
if [ "$mitigation_count" -ge 3 ]; then
confidence=$((confidence - 30)) # Lots of context = probably legitimate
elif [ "$mitigation_count" -ge 2 ]; then
confidence=$((confidence - 20))
elif [ "$mitigation_count" -ge 1 ]; then
confidence=$((confidence - 10))
fi
# Check for attack pattern matches (increases confidence)
local pattern_match=$(match_attack_patterns "$findings")
local pattern_boost=$(echo "$pattern_match" | cut -d'|' -f1)
local patterns=$(echo "$pattern_match" | cut -d'|' -f2-)
confidence=$((confidence + pattern_boost))
# Check baseline deviations (if available)
if [ -f "$BASELINE_FILE" ]; then
load_baseline
# Check SSH key deviation
local current_keys=$(grep -v "^#" /root/.ssh/authorized_keys 2>/dev/null | grep -c "ssh-" || echo 0)
if [ "$BASELINE_SSH_KEY_COUNT" -gt 0 ]; then
local key_deviation=$(check_deviation_from_baseline "keys" "$current_keys" "$BASELINE_SSH_KEY_COUNT")
if [ "$key_deviation" -gt 50 ]; then
confidence=$((confidence + 15)) # Significant deviation from normal
fi
fi
fi
# Cap confidence at 0-100
[ "$confidence" -lt 0 ] && confidence=0
[ "$confidence" -gt 100 ] && confidence=100
# Determine confidence level
local confidence_level="MEDIUM"
if [ "$confidence" -ge 75 ]; then
confidence_level="HIGH"
elif [ "$confidence" -lt 40 ]; then
confidence_level="LOW"
fi
echo "$confidence|$confidence_level|$patterns"
}
cross_validate_finding() {
local finding_type=$1
local finding_data=$2
local validation_score=0
local validation_sources=""
case "$finding_type" in
"password-change")
# Cross-check with multiple sources
# 1. /etc/shadow timestamp
if [ -f /etc/shadow ]; then
local shadow_age=$(($(date +%s) - $(stat -c %Y /etc/shadow)))
if [ "$shadow_age" -lt 86400 ]; then
validation_score=$((validation_score + 1))
validation_sources="${validation_sources}shadow-timestamp "
fi
fi
# 2. /var/log/secure entries
if grep -q "password changed" /var/log/secure 2>/dev/null; then
validation_score=$((validation_score + 1))
validation_sources="${validation_sources}secure-log "
fi
# 3. Audit log entries (if available)
if [ -f /var/log/audit/audit.log ] && ausearch -m USER_CHAUTHTOK -ts recent >/dev/null 2>&1; then
validation_score=$((validation_score + 1))
validation_sources="${validation_sources}audit-log "
fi
;;
"user-creation")
# 1. /etc/passwd timestamp
local passwd_age=$(($(date +%s) - $(stat -c %Y /etc/passwd)))
if [ "$passwd_age" -lt 604800 ]; then # 7 days
validation_score=$((validation_score + 1))
validation_sources="${validation_sources}passwd-timestamp "
fi
# 2. Home directory existence and age
local username=$(echo "$finding_data" | cut -d'(' -f1)
if [ -d "/home/$username" ]; then
validation_score=$((validation_score + 1))
validation_sources="${validation_sources}home-dir-exists "
fi
# 3. System logs
if grep -q "new user" /var/log/secure 2>/dev/null | grep -q "$username"; then
validation_score=$((validation_score + 1))
validation_sources="${validation_sources}secure-log "
fi
;;
"ssh-key")
# 1. File modification time
if [ -f /root/.ssh/authorized_keys ]; then
local key_age=$(($(date +%s) - $(stat -c %Y /root/.ssh/authorized_keys)))
if [ "$key_age" -lt 604800 ]; then
validation_score=$((validation_score + 1))
validation_sources="${validation_sources}key-file-timestamp "
fi
fi
# 2. SSH log entries
if grep -q "Accepted publickey" /var/log/secure 2>/dev/null | tail -100 | grep -q "root"; then
validation_score=$((validation_score + 1))
validation_sources="${validation_sources}ssh-log "
fi
;;
esac
echo "$validation_score|$validation_sources"
}
#
# 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
}
#
# PHASE 2A FALSE POSITIVE REDUCTION - Additional correlation checks
#
check_tty_session() {
local user=${1:-root}
local timestamp=${2:-$(date +%s)}
# Check if user has an active TTY/PTY session
# Real admin sessions have TTY, automated scripts don't
# Check currently logged in users
local tty_info=$(w -h 2>/dev/null | awk -v user="$user" '$1 == user {print $2}')
if [ -n "$tty_info" ]; then
echo "tty-session:$tty_info"
return 0
fi
# Check recent TTY allocations in /var/log/secure (within last hour)
if [ -f /var/log/secure ]; then
local recent_tty=$(awk -v user="$user" '
/pam_unix.*session opened/ && $0 ~ user {
if ($0 ~ /pts\/[0-9]+/) {
match($0, /pts\/[0-9]+/)
tty = substr($0, RSTART, RLENGTH)
print tty
exit
}
}
' /var/log/secure 2>/dev/null | tail -1)
if [ -n "$recent_tty" ]; then
echo "recent-tty:$recent_tty"
return 0
fi
fi
echo "no-tty"
return 1
}
check_recent_login() {
local user=$1
local event_time=${2:-$(date +%s)}
# Check if user logged in within last 2 hours
# If yes, their changes are likely legitimate
if [ -z "$user" ]; then
echo "unknown:0"
return 1
fi
# Use 'last' command to get recent logins
local last_login=$(last -F "$user" 2>/dev/null | head -1)
if [ -z "$last_login" ] || echo "$last_login" | grep -q "^$"; then
echo "no-login:0"
return 1
fi
# Parse login time from last command output
# Format: user pts/0 ip Mon Feb 3 14:30:00 2026 - 16:45:00 (2:15)
local login_time=$(echo "$last_login" | awk '{
# Extract timestamp: Mon Feb 3 14:30:00 2026
month = $4
day = $5
time = $6
year = $7
# Convert to date command format
timestamp = month " " day " " year " " time
cmd = "date -d \"" timestamp "\" +%s 2>/dev/null"
cmd | getline epoch
close(cmd)
if (epoch > 0) {
print epoch
}
}')
if [ -z "$login_time" ] || [ "$login_time" = "0" ]; then
echo "no-login:0"
return 1
fi
# Calculate hours since login
local seconds_since=$(( event_time - login_time ))
local hours_since=$(( seconds_since / 3600 ))
# If negative or within last 2 hours, return as recent
if [ "$hours_since" -lt 0 ] || [ "$hours_since" -le 2 ]; then
local hours_display=$(echo "scale=1; $seconds_since / 3600" | bc 2>/dev/null || echo "$hours_since")
echo "recent-login:${hours_display}h"
return 0
fi
echo "old-login:${hours_since}h"
return 1
}
check_package_ownership() {
local file=$1
# Check if file belongs to an installed package
# Helps catch package operations not detected in logs
if [ ! -f "$file" ] && [ ! -d "$file" ]; then
echo "not-found"
return 1
fi
# Try RPM-based systems (RHEL, CentOS, AlmaLinux, Rocky, Fedora)
if command -v rpm >/dev/null 2>&1; then
local pkg=$(rpm -qf "$file" 2>/dev/null)
if [ $? -eq 0 ] && [ "$pkg" != "file $file is not owned by any package" ]; then
# Extract just package name without version
local pkg_name=$(echo "$pkg" | sed 's/-[0-9].*//')
echo "pkg-owned:$pkg_name"
return 0
fi
fi
# Try DEB-based systems (Debian, Ubuntu)
if command -v dpkg >/dev/null 2>&1; then
local pkg=$(dpkg -S "$file" 2>/dev/null | cut -d: -f1)
if [ -n "$pkg" ]; then
echo "pkg-owned:$pkg"
return 0
fi
fi
echo "not-owned"
return 1
}
check_maintenance_mode() {
# Check for various maintenance mode indicators
local indicators=""
# Check for /etc/nologin (standard maintenance file)
if [ -f /etc/nologin ]; then
indicators="${indicators}nologin "
fi
# Check for custom maintenance flags
if [ -f /var/run/maintenance.flag ]; then
indicators="${indicators}maintenance.flag "
fi
if [ -f /root/.maintenance ]; then
indicators="${indicators}root-maintenance "
fi
# Check cPanel maintenance mode
if [ -f /usr/local/cpanel/bin/whmapi1 ]; then
local cpanel_maint=$(whmapi1 get_tweaksetting key=maintenance_mode 2>/dev/null | grep -A1 "value:" | tail -1 | awk '{print $2}')
if [ "$cpanel_maint" = "1" ]; then
indicators="${indicators}cpanel-maint "
fi
fi
# Check /etc/motd or /etc/issue for maintenance notices
if grep -qi "maintenance" /etc/motd 2>/dev/null; then
indicators="${indicators}motd-notice "
fi
if grep -qi "maintenance" /etc/issue 2>/dev/null; then
indicators="${indicators}issue-notice "
fi
if [ -n "$indicators" ]; then
echo "maintenance-mode:$(echo $indicators | sed 's/ $//')"
return 0
fi
echo "no-maintenance"
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
# PHASE 2A: Check for TTY session (real terminal vs automated)
local tty_session=$(check_tty_session "root")
local has_tty=0
if [[ "$tty_session" =~ ^tty-session: ]] || [[ "$tty_session" =~ ^recent-tty: ]]; then
has_tty=1
details="${details}[$tty_session] "
fi
# PHASE 2A: Check for recent login (within 2 hours)
local recent_login=$(check_recent_login "root")
local login_recent=0
if [[ "$recent_login" =~ ^recent-login: ]]; then
login_recent=1
details="${details}[$recent_login] "
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
# PHASE 2A: Check for maintenance mode
local maint_mode=$(check_maintenance_mode)
local in_maintenance=0
if [[ "$maint_mode" =~ ^maintenance-mode: ]]; then
in_maintenance=1
details="${details}[$maint_mode] "
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))
# PHASE 2A: Reduce if TTY session present (real admin at terminal)
[ "$has_tty" -eq 1 ] && base_risk=$((base_risk - 7))
# PHASE 2A: Reduce if recent login (logged in within 2h)
[ "$login_recent" -eq 1 ] && base_risk=$((base_risk - 8))
# PHASE 2A: Reduce if maintenance mode
[ "$in_maintenance" -eq 1 ] && base_risk=$((base_risk - 14))
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
[ "$has_tty" -eq 1 ] && [ "$base_risk" -gt 1 ] && base_risk=$((base_risk - 1))
[ "$login_recent" -eq 1 ] && [ "$base_risk" -gt 1 ] && base_risk=$((base_risk - 1))
[ "$in_maintenance" -eq 1 ] && [ "$base_risk" -gt 1 ] && base_risk=$((base_risk - 1))
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
[ "$has_tty" -eq 1 ] && base_risk=$((base_risk - 2))
[ "$login_recent" -eq 1 ] && base_risk=$((base_risk - 2))
[ "$in_maintenance" -eq 1 ] && base_risk=$((base_risk - 3))
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))
# PHASE 2A: Reduce slightly if TTY/login/maintenance (still suspicious, but less)
[ "$has_tty" -eq 1 ] && base_risk=$((base_risk - 5))
[ "$login_recent" -eq 1 ] && base_risk=$((base_risk - 5))
[ "$in_maintenance" -eq 1 ] && base_risk=$((base_risk - 8))
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
# PHASE 2A: Check for TTY session, recent login, maintenance mode
local tty_session=$(check_tty_session "root")
local has_tty=0
[[ "$tty_session" =~ ^tty-session: ]] || [[ "$tty_session" =~ ^recent-tty: ]] && has_tty=1
local recent_login=$(check_recent_login "root")
local login_recent=0
[[ "$recent_login" =~ ^recent-login: ]] && login_recent=1
local maint_mode=$(check_maintenance_mode)
local in_maintenance=0
[[ "$maint_mode" =~ ^maintenance-mode: ]] && in_maintenance=1
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))
# PHASE 2A: Additional reductions
[ "$has_tty" -eq 1 ] && base_risk=$((base_risk - 3))
[ "$login_recent" -eq 1 ] && base_risk=$((base_risk - 3))
[ "$in_maintenance" -eq 1 ] && base_risk=$((base_risk - 4))
risk=$((risk + base_risk))
else
# Multiple users
local base_risk=25
[ "$admin_active" -eq 1 ] && base_risk=15
# PHASE 2A: Additional reductions
[ "$has_tty" -eq 1 ] && base_risk=$((base_risk - 4))
[ "$login_recent" -eq 1 ] && base_risk=$((base_risk - 4))
[ "$in_maintenance" -eq 1 ] && base_risk=$((base_risk - 5))
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
# PHASE 2A: Check TTY, recent login, maintenance mode
local tty_session=$(check_tty_session "root")
local has_tty=0
[[ "$tty_session" =~ ^tty-session: ]] || [[ "$tty_session" =~ ^recent-tty: ]] && has_tty=1
local recent_login=$(check_recent_login "root")
local login_recent=0
[[ "$recent_login" =~ ^recent-login: ]] && login_recent=1
local maint_mode=$(check_maintenance_mode)
local in_maintenance=0
[[ "$maint_mode" =~ ^maintenance-mode: ]] && in_maintenance=1
# 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))
# PHASE 2A: Check package ownership
local pkg_owned=$(check_package_ownership "/etc/passwd")
local is_pkg_owned=0
[[ "$pkg_owned" =~ ^pkg-owned: ]] && is_pkg_owned=1
# 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,"
[ "$has_tty" -eq 1 ] && context="${context}tty-session,"
[ "$login_recent" -eq 1 ] && context="${context}recent-login,"
[ "$in_maintenance" -eq 1 ] && context="${context}maintenance,"
[ "$is_pkg_owned" -eq 1 ] && context="${context}$pkg_owned,"
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))
# PHASE 2A: Additional reductions
[ "$has_tty" -eq 1 ] && base_risk=$((base_risk - 3))
[ "$login_recent" -eq 1 ] && base_risk=$((base_risk - 3))
[ "$in_maintenance" -eq 1 ] && base_risk=$((base_risk - 5))
[ "$is_pkg_owned" -eq 1 ] && base_risk=$((base_risk - 4))
# Ensure minimum risk
[ "$base_risk" -lt 0 ] && base_risk=0
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))
# PHASE 2A: Check package ownership
local pkg_owned_shadow=$(check_package_ownership "/etc/shadow")
local is_pkg_owned_shadow=0
[[ "$pkg_owned_shadow" =~ ^pkg-owned: ]] && is_pkg_owned_shadow=1
# Build context string for shadow
local context_shadow=""
[ "$pkg_activity" != "none" ] && context_shadow="${context_shadow}$pkg_activity,"
[ "$admin_active" -eq 1 ] && context_shadow="${context_shadow}admin-active,"
[ "$safe_window" -eq 1 ] && context_shadow="${context_shadow}safe-window,"
[ "$has_tty" -eq 1 ] && context_shadow="${context_shadow}tty-session,"
[ "$login_recent" -eq 1 ] && context_shadow="${context_shadow}recent-login,"
[ "$in_maintenance" -eq 1 ] && context_shadow="${context_shadow}maintenance,"
[ "$is_pkg_owned_shadow" -eq 1 ] && context_shadow="${context_shadow}$pkg_owned_shadow,"
context_shadow=${context_shadow%,}
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))
# PHASE 2A: Additional reductions
[ "$has_tty" -eq 1 ] && base_risk=$((base_risk - 3))
[ "$login_recent" -eq 1 ] && base_risk=$((base_risk - 3))
[ "$in_maintenance" -eq 1 ] && base_risk=$((base_risk - 5))
[ "$is_pkg_owned_shadow" -eq 1 ] && base_risk=$((base_risk - 4))
[ "$base_risk" -lt 0 ] && base_risk=0
if [ -n "$context_shadow" ]; then
findings="${findings}/etc/shadow-Modified-${shadow_hours}h-ago[$context_shadow] "
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))
# PHASE 2A: Check package ownership
local pkg_owned_group=$(check_package_ownership "/etc/group")
local is_pkg_owned_group=0
[[ "$pkg_owned_group" =~ ^pkg-owned: ]] && is_pkg_owned_group=1
# Build context string for group
local context_group=""
[ "$pkg_activity" != "none" ] && context_group="${context_group}$pkg_activity,"
[ "$admin_active" -eq 1 ] && context_group="${context_group}admin-active,"
[ "$safe_window" -eq 1 ] && context_group="${context_group}safe-window,"
[ "$has_tty" -eq 1 ] && context_group="${context_group}tty-session,"
[ "$login_recent" -eq 1 ] && context_group="${context_group}recent-login,"
[ "$in_maintenance" -eq 1 ] && context_group="${context_group}maintenance,"
[ "$is_pkg_owned_group" -eq 1 ] && context_group="${context_group}$pkg_owned_group,"
context_group=${context_group%,}
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))
# PHASE 2A: Additional reductions
[ "$has_tty" -eq 1 ] && base_risk=$((base_risk - 2))
[ "$login_recent" -eq 1 ] && base_risk=$((base_risk - 2))
[ "$in_maintenance" -eq 1 ] && base_risk=$((base_risk - 4))
[ "$is_pkg_owned_group" -eq 1 ] && base_risk=$((base_risk - 3))
[ "$base_risk" -lt 0 ] && base_risk=0
if [ -n "$context_group" ]; then
findings="${findings}/etc/group-Modified-${group_hours}h-ago[$context_group] "
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$/ || shell ~ /\/jailshell$/) 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 -e " ${YELLOW}Running comprehensive compromise detection...${NC}" >&2
echo "" >&2
# Load baseline for comparison
load_baseline
local total_risk=0
local all_findings=""
local all_mitigations=""
local evidence_items=0
# 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
# CONFIDENCE CALCULATION: Calculate how confident we are this is a real threat
local confidence_result=$(calculate_confidence_score "$total_risk" "$all_findings" "$all_mitigations")
local confidence_score=$(echo "$confidence_result" | cut -d'|' -f1)
local confidence_level=$(echo "$confidence_result" | cut -d'|' -f2)
local matched_patterns=$(echo "$confidence_result" | cut -d'|' -f3)
# Update baseline with current state (for future comparisons)
update_baseline
# Return: risk|findings|confidence_score|confidence_level|patterns
echo "$total_risk|$all_findings|$confidence_score|$confidence_level|$matched_patterns"
}
#
# AUTOMATED RESPONSE
#
trigger_automated_response() {
local ip=$1
local risk_score=${2:-0}
local username=$3
local panel=$4
# Skip if risk_score is not a valid number
if ! [[ "$risk_score" =~ ^[0-9]+$ ]]; then
echo "Warning: Invalid risk_score '$risk_score', skipping automated response" >&2
return 1
fi
# 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)
local confidence_score=$(echo "$compromise_result" | cut -d'|' -f3)
local confidence_level=$(echo "$compromise_result" | cut -d'|' -f4)
local matched_patterns=$(echo "$compromise_result" | cut -d'|' -f5-)
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 -e "${RED} Confidence: $confidence_level ($confidence_score/100)${NC}"
[ -n "$matched_patterns" ] && echo -e "${RED} Attack Patterns: $matched_patterns${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 -e "${YELLOW} Confidence: $confidence_level ($confidence_score/100)${NC}"
[ -n "$matched_patterns" ] && echo -e "${YELLOW} Attack Patterns: $matched_patterns${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}"
if [ "$confidence_level" = "HIGH" ]; then
echo " 1. HIGH confidence - Likely a real threat, investigate immediately"
echo " 2. Run rootkit scan: rkhunter --check"
echo " 3. Check for unauthorized access"
elif [ "$confidence_level" = "LOW" ]; then
echo " 1. LOW confidence - May be legitimate activity"
echo " 2. Review context in findings (look for [brackets])"
echo " 3. Consider whitelisting if this is normal for your environment"
else
echo " 1. Review all findings carefully"
echo " 2. Run rootkit scan: rkhunter --check"
echo " 3. Investigate recent account/file changes"
fi
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 -e "${BLUE} Confidence: $confidence_level ($confidence_score/100)${NC}"
[ -n "$matched_patterns" ] && echo -e "${BLUE} Note: $matched_patterns${NC}"
echo ""
echo "Issues found:"
for finding in $(echo "$compromise_findings" | tr ' ' '\n'); do
echo -e " ${BLUE}${NC} $(echo $finding | tr '-' ' ')"
done
echo ""
if [ "$confidence_level" = "LOW" ]; then
echo "LOW confidence - Likely legitimate activity with context:"
echo " • Look for [admin-active], [yum_activity], [cpanel], etc. in findings"
echo " • These indicate known legitimate sources"
echo " • Review when convenient, no urgent action needed"
else
echo "Review these items when convenient."
fi
else
echo -e "${GREEN}✓ No compromise indicators detected${NC}"
echo -e "${GREEN} Confidence: HIGH (100/100)${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