diff --git a/modules/security/suspicious-login-monitor.sh b/modules/security/suspicious-login-monitor.sh index f4c2ea5..985b884 100755 --- a/modules/security/suspicious-login-monitor.sh +++ b/modules/security/suspicious-login-monitor.sh @@ -950,6 +950,109 @@ correlate_with_threat_intel() { # 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/,$//') + findings="${findings}Recent-Password-Changes:$pw_count-accounts " + details="${details}Changed-passwords:$pw_users " + + # Higher risk if root password was changed + if echo "$recent_pw_changes" | grep -q "^root:"; then + risk=$((risk + 35)) + else + risk=$((risk + 15)) + 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="" + 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) " + fi + fi + done + + if [ -n "$new_users" ]; then + findings="${findings}Recently-Created-Users:$new_users " + risk=$((risk + 25)) + 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 @@ -963,28 +1066,13 @@ check_backdoor_accounts() { risk=$((risk + 50)) fi - # Check for accounts with no password - local no_pass=$(awk -F: '$2 == "" {print $1}' /etc/shadow 2>/dev/null) + # 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' ',') " + findings="${findings}No-Password-Accounts:$(echo $no_pass | tr '\n' ',' | head -c 100) " risk=$((risk + 30)) fi - # Check for recently added users (last 7 days) - local recent_users=$(awk -F: -v cutoff=$(date -d '7 days ago' +%s) ' - $3 >= 1000 { - cmd = "stat -c %Y /home/" $1 " 2>/dev/null" - cmd | getline created - close(cmd) - if (created > cutoff) print $1 - } - ' /etc/passwd) - - if [ -n "$recent_users" ]; then - findings="${findings}Recently-Added-Users:$(echo $recent_users | tr '\n' ',') " - risk=$((risk + 20)) - 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 { @@ -1048,17 +1136,37 @@ check_system_file_tampering() { # 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 - findings="${findings}/etc/passwd-Modified-Recently " + local passwd_hours=$((passwd_age / 3600)) + findings="${findings}/etc/passwd-Modified-${passwd_hours}h-ago " risk=$((risk + 25)) 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 - findings="${findings}/etc/shadow-Modified-Recently " + local shadow_hours=$((shadow_age / 3600)) + findings="${findings}/etc/shadow-Modified-${shadow_hours}h-ago " risk=$((risk + 25)) fi + # Check /etc/group modification time + local group_age=$(($(date +%s) - $(stat -c %Y /etc/group 2>/dev/null))) + if [ "$group_age" -lt 86400 ]; then + local group_hours=$((group_age / 3600)) + findings="${findings}/etc/group-Modified-${group_hours}h-ago " + risk=$((risk + 20)) + 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)) + findings="${findings}/etc/gshadow-Modified-${gshadow_hours}h-ago " + risk=$((risk + 20)) + 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 != "" { @@ -1309,13 +1417,25 @@ perform_compromise_detection() { local total_risk=0 local all_findings="" - # Run all compromise checks - local result=$(check_backdoor_accounts) + # 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-) @@ -1692,6 +1812,33 @@ main() { 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-) @@ -1739,6 +1886,8 @@ main() { echo -e "${GREEN}✓ No compromise indicators detected${NC}" echo "" echo "System integrity checks:" + echo " ✓ No suspicious password changes detected" + echo " ✓ No suspicious user/group changes" echo " ✓ No unauthorized UID 0 accounts" echo " ✓ No suspicious SSH keys" echo " ✓ No system file tampering detected"