test-repo/scripts/security-monitors/ssh-monitor.sh

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 "$@"