125 lines
4.0 KiB
Bash
Executable File
125 lines
4.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -o errexit
|
|
set -o nounset
|
|
set -o pipefail
|
|
|
|
since_value="1 hour ago"
|
|
warning_count=20
|
|
critical_count=50
|
|
top_count=10
|
|
|
|
usage() {
|
|
cat <<'USAGE'
|
|
Usage: check_failed_ssh_logins.sh [--since TEXT] [--warning COUNT] [--critical COUNT] [--top N] [--help]
|
|
|
|
Detect failed SSH login bursts from journal or readable authentication logs.
|
|
USAGE
|
|
}
|
|
|
|
is_number() {
|
|
[[ "$1" =~ ^[0-9]+$ ]]
|
|
}
|
|
|
|
while (($# > 0)); do
|
|
case "$1" in
|
|
--since) [[ $# -ge 2 ]] || { printf 'CRITICAL: --since requires a value\n'; exit 2; }; since_value="$2"; shift 2 ;;
|
|
--warning) [[ $# -ge 2 ]] || { printf 'CRITICAL: --warning requires a value\n'; exit 2; }; warning_count="$2"; shift 2 ;;
|
|
--critical) [[ $# -ge 2 ]] || { printf 'CRITICAL: --critical requires a value\n'; exit 2; }; critical_count="$2"; shift 2 ;;
|
|
--top) [[ $# -ge 2 ]] || { printf 'CRITICAL: --top requires a value\n'; exit 2; }; top_count="$2"; shift 2 ;;
|
|
--help|-h) usage; exit 0 ;;
|
|
*) printf 'CRITICAL: unknown option: %s\n' "$1"; usage; exit 2 ;;
|
|
esac
|
|
done
|
|
|
|
for value in "$warning_count" "$critical_count" "$top_count"; do
|
|
if ! is_number "$value"; then
|
|
printf 'CRITICAL: numeric option expected, got: %s\n' "$value"
|
|
exit 2
|
|
fi
|
|
done
|
|
if ((warning_count >= critical_count)); then
|
|
printf 'CRITICAL: --warning must be lower than --critical\n'
|
|
exit 2
|
|
fi
|
|
|
|
tmp_log="$(mktemp)"
|
|
trap 'rm -f "$tmp_log"' EXIT
|
|
log_source="journalctl"
|
|
|
|
if command -v journalctl >/dev/null 2>&1; then
|
|
journalctl --since "$since_value" --no-pager 2>/dev/null \
|
|
| grep -Ei 'sshd.*(Failed password|Invalid user|authentication failure)|authentication failure.*sshd' > "$tmp_log" || true
|
|
else
|
|
log_source="log file fallback"
|
|
fi
|
|
|
|
if [[ ! -s "$tmp_log" ]]; then
|
|
for log_file in /var/log/auth.log /var/log/secure /var/log/messages; do
|
|
if [[ -r "$log_file" ]]; then
|
|
grep -Ei 'sshd.*(Failed password|Invalid user|authentication failure)|authentication failure.*sshd' "$log_file" >> "$tmp_log" || true
|
|
log_source="$log_file"
|
|
fi
|
|
done
|
|
fi
|
|
|
|
attempts="$(wc -l < "$tmp_log" | awk '{print $1}')"
|
|
|
|
status="OK"
|
|
exit_code=0
|
|
if ((attempts >= critical_count)); then
|
|
status="CRITICAL"
|
|
exit_code=3
|
|
elif ((attempts >= warning_count)); then
|
|
status="WARNING"
|
|
exit_code=1
|
|
fi
|
|
|
|
printf '%s: Found %s failed SSH login attempt(s) for requested window\n\n' "$status" "$attempts"
|
|
|
|
printf 'Top source IPs:\n'
|
|
if [[ -s "$tmp_log" ]]; then
|
|
grep -Eo 'from ([0-9]{1,3}\.){3}[0-9]{1,3}|rhost=([0-9]{1,3}\.){3}[0-9]{1,3}' "$tmp_log" \
|
|
| sed -E 's/^(from|rhost=) //' \
|
|
| sort | uniq -c | sort -rn | head -n "$top_count" || true
|
|
else
|
|
printf 'OK: no failed SSH attempts found in available sources\n'
|
|
fi
|
|
printf '\n'
|
|
|
|
printf 'Top attempted users:\n'
|
|
if [[ -s "$tmp_log" ]]; then
|
|
sed -nE 's/.*Invalid user ([^ ]+).*/\1/p; s/.*Failed password for invalid user ([^ ]+).*/\1/p; s/.*Failed password for ([^ ]+).*/\1/p; s/.*user=([^ ]+).*/\1/p' "$tmp_log" \
|
|
| sort | uniq -c | sort -rn | head -n "$top_count" || true
|
|
else
|
|
printf 'OK: no attempted users extracted\n'
|
|
fi
|
|
printf '\n'
|
|
|
|
printf 'Sample recent lines:\n'
|
|
if [[ -s "$tmp_log" ]]; then
|
|
tail -n "$top_count" "$tmp_log"
|
|
else
|
|
printf 'OK: no sample lines available\n'
|
|
fi
|
|
printf '\n\n'
|
|
|
|
printf 'Evidence:\n'
|
|
printf 'Thresholds: warning=%s critical=%s since="%s"\n' "$warning_count" "$critical_count" "$since_value"
|
|
printf 'Log source: %s\n' "$log_source"
|
|
if [[ "$log_source" != "journalctl" ]]; then
|
|
printf 'WARNING: log file fallback may include entries outside the requested --since window\n'
|
|
fi
|
|
if [[ "${EUID:-$(id -u 2>/dev/null || printf '1')}" != "0" ]]; then
|
|
printf 'WARNING: running without root; authentication log visibility may be limited\n'
|
|
fi
|
|
printf '\n'
|
|
|
|
printf 'Recommended next steps:\n'
|
|
printf -- '- Verify source IPs against expected scanners, admins, or automation\n'
|
|
printf -- '- Check firewall, fail2ban, or security tooling state\n'
|
|
printf -- '- Confirm whether the attempts are expected for this host\n'
|
|
printf -- '- Review successful logins too, not only failures\n'
|
|
printf -- '- Attach this output to incident ticket\n'
|
|
|
|
exit "$exit_code"
|