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.
This commit is contained in:
Executable
+523
@@ -0,0 +1,523 @@
|
||||
#!/bin/bash
|
||||
################################################################################
|
||||
# Script Name: password-expiry-exporter.sh
|
||||
# Version: 1.0
|
||||
# Description: Prometheus textfile collector exporter for password expiry
|
||||
# Monitors password expiry for local and AD (domain) accounts
|
||||
#
|
||||
# Author: Phil Connor
|
||||
# Contact: contact@mylinux.work
|
||||
# Website: https://mylinux.work
|
||||
# License: MIT
|
||||
# Date: 2026-03-10
|
||||
#
|
||||
# Prerequisites:
|
||||
# - node_exporter with textfile collector enabled
|
||||
# - /var/lib/node_exporter directory exists
|
||||
# - Run as root (to read /etc/shadow and query all users)
|
||||
# - For AD accounts: SSSD/realm joined or ldapsearch + Kerberos ticket
|
||||
#
|
||||
# Usage:
|
||||
# # Export metrics for all non-system local users
|
||||
# sudo ./password-expiry-exporter.sh
|
||||
#
|
||||
# # Export for specific users
|
||||
# USER_LIST="jsmith,admin,svc-backup" sudo ./password-expiry-exporter.sh
|
||||
#
|
||||
# # Include domain (AD) users
|
||||
# INCLUDE_DOMAIN=1 sudo ./password-expiry-exporter.sh
|
||||
#
|
||||
# # Dry run (output to stdout)
|
||||
# sudo ./password-expiry-exporter.sh --dry-run
|
||||
#
|
||||
# # Debug mode
|
||||
# DEBUG=1 sudo ./password-expiry-exporter.sh
|
||||
#
|
||||
# Metrics Exported:
|
||||
# - linux_password_expiry_days{user,type} - Days until password expires (-1 = expired, -2 = never)
|
||||
# - linux_password_expired{user,type} - Whether the password has expired (1/0)
|
||||
# - linux_password_never_expires{user,type} - Whether the password is set to never expire (1/0)
|
||||
# - linux_password_last_change_days{user,type} - Days since the password was last changed
|
||||
# - linux_password_expiry_warning_days{user,type} - Warning period in days before expiry
|
||||
#
|
||||
# Configuration:
|
||||
# Environment: USER_LIST (comma-separated), INCLUDE_DOMAIN, MIN_UID
|
||||
# Config file: /etc/password-expiry-exporter.conf (one user per line)
|
||||
# Textfile directory: /var/lib/node_exporter
|
||||
#
|
||||
################################################################################
|
||||
|
||||
set -o pipefail
|
||||
|
||||
# ============================================================================
|
||||
# CONFIGURATION
|
||||
# ============================================================================
|
||||
|
||||
readonly VERSION="1.0"
|
||||
readonly SCRIPT_NAME="${0##*/}"
|
||||
readonly TEXTFILE_DIR="${TEXTFILE_DIR:-/var/lib/node_exporter}"
|
||||
readonly OUTPUT_FILE="${TEXTFILE_DIR}/password_expiry.prom"
|
||||
readonly CONFIG_FILE="${CONFIG_FILE:-/etc/password-expiry-exporter.conf}"
|
||||
readonly TMP_FILE="${OUTPUT_FILE}.$$"
|
||||
readonly MIN_UID="${MIN_UID:-1000}"
|
||||
readonly INCLUDE_DOMAIN="${INCLUDE_DOMAIN:-0}"
|
||||
|
||||
# Runtime flags
|
||||
DRY_RUN=false
|
||||
DEBUG=${DEBUG:-}
|
||||
|
||||
# ============================================================================
|
||||
# HELPER FUNCTIONS
|
||||
# ============================================================================
|
||||
|
||||
debug_echo() {
|
||||
if [[ -n "$DEBUG" ]]; then
|
||||
echo "[DEBUG] $*" >&2
|
||||
fi
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo "[ERROR] $*" >&2
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
rm -f "$TMP_FILE"
|
||||
}
|
||||
|
||||
trap cleanup EXIT
|
||||
|
||||
show_help() {
|
||||
cat <<EOF
|
||||
Usage: $SCRIPT_NAME [OPTIONS]
|
||||
|
||||
Prometheus textfile collector exporter for password expiry.
|
||||
Monitors password expiry for local and AD (domain) accounts.
|
||||
|
||||
OPTIONS:
|
||||
--dry-run Output metrics to stdout instead of writing to file
|
||||
--debug Enable debug output
|
||||
--help Show this help message
|
||||
--version Show version
|
||||
|
||||
CONFIGURATION:
|
||||
Users can be configured in three ways (in priority order):
|
||||
|
||||
1. Environment variable (comma-separated):
|
||||
USER_LIST="jsmith,admin" $SCRIPT_NAME
|
||||
|
||||
2. Config file (one user per line):
|
||||
/etc/password-expiry-exporter.conf
|
||||
|
||||
3. Auto-discovery (default):
|
||||
All local users with UID >= $MIN_UID
|
||||
|
||||
ENVIRONMENT VARIABLES:
|
||||
USER_LIST Comma-separated list of users to monitor
|
||||
CONFIG_FILE Path to config file (default: /etc/password-expiry-exporter.conf)
|
||||
TEXTFILE_DIR Textfile collector directory (default: /var/lib/node_exporter)
|
||||
MIN_UID Minimum UID for auto-discovery (default: 1000)
|
||||
INCLUDE_DOMAIN Set to 1 to include domain/AD users in auto-discovery
|
||||
DEBUG Enable debug output when set to any value
|
||||
|
||||
EXAMPLES:
|
||||
sudo $SCRIPT_NAME
|
||||
sudo $SCRIPT_NAME --dry-run
|
||||
USER_LIST="admin,deploy" sudo $SCRIPT_NAME
|
||||
INCLUDE_DOMAIN=1 sudo $SCRIPT_NAME
|
||||
DEBUG=1 sudo $SCRIPT_NAME --dry-run
|
||||
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
show_version() {
|
||||
echo "$SCRIPT_NAME version $VERSION"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# ACCOUNT DETECTION
|
||||
# ============================================================================
|
||||
|
||||
is_domain_joined() {
|
||||
if command -v realm &>/dev/null && realm list 2>/dev/null | grep -q "configured:"; then
|
||||
return 0
|
||||
elif [[ -f /etc/sssd/sssd.conf ]] && systemctl is-active sssd &>/dev/null; then
|
||||
return 0
|
||||
elif grep -qE "^passwd:.*sss|winbind" /etc/nsswitch.conf 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
strip_domain() {
|
||||
local user="$1"
|
||||
user="${user##*\\}"
|
||||
user="${user%%@*}"
|
||||
echo "$user"
|
||||
}
|
||||
|
||||
get_account_type() {
|
||||
local user="$1"
|
||||
local uid
|
||||
uid=$(id -u "$user" 2>/dev/null) || return 1
|
||||
|
||||
if grep -q "^${user}:" /etc/passwd 2>/dev/null; then
|
||||
echo "local"
|
||||
elif (( uid >= 100000 )); then
|
||||
echo "domain"
|
||||
else
|
||||
echo "local"
|
||||
fi
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# USER DISCOVERY
|
||||
# ============================================================================
|
||||
|
||||
load_users() {
|
||||
local users=()
|
||||
|
||||
if [[ -n "${USER_LIST:-}" ]]; then
|
||||
debug_echo "Loading users from USER_LIST environment variable"
|
||||
local raw_users=()
|
||||
IFS=',' read -ra raw_users <<< "$USER_LIST"
|
||||
for u in "${raw_users[@]}"; do
|
||||
users+=("$(strip_domain "$u")")
|
||||
done
|
||||
elif [[ -f "$CONFIG_FILE" ]]; then
|
||||
debug_echo "Loading users from config file: $CONFIG_FILE"
|
||||
while IFS= read -r line; do
|
||||
line="${line%%#*}"
|
||||
line="${line// /}"
|
||||
if [[ -n "$line" ]]; then
|
||||
users+=("$(strip_domain "$line")")
|
||||
fi
|
||||
done < "$CONFIG_FILE"
|
||||
else
|
||||
debug_echo "Auto-discovering local users with UID >= $MIN_UID"
|
||||
while IFS=: read -r username _ uid _; do
|
||||
if (( uid >= MIN_UID )) && (( uid < 65534 )); then
|
||||
users+=("$username")
|
||||
fi
|
||||
done < /etc/passwd
|
||||
|
||||
if [[ "$INCLUDE_DOMAIN" == "1" ]] && is_domain_joined; then
|
||||
debug_echo "Including domain users from getent"
|
||||
while IFS=: read -r username _ uid _; do
|
||||
if (( uid >= 100000 )); then
|
||||
users+=("$username")
|
||||
fi
|
||||
done < <(getent passwd 2>/dev/null)
|
||||
fi
|
||||
|
||||
# Fall back to root if no regular users found (e.g. containers, minimal installs)
|
||||
if [[ ${#users[@]} -eq 0 ]]; then
|
||||
debug_echo "No users with UID >= $MIN_UID found, falling back to root"
|
||||
users+=("root")
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ ${#users[@]} -eq 0 ]]; then
|
||||
log_error "No users found to monitor"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
debug_echo "Monitoring ${#users[@]} users: ${users[*]}"
|
||||
printf '%s\n' "${users[@]}"
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# PASSWORD EXPIRY QUERIES
|
||||
# ============================================================================
|
||||
|
||||
get_local_expiry_info() {
|
||||
local user="$1"
|
||||
|
||||
if ! chage_output=$(chage -l "$user" 2>/dev/null); then
|
||||
debug_echo "chage failed for $user"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local expiry_str last_change_str warn_str
|
||||
expiry_str=$(echo "$chage_output" | grep "Password expires" | cut -d: -f2- | xargs)
|
||||
last_change_str=$(echo "$chage_output" | grep "Last password change" | cut -d: -f2- | xargs)
|
||||
warn_str=$(echo "$chage_output" | grep "Number of days of warning" | cut -d: -f2- | xargs)
|
||||
|
||||
local now_epoch
|
||||
now_epoch=$(date +%s)
|
||||
|
||||
# Password expiry
|
||||
local expiry_days=-2 # default: never
|
||||
local expired=0
|
||||
local never_expires=1
|
||||
|
||||
if [[ -n "$expiry_str" && "$expiry_str" != "never" ]]; then
|
||||
local expiry_epoch
|
||||
expiry_epoch=$(date -d "$expiry_str" +%s 2>/dev/null) || true
|
||||
if [[ -n "$expiry_epoch" ]]; then
|
||||
expiry_days=$(( (expiry_epoch - now_epoch) / 86400 ))
|
||||
never_expires=0
|
||||
if (( expiry_days < 0 )); then
|
||||
expired=1
|
||||
expiry_days=-1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Last password change
|
||||
local last_change_days=-1
|
||||
if [[ -n "$last_change_str" && "$last_change_str" != "never" ]]; then
|
||||
local last_change_epoch
|
||||
last_change_epoch=$(date -d "$last_change_str" +%s 2>/dev/null) || true
|
||||
if [[ -n "$last_change_epoch" ]]; then
|
||||
last_change_days=$(( (now_epoch - last_change_epoch) / 86400 ))
|
||||
fi
|
||||
fi
|
||||
|
||||
# Warning days
|
||||
local warning_days=0
|
||||
if [[ -n "$warn_str" && "$warn_str" =~ ^[0-9]+$ ]]; then
|
||||
warning_days="$warn_str"
|
||||
fi
|
||||
|
||||
echo "${expiry_days}|${expired}|${never_expires}|${last_change_days}|${warning_days}"
|
||||
}
|
||||
|
||||
get_domain_expiry_info() {
|
||||
local user="$1"
|
||||
|
||||
# Try chage first (works via SSSD PAM integration)
|
||||
if chage_output=$(chage -l "$user" 2>/dev/null); then
|
||||
local expiry_str
|
||||
expiry_str=$(echo "$chage_output" | grep "Password expires" | cut -d: -f2- | xargs)
|
||||
|
||||
if [[ -n "$expiry_str" && "$expiry_str" != "never" ]]; then
|
||||
get_local_expiry_info "$user"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
# Fall back to ldapsearch
|
||||
if ! command -v ldapsearch &>/dev/null; then
|
||||
debug_echo "ldapsearch not available for domain user $user"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local domain dc_host base_dn
|
||||
domain=$(hostname -d 2>/dev/null || dnsdomainname 2>/dev/null || echo "")
|
||||
if [[ -z "$domain" ]]; then
|
||||
debug_echo "Cannot determine domain for ldapsearch"
|
||||
return 1
|
||||
fi
|
||||
|
||||
dc_host=$(host -t SRV "_ldap._tcp.${domain}" 2>/dev/null | head -1 | awk '{print $NF}' | sed 's/\.$//')
|
||||
base_dn=$(echo "$domain" | sed 's/\./,DC=/g; s/^/DC=/')
|
||||
|
||||
if [[ -z "$dc_host" ]]; then
|
||||
debug_echo "Cannot find domain controller for $domain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local result
|
||||
result=$(ldapsearch -LLL -H "ldap://${dc_host}" -Y GSSAPI -Q \
|
||||
-b "$base_dn" \
|
||||
"(sAMAccountName=${user})" \
|
||||
msDS-UserPasswordExpiryTimeComputed pwdLastSet 2>/dev/null)
|
||||
|
||||
local expiry_ticks last_set_ticks
|
||||
expiry_ticks=$(echo "$result" | grep "msDS-UserPasswordExpiryTimeComputed:" | awk '{print $2}')
|
||||
last_set_ticks=$(echo "$result" | grep "pwdLastSet:" | awk '{print $2}')
|
||||
|
||||
local now_epoch
|
||||
now_epoch=$(date +%s)
|
||||
|
||||
# Password expiry
|
||||
local expiry_days=-2
|
||||
local expired=0
|
||||
local never_expires=1
|
||||
|
||||
if [[ -n "$expiry_ticks" && "$expiry_ticks" != "0" && "$expiry_ticks" != "9223372036854775807" ]]; then
|
||||
local expiry_epoch
|
||||
expiry_epoch=$(echo "($expiry_ticks - 116444736000000000) / 10000000" | bc 2>/dev/null)
|
||||
if [[ -n "$expiry_epoch" ]]; then
|
||||
expiry_days=$(( (expiry_epoch - now_epoch) / 86400 ))
|
||||
never_expires=0
|
||||
if (( expiry_days < 0 )); then
|
||||
expired=1
|
||||
expiry_days=-1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Last password change
|
||||
local last_change_days=-1
|
||||
if [[ -n "$last_set_ticks" && "$last_set_ticks" != "0" ]]; then
|
||||
local last_set_epoch
|
||||
last_set_epoch=$(echo "($last_set_ticks - 116444736000000000) / 10000000" | bc 2>/dev/null)
|
||||
if [[ -n "$last_set_epoch" ]]; then
|
||||
last_change_days=$(( (now_epoch - last_set_epoch) / 86400 ))
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "${expiry_days}|${expired}|${never_expires}|${last_change_days}|0"
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# METRICS COLLECTION
|
||||
# ============================================================================
|
||||
|
||||
collect_metrics() {
|
||||
local users=()
|
||||
while IFS= read -r u; do
|
||||
users+=("$u")
|
||||
done < <(load_users)
|
||||
|
||||
local output=""
|
||||
local has_data=false
|
||||
|
||||
# Collect data for all users first
|
||||
declare -A user_data
|
||||
declare -A user_types
|
||||
|
||||
for user in "${users[@]}"; do
|
||||
local account_type
|
||||
account_type=$(get_account_type "$user") || continue
|
||||
user_types["$user"]="$account_type"
|
||||
|
||||
local info
|
||||
if [[ "$account_type" == "domain" ]]; then
|
||||
info=$(get_domain_expiry_info "$user") || continue
|
||||
else
|
||||
info=$(get_local_expiry_info "$user") || continue
|
||||
fi
|
||||
|
||||
user_data["$user"]="$info"
|
||||
has_data=true
|
||||
debug_echo "User $user ($account_type): $info"
|
||||
done
|
||||
|
||||
if [[ "$has_data" == "false" ]]; then
|
||||
log_error "Could not collect expiry data for any users"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Expiry days ---
|
||||
output+="# HELP linux_password_expiry_days Days until password expires (-1=expired, -2=never)\n"
|
||||
output+="# TYPE linux_password_expiry_days gauge\n"
|
||||
for user in "${users[@]}"; do
|
||||
[[ -z "${user_data[$user]:-}" ]] && continue
|
||||
local type="${user_types[$user]}"
|
||||
local days
|
||||
days=$(echo "${user_data[$user]}" | cut -d'|' -f1)
|
||||
output+="linux_password_expiry_days{user=\"${user}\",type=\"${type}\"} ${days}\n"
|
||||
done
|
||||
|
||||
# --- Expired flag ---
|
||||
output+="# HELP linux_password_expired Whether the password has expired (1=yes, 0=no)\n"
|
||||
output+="# TYPE linux_password_expired gauge\n"
|
||||
for user in "${users[@]}"; do
|
||||
[[ -z "${user_data[$user]:-}" ]] && continue
|
||||
local type="${user_types[$user]}"
|
||||
local expired
|
||||
expired=$(echo "${user_data[$user]}" | cut -d'|' -f2)
|
||||
output+="linux_password_expired{user=\"${user}\",type=\"${type}\"} ${expired}\n"
|
||||
done
|
||||
|
||||
# --- Never expires flag ---
|
||||
output+="# HELP linux_password_never_expires Whether the password is set to never expire (1=yes, 0=no)\n"
|
||||
output+="# TYPE linux_password_never_expires gauge\n"
|
||||
for user in "${users[@]}"; do
|
||||
[[ -z "${user_data[$user]:-}" ]] && continue
|
||||
local type="${user_types[$user]}"
|
||||
local never
|
||||
never=$(echo "${user_data[$user]}" | cut -d'|' -f3)
|
||||
output+="linux_password_never_expires{user=\"${user}\",type=\"${type}\"} ${never}\n"
|
||||
done
|
||||
|
||||
# --- Last change days ---
|
||||
output+="# HELP linux_password_last_change_days Days since the password was last changed\n"
|
||||
output+="# TYPE linux_password_last_change_days gauge\n"
|
||||
for user in "${users[@]}"; do
|
||||
[[ -z "${user_data[$user]:-}" ]] && continue
|
||||
local type="${user_types[$user]}"
|
||||
local last_change
|
||||
last_change=$(echo "${user_data[$user]}" | cut -d'|' -f4)
|
||||
output+="linux_password_last_change_days{user=\"${user}\",type=\"${type}\"} ${last_change}\n"
|
||||
done
|
||||
|
||||
# --- Warning days ---
|
||||
output+="# HELP linux_password_expiry_warning_days Warning period in days before password expiry\n"
|
||||
output+="# TYPE linux_password_expiry_warning_days gauge\n"
|
||||
for user in "${users[@]}"; do
|
||||
[[ -z "${user_data[$user]:-}" ]] && continue
|
||||
local type="${user_types[$user]}"
|
||||
local warn
|
||||
warn=$(echo "${user_data[$user]}" | cut -d'|' -f5)
|
||||
output+="linux_password_expiry_warning_days{user=\"${user}\",type=\"${type}\"} ${warn}\n"
|
||||
done
|
||||
|
||||
printf '%b' "$output"
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# OUTPUT
|
||||
# ============================================================================
|
||||
|
||||
write_metrics() {
|
||||
local metrics
|
||||
metrics=$(collect_metrics)
|
||||
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
echo "$metrics"
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ ! -d "$TEXTFILE_DIR" ]]; then
|
||||
log_error "Textfile collector directory does not exist: $TEXTFILE_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "$metrics" > "$TMP_FILE"
|
||||
mv "$TMP_FILE" "$OUTPUT_FILE"
|
||||
debug_echo "Metrics written to $OUTPUT_FILE"
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# MAIN
|
||||
# ============================================================================
|
||||
|
||||
main() {
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--dry-run)
|
||||
DRY_RUN=true
|
||||
shift
|
||||
;;
|
||||
--debug)
|
||||
DEBUG=1
|
||||
shift
|
||||
;;
|
||||
--help|-h)
|
||||
show_help
|
||||
;;
|
||||
--version|-v)
|
||||
show_version
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown option: $1"
|
||||
echo "Use --help for usage information" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
log_error "This script must be run as root to read password expiry for all users"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
write_metrics
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user