Files
chiefgeek a1a17e81a1 Sync all scripts from website downloads — 352 scripts total
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.
2026-05-25 03:31:08 +02:00

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 "$@"