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.
586 lines
21 KiB
Bash
586 lines
21 KiB
Bash
#!/usr/bin/env bash
|
||
|
||
######################################################################################
|
||
#### ssh-key-auditor.sh — Audit SSH keys across user accounts for compliance ####
|
||
#### Scans authorized_keys, flags weak keys, detects duplicates, checks perms. ####
|
||
#### Requires: bash 4+, coreutils, ssh-keygen ####
|
||
#### ####
|
||
#### Author: Phil Connor ####
|
||
#### Contact: contact@mylinux.work ####
|
||
#### License: MIT ####
|
||
#### Version 1.01 ####
|
||
#### ####
|
||
#### Usage: ####
|
||
#### sudo ./ssh-key-auditor.sh ####
|
||
#### sudo ./ssh-key-auditor.sh --min-rsa-bits 4096 --format tap ####
|
||
#### ####
|
||
#### See --help for all options. ####
|
||
######################################################################################
|
||
|
||
set -euo pipefail
|
||
|
||
# ── Defaults ──────────────────────────────────────────────────────────
|
||
SCAN_PATH="${SCAN_PATH:-/home}"
|
||
SCAN_SYSTEM_USERS="${SCAN_SYSTEM_USERS:-false}"
|
||
MIN_RSA_BITS="${MIN_RSA_BITS:-2048}"
|
||
SKIP_DUPLICATES="${SKIP_DUPLICATES:-false}"
|
||
OUTPUT_FORMAT="${OUTPUT_FORMAT:-text}" # text, tap, junit
|
||
JUNIT_FILE="${JUNIT_FILE:-ssh-audit-results.xml}"
|
||
VERBOSE="${VERBOSE:-false}"
|
||
COLOR="${COLOR:-auto}"
|
||
|
||
# ── State ─────────────────────────────────────────────────────────────
|
||
PASS=0
|
||
FAIL=0
|
||
SKIP=0
|
||
TOTAL=0
|
||
RESULTS=()
|
||
START_TIME=""
|
||
|
||
# Key tracking for duplicate detection and summary
|
||
declare -A KEY_FINGERPRINTS # fingerprint -> "user1 user2 ..."
|
||
declare -A KEY_TYPE_COUNTS # type -> count
|
||
TOTAL_KEYS=0
|
||
USERS_WITH_KEYS=0
|
||
|
||
# ── Colors ────────────────────────────────────────────────────────────
|
||
setup_colors() {
|
||
if [[ "$COLOR" == "never" ]]; then
|
||
RED="" GREEN="" YELLOW="" BLUE="" BOLD="" RESET=""
|
||
return
|
||
fi
|
||
if [[ "$COLOR" == "always" ]] || [[ -t 1 ]]; then
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[0;33m'
|
||
BLUE='\033[0;34m'
|
||
BOLD='\033[1m'
|
||
RESET='\033[0m'
|
||
else
|
||
RED="" GREEN="" YELLOW="" BLUE="" BOLD="" RESET=""
|
||
fi
|
||
}
|
||
|
||
# ── Logging ───────────────────────────────────────────────────────────
|
||
log() { echo -e "${BLUE}[INFO]${RESET} $*"; }
|
||
warn() { echo -e "${YELLOW}[WARN]${RESET} $*" >&2; }
|
||
err() { echo -e "${RED}[ERROR]${RESET} $*" >&2; }
|
||
verbose() { if [[ "$VERBOSE" == "true" ]]; then echo -e "${BLUE}[DEBUG]${RESET} $*"; fi; }
|
||
|
||
# ── Test Result Recording ─────────────────────────────────────────────
|
||
record_pass() {
|
||
local name="$1"
|
||
local detail="${2:-}"
|
||
((PASS++)) || true
|
||
((TOTAL++)) || true
|
||
RESULTS+=("PASS|${name}|${detail}")
|
||
if [[ "$OUTPUT_FORMAT" == "tap" ]]; then
|
||
echo "ok ${TOTAL} - ${name}"
|
||
else
|
||
echo -e " ${GREEN}✓${RESET} ${name}${detail:+ — ${detail}}"
|
||
fi
|
||
}
|
||
|
||
record_fail() {
|
||
local name="$1"
|
||
local detail="${2:-}"
|
||
((FAIL++)) || true
|
||
((TOTAL++)) || true
|
||
RESULTS+=("FAIL|${name}|${detail}")
|
||
if [[ "$OUTPUT_FORMAT" == "tap" ]]; then
|
||
echo "not ok ${TOTAL} - ${name}"
|
||
[[ -n "$detail" ]] && echo " # ${detail}"
|
||
else
|
||
echo -e " ${RED}✗${RESET} ${name}${detail:+ — ${detail}}"
|
||
fi
|
||
}
|
||
|
||
record_skip() {
|
||
local name="$1"
|
||
local reason="${2:-}"
|
||
((SKIP++)) || true
|
||
((TOTAL++)) || true
|
||
RESULTS+=("SKIP|${name}|${reason}")
|
||
if [[ "$OUTPUT_FORMAT" == "tap" ]]; then
|
||
echo "ok ${TOTAL} - ${name} # SKIP ${reason}"
|
||
else
|
||
echo -e " ${YELLOW}⊘${RESET} ${name}${reason:+ — ${reason}}"
|
||
fi
|
||
}
|
||
|
||
# ── JUnit XML Writer ──────────────────────────────────────────────────
|
||
write_junit() {
|
||
local end_time
|
||
end_time=$(date +%s)
|
||
local duration=$(( end_time - START_TIME ))
|
||
|
||
cat > "$JUNIT_FILE" <<JUNIT_EOF
|
||
<?xml version="1.0" encoding="UTF-8"?>
|
||
<testsuites tests="${TOTAL}" failures="${FAIL}" skipped="${SKIP}" time="${duration}">
|
||
<testsuite name="ssh-key-auditor" tests="${TOTAL}" failures="${FAIL}" skipped="${SKIP}" time="${duration}">
|
||
JUNIT_EOF
|
||
|
||
for result in "${RESULTS[@]}"; do
|
||
local status name detail
|
||
status=$(echo "$result" | cut -d'|' -f1)
|
||
name=$(echo "$result" | cut -d'|' -f2)
|
||
detail=$(echo "$result" | cut -d'|' -f3)
|
||
|
||
# XML-escape
|
||
name="${name//&/&}"
|
||
name="${name//</<}"
|
||
name="${name//>/>}"
|
||
name="${name//\"/"}"
|
||
detail="${detail//&/&}"
|
||
detail="${detail//</<}"
|
||
detail="${detail//>/>}"
|
||
detail="${detail//\"/"}"
|
||
|
||
case "$status" in
|
||
PASS)
|
||
echo " <testcase name=\"${name}\" classname=\"ssh-audit\">" >> "$JUNIT_FILE"
|
||
[[ -n "$detail" ]] && echo " <system-out>${detail}</system-out>" >> "$JUNIT_FILE"
|
||
echo " </testcase>" >> "$JUNIT_FILE"
|
||
;;
|
||
FAIL)
|
||
echo " <testcase name=\"${name}\" classname=\"ssh-audit\">" >> "$JUNIT_FILE"
|
||
echo " <failure message=\"${detail}\">FAILED: ${name} — ${detail}</failure>" >> "$JUNIT_FILE"
|
||
echo " </testcase>" >> "$JUNIT_FILE"
|
||
;;
|
||
SKIP)
|
||
echo " <testcase name=\"${name}\" classname=\"ssh-audit\">" >> "$JUNIT_FILE"
|
||
echo " <skipped message=\"${detail}\"/>" >> "$JUNIT_FILE"
|
||
echo " </testcase>" >> "$JUNIT_FILE"
|
||
;;
|
||
esac
|
||
done
|
||
|
||
echo " </testsuite>" >> "$JUNIT_FILE"
|
||
echo "</testsuites>" >> "$JUNIT_FILE"
|
||
|
||
log "JUnit report written to ${JUNIT_FILE}"
|
||
}
|
||
|
||
# ══════════════════════════════════════════════════════════════════════
|
||
# AUDIT FUNCTIONS
|
||
# ══════════════════════════════════════════════════════════════════════
|
||
|
||
# ── Check .ssh directory permissions ──────────────────────────────────
|
||
check_ssh_dir_perms() {
|
||
local user="$1"
|
||
local ssh_dir="$2"
|
||
|
||
if [[ ! -d "$ssh_dir" ]]; then
|
||
verbose "No .ssh directory for ${user}"
|
||
return
|
||
fi
|
||
|
||
local perms
|
||
perms=$(stat -c '%a' "$ssh_dir" 2>/dev/null) || return
|
||
|
||
if [[ "$perms" == "700" ]]; then
|
||
record_pass "[${user}] .ssh directory permissions" "${perms}"
|
||
else
|
||
record_fail "[${user}] .ssh directory permissions" "${perms} (expected 700)"
|
||
fi
|
||
}
|
||
|
||
# ── Check authorized_keys file permissions ────────────────────────────
|
||
check_authkeys_perms() {
|
||
local user="$1"
|
||
local authkeys_file="$2"
|
||
|
||
if [[ ! -f "$authkeys_file" ]]; then
|
||
return
|
||
fi
|
||
|
||
local perms
|
||
perms=$(stat -c '%a' "$authkeys_file" 2>/dev/null) || return
|
||
|
||
case "$perms" in
|
||
600|644)
|
||
record_pass "[${user}] authorized_keys permissions" "${perms}"
|
||
;;
|
||
*)
|
||
local world_bit="${perms:2:1}"
|
||
if [[ "$world_bit" =~ [2367] ]]; then
|
||
record_fail "[${user}] authorized_keys permissions" "${perms} (world-writable)"
|
||
else
|
||
record_fail "[${user}] authorized_keys permissions" "${perms} (expected 600 or 644)"
|
||
fi
|
||
;;
|
||
esac
|
||
}
|
||
|
||
# ── Check for deprecated authorized_keys2 ─────────────────────────────
|
||
check_authkeys2() {
|
||
local user="$1"
|
||
local ssh_dir="$2"
|
||
|
||
local ak2="${ssh_dir}/authorized_keys2"
|
||
if [[ -f "$ak2" ]]; then
|
||
record_fail "[${user}] authorized_keys2 present" "deprecated, migrate to authorized_keys"
|
||
fi
|
||
}
|
||
|
||
# ── Check private key permissions ─────────────────────────────────────
|
||
check_private_key_perms() {
|
||
local user="$1"
|
||
local ssh_dir="$2"
|
||
|
||
local privkey_names=("id_rsa" "id_ed25519" "id_ecdsa" "id_dsa" "id_xmss")
|
||
|
||
for keyname in "${privkey_names[@]}"; do
|
||
local keyfile="${ssh_dir}/${keyname}"
|
||
if [[ -f "$keyfile" ]]; then
|
||
local perms
|
||
perms=$(stat -c '%a' "$keyfile" 2>/dev/null) || continue
|
||
local world_bit="${perms:2:1}"
|
||
if [[ "$world_bit" != "0" ]]; then
|
||
record_fail "[${user}] Private key ${keyname}" "world-readable (${perms})"
|
||
else
|
||
verbose "[${user}] Private key ${keyname} permissions OK (${perms})"
|
||
fi
|
||
fi
|
||
done
|
||
}
|
||
|
||
# ── Audit a single key line ───────────────────────────────────────────
|
||
audit_key_line() {
|
||
local user="$1"
|
||
local key_num="$2"
|
||
local line="$3"
|
||
local authkeys_file="$4"
|
||
|
||
# Skip empty lines and comments
|
||
[[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && return
|
||
|
||
# Check for command= or from= restrictions
|
||
local restrictions=""
|
||
if [[ "$line" =~ ^(.*[[:space:]])?(command=\"[^\"]*\") ]]; then
|
||
restrictions="${BASH_REMATCH[2]}"
|
||
fi
|
||
if [[ "$line" =~ ^(.*[[:space:]])?(from=\"[^\"]*\") ]]; then
|
||
restrictions="${restrictions:+${restrictions}, }${BASH_REMATCH[2]}"
|
||
fi
|
||
|
||
# Write key to temp file for ssh-keygen parsing
|
||
local tmpkey
|
||
tmpkey=$(mktemp)
|
||
echo "$line" > "$tmpkey"
|
||
|
||
local key_info
|
||
key_info=$(ssh-keygen -l -f "$tmpkey" 2>/dev/null) || {
|
||
rm -f "$tmpkey"
|
||
verbose "[${user}] Key ${key_num}: could not parse"
|
||
return
|
||
}
|
||
rm -f "$tmpkey"
|
||
|
||
# Parse ssh-keygen output: "2048 SHA256:xxx comment (RSA)"
|
||
local bits key_type comment fingerprint
|
||
bits=$(echo "$key_info" | awk '{print $1}')
|
||
fingerprint=$(echo "$key_info" | awk '{print $2}')
|
||
key_type=$(echo "$key_info" | grep -oP '\(([A-Z0-9]+)\)' | tr -d '()')
|
||
comment=$(echo "$key_info" | awk '{for(i=3;i<NF;i++) printf "%s ", $i; print ""}' | sed 's/ *$//')
|
||
|
||
# Normalize key type to lowercase for counting
|
||
local key_type_lower
|
||
key_type_lower=$(echo "$key_type" | tr '[:upper:]' '[:lower:]')
|
||
|
||
((TOTAL_KEYS++)) || true
|
||
KEY_TYPE_COUNTS["$key_type_lower"]=$(( ${KEY_TYPE_COUNTS["$key_type_lower"]:-0} + 1 ))
|
||
|
||
# Track fingerprint for duplicate detection
|
||
if [[ "$SKIP_DUPLICATES" != "true" && -n "$fingerprint" ]]; then
|
||
KEY_FINGERPRINTS["$fingerprint"]="${KEY_FINGERPRINTS["$fingerprint"]:-} ${user}"
|
||
fi
|
||
|
||
# Key type checks
|
||
case "$key_type" in
|
||
DSA)
|
||
record_fail "[${user}] Key ${key_num}: ${key_type_lower} (${bits} bits)" "DEPRECATED key type (DSA)"
|
||
;;
|
||
RSA)
|
||
if [[ "$bits" -lt "$MIN_RSA_BITS" ]]; then
|
||
record_fail "[${user}] Key ${key_num}: ${key_type_lower} (${bits} bits)" "below minimum ${MIN_RSA_BITS} bits"
|
||
else
|
||
record_pass "[${user}] Key ${key_num}: ssh-${key_type_lower} (${bits} bits)" "${comment:-no comment}"
|
||
fi
|
||
;;
|
||
ECDSA|ED25519)
|
||
record_pass "[${user}] Key ${key_num}: ssh-${key_type_lower} (${bits} bits)" "${comment:-no comment}"
|
||
;;
|
||
*)
|
||
record_pass "[${user}] Key ${key_num}: ${key_type_lower} (${bits} bits)" "${comment:-no comment}"
|
||
;;
|
||
esac
|
||
|
||
# Check for missing comment
|
||
if [[ -z "$comment" || "$comment" == "$fingerprint" ]]; then
|
||
record_fail "[${user}] Key ${key_num}: ${key_type_lower} (${bits} bits)" "no comment/identifier"
|
||
fi
|
||
|
||
# Note restrictions
|
||
if [[ -n "$restrictions" ]]; then
|
||
record_skip "[${user}] Key ${key_num} has restriction" "${restrictions}"
|
||
fi
|
||
}
|
||
|
||
# ── Audit a single user ───────────────────────────────────────────────
|
||
audit_user() {
|
||
local home_dir="$1"
|
||
local user
|
||
user=$(basename "$home_dir")
|
||
|
||
local ssh_dir="${home_dir}/.ssh"
|
||
local authkeys_file="${ssh_dir}/authorized_keys"
|
||
|
||
# Skip if no .ssh directory at all
|
||
if [[ ! -d "$ssh_dir" ]]; then
|
||
verbose "Skipping ${user} — no .ssh directory"
|
||
return
|
||
fi
|
||
|
||
if [[ "$OUTPUT_FORMAT" != "tap" ]]; then
|
||
echo ""
|
||
echo -e "${BOLD}User: ${user}${RESET}"
|
||
fi
|
||
|
||
# Permission checks
|
||
check_ssh_dir_perms "$user" "$ssh_dir"
|
||
check_authkeys_perms "$user" "$authkeys_file"
|
||
check_authkeys2 "$user" "$ssh_dir"
|
||
check_private_key_perms "$user" "$ssh_dir"
|
||
|
||
# Key audit
|
||
if [[ -f "$authkeys_file" && -r "$authkeys_file" ]]; then
|
||
local key_num=0
|
||
local user_has_keys=false
|
||
local ak_contents
|
||
ak_contents=$(< "$authkeys_file")
|
||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||
# Skip empty lines and comments
|
||
[[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && continue
|
||
((key_num++)) || true
|
||
user_has_keys=true
|
||
audit_key_line "$user" "$key_num" "$line" "$authkeys_file"
|
||
done <<< "$ak_contents"
|
||
if [[ "$user_has_keys" == "true" ]]; then
|
||
((USERS_WITH_KEYS++)) || true
|
||
fi
|
||
else
|
||
verbose "[${user}] No authorized_keys file or not readable"
|
||
fi
|
||
}
|
||
|
||
# ── Check for duplicate keys across users ─────────────────────────────
|
||
check_duplicates() {
|
||
if [[ "$SKIP_DUPLICATES" == "true" ]]; then
|
||
return
|
||
fi
|
||
|
||
local found_dupes=false
|
||
|
||
for fp in "${!KEY_FINGERPRINTS[@]}"; do
|
||
local users_str="${KEY_FINGERPRINTS[$fp]}"
|
||
# Trim leading space, deduplicate
|
||
users_str=$(echo "$users_str" | xargs -n1 | sort -u | xargs)
|
||
local user_count
|
||
user_count=$(echo "$users_str" | wc -w)
|
||
|
||
if [[ "$user_count" -gt 1 ]]; then
|
||
if [[ "$found_dupes" == "false" && "$OUTPUT_FORMAT" != "tap" ]]; then
|
||
echo ""
|
||
echo -e "${BOLD}Duplicate Keys${RESET}"
|
||
found_dupes=true
|
||
fi
|
||
record_fail "Duplicate key found" "${fp} present in: ${users_str}"
|
||
fi
|
||
done
|
||
}
|
||
|
||
# ══════════════════════════════════════════════════════════════════════
|
||
# OUTPUT
|
||
# ══════════════════════════════════════════════════════════════════════
|
||
|
||
print_summary() {
|
||
local end_time
|
||
end_time=$(date +%s)
|
||
local duration=$(( end_time - START_TIME ))
|
||
|
||
# Build type breakdown string
|
||
local type_breakdown=""
|
||
for ktype in "${!KEY_TYPE_COUNTS[@]}"; do
|
||
local count="${KEY_TYPE_COUNTS[$ktype]}"
|
||
type_breakdown="${type_breakdown:+${type_breakdown}, }${count}× ${ktype}"
|
||
done
|
||
|
||
echo ""
|
||
echo -e "${BOLD}────────────────────────────────────────${RESET}"
|
||
echo -e "${BOLD}Summary${RESET}"
|
||
echo -e " Users scanned: ${USERS_WITH_KEYS}"
|
||
echo -e " Total keys: ${TOTAL_KEYS}"
|
||
if [[ -n "$type_breakdown" ]]; then
|
||
echo -e " Key types: ${type_breakdown}"
|
||
fi
|
||
echo -e " ${GREEN}${PASS} passed${RESET} ${RED}${FAIL} failed${RESET} ${YELLOW}${SKIP} skipped${RESET} (${duration}s)"
|
||
echo -e "${BOLD}────────────────────────────────────────${RESET}"
|
||
|
||
if [[ $FAIL -eq 0 ]]; then
|
||
echo -e "${GREEN}${BOLD}All checks passed.${RESET}"
|
||
else
|
||
echo -e "${RED}${BOLD}${FAIL} issue(s) found.${RESET}"
|
||
fi
|
||
}
|
||
|
||
print_tap_header() {
|
||
echo "TAP version 13"
|
||
}
|
||
|
||
print_tap_footer() {
|
||
echo "1..${TOTAL}"
|
||
echo "# pass ${PASS}"
|
||
echo "# fail ${FAIL}"
|
||
echo "# skip ${SKIP}"
|
||
}
|
||
|
||
# ══════════════════════════════════════════════════════════════════════
|
||
# MAIN
|
||
# ══════════════════════════════════════════════════════════════════════
|
||
|
||
usage() {
|
||
cat <<EOF
|
||
Usage: $(basename "$0") [OPTIONS]
|
||
|
||
Audit SSH authorized_keys across user accounts. Flags weak key types, short
|
||
key lengths, duplicates, and permission issues. Read-only — never modifies keys.
|
||
|
||
Requires: bash 4+, coreutils, ssh-keygen
|
||
|
||
Options:
|
||
--scan-path PATH Base path to scan for user home dirs (default: /home)
|
||
--min-rsa-bits N Minimum RSA key length in bits (default: 2048)
|
||
--format FORMAT Output: text (default), tap, junit
|
||
--junit-file FILE JUnit output path (default: ssh-audit-results.xml)
|
||
--verbose Show debug output
|
||
--no-color Disable colored output
|
||
--help Show this help
|
||
|
||
Environment variables:
|
||
SCAN_PATH Base scan path (default: /home)
|
||
SCAN_SYSTEM_USERS Also scan /root and system accounts (default: false)
|
||
MIN_RSA_BITS Minimum RSA key bits (default: 2048)
|
||
SKIP_DUPLICATES Skip cross-user duplicate detection (default: false)
|
||
OUTPUT_FORMAT Output format (default: text)
|
||
JUNIT_FILE JUnit output path (default: ssh-audit-results.xml)
|
||
VERBOSE Debug output (default: false)
|
||
COLOR Color mode: auto, always, never (default: auto)
|
||
|
||
Examples:
|
||
# Scan all users under /home
|
||
sudo ./$(basename "$0")
|
||
|
||
# Require 4096-bit RSA, JUnit output
|
||
sudo ./$(basename "$0") --min-rsa-bits 4096 --format junit
|
||
|
||
# TAP output for CI pipeline
|
||
sudo ./$(basename "$0") --format tap
|
||
|
||
# Scan specific path
|
||
sudo ./$(basename "$0") --scan-path /srv/users
|
||
EOF
|
||
}
|
||
|
||
main() {
|
||
# Parse arguments
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
--scan-path) SCAN_PATH="$2"; shift ;;
|
||
--min-rsa-bits) MIN_RSA_BITS="$2"; shift ;;
|
||
--format) OUTPUT_FORMAT="$2"; shift ;;
|
||
--junit-file) JUNIT_FILE="$2"; shift ;;
|
||
--verbose) VERBOSE=true ;;
|
||
--no-color) COLOR=never ;;
|
||
--help|-h) usage; exit 0 ;;
|
||
*) err "Unknown option: $1"; usage; exit 1 ;;
|
||
esac
|
||
shift
|
||
done
|
||
|
||
setup_colors
|
||
|
||
# Validate scan path
|
||
if [[ ! -d "$SCAN_PATH" ]]; then
|
||
err "Scan path does not exist: ${SCAN_PATH}"
|
||
exit 1
|
||
fi
|
||
|
||
START_TIME=$(date +%s)
|
||
|
||
if [[ "$OUTPUT_FORMAT" == "tap" ]]; then
|
||
print_tap_header
|
||
else
|
||
echo ""
|
||
echo -e "${BOLD}SSH Key Auditor${RESET}"
|
||
echo -e "Scan path: ${SCAN_PATH}"
|
||
echo -e "Time: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||
fi
|
||
|
||
# Build list of directories to scan
|
||
local -a scan_dirs=()
|
||
|
||
# Scan user home directories under SCAN_PATH
|
||
if [[ -d "$SCAN_PATH" ]]; then
|
||
for dir in "${SCAN_PATH}"/*/; do
|
||
[[ -d "$dir" ]] && scan_dirs+=("$dir")
|
||
done
|
||
fi
|
||
|
||
# Optionally scan system users
|
||
if [[ "$SCAN_SYSTEM_USERS" == "true" ]]; then
|
||
[[ -d "/root" ]] && scan_dirs+=("/root/")
|
||
# Scan common system user home dirs from /etc/passwd
|
||
while IFS=: read -r _ _ uid _ _ home _; do
|
||
if [[ "$uid" -lt 1000 && "$uid" -gt 0 && -d "$home/.ssh" && "$home" != "/root" ]]; then
|
||
scan_dirs+=("${home}/")
|
||
fi
|
||
done < /etc/passwd
|
||
fi
|
||
|
||
if [[ ${#scan_dirs[@]} -eq 0 ]]; then
|
||
warn "No directories found to scan in ${SCAN_PATH}"
|
||
if [[ "$OUTPUT_FORMAT" == "tap" ]]; then
|
||
print_tap_footer
|
||
fi
|
||
exit 0
|
||
fi
|
||
|
||
verbose "Scanning ${#scan_dirs[@]} directories"
|
||
|
||
# Audit each user
|
||
for dir in "${scan_dirs[@]}"; do
|
||
# Remove trailing slash for basename
|
||
dir="${dir%/}"
|
||
audit_user "$dir"
|
||
done
|
||
|
||
# Check for duplicates across users
|
||
check_duplicates
|
||
|
||
# Output
|
||
if [[ "$OUTPUT_FORMAT" == "tap" ]]; then
|
||
print_tap_footer
|
||
elif [[ "$OUTPUT_FORMAT" == "junit" ]]; then
|
||
print_summary
|
||
write_junit
|
||
else
|
||
print_summary
|
||
fi
|
||
|
||
# Exit code
|
||
[[ $FAIL -eq 0 ]] && exit 0 || exit 1
|
||
}
|
||
|
||
main "$@"
|