captaincore/lib/remote-scripts/report-compromised-passwords
Austin Ginder 122bf169bf ๐Ÿ“ฆ NEW: report-compromised-passwords remote script
Surfaces HIBP-flagged login attempts blocked by CaptainCore Helper's
PasswordSecurity module. Reads the captaincore_security_log table and
emits pipe-delimited rows, intended for fleet aggregation via
`captaincore ssh @all --script=report-compromised-passwords --quiet --label`.
2026-05-11 21:38:08 -04:00

128 lines
4.5 KiB
Bash
Executable file

#!/usr/bin/env bash
#
# Reports compromised-password login attempts blocked by CaptainCore Helper.
#
# CaptainCore Helper's PasswordSecurity module checks every login attempt
# (and profile password save) against the HIBP breached-passwords list.
# When a compromised password is detected for an enforced role, the login
# is blocked and logged as `password_compromised_blocked`. Lower-privilege
# roles (when not enforced) are logged as `password_compromised_warned`
# without being blocked.
#
# This script reads the captaincore_security_log table and emits one line
# per matching event. Designed for fleet aggregation:
#
# captaincore ssh @all --script=report-compromised-passwords --quiet --label --parallel=40
#
# Output format (pipe-delimited):
# action|log_time|user_login|ip_address|context
#
# Flags:
# --quiet Only output if findings are present (fleet-friendly)
# --include-warned Also include `password_compromised_warned` events
# (default: blocked only)
# --days=N Limit to events in the last N days (default: all-time)
#
set -uo pipefail
# โ”€โ”€ Argument parsing โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
quiet=""
include_warned=""
days=""
for _arg in "$@"; do
case "$_arg" in
--quiet) quiet=true ;;
--include-warned) include_warned=true ;;
--days=*) days="${_arg#*=}" ;;
--*) ;;
*) WP_ROOT="$_arg" ;;
esac
done
WP_ROOT="${WP_ROOT:-.}"
cd "$WP_ROOT" 2>/dev/null || { echo "ERROR: Cannot access $WP_ROOT"; exit 1; }
if [ ! -f "wp-config.php" ] && [ -f "public/wp-config.php" ]; then
cd public
elif [ ! -f "wp-config.php" ] && [ -f "public_html/wp-config.php" ]; then
cd public_html
fi
if [ ! -f "wp-config.php" ]; then
[ -z "$quiet" ] && echo "WordPress not found."
exit 0
fi
if ! command -v wp &>/dev/null; then
echo "ERROR: WP-CLI not available"
exit 1
fi
WP_FLAGS="--skip-themes --skip-plugins --skip-packages"
PREFIX=$(wp db prefix $WP_FLAGS 2>/dev/null)
if [ -z "$PREFIX" ]; then
[ -z "$quiet" ] && echo "ERROR: could not read table prefix (DB unreachable?)"
exit 1
fi
TABLE="${PREFIX}captaincore_security_log"
# Bail out silently if the table doesn't exist (older helper or fresh install).
table_exists=$(wp db query "SHOW TABLES LIKE '${TABLE}'" --skip-column-names $WP_FLAGS 2>/dev/null)
if [ -z "$table_exists" ]; then
[ -z "$quiet" ] && echo "Security log table not present."
exit 0
fi
# โ”€โ”€ Build action filter โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
if [ -n "$include_warned" ]; then
action_clause="action IN ('password_compromised_blocked', 'password_compromised_warned')"
else
action_clause="action = 'password_compromised_blocked'"
fi
# โ”€โ”€ Optional --days=N window โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
time_clause=""
if [ -n "$days" ]; then
if ! [[ "$days" =~ ^[0-9]+$ ]]; then
echo "ERROR: --days must be a positive integer"
exit 1
fi
time_clause="AND log_time >= DATE_SUB(NOW(), INTERVAL ${days} DAY)"
fi
# โ”€โ”€ Query โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
read -r -d '' QUERY <<SQL || true
SELECT
action,
log_time,
COALESCE(NULLIF(user_login, ''), 'unknown') AS user_login,
COALESCE(NULLIF(ip_address, ''), 'unknown') AS ip_address,
log_data
FROM \`${TABLE}\`
WHERE ${action_clause}
${time_clause}
ORDER BY log_time ASC
SQL
results=$(wp db query "$QUERY" --skip-column-names --batch $WP_FLAGS 2>/dev/null)
if [ -z "$results" ]; then
[ -z "$quiet" ] && echo "No compromised-password events recorded."
exit 0
fi
# โ”€โ”€ Emit one line per event โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# log_data is JSON โ€” pull `context` (e.g. "wp_login", "profile_update") if present.
echo "$results" | awk 'BEGIN { FS="\t"; OFS="|" } {
action=$1; ts=$2; login=$3; ip=$4; data=$5
context = "unknown"
# Cheap JSON sniff for "context":"..." โ€” avoids a php/jq dependency.
if (match(data, /"context":"[^"]*"/)) {
context = substr(data, RSTART + 11, RLENGTH - 12)
}
print action, ts, login, ip, context
}'