a1a17e81a1
Includes updated JS challenge scripts with Claude-User whitelist, same-site referer bypass, Blackbox-Exporter allowed bot, and all new exporters, cheat sheets, and automation scripts.
477 lines
18 KiB
Bash
477 lines
18 KiB
Bash
#!/usr/bin/env bash
|
|
|
|
#########################################################################################
|
|
#### user-audit.sh — Audit local user accounts, sudo access, SSH keys, and passwords ####
|
|
#### Shows last login, password age, group memberships, and security warnings ####
|
|
#### ####
|
|
#### Author: Phil Connor ####
|
|
#### Contact: contact@mylinux.work ####
|
|
#### License: MIT ####
|
|
#### Version 1.00 ####
|
|
#### ####
|
|
#### Usage: ####
|
|
#### ./user-audit.sh ####
|
|
#### ./user-audit.sh --system ####
|
|
#### ./user-audit.sh --section sudo,ssh ####
|
|
#### ####
|
|
#### See --help for all options. ####
|
|
#########################################################################################
|
|
|
|
set -euo pipefail
|
|
|
|
# ── Defaults ──────────────────────────────────────────────────────────
|
|
SECTIONS="${SECTIONS:-all}"
|
|
VERBOSE="${VERBOSE:-false}"
|
|
COLOR="${COLOR:-auto}"
|
|
FILTER="${FILTER:-human}"
|
|
WARN_DAYS="${WARN_DAYS:-14}"
|
|
|
|
# ── State ─────────────────────────────────────────────────────────────
|
|
SCRIPT_NAME="$(basename "$0")"
|
|
readonly SCRIPT_NAME
|
|
TOTAL_USERS=0
|
|
SUDO_USERS=0
|
|
SSH_KEY_USERS=0
|
|
PASSWORD_WARNINGS=0
|
|
|
|
# ── Colors ────────────────────────────────────────────────────────────
|
|
setup_colors() {
|
|
if [[ "$COLOR" == "never" ]]; then
|
|
RED="" GREEN="" YELLOW="" CYAN="" BOLD="" DIM="" RESET=""
|
|
return
|
|
fi
|
|
if [[ "$COLOR" == "always" ]] || [[ -t 1 ]]; then
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[0;33m'
|
|
CYAN='\033[0;36m'
|
|
BOLD='\033[1m'
|
|
DIM='\033[2m'
|
|
RESET='\033[0m'
|
|
else
|
|
RED="" GREEN="" YELLOW="" CYAN="" BOLD="" DIM="" RESET=""
|
|
fi
|
|
}
|
|
|
|
# ── Logging ───────────────────────────────────────────────────────────
|
|
log() { echo -e "${CYAN}[INFO]${RESET} $*"; }
|
|
warn() { echo -e "${YELLOW}[WARN]${RESET} $*" >&2; }
|
|
verbose() { if [[ "$VERBOSE" == "true" ]]; then echo -e "${DIM}[DEBUG]${RESET} $*"; fi; }
|
|
|
|
# ── Helpers ───────────────────────────────────────────────────────────
|
|
section_header() {
|
|
echo ""
|
|
echo -e " ${BOLD}${CYAN}── $1 ──${RESET}"
|
|
echo ""
|
|
}
|
|
|
|
field() {
|
|
printf " ${BOLD}%-22s${RESET} %s\n" "$1" "$2"
|
|
}
|
|
|
|
field_color() {
|
|
printf " ${BOLD}%-22s${RESET} %b\n" "$1" "$2"
|
|
}
|
|
|
|
should_show() {
|
|
[[ "$SECTIONS" == "all" ]] || [[ ",$SECTIONS," == *",$1,"* ]]
|
|
}
|
|
|
|
is_sudo_user() {
|
|
local user="$1"
|
|
local sudo_groups="sudo wheel adm"
|
|
for grp in $sudo_groups; do
|
|
if getent group "$grp" 2>/dev/null | grep -qw "$user"; then
|
|
return 0
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
get_ssh_key_count() {
|
|
local user="$1"
|
|
local home_dir="$2"
|
|
local auth_file="${home_dir}/.ssh/authorized_keys"
|
|
if [[ -r "$auth_file" ]]; then
|
|
grep -cE "^(ssh-|ecdsa-|sk-)" "$auth_file" 2>/dev/null || echo "0"
|
|
else
|
|
echo "0"
|
|
fi
|
|
}
|
|
|
|
get_last_login() {
|
|
local user="$1"
|
|
local last_info
|
|
last_info=$(lastlog -u "$user" 2>/dev/null | tail -1)
|
|
if echo "$last_info" | grep -q "Never logged in"; then
|
|
echo "Never"
|
|
else
|
|
echo "$last_info" | awk '{print $4, $5, $6, $7, $9}' 2>/dev/null || echo "Unknown"
|
|
fi
|
|
}
|
|
|
|
get_password_age() {
|
|
local user="$1"
|
|
if [[ ! -r /etc/shadow ]]; then
|
|
echo "no-access"
|
|
return
|
|
fi
|
|
local shadow_entry
|
|
shadow_entry=$(grep "^${user}:" /etc/shadow 2>/dev/null || true)
|
|
if [[ -z "$shadow_entry" ]]; then
|
|
echo "no-entry"
|
|
return
|
|
fi
|
|
local last_change
|
|
last_change=$(echo "$shadow_entry" | cut -d: -f3)
|
|
if [[ -z "$last_change" || "$last_change" == "0" ]]; then
|
|
echo "must-change"
|
|
return
|
|
fi
|
|
local today
|
|
today=$(( $(date +%s) / 86400 ))
|
|
local age=$(( today - last_change ))
|
|
echo "$age"
|
|
}
|
|
|
|
get_password_max_days() {
|
|
local user="$1"
|
|
if [[ ! -r /etc/shadow ]]; then
|
|
echo ""
|
|
return
|
|
fi
|
|
local shadow_entry
|
|
shadow_entry=$(grep "^${user}:" /etc/shadow 2>/dev/null || true)
|
|
if [[ -z "$shadow_entry" ]]; then
|
|
echo ""
|
|
return
|
|
fi
|
|
local max_days
|
|
max_days=$(echo "$shadow_entry" | cut -d: -f5)
|
|
if [[ -z "$max_days" || "$max_days" == "99999" ]]; then
|
|
echo ""
|
|
else
|
|
echo "$max_days"
|
|
fi
|
|
}
|
|
|
|
password_status_color() {
|
|
local age="$1"
|
|
local max_days="$2"
|
|
|
|
if [[ "$age" == "no-access" ]]; then
|
|
echo "${DIM}(shadow unreadable)${RESET}"
|
|
return
|
|
fi
|
|
if [[ "$age" == "no-entry" ]]; then
|
|
echo "${DIM}N/A${RESET}"
|
|
return
|
|
fi
|
|
if [[ "$age" == "must-change" ]]; then
|
|
echo "${YELLOW}must change${RESET}"
|
|
PASSWORD_WARNINGS=$((PASSWORD_WARNINGS + 1))
|
|
return
|
|
fi
|
|
|
|
local status="${age} days"
|
|
if [[ -n "$max_days" ]]; then
|
|
local remaining=$(( max_days - age ))
|
|
if [[ "$remaining" -le 0 ]]; then
|
|
status="${RED}EXPIRED (${age}d / ${max_days}d max)${RESET}"
|
|
PASSWORD_WARNINGS=$((PASSWORD_WARNINGS + 1))
|
|
elif [[ "$remaining" -le "$WARN_DAYS" ]]; then
|
|
status="${YELLOW}${age}d (expires in ${remaining}d)${RESET}"
|
|
PASSWORD_WARNINGS=$((PASSWORD_WARNINGS + 1))
|
|
else
|
|
status="${GREEN}${age}d (${remaining}d remaining)${RESET}"
|
|
fi
|
|
else
|
|
status="${GREEN}${age}d (no expiry)${RESET}"
|
|
fi
|
|
echo "$status"
|
|
}
|
|
|
|
shell_color() {
|
|
local shell="$1"
|
|
case "$shell" in
|
|
*/nologin|*/false)
|
|
echo "${RED}${shell}${RESET}" ;;
|
|
*)
|
|
echo "${GREEN}${shell}${RESET}" ;;
|
|
esac
|
|
}
|
|
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
# USER LISTING
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
|
|
show_users() {
|
|
section_header "User Accounts"
|
|
|
|
printf " ${BOLD}%-16s %6s %-18s %-14s %s${RESET}\n" "USERNAME" "UID" "SHELL" "LAST LOGIN" "PASSWORD AGE"
|
|
printf " %s\n" "$(printf '%.0s─' {1..78})"
|
|
|
|
while IFS=: read -r username _ uid _ _ home shell; do
|
|
if [[ "$FILTER" == "human" && "$uid" -lt 1000 && "$username" != "root" ]]; then
|
|
continue
|
|
fi
|
|
|
|
TOTAL_USERS=$((TOTAL_USERS + 1))
|
|
|
|
local last_login
|
|
last_login=$(get_last_login "$username")
|
|
if [[ ${#last_login} -gt 14 ]]; then
|
|
last_login="${last_login:0:14}"
|
|
fi
|
|
|
|
local pw_age pw_max pw_status
|
|
pw_age=$(get_password_age "$username")
|
|
pw_max=$(get_password_max_days "$username")
|
|
pw_status=$(password_status_color "$pw_age" "$pw_max")
|
|
|
|
local shell_display
|
|
shell_display=$(shell_color "$shell")
|
|
|
|
printf " %-16s %6s %b %-14s %b\n" \
|
|
"$username" "$uid" \
|
|
"$(printf '%-18b' "$shell_display")" \
|
|
"$last_login" "$pw_status"
|
|
|
|
verbose " ${username}: home=${home} shell=${shell}"
|
|
done < /etc/passwd
|
|
}
|
|
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
# SUDO ACCESS
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
|
|
show_sudo() {
|
|
section_header "Sudo / Wheel Membership"
|
|
|
|
local found=0
|
|
while IFS=: read -r username _ uid _ _ _ _; do
|
|
if [[ "$FILTER" == "human" && "$uid" -lt 1000 && "$username" != "root" ]]; then
|
|
continue
|
|
fi
|
|
if is_sudo_user "$username"; then
|
|
local groups_list
|
|
groups_list=$(groups "$username" 2>/dev/null | cut -d: -f2 | xargs)
|
|
printf " ${GREEN}✓${RESET} %-20s %s\n" "$username" "$groups_list"
|
|
SUDO_USERS=$((SUDO_USERS + 1))
|
|
found=1
|
|
fi
|
|
done < /etc/passwd
|
|
|
|
if [[ "$found" -eq 0 ]]; then
|
|
echo " No sudo/wheel users found"
|
|
fi
|
|
|
|
# Check sudoers.d
|
|
if [[ -d /etc/sudoers.d ]] && [[ -r /etc/sudoers.d ]]; then
|
|
local sudoers_files
|
|
sudoers_files=$(find /etc/sudoers.d -type f ! -name '.*' 2>/dev/null | wc -l)
|
|
if [[ "$sudoers_files" -gt 0 ]]; then
|
|
echo ""
|
|
field "sudoers.d files:" "$sudoers_files"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
# SSH KEYS
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
|
|
show_ssh() {
|
|
section_header "SSH Authorized Keys"
|
|
|
|
printf " ${BOLD}%-20s %s${RESET}\n" "USERNAME" "KEY COUNT"
|
|
printf " %s\n" "$(printf '%.0s─' {1..35})"
|
|
|
|
local found=0
|
|
while IFS=: read -r username _ uid _ _ home _; do
|
|
if [[ "$FILTER" == "human" && "$uid" -lt 1000 && "$username" != "root" ]]; then
|
|
continue
|
|
fi
|
|
local key_count
|
|
key_count=$(get_ssh_key_count "$username" "$home")
|
|
if [[ "$key_count" -gt 0 ]]; then
|
|
printf " %-20s ${GREEN}%s${RESET}\n" "$username" "$key_count"
|
|
SSH_KEY_USERS=$((SSH_KEY_USERS + 1))
|
|
found=1
|
|
fi
|
|
done < /etc/passwd
|
|
|
|
if [[ "$found" -eq 0 ]]; then
|
|
echo " No SSH authorized keys found"
|
|
fi
|
|
}
|
|
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
# PASSWORD STATUS
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
|
|
show_passwords() {
|
|
section_header "Password Status"
|
|
|
|
if [[ ! -r /etc/shadow ]]; then
|
|
warn "Cannot read /etc/shadow — run as root for password details"
|
|
return
|
|
fi
|
|
|
|
printf " ${BOLD}%-16s %-12s %-12s %s${RESET}\n" "USERNAME" "LAST CHANGE" "MAX DAYS" "STATUS"
|
|
printf " %s\n" "$(printf '%.0s─' {1..62})"
|
|
|
|
while IFS=: read -r username _ uid _ _ _ _; do
|
|
if [[ "$FILTER" == "human" && "$uid" -lt 1000 && "$username" != "root" ]]; then
|
|
continue
|
|
fi
|
|
|
|
local shadow_entry
|
|
shadow_entry=$(grep "^${username}:" /etc/shadow 2>/dev/null || true)
|
|
if [[ -z "$shadow_entry" ]]; then
|
|
continue
|
|
fi
|
|
|
|
local pw_field last_change max_days
|
|
pw_field=$(echo "$shadow_entry" | cut -d: -f2)
|
|
last_change=$(echo "$shadow_entry" | cut -d: -f3)
|
|
max_days=$(echo "$shadow_entry" | cut -d: -f5)
|
|
|
|
# Skip locked/disabled accounts
|
|
if [[ "$pw_field" == "!" || "$pw_field" == "!!" || "$pw_field" == "*" ]]; then
|
|
local lock_status="${DIM}locked${RESET}"
|
|
printf " %-16s %-12s %-12s %b\n" "$username" "-" "-" "$lock_status"
|
|
continue
|
|
fi
|
|
|
|
local change_date="-"
|
|
if [[ -n "$last_change" && "$last_change" != "0" ]]; then
|
|
change_date=$(date -d "1970-01-01 + ${last_change} days" +%Y-%m-%d 2>/dev/null || echo "$last_change")
|
|
fi
|
|
|
|
local max_display="-"
|
|
if [[ -n "$max_days" && "$max_days" != "99999" ]]; then
|
|
max_display="${max_days}d"
|
|
fi
|
|
|
|
local pw_age pw_status
|
|
pw_age=$(get_password_age "$username")
|
|
pw_status=$(password_status_color "$pw_age" "$(get_password_max_days "$username")")
|
|
|
|
printf " %-16s %-12s %-12s %b\n" "$username" "$change_date" "$max_display" "$pw_status"
|
|
done < /etc/passwd
|
|
}
|
|
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
# SUMMARY
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
|
|
print_summary() {
|
|
echo ""
|
|
echo -e " ${BOLD}══════════════════════════════════════════${RESET}"
|
|
echo -e " ${BOLD}User Audit Summary${RESET}"
|
|
echo -e " ${BOLD}══════════════════════════════════════════${RESET}"
|
|
|
|
field "Total users shown:" "$TOTAL_USERS"
|
|
field_color "Sudo/wheel users:" "${GREEN}${SUDO_USERS}${RESET}"
|
|
field "Users with SSH keys:" "$SSH_KEY_USERS"
|
|
|
|
if [[ "$PASSWORD_WARNINGS" -gt 0 ]]; then
|
|
field_color "Password warnings:" "${YELLOW}${PASSWORD_WARNINGS}${RESET}"
|
|
else
|
|
field_color "Password warnings:" "${GREEN}0${RESET}"
|
|
fi
|
|
|
|
echo ""
|
|
}
|
|
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
# USAGE
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
${SCRIPT_NAME} — Audit local user accounts, sudo access, SSH keys, and passwords
|
|
|
|
USAGE:
|
|
${SCRIPT_NAME} [OPTIONS]
|
|
|
|
OPTIONS:
|
|
--human Show only human users (UID >= 1000 + root, default)
|
|
--system Include system users (all UIDs)
|
|
--section SECTIONS Comma-separated sections to show (default: all)
|
|
Available: users, sudo, ssh, passwords
|
|
--warn-days DAYS Password expiry warning threshold (default: ${WARN_DAYS})
|
|
--verbose Enable debug output
|
|
--no-color Disable colored output
|
|
--help Show this help
|
|
|
|
EXAMPLES:
|
|
# Audit human users (default)
|
|
./user-audit.sh
|
|
|
|
# Include system accounts
|
|
./user-audit.sh --system
|
|
|
|
# Show only sudo and SSH key info
|
|
./user-audit.sh --section sudo,ssh
|
|
|
|
# Full audit with password details (needs root)
|
|
sudo ./user-audit.sh --section passwords
|
|
EOF
|
|
}
|
|
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
# ARGUMENT PARSING
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
|
|
parse_args() {
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--human)
|
|
FILTER="human"; shift ;;
|
|
--system)
|
|
FILTER="system"; shift ;;
|
|
--section)
|
|
SECTIONS="$2"; shift 2 ;;
|
|
--warn-days)
|
|
WARN_DAYS="$2"; shift 2 ;;
|
|
--verbose)
|
|
VERBOSE="true"; shift ;;
|
|
--no-color)
|
|
COLOR="never"; shift ;;
|
|
--help|-h)
|
|
setup_colors
|
|
usage
|
|
exit 0 ;;
|
|
*)
|
|
echo "Unknown option: $1" >&2
|
|
echo "Run ${SCRIPT_NAME} --help for usage" >&2
|
|
exit 1 ;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
# MAIN
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
|
|
main() {
|
|
parse_args "$@"
|
|
setup_colors
|
|
|
|
echo ""
|
|
echo -e "${BOLD}User Audit — $(hostname -f 2>/dev/null || hostname)${RESET}"
|
|
echo -e "${DIM}$(date '+%Y-%m-%d %H:%M:%S %Z')${RESET}"
|
|
echo -e "Filter: ${FILTER}"
|
|
|
|
should_show "users" && show_users
|
|
should_show "sudo" && show_sudo
|
|
should_show "ssh" && show_ssh
|
|
should_show "passwords" && show_passwords
|
|
|
|
print_summary
|
|
}
|
|
|
|
main "$@"
|