150 lines
5.0 KiB
Bash
Executable File
150 lines
5.0 KiB
Bash
Executable File
#!/bin/zsh
|
|
#
|
|
# SSH Failed Login Monitor
|
|
# Detects failed SSH login attempts and sends alerts via Telegram
|
|
# Runs every minute via cron for near-instant detection
|
|
#
|
|
|
|
STATE_DIR="/Users/mattbruce/.openclaw/workspace/scripts/security-monitors/state"
|
|
LOG_FILE="/Users/mattbruce/.openclaw/workspace/scripts/security-monitors/logs/ssh-monitor.log"
|
|
ALERT_COOLDOWN_FILE="$STATE_DIR/ssh-alert-cooldown"
|
|
LAST_CHECK_FILE="$STATE_DIR/ssh-last-check"
|
|
|
|
# Create directories
|
|
mkdir -p "$(dirname $LOG_FILE)" "$STATE_DIR"
|
|
|
|
# Telegram config - read from OpenClaw config
|
|
TELEGRAM_CHAT_ID="${TELEGRAM_CHAT_ID:-default}"
|
|
|
|
# Timestamp helper
|
|
timestamp() {
|
|
date '+%Y-%m-%d %H:%M:%S %Z'
|
|
}
|
|
|
|
# Log to file
|
|
log() {
|
|
echo "[$(timestamp)] $1" >> "$LOG_FILE"
|
|
}
|
|
|
|
# Send Telegram alert via OpenClaw message tool
|
|
send_alert() {
|
|
local message="$1"
|
|
# Use the OpenClaw delivery queue or message tool
|
|
# Since we're in a script, we'll write to a queue that can be picked up
|
|
echo "$(timestamp) | ALERT | $message" >> "$STATE_DIR/alerts.queue"
|
|
|
|
# Also try direct message if possible (this requires the OpenClaw agent to be running)
|
|
# For now, we log and rely on the agent heartbeat to check this queue
|
|
}
|
|
|
|
# Get failed SSH attempts since last check
|
|
check_failed_ssh() {
|
|
local last_check=0
|
|
if [[ -f "$LAST_CHECK_FILE" ]]; then
|
|
last_check=$(cat "$LAST_CHECK_FILE")
|
|
fi
|
|
|
|
local current_time=$(date +%s)
|
|
echo "$current_time" > "$LAST_CHECK_FILE"
|
|
|
|
# Method 1: Check last command for failed attempts
|
|
local failed_attempts=0
|
|
local attempt_details=""
|
|
|
|
# On macOS, use 'last' command and filter for failed entries (they show as still logged in but with date)
|
|
# Failed SSH attempts typically show in last as entries that don't have a logout time
|
|
attempt_details=$(last -f /var/log/wtmp 2>/dev/null | grep -E "ssh|pts" | head -20 || echo "")
|
|
|
|
# Method 2: Check macOS unified logs for SSH authentication failures (more reliable)
|
|
# This captures PAM authentication failures
|
|
if command -v log >/dev/null 2>&1; then
|
|
local log_output
|
|
log_output=$(log show --predicate 'subsystem == "com.openssh.sshd" OR process == "sshd"' --info --last 2m 2>/dev/null | grep -i "failed\|invalid\|authentication failure" | head -10 || echo "")
|
|
if [[ -n "$log_output" ]]; then
|
|
failed_attempts=$(echo "$log_output" | wc -l | tr -d ' ')
|
|
attempt_details="$log_output"
|
|
fi
|
|
fi
|
|
|
|
# Method 3: Check system.log for auth failures (fallback)
|
|
if [[ "$failed_attempts" -eq 0 ]] && [[ -f /var/log/system.log ]]; then
|
|
local system_log_entries
|
|
system_log_entries=$(grep -i "authentication failure\|failed password" /var/log/system.log 2>/dev/null | tail -10 || echo "")
|
|
if [[ -n "$system_log_entries" ]]; then
|
|
# Count only recent entries (within last 2 minutes)
|
|
local recent_entries
|
|
recent_entries=$(echo "$system_log_entries" | grep "$(date '+%b %e')" || echo "")
|
|
if [[ -n "$recent_entries" ]]; then
|
|
failed_attempts=$(echo "$recent_entries" | wc -l | tr -d ' ')
|
|
attempt_details="$recent_entries"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Get source IPs from the attempts
|
|
local source_ips=""
|
|
if [[ -n "$attempt_details" ]]; then
|
|
source_ips=$(echo "$attempt_details" | grep -oE "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+" | sort -u | head -5 | tr '\n' ', ')
|
|
fi
|
|
|
|
echo "${failed_attempts}|${source_ips}|${attempt_details}"
|
|
}
|
|
|
|
# Main monitoring logic
|
|
main() {
|
|
local result
|
|
result=$(check_failed_ssh)
|
|
local failed_count=$(echo "$result" | cut -d'|' -f1)
|
|
local source_ips=$(echo "$result" | cut -d'|' -f2)
|
|
local details=$(echo "$result" | cut -d'|' -f3-)
|
|
|
|
# Check cooldown (don't alert more than once per 5 minutes for same pattern)
|
|
local cooldown_expired=true
|
|
if [[ -f "$ALERT_COOLDOWN_FILE" ]]; then
|
|
local last_alert=$(cat "$ALERT_COOLDOWN_FILE")
|
|
local current_time=$(date +%s)
|
|
if [[ $((current_time - last_alert)) -lt 300 ]]; then
|
|
cooldown_expired=false
|
|
fi
|
|
fi
|
|
|
|
if [[ "$failed_count" -gt 0 ]] && [[ "$cooldown_expired" == "true" ]]; then
|
|
# Log the detection
|
|
log "⚠️ SECURITY ALERT: $failed_count failed SSH attempt(s) detected"
|
|
log "Source IPs: $source_ips"
|
|
|
|
# Create alert message
|
|
local hostname=$(hostname -s)
|
|
local alert_msg="🚨 **SECURITY ALERT: Failed SSH Login Attempts** 🚨
|
|
|
|
**Host:** $hostname
|
|
**Time:** $(timestamp)
|
|
**Failed Attempts:** $failed_count
|
|
**Source IP(s):** ${source_ips:-Unknown}
|
|
|
|
Please check system security immediately.
|
|
|
|
_Detected by OpenClaw SSH Monitor_"
|
|
|
|
# Send alert
|
|
send_alert "$alert_msg"
|
|
|
|
# Update cooldown
|
|
date +%s > "$ALERT_COOLDOWN_FILE"
|
|
|
|
# Log to daily security log
|
|
local daily_log="/Users/mattbruce/.openclaw/workspace/memory/$(date '+%Y-%m-%d')-security.log"
|
|
echo "SSH_FAILED_ATTEMPTS|$(timestamp)|$failed_count|$source_ips" >> "$daily_log"
|
|
elif [[ "$failed_count" -gt 0 ]]; then
|
|
log "Detected $failed_count failed SSH attempts (cooldown active, no alert sent)"
|
|
fi
|
|
|
|
# Always log that we checked (for debugging)
|
|
if [[ "${DEBUG:-}" == "1" ]]; then
|
|
log "Check completed: $failed_count failed attempts found"
|
|
fi
|
|
}
|
|
|
|
# Run main function
|
|
main "$@"
|