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:
@@ -0,0 +1,585 @@
|
||||
#!/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 "$@"
|
||||
Reference in New Issue
Block a user