0be6dbe551
Issue: Bash arithmetic expansion does not support ternary operators Lines 1789-1791 used: base_risk=$((base_risk < 2 ? base_risk : base_risk - 1)) This caused syntax error: "error token is..." Fix: Replace ternary operators with proper conditional logic: - [ "$has_tty" -eq 1 ] && [ "$base_risk" -gt 1 ] && base_risk=$((base_risk - 1)) This achieves the same result (prevent risk from going below 1) without using unsupported ternary syntax. Testing: ✓ Syntax validation passed ✓ Script runs without errors Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
3100 lines
109 KiB
Bash
Executable File
3100 lines
109 KiB
Bash
Executable File
#!/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$/) next
|
||
# If we get here, shell is suspicious
|
||
print $1":"shell
|
||
}' /etc/passwd 2>/dev/null)
|
||
if [ -n "$backdoor_shells" ]; then
|
||
findings="${findings}Suspicious-Login-Shells:$backdoor_shells "
|
||
risk=$((risk + 30))
|
||
fi
|
||
|
||
# Check sudoers for unauthorized entries
|
||
if [ -f /etc/sudoers ]; then
|
||
local suspicious_sudo=$(grep -v "^#" /etc/sudoers 2>/dev/null | grep "NOPASSWD" | grep -v "root")
|
||
if [ -n "$suspicious_sudo" ]; then
|
||
findings="${findings}Suspicious-Sudoers-Entries "
|
||
risk=$((risk + 35))
|
||
fi
|
||
fi
|
||
|
||
echo "$risk|$findings"
|
||
}
|
||
|
||
check_suspicious_processes() {
|
||
echo " Checking for suspicious processes..." >&2
|
||
|
||
local findings=""
|
||
local risk=0
|
||
|
||
# Check for processes with suspicious names
|
||
local suspicious_procs=$(ps aux | grep -E "nc -l|ncat -l|/dev/tcp|bash -i|perl.*socket|python.*socket" | grep -v grep)
|
||
if [ -n "$suspicious_procs" ]; then
|
||
findings="${findings}Reverse-Shell-Processes "
|
||
risk=$((risk + 50))
|
||
fi
|
||
|
||
# Check for hidden processes (spaces in name)
|
||
local hidden_procs=$(ps aux | awk '$11 ~ /^[ ]+$/ {print $2}')
|
||
if [ -n "$hidden_procs" ]; then
|
||
findings="${findings}Hidden-Processes "
|
||
risk=$((risk + 40))
|
||
fi
|
||
|
||
# Check for processes running from /tmp or /dev/shm
|
||
local suspicious_tmp_procs=0
|
||
local tmp_proc_pids=$(ps aux | awk '$11 ~ /\/(tmp|dev\/shm)/ {print $2}' 2>/dev/null)
|
||
|
||
# FALSE POSITIVE REDUCTION: Check parent process
|
||
for pid in $tmp_proc_pids; do
|
||
local ppid=$(get_process_parent "$pid")
|
||
if [ -n "$ppid" ] && ! is_legitimate_parent "$ppid"; then
|
||
suspicious_tmp_procs=$((suspicious_tmp_procs + 1))
|
||
fi
|
||
done
|
||
|
||
if [ "$suspicious_tmp_procs" -gt 0 ]; then
|
||
findings="${findings}Suspicious-Processes-From-Tmp:$suspicious_tmp_procs "
|
||
risk=$((risk + 30))
|
||
fi
|
||
|
||
# Check for unusual network connections
|
||
local suspicious_conns=$(netstat -antp 2>/dev/null | grep ESTABLISHED | awk '{print $5}' | cut -d: -f1 | sort -u | wc -l)
|
||
if [ "$suspicious_conns" -gt 50 ]; then
|
||
findings="${findings}Excessive-Network-Connections:$suspicious_conns "
|
||
risk=$((risk + 20))
|
||
fi
|
||
|
||
echo "$risk|$findings"
|
||
}
|
||
|
||
check_backdoor_cron_jobs() {
|
||
echo " Checking for backdoor cron jobs..." >&2
|
||
|
||
local findings=""
|
||
local risk=0
|
||
|
||
# Check root crontab
|
||
local suspicious_cron=$(crontab -l 2>/dev/null | grep -v "^#" | grep -E "wget|curl|nc |bash -i|/tmp/|/dev/shm|base64")
|
||
if [ -n "$suspicious_cron" ]; then
|
||
findings="${findings}Suspicious-Root-Cron-Jobs "
|
||
risk=$((risk + 45))
|
||
fi
|
||
|
||
# Check /etc/cron.d for suspicious entries (exclude false positives)
|
||
local suspicious_cron_d=$(grep -rE "wget.*\||curl.*\||bash -i|/dev/tcp" /etc/cron.d/ 2>/dev/null | grep -v "^#" | grep -v "cpanel\|license_sync")
|
||
if [ -n "$suspicious_cron_d" ]; then
|
||
findings="${findings}Suspicious-Cron.d-Entries "
|
||
risk=$((risk + 45))
|
||
fi
|
||
|
||
# Check for cron jobs in unusual locations
|
||
local unusual_crons=$(find /tmp /var/tmp /dev/shm -name "cron*" -o -name "*.cron" 2>/dev/null)
|
||
if [ -n "$unusual_crons" ]; then
|
||
findings="${findings}Cron-Jobs-In-Unusual-Locations "
|
||
risk=$((risk + 40))
|
||
fi
|
||
|
||
echo "$risk|$findings"
|
||
}
|
||
|
||
check_bash_history_malicious_commands() {
|
||
echo " Analyzing bash history for malicious commands..." >&2
|
||
|
||
local findings=""
|
||
local risk=0
|
||
|
||
if [ -f /root/.bash_history ]; then
|
||
# Check for common attack commands (exclude legitimate installers)
|
||
local malicious_cmds=$(grep -E "wget.*\/tmp\/.*sh|curl.*(base64|eval)|nc -l|bash -i.*\/dev\/tcp|chmod \+s \/|chattr \+i" /root/.bash_history 2>/dev/null | grep -v "claude.ai\|github.com\|githubusercontent")
|
||
if [ -n "$malicious_cmds" ]; then
|
||
findings="${findings}Malicious-Commands-In-History "
|
||
risk=$((risk + 40))
|
||
fi
|
||
|
||
# Check for history clearing attempts
|
||
local history_tampering=$(grep -E "history -c|rm.*bash_history|unset HISTFILE" /root/.bash_history 2>/dev/null)
|
||
if [ -n "$history_tampering" ]; then
|
||
findings="${findings}History-Tampering-Detected "
|
||
risk=$((risk + 35))
|
||
fi
|
||
|
||
# Check for password hash modifications
|
||
local passwd_mods=$(grep -E "echo.*\/etc\/passwd|echo.*\/etc\/shadow|vipw|usermod.*-p" /root/.bash_history 2>/dev/null)
|
||
if [ -n "$passwd_mods" ]; then
|
||
findings="${findings}Password-File-Manipulation "
|
||
risk=$((risk + 45))
|
||
fi
|
||
fi
|
||
|
||
# Check if history is disabled
|
||
if ! grep -q "^HISTFILE=" /root/.bashrc 2>/dev/null && [ ! -f /root/.bash_history ]; then
|
||
findings="${findings}Bash-History-Disabled "
|
||
risk=$((risk + 25))
|
||
fi
|
||
|
||
echo "$risk|$findings"
|
||
}
|
||
|
||
check_web_shells() {
|
||
echo " Scanning for web shells..." >&2
|
||
|
||
local findings=""
|
||
local risk=0
|
||
local web_roots=""
|
||
|
||
# Determine web roots based on panel
|
||
if [ -d /home ]; then
|
||
web_roots="/home/*/public_html /var/www/html /usr/local/apache/htdocs"
|
||
fi
|
||
|
||
# FALSE POSITIVE REDUCTION: Only scan very recent files (last 7 days) with suspicious patterns
|
||
# Look for multiple suspicious indicators, not just one function
|
||
local suspicious_files=$(find $web_roots -type f -name "*.php" -mtime -7 2>/dev/null | head -50 | xargs grep -l "eval.*base64\|system.*base64\|exec.*\$_\|shell_exec.*POST\|assert.*base64" 2>/dev/null | head -10)
|
||
|
||
if [ -n "$suspicious_files" ]; then
|
||
local file_count=0
|
||
|
||
# Check each file more carefully
|
||
for file in $suspicious_files; do
|
||
local file_age=$(($(date +%s) - $(stat -c %Y "$file" 2>/dev/null)))
|
||
local file_days=$((file_age / 86400))
|
||
|
||
# Very recent files (< 24 hours) are more suspicious
|
||
if [ "$file_days" -lt 1 ]; then
|
||
file_count=$((file_count + 1))
|
||
elif [ "$file_days" -lt 3 ]; then
|
||
# 1-3 days old, check for obfuscation
|
||
if grep -q "base64_decode.*base64_decode\|eval.*eval\|gzinflate" "$file" 2>/dev/null; then
|
||
file_count=$((file_count + 1))
|
||
fi
|
||
fi
|
||
done
|
||
|
||
if [ "$file_count" -gt 0 ]; then
|
||
findings="${findings}Potential-Web-Shells:$file_count "
|
||
risk=$((risk + 35))
|
||
fi
|
||
fi
|
||
|
||
# Check for suspicious PHP files in unusual locations
|
||
local unusual_php=$(find /tmp /var/tmp /dev/shm -name "*.php" 2>/dev/null)
|
||
if [ -n "$unusual_php" ]; then
|
||
findings="${findings}PHP-Files-In-Tmp "
|
||
risk=$((risk + 40))
|
||
fi
|
||
|
||
echo "$risk|$findings"
|
||
}
|
||
|
||
check_rootkit_indicators() {
|
||
echo " Checking for rootkit indicators..." >&2
|
||
|
||
local findings=""
|
||
local risk=0
|
||
|
||
# Check for common rootkit files
|
||
local rootkit_files="/dev/.hid /usr/bin/.sniffer /usr/local/hide /lib/libproc.a"
|
||
for file in $rootkit_files; do
|
||
if [ -e "$file" ]; then
|
||
findings="${findings}Rootkit-File:$file "
|
||
risk=$((risk + 50))
|
||
fi
|
||
done
|
||
|
||
# Check for suspicious kernel modules
|
||
local suspicious_modules=$(lsmod | grep -E "^(diamond|phalanx|beastkit)")
|
||
if [ -n "$suspicious_modules" ]; then
|
||
findings="${findings}Suspicious-Kernel-Modules "
|
||
risk=$((risk + 50))
|
||
fi
|
||
|
||
# Check for modified binaries (ls, ps, netstat) - look for backdoor/rootkit strings only
|
||
for cmd in /bin/ls /bin/ps /bin/netstat; do
|
||
if [ -f "$cmd" ]; then
|
||
local strings_check=$(strings "$cmd" 2>/dev/null | grep -iE "backdoor|rootkit|\bhidden\b.*process|\bhide\b.*file")
|
||
if [ -n "$strings_check" ]; then
|
||
findings="${findings}Modified-Binary:$cmd "
|
||
risk=$((risk + 50))
|
||
fi
|
||
fi
|
||
done
|
||
|
||
# Check for hidden directories (... or . or . )
|
||
local hidden_dirs=$(find / -maxdepth 3 -type d -name "..." -o -name ". " -o -name ". " 2>/dev/null)
|
||
if [ -n "$hidden_dirs" ]; then
|
||
findings="${findings}Hidden-Directories "
|
||
risk=$((risk + 35))
|
||
fi
|
||
|
||
echo "$risk|$findings"
|
||
}
|
||
|
||
check_suspicious_network_activity() {
|
||
echo " Analyzing network connections..." >&2
|
||
|
||
local findings=""
|
||
local risk=0
|
||
|
||
# Check for connections to unusual ports (reverse shells often use 4444, 5555, 6666, 1337)
|
||
local suspicious_ports=$(netstat -antp 2>/dev/null | grep ESTABLISHED | awk '{print $4}' | cut -d: -f2 | grep -E "^(4444|5555|6666|1337|31337)$")
|
||
if [ -n "$suspicious_ports" ]; then
|
||
findings="${findings}Suspicious-Port-Connections "
|
||
risk=$((risk + 45))
|
||
fi
|
||
|
||
# Check for IRC connections (common in botnets)
|
||
local irc_conns=$(netstat -antp 2>/dev/null | grep ESTABLISHED | grep ":6667\|:6666\|:7000")
|
||
if [ -n "$irc_conns" ]; then
|
||
findings="${findings}IRC-Connections-Detected "
|
||
risk=$((risk + 30))
|
||
fi
|
||
|
||
# Check for excessive outbound connections from web server
|
||
if command -v pgrep &>/dev/null; then
|
||
local httpd_conns=$(lsof -p $(pgrep -d, httpd 2>/dev/null) 2>/dev/null | grep -c ESTABLISHED)
|
||
if [ "$httpd_conns" -gt 100 ]; then
|
||
findings="${findings}Excessive-Web-Server-Connections:$httpd_conns "
|
||
risk=$((risk + 25))
|
||
fi
|
||
fi
|
||
|
||
echo "$risk|$findings"
|
||
}
|
||
|
||
perform_compromise_detection() {
|
||
local ip=$1
|
||
|
||
echo " ${YELLOW}Running comprehensive compromise detection...${NC}" >&2
|
||
echo "" >&2
|
||
|
||
# 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
|
||
local username=$3
|
||
local panel=$4
|
||
|
||
# CRITICAL: 85-100
|
||
if [ $risk_score -ge $RISK_CRITICAL ] && [ "$SUSPICIOUS_LOGIN_AUTO_BLOCK" = "yes" ]; then
|
||
echo -e "\n${RED}🚨 CRITICAL RISK: Triggering automated response${NC}"
|
||
|
||
# 1. Block IP
|
||
if command -v csf &>/dev/null; then
|
||
echo " [1/3] Blocking IP via CSF..."
|
||
if csf -d "$ip" "Suspicious login (risk: $risk_score)" 2>/dev/null; then
|
||
echo -e " ${GREEN}✓ IP blocked via CSF${NC}"
|
||
else
|
||
echo -e " ${RED}✗ CSF block failed${NC}"
|
||
fi
|
||
else
|
||
echo " [1/3] Blocking IP via iptables..."
|
||
if iptables -I INPUT -s "$ip" -j DROP 2>/dev/null; then
|
||
echo -e " ${GREEN}✓ IP blocked via iptables${NC}"
|
||
else
|
||
echo -e " ${RED}✗ iptables block failed${NC}"
|
||
fi
|
||
fi
|
||
|
||
# 2. Trigger rkhunter scan
|
||
if [ "$SUSPICIOUS_LOGIN_AUTO_SCAN" = "yes" ] && [ -f "$MALWARE_SCANNER" ]; then
|
||
echo " [2/3] Triggering rootkit scan..."
|
||
# Run in background with timeout
|
||
timeout 300 bash "$MALWARE_SCANNER" --scanner rkhunter --quick &>/dev/null &
|
||
local scan_pid=$!
|
||
echo -e " ${GREEN}✓ Rootkit scan initiated (PID: $scan_pid)${NC}"
|
||
else
|
||
echo " [2/3] Rootkit scan (skipped - not configured)"
|
||
fi
|
||
|
||
# 3. CSI recommendation (cPanel only)
|
||
if [ "$panel" = "cpanel" ]; then
|
||
echo " [3/3] CSI scan recommended"
|
||
echo ""
|
||
echo " Run comprehensive security scan:"
|
||
echo " wget https://raw.githubusercontent.com/CpanelInc/tech-CSI/master/csi.pl"
|
||
echo " perl csi.pl --full"
|
||
fi
|
||
|
||
# HIGH: 70-84
|
||
elif [ $risk_score -ge $RISK_HIGH ]; then
|
||
echo -e "\n${YELLOW}⚠️ HIGH RISK: Manual review recommended${NC}"
|
||
|
||
if [ "$SUSPICIOUS_LOGIN_AUTO_BLOCK" = "yes" ] && command -v csf &>/dev/null; then
|
||
echo " [1/2] Adding temporary rate limit..."
|
||
if csf -tr "$ip" 300 "Rate limit: suspicious login" 2>/dev/null; then
|
||
echo -e " ${GREEN}✓ Rate limited for 5 minutes${NC}"
|
||
fi
|
||
fi
|
||
|
||
echo " [2/2] Schedule security scan for review"
|
||
|
||
# MEDIUM: 50-69
|
||
elif [ $risk_score -ge $RISK_MEDIUM ]; then
|
||
echo -e "\n${BLUE}ℹ️ MEDIUM RISK: Monitoring recommended${NC}"
|
||
|
||
# LOW: <50
|
||
else
|
||
echo -e "\n${GREEN}✓ LOW RISK: Logged for analysis${NC}"
|
||
fi
|
||
}
|
||
|
||
#
|
||
# REPORTING
|
||
#
|
||
|
||
generate_report() {
|
||
local panel=$1
|
||
|
||
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
||
echo -e "${CYAN} SUSPICIOUS LOGIN MONITOR - Integrated Security Report${NC}"
|
||
echo -e "${CYAN} Generated: $(date '+%Y-%m-%d %H:%M:%S')${NC}"
|
||
echo -e "${CYAN} Scanning: Last $HOURS hours${NC}"
|
||
echo -e "${CYAN} Panel: $panel${NC}"
|
||
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
||
echo ""
|
||
|
||
# Count total events
|
||
local total_events=$(cat "$SSH_EVENTS" "$PANEL_EVENTS" 2>/dev/null | wc -l)
|
||
local successful=$(cat "$SSH_EVENTS" "$PANEL_EVENTS" 2>/dev/null | grep -c "|success$")
|
||
local failed=$(cat "$SSH_EVENTS" "$PANEL_EVENTS" 2>/dev/null | grep -c "|failed$")
|
||
local root_count=$(cat "$SSH_EVENTS" "$PANEL_EVENTS" 2>/dev/null | grep -c "|root|")
|
||
|
||
if [ ! -s "$SUSPICIOUS_IPS" ]; then
|
||
echo -e "${GREEN}✓ No suspicious login activity detected${NC}"
|
||
echo ""
|
||
echo "SUMMARY:"
|
||
echo " Total Login Events: $total_events"
|
||
echo " Successful: $successful"
|
||
echo " Failed: $failed"
|
||
echo " Root Logins: $root_count"
|
||
echo ""
|
||
return 0
|
||
fi
|
||
|
||
local critical_count=$(awk -F'|' -v thresh=$RISK_CRITICAL '$2 >= thresh' "$SUSPICIOUS_IPS" | wc -l)
|
||
local high_count=$(awk -F'|' -v crit=$RISK_CRITICAL -v high=$RISK_HIGH '$2 >= high && $2 < crit' "$SUSPICIOUS_IPS" | wc -l)
|
||
|
||
if [ $critical_count -gt 0 ]; then
|
||
echo -e "${RED}🚨 CRITICAL ALERTS ($critical_count):${NC}"
|
||
echo ""
|
||
|
||
awk -F'|' -v thresh=$RISK_CRITICAL '$2 >= thresh' "$SUSPICIOUS_IPS" | head -n 5 | while IFS='|' read -r ip risk reasons successful failed root users services; do
|
||
echo -e " ${RED}[CRITICAL] $ip - Risk: $risk/100${NC}"
|
||
echo " ┌─────────────────────────────────────────────────────────"
|
||
echo " │ LOGIN EVENT"
|
||
echo " ├─────────────────────────────────────────────────────────"
|
||
echo " │ IP: $ip"
|
||
echo " │ Successful logins: ${successful:-0}"
|
||
echo " │ Failed attempts: ${failed:-0}"
|
||
echo " │ Root logins: ${root:-0}"
|
||
echo " │ Users: $users"
|
||
echo " │ Services: $services"
|
||
echo " │ Initial Risk Factors: $reasons"
|
||
echo " │ Initial Risk: $risk/100"
|
||
echo " │"
|
||
|
||
# Cross-reference with bot analyzer
|
||
echo " ├─────────────────────────────────────────────────────────"
|
||
echo " │ ACCESS LOG CORRELATION (bot-analyzer.sh)"
|
||
echo " ├─────────────────────────────────────────────────────────"
|
||
local correlation=$(correlate_with_access_logs "$ip" "$risk")
|
||
local corr_risk=$(echo "$correlation" | cut -d'|' -f1)
|
||
local corr_attacks=$(echo "$correlation" | cut -d'|' -f2-)
|
||
|
||
if [ "$corr_risk" != "0" ] && [ -n "$corr_attacks" ]; then
|
||
echo " │ ⚠️ Web attack activity detected:"
|
||
for attack in $corr_attacks; do
|
||
echo " │ - $attack"
|
||
done
|
||
risk=$((risk + corr_risk))
|
||
[ $risk -gt 100 ] && risk=100
|
||
else
|
||
echo " │ $corr_attacks"
|
||
fi
|
||
echo " │"
|
||
|
||
# Check IP reputation
|
||
echo " ├─────────────────────────────────────────────────────────"
|
||
echo " │ IP REPUTATION"
|
||
echo " ├─────────────────────────────────────────────────────────"
|
||
local rep_result=$(check_ip_reputation "$ip" "$risk")
|
||
local rep_risk=$(echo "$rep_result" | cut -d'|' -f1)
|
||
local rep_notes=$(echo "$rep_result" | cut -d'|' -f2-)
|
||
|
||
if [ "$rep_risk" != "0" ]; then
|
||
echo " │ $rep_notes"
|
||
risk=$((risk + rep_risk))
|
||
[ $risk -gt 100 ] && risk=100
|
||
else
|
||
echo " │ $rep_notes"
|
||
fi
|
||
echo " │"
|
||
|
||
# Threat intelligence
|
||
echo " ├─────────────────────────────────────────────────────────"
|
||
echo " │ THREAT INTELLIGENCE"
|
||
echo " ├─────────────────────────────────────────────────────────"
|
||
local threat_result=$(correlate_with_threat_intel "$ip")
|
||
local threat_risk=$(echo "$threat_result" | cut -d'|' -f1)
|
||
local threat_notes=$(echo "$threat_result" | cut -d'|' -f2-)
|
||
|
||
if [ "$threat_risk" != "0" ]; then
|
||
echo " │ ⚠️ $threat_notes"
|
||
risk=$((risk + threat_risk))
|
||
[ $risk -gt 100 ] && risk=100
|
||
else
|
||
echo " │ $threat_notes"
|
||
fi
|
||
|
||
echo " │"
|
||
|
||
# COMPROMISE DETECTION - Check if server is actually rooted
|
||
echo " ├─────────────────────────────────────────────────────────"
|
||
echo -e " │ ${RED}COMPROMISE DETECTION - System Integrity Check${NC}"
|
||
echo " ├─────────────────────────────────────────────────────────"
|
||
local compromise_result=$(perform_compromise_detection "$ip")
|
||
local compromise_risk=$(echo "$compromise_result" | cut -d'|' -f1)
|
||
local compromise_findings=$(echo "$compromise_result" | cut -d'|' -f2-)
|
||
|
||
if [ "$compromise_risk" -ge 50 ]; then
|
||
echo -e " │ ${RED}🚨 COMPROMISE CONFIRMED - $compromise_risk risk points${NC}"
|
||
echo " │"
|
||
echo " │ Indicators of compromise found:"
|
||
# Parse and display findings
|
||
for finding in $(echo "$compromise_findings" | tr ' ' '\n'); do
|
||
echo " │ • $finding"
|
||
done
|
||
risk=$((risk + compromise_risk))
|
||
[ $risk -gt 100 ] && risk=100
|
||
elif [ "$compromise_risk" -gt 0 ]; then
|
||
echo -e " │ ${YELLOW}⚠️ Suspicious indicators found - $compromise_risk risk points${NC}"
|
||
echo " │"
|
||
for finding in $(echo "$compromise_findings" | tr ' ' '\n'); do
|
||
echo " │ • $finding"
|
||
done
|
||
risk=$((risk + compromise_risk))
|
||
[ $risk -gt 100 ] && risk=100
|
||
else
|
||
echo -e " │ ${GREEN}✓ No compromise indicators detected${NC}"
|
||
echo " │ System integrity checks passed"
|
||
fi
|
||
|
||
echo " │"
|
||
echo " ├─────────────────────────────────────────────────────────"
|
||
if [ "$compromise_risk" -ge 50 ]; then
|
||
echo -e " │ ${RED}FINAL RISK SCORE: $risk/100 - SERVER LIKELY COMPROMISED${NC}"
|
||
else
|
||
echo -e " │ ${RED}FINAL RISK SCORE: $risk/100 - CRITICAL${NC}"
|
||
fi
|
||
echo " └─────────────────────────────────────────────────────────"
|
||
|
||
# Trigger automated response
|
||
trigger_automated_response "$ip" "$risk" "$(echo "$users" | cut -d',' -f1)" "$panel"
|
||
|
||
echo ""
|
||
done
|
||
fi
|
||
|
||
if [ $high_count -gt 0 ]; then
|
||
echo -e "${YELLOW}⚠️ HIGH ALERTS ($high_count):${NC}"
|
||
echo ""
|
||
|
||
awk -F'|' -v crit=$RISK_CRITICAL -v high=$RISK_HIGH '$2 >= high && $2 < crit' "$SUSPICIOUS_IPS" | head -n 5 | while IFS='|' read -r ip risk reasons successful failed root users services; do
|
||
echo -e " ${YELLOW}[HIGH] $ip - Risk: $risk/100${NC}"
|
||
echo " Successful: ${successful:-0} | Failed: ${failed:-0} | Root: ${root:-0}"
|
||
echo " Reasons: $reasons"
|
||
|
||
# Quick correlation
|
||
local correlation=$(correlate_with_access_logs "$ip" "$risk")
|
||
local corr_attacks=$(echo "$correlation" | cut -d'|' -f2-)
|
||
if [ -n "$corr_attacks" ] && [ "$corr_attacks" != "No access log activity" ]; then
|
||
echo " Web attacks: $corr_attacks"
|
||
fi
|
||
|
||
echo ""
|
||
done
|
||
fi
|
||
|
||
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
||
echo "SUMMARY:"
|
||
echo ""
|
||
echo " Total Login Events: $total_events"
|
||
echo " Successful: $successful"
|
||
echo " Failed: $failed"
|
||
echo " Root Logins: $root_count"
|
||
echo ""
|
||
echo " Suspicious IPs: $(wc -l < "$SUSPICIOUS_IPS")"
|
||
echo " Critical Risk: $critical_count"
|
||
echo " High Risk: $high_count"
|
||
echo ""
|
||
|
||
# Integration status
|
||
echo " Integration Status:"
|
||
local bot_report=$(ls -t "$TOOLKIT_ROOT"/tmp/bot_analysis_report_*.txt /tmp/bot_analysis_report_*.txt 2>/dev/null | head -n1)
|
||
[ -n "$bot_report" ] && echo " ✓ Bot Analyzer: Available" || echo " ✗ Bot Analyzer: No recent data"
|
||
[ -f "$IP_REPUTATION_LIB" ] && echo " ✓ IP Reputation: Available" || echo " ✗ IP Reputation: Not available"
|
||
[ -f "$THREAT_INTEL_LIB" ] && echo " ✓ Threat Intelligence: Available" || echo " ✗ Threat Intelligence: Not available"
|
||
|
||
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
||
}
|
||
|
||
#
|
||
# MAIN
|
||
#
|
||
|
||
main() {
|
||
echo -e "${CYAN}Starting Suspicious Login Monitor...${NC}"
|
||
echo ""
|
||
|
||
# Detect panel
|
||
local panel=$(detect_panel)
|
||
echo "Detected panel: $panel"
|
||
echo ""
|
||
|
||
# Parse logs (universal parsers first)
|
||
parse_ssh_logins "$HOURS"
|
||
parse_wtmp_logins "$HOURS"
|
||
parse_btmp_logins "$HOURS"
|
||
parse_sudo_escalation "$HOURS"
|
||
|
||
# Parse panel-specific logs
|
||
case "$panel" in
|
||
cpanel)
|
||
parse_cpanel_logins "$HOURS"
|
||
;;
|
||
plesk)
|
||
parse_plesk_logins "$HOURS"
|
||
;;
|
||
interworx)
|
||
parse_interworx_logins "$HOURS"
|
||
;;
|
||
esac
|
||
|
||
# Analyze
|
||
detect_anomalies
|
||
|
||
# Generate report
|
||
generate_report "$panel" | tee "$REPORT_FILE"
|
||
|
||
echo ""
|
||
echo "Report saved to: $REPORT_FILE"
|
||
|
||
# ALWAYS run system-wide compromise detection (regardless of login activity)
|
||
echo ""
|
||
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
||
echo -e "${CYAN} SYSTEM COMPROMISE DETECTION - Integrity Check${NC}"
|
||
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
||
echo ""
|
||
|
||
# Show root password change date (always display)
|
||
echo -e "${BLUE}Root Account Status:${NC}"
|
||
if [ -f /etc/shadow ]; then
|
||
local root_pw_date=$(awk -F: '$1 == "root" {print $3}' /etc/shadow 2>/dev/null)
|
||
if [ -n "$root_pw_date" ] && [ "$root_pw_date" != "0" ]; then
|
||
# Convert days since epoch to actual date
|
||
local pw_date=$(date -d "1970-01-01 + $root_pw_date days" "+%Y-%m-%d" 2>/dev/null)
|
||
local pw_age_days=$(( $(date +%s) / 86400 - root_pw_date ))
|
||
|
||
echo " Last password change: $pw_date ($pw_age_days days ago)"
|
||
|
||
# Warn if password hasn't been changed in a long time or changed very recently
|
||
if [ "$pw_age_days" -lt 1 ]; then
|
||
echo -e " ${RED}⚠️ Password changed TODAY - verify this was authorized${NC}"
|
||
elif [ "$pw_age_days" -lt 7 ]; then
|
||
echo -e " ${YELLOW}⚠️ Password changed within last 7 days${NC}"
|
||
elif [ "$pw_age_days" -gt 365 ]; then
|
||
echo -e " ${YELLOW}⚠️ Password not changed in over a year (consider rotating)${NC}"
|
||
else
|
||
echo " Status: Normal"
|
||
fi
|
||
else
|
||
echo -e " ${RED}⚠️ Unable to determine password age${NC}"
|
||
fi
|
||
fi
|
||
echo ""
|
||
|
||
local compromise_result=$(perform_compromise_detection "system-wide")
|
||
local compromise_risk=$(echo "$compromise_result" | cut -d'|' -f1)
|
||
local compromise_findings=$(echo "$compromise_result" | cut -d'|' -f2)
|
||
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
|