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.
349 lines
13 KiB
Bash
Executable File
349 lines
13 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
#########################################################################################
|
|
#### password-expiry-check.sh — Check password expiry for AD and local accounts ####
|
|
#### Auto-detects domain vs local. Desktop notifications when expiry is near. ####
|
|
#### Requires: bash, chage. Optional: ldapsearch, notify-send, zenity ####
|
|
#### ####
|
|
#### Author: Phil Connor ####
|
|
#### Contact: contact@mylinux.work ####
|
|
#### License: MIT ####
|
|
#### Version 1.02 ####
|
|
#### ####
|
|
#### Usage: ####
|
|
#### ./password-expiry-check.sh ####
|
|
#### ./password-expiry-check.sh -w 30 ####
|
|
#### ./password-expiry-check.sh -u jdoe --test ####
|
|
#### ####
|
|
#### See --help for all options. ####
|
|
#########################################################################################
|
|
|
|
set -u
|
|
|
|
DEBUG="${DEBUG:-}"
|
|
TEST_MODE=false
|
|
QUIET=false
|
|
WARNING_DAYS=14
|
|
USERNAME="${USER}"
|
|
ACCOUNT_TYPE=""
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
Usage: $(basename "$0") [OPTIONS]
|
|
|
|
Check password expiration for domain (AD/SSSD) and local Linux accounts.
|
|
Auto-detects account type. Sends desktop notifications when expiry is near.
|
|
|
|
Options:
|
|
-w, --warning-days N Warning threshold in days (default: 14)
|
|
-u, --user USER Check a specific user (default: current user)
|
|
-q, --quiet Suppress output unless password is within warning window
|
|
--debug Show debug output
|
|
--test Force notification even if expiry is not near
|
|
-h, --help Show this help
|
|
|
|
Environment:
|
|
DEBUG Set non-empty to enable debug output
|
|
|
|
Examples:
|
|
$(basename "$0") # check current user, 14-day threshold
|
|
$(basename "$0") -w 30 # warn if expiring within 30 days
|
|
$(basename "$0") -u jdoe # check a specific user
|
|
$(basename "$0") --test # force notification for testing
|
|
$(basename "$0") --debug # show detection and query details
|
|
$(basename "$0") -q # silent unless expiry is near (for /etc/bashrc)
|
|
|
|
Schedule:
|
|
@daily /path/to/$(basename "$0")
|
|
EOF
|
|
}
|
|
|
|
# Parse flags
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
-h|--help) usage; exit 0 ;;
|
|
--debug) DEBUG=1 ;;
|
|
--test) TEST_MODE=true ;;
|
|
-q|--quiet) QUIET=true ;;
|
|
-w|--warning-days) WARNING_DAYS="$2"; shift ;;
|
|
-u|--user) USERNAME="$2"; shift ;;
|
|
[0-9]*) WARNING_DAYS="$1" ;; # legacy positional arg
|
|
*) echo "Unknown option: $1" >&2; usage; exit 1 ;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
# Strip domain prefix (DOMAIN\user) or suffix (user@domain) for queries
|
|
SAM_ACCOUNT_NAME="${USERNAME##*\\}"
|
|
SAM_ACCOUNT_NAME="${SAM_ACCOUNT_NAME%%@*}"
|
|
|
|
debug_echo() {
|
|
if [[ -n "$DEBUG" ]]; then
|
|
echo "[DEBUG] $*" >&2
|
|
fi
|
|
}
|
|
|
|
debug_echo "USERNAME=$USERNAME SAM_ACCOUNT_NAME=$SAM_ACCOUNT_NAME"
|
|
|
|
# --- Helper functions ---
|
|
|
|
die() { echo "ERROR: $*" >&2; exit 1; }
|
|
|
|
show_notification() {
|
|
local title="$1"
|
|
local message="$2"
|
|
local urgency="${3:-normal}" # low, normal, critical
|
|
|
|
# Terminal banner (always shown)
|
|
# Wrap message to fit within the box (max ~42 chars per line)
|
|
local wrapped
|
|
wrapped=$(echo "$message" | fold -s -w 42)
|
|
|
|
# Strip emoji for terminal (variable-width chars break alignment)
|
|
local term_title="${title#⚠ }"
|
|
|
|
echo ""
|
|
echo "╔══════════════════════════════════════════════╗"
|
|
echo "║ ║"
|
|
printf "║ %-42s ║\n" "$term_title"
|
|
echo "║──────────────────────────────────────────────║"
|
|
while IFS= read -r line; do
|
|
printf "║ %-42s ║\n" "$line"
|
|
done <<< "$wrapped"
|
|
echo "║ ║"
|
|
echo "╚══════════════════════════════════════════════╝"
|
|
echo ""
|
|
|
|
# Desktop dialog — requires user to click OK to dismiss
|
|
if command -v zenity &>/dev/null && [[ -n "${DISPLAY:-}${WAYLAND_DISPLAY:-}" ]]; then
|
|
zenity --warning --title="$title" --text="$message" --width=400 2>/dev/null &
|
|
elif command -v notify-send &>/dev/null && [[ -n "${DISPLAY:-}${WAYLAND_DISPLAY:-}" ]]; then
|
|
# Fallback: notify-send with critical urgency (persistent on most DEs)
|
|
notify-send --urgency=critical --icon=dialog-warning "$title" "$message"
|
|
fi
|
|
}
|
|
|
|
# --- Account type detection ---
|
|
|
|
detect_account_type() {
|
|
# Check if the system is domain-joined
|
|
local domain_joined=false
|
|
|
|
if command -v realm &>/dev/null && realm list 2>/dev/null | grep -q "configured:"; then
|
|
domain_joined=true
|
|
debug_echo "Domain detected via: realm"
|
|
elif [[ -f /etc/sssd/sssd.conf ]] && systemctl is-active sssd &>/dev/null; then
|
|
domain_joined=true
|
|
debug_echo "Domain detected via: sssd.conf"
|
|
elif grep -qE "^passwd:.*sss|winbind" /etc/nsswitch.conf 2>/dev/null; then
|
|
domain_joined=true
|
|
debug_echo "Domain detected via: nsswitch.conf"
|
|
fi
|
|
debug_echo "domain_joined=$domain_joined"
|
|
|
|
if [[ "$domain_joined" == true ]]; then
|
|
# Check if this specific user is a domain account
|
|
# Domain users typically have a UID >= 100000 (SSSD default range)
|
|
# and their passwd entry comes from sss/winbind, not local files
|
|
local user_source
|
|
user_source=$(getent passwd "$SAM_ACCOUNT_NAME" 2>/dev/null | cut -d: -f3)
|
|
|
|
if [[ -n "$user_source" ]] && (( user_source >= 100000 )); then
|
|
ACCOUNT_TYPE="domain"
|
|
return
|
|
fi
|
|
|
|
# Also check if user exists in /etc/passwd (local) vs only via NSS
|
|
if grep -q "^${SAM_ACCOUNT_NAME}:" /etc/passwd 2>/dev/null; then
|
|
ACCOUNT_TYPE="local"
|
|
return
|
|
fi
|
|
|
|
# User not in /etc/passwd but system is domain-joined — likely domain account
|
|
ACCOUNT_TYPE="domain"
|
|
return
|
|
fi
|
|
|
|
ACCOUNT_TYPE="local"
|
|
}
|
|
|
|
# --- Local account expiry ---
|
|
|
|
get_expiry_local() {
|
|
local expiry_str
|
|
expiry_str=$(chage -l "$SAM_ACCOUNT_NAME" 2>/dev/null | grep "Password expires" | cut -d: -f2- | xargs || true)
|
|
|
|
if [[ -z "$expiry_str" || "$expiry_str" == "never" ]]; then
|
|
echo "never"
|
|
return
|
|
fi
|
|
|
|
# Convert to epoch
|
|
date -d "$expiry_str" +%s 2>/dev/null || echo ""
|
|
}
|
|
|
|
# --- Domain account expiry via SSSD/chage ---
|
|
|
|
get_expiry_sssd() {
|
|
local expiry_str
|
|
expiry_str=$(chage -l "$SAM_ACCOUNT_NAME" 2>/dev/null | grep "Password expires" | cut -d: -f2- | xargs || true)
|
|
debug_echo "SSSD/chage result for '$SAM_ACCOUNT_NAME': '$expiry_str'"
|
|
|
|
if [[ -z "$expiry_str" || "$expiry_str" == "never" ]]; then
|
|
echo ""
|
|
return
|
|
fi
|
|
|
|
# Convert to epoch
|
|
date -d "$expiry_str" +%s 2>/dev/null || echo ""
|
|
}
|
|
|
|
# --- Domain account expiry via ldapsearch ---
|
|
|
|
get_expiry_ldap() {
|
|
if ! command -v ldapsearch &>/dev/null; then
|
|
debug_echo "ldapsearch not found, skipping LDAP method"
|
|
echo ""
|
|
return
|
|
fi
|
|
|
|
local domain realm dc_host base_dn
|
|
domain=$(hostname -d 2>/dev/null || dnsdomainname 2>/dev/null || echo "")
|
|
debug_echo "LDAP domain: '$domain'"
|
|
if [[ -z "$domain" ]]; then
|
|
debug_echo "Could not determine domain, skipping LDAP"
|
|
echo ""
|
|
return
|
|
fi
|
|
|
|
realm=$(echo "$domain" | tr '[:lower:]' '[:upper:]')
|
|
local dc_hosts
|
|
dc_hosts=$(host -t SRV "_ldap._tcp.${domain}" 2>/dev/null | awk '{print $NF}' | sed 's/\.$//')
|
|
base_dn=$(echo "$domain" | sed 's/\./,DC=/g; s/^/DC=/')
|
|
debug_echo "LDAP dc_hosts='$(echo $dc_hosts)' base_dn='$base_dn'"
|
|
|
|
if [[ -z "$dc_hosts" ]]; then
|
|
debug_echo "Could not find domain controller via SRV lookup"
|
|
echo ""
|
|
return
|
|
fi
|
|
|
|
local result=""
|
|
local stderr_target="/dev/null"
|
|
[[ -n "$DEBUG" ]] && stderr_target="/dev/stderr"
|
|
|
|
for dc_host in $dc_hosts; do
|
|
debug_echo "Trying DC: ldapsearch -H ldap://${dc_host} -b $base_dn (sAMAccountName=${SAM_ACCOUNT_NAME})"
|
|
result=$(ldapsearch -LLL -H "ldap://${dc_host}" -Y GSSAPI -Q \
|
|
-b "$base_dn" \
|
|
"(sAMAccountName=${SAM_ACCOUNT_NAME})" \
|
|
msDS-UserPasswordExpiryTimeComputed 2>"$stderr_target" \
|
|
| grep "msDS-UserPasswordExpiryTimeComputed:" | awk '{print $2}' || true)
|
|
|
|
if [[ -n "$result" ]]; then
|
|
debug_echo "Got result from $dc_host"
|
|
break
|
|
fi
|
|
debug_echo "No result from $dc_host, trying next..."
|
|
done
|
|
debug_echo "LDAP msDS-UserPasswordExpiryTimeComputed: '$result'"
|
|
|
|
if [[ -z "$result" || "$result" == "0" || "$result" == "9223372036854775807" ]]; then
|
|
debug_echo "LDAP result empty, zero, or never-expire sentinel"
|
|
echo ""
|
|
return
|
|
fi
|
|
|
|
# AD timestamp is 100-nanosecond intervals since 1601-01-01
|
|
# Convert to Unix epoch: subtract 116444736000000000, divide by 10000000
|
|
local unix_epoch
|
|
unix_epoch=$(echo "($result - 116444736000000000) / 10000000" | bc 2>/dev/null)
|
|
debug_echo "Converted epoch: $unix_epoch"
|
|
echo "$unix_epoch"
|
|
}
|
|
|
|
# --- Main ---
|
|
|
|
detect_account_type
|
|
|
|
expiry_epoch=""
|
|
|
|
if [[ "$ACCOUNT_TYPE" == "domain" ]]; then
|
|
# Try SSSD/chage first (simplest on domain-joined systems)
|
|
expiry_epoch=$(get_expiry_sssd)
|
|
|
|
# Fall back to ldapsearch
|
|
if [[ -z "$expiry_epoch" ]]; then
|
|
expiry_epoch=$(get_expiry_ldap)
|
|
fi
|
|
|
|
if [[ -z "$expiry_epoch" ]]; then
|
|
if [[ "$QUIET" != "true" ]]; then
|
|
echo "Account Type: Domain"
|
|
echo "User: ${USERNAME}"
|
|
echo "Password is set to never expire or could not determine expiry date."
|
|
echo "Ensure this host is domain-joined (realm/SSSD) or has LDAP access."
|
|
fi
|
|
[[ "$TEST_MODE" == true ]] && show_notification "Password Expiry Test" "Account: ${USERNAME} (domain) — password never expires or expiry unknown. (--test mode)" "normal"
|
|
exit 0
|
|
fi
|
|
else
|
|
# Local account
|
|
expiry_epoch=$(get_expiry_local)
|
|
|
|
if [[ "$expiry_epoch" == "never" ]]; then
|
|
if [[ "$QUIET" != "true" ]]; then
|
|
echo "Account Type: Local"
|
|
echo "User: ${USERNAME}"
|
|
echo "Password is set to never expire."
|
|
fi
|
|
[[ "$TEST_MODE" == true ]] && show_notification "Password Expiry Test" "Account: ${USERNAME} (local) — password never expires. (--test mode)" "normal"
|
|
exit 0
|
|
fi
|
|
|
|
if [[ -z "$expiry_epoch" ]]; then
|
|
if [[ "$QUIET" != "true" ]]; then
|
|
echo "Account Type: Local"
|
|
echo "User: ${USERNAME}"
|
|
echo "Could not determine password expiry date."
|
|
fi
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
now_epoch=$(date +%s)
|
|
days_remaining=$(( (expiry_epoch - now_epoch) / 86400 ))
|
|
expiry_date=$(date -d "@${expiry_epoch}" "+%Y-%m-%d %H:%M" 2>/dev/null || date -r "${expiry_epoch}" "+%Y-%m-%d %H:%M" 2>/dev/null)
|
|
|
|
if [[ "$QUIET" == "true" ]] && (( days_remaining > WARNING_DAYS )) && [[ "$TEST_MODE" != true ]]; then
|
|
# Quiet mode and password is not near expiry — suppress all output
|
|
exit 0
|
|
fi
|
|
|
|
echo "Account Type: ${ACCOUNT_TYPE^}"
|
|
echo "User: ${USERNAME}"
|
|
echo "Password Expires: ${expiry_date}"
|
|
echo "Days Remaining: ${days_remaining}"
|
|
|
|
if (( days_remaining <= 0 )); then
|
|
show_notification \
|
|
"⚠ PASSWORD EXPIRED" \
|
|
"Your password EXPIRED (${expiry_date}). Run passwd to change it now." \
|
|
"critical"
|
|
elif (( days_remaining <= WARNING_DAYS )); then
|
|
urgency="normal"
|
|
(( days_remaining <= 3 )) && urgency="critical"
|
|
|
|
show_notification \
|
|
"⚠ Password Expiring Soon" \
|
|
"Your password expires in ${days_remaining} day(s) on ${expiry_date}. Change it with: passwd" \
|
|
"$urgency"
|
|
elif [[ "$TEST_MODE" == true ]]; then
|
|
show_notification \
|
|
"Password Expiry Test" \
|
|
"Your password expires in ${days_remaining} day(s) on ${expiry_date}. (--test mode)" \
|
|
"normal"
|
|
else
|
|
echo "Password expiry is more than ${WARNING_DAYS} days away. No notification needed."
|
|
fi
|