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:
2026-05-25 03:31:08 +02:00
parent dbd6bf0324
commit a1a17e81a1
332 changed files with 174509 additions and 1106 deletions
+585
View File
@@ -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//&/&amp;}"
name="${name//</&lt;}"
name="${name//>/&gt;}"
name="${name//\"/&quot;}"
detail="${detail//&/&amp;}"
detail="${detail//</&lt;}"
detail="${detail//>/&gt;}"
detail="${detail//\"/&quot;}"
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 "$@"