back to scripts

cyber-watch.sh

bash 90 lines secrets redacted

Reads the auth journal for SSH brute-force candidates and other suspicious activity, producing a windowed security report.

Note Live script from my home-lab server. Tokens, IDs, phone numbers and other secrets have been replaced with placeholders like <WHATSAPP_GROUP_ID> — everything else is the real, running code.
#!/usr/bin/env bash
set -euo pipefail

REPORT_DIR=/home/lanky/reports
STATE_DIR=/home/lanky/.local/state/lanky-cyber
GROUP_ID=<WHATSAPP_GROUP_ID>
mkdir -p "$REPORT_DIR" "$STATE_DIR"

stamp=$(date +%Y%m%d-%H%M%S)
report="$REPORT_DIR/cyber-watch-$stamp.txt"
latest="$REPORT_DIR/cyber-watch-latest.txt"
state="$STATE_DIR/bruteforce-alerts.jsonl"
threshold=${BRUTE_FORCE_THRESHOLD:-8}
window="${BRUTE_FORCE_WINDOW:-15 minutes ago}"

ssh_failures=$(mktemp)
trap 'rm -f "$ssh_failures"' EXIT

read_ssh_journal() {
  if [ "$(id -u)" -eq 0 ]; then
    journalctl -u ssh --since "$window" --no-pager
  elif sudo -n journalctl -u ssh --since "$window" --no-pager; then
    return 0
  else
    echo "ERROR: Cyber watch could not read the SSH journal with its current permissions." >&2
    return 1
  fi
}

if ! read_ssh_journal >/dev/null; then
  exit 1
fi

{
  read_ssh_journal |
    awk '
      /Failed password|Invalid user|authentication failure/ {
        for (i=1;i<=NF;i++) {
          if ($i == "from" && (i+1)<=NF) print $(i+1)
          else if ($i ~ /^rhost=/) { sub(/^rhost=/,"",$i); print $i }
        }
      }' |
    grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' |
    grep -Ev '^(10|127)\.|^192\.168\.|^172\.(1[6-9]|2[0-9]|3[0-1])\.|^169\.254\.' || true
} | sort | uniq -c | sort -nr > "$ssh_failures"

{
  echo "Cyber Analyst watch report"
  echo "Generated: $(date -Is)"
  echo "Window: $window"
  echo
  echo "== SSH brute-force candidates =="
  if [ -s "$ssh_failures" ]; then
    cat "$ssh_failures"
  else
    echo "No external brute-force candidates detected."
  fi
  echo
  echo "== Recent authentication warnings =="
  read_ssh_journal |
    grep -Ei 'failed|invalid|authentication failure|disconnect|error' |
    tail -40 || true
  echo
  echo "== Internet-facing service snapshot =="
  ss -tulpen 2>/dev/null | awk 'NR==1 || /0\.0\.0\.0|\[::\]|:::/' | sed -n '1,80p' || true
} > "$report"
rm -f "$latest"
cp "$report" "$latest"
chown lanky:lanky "$report" "$latest" 2>/dev/null || true

alert_lines=$(awk -v threshold="$threshold" '$1 >= threshold {print $0}' "$ssh_failures")
if [ -n "$alert_lines" ]; then
  key=$(printf '%s\n' "$alert_lines" | sha256sum | awk '{print $1}')
  if ! grep -q "$key" "$state" 2>/dev/null; then
    printf '{"timestamp":"%s","key":"%s","findings":%s}\n' "$(date -Is)" "$key" "$(printf '%s' "$alert_lines" | jq -Rs .)" >> "$state"
    friendly=$(/home/lanky/scripts/person-friendly-report.sh cyber-watch "$report")
    message="SECURITY ALERT: possible external SSH brute-force detected. Offending IP counts: ${alert_lines}

${friendly}

I will NOT auto-shutdown as L1; L3/Lanky approval is required for shutdown or blocking."
    sudo -u lanky openclaw message send --channel whatsapp --account default --target "$GROUP_ID" --message "$message" >/dev/null 2>&1 || true
    (
      sleep 300
      sudo -u lanky openclaw message send --channel whatsapp --account default --target "$GROUP_ID" --message "SECURITY ESCALATION REMINDER: brute-force alert still requires L3/Lanky review. No automatic shutdown was performed. Raw report: $report" >/dev/null 2>&1 || true
    ) &
  fi
fi

echo "$report"

back to scripts