mirror of
https://ghproxy.net/https://github.com/CaptainCore/captaincore.git
synced 2026-08-04 14:10:32 +08:00
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`.
128 lines
4.5 KiB
Bash
Executable file
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
|
|
}'
|