diff --git a/README.md b/README.md index 0c358f7..9c064e3 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,72 @@ -# ssh-login-notification -**Видео инструкция** +[alt text](img/image.png)! -[![Watch the video](https://img.youtube.com/vi/a6gkXZ-2pQI/0.jpg)](https://youtu.be/a6gkXZ-2pQI) +[alt text](img/image.png) -Данный скрипт, при каждом новом входе по SSH, отправляет Вам уведомление в телеграм. -![alt tag](https://github.com/unixhostpro/ssh-login-notification/blob/master/sshlogin.png) +Сохраните скрипт +sudo nano /usr/local/bin/telegram-ssh-notify.sh -Для работы скрипта Вам понадобится jq +Вставьте код и сделайте исполняемым: sudo chmod +x /usr/local/bin/telegram-ssh-notify.sh -Установка jq Ubuntu / Linux Mint / Debian -> sudo apt install jq +Создайте конфигурационный файл -Установка jq CentOS -> sudo yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
-> sudo yum install jq +sudo nano /etc/telegram-ssh-notify.conf -Установка : -Копируем скрипт в /usr/local/bin/ -> wget -P /usr/local/bin/ https://raw.githubusercontent.com/unixhostpro/ssh-login-notification/master/ssh_login_info.sh -Устанавливаем права на запуск -> chmod +x /usr/local/bin/ssh_login_info.sh +TELEGRAM_TOKEN= +TELEGRAM_CHAT_ID= +TELEGRAM_TOPIC_ID= +MAX_ATTEMPTS_BEFORE_CRITICAL=20 +CRITICAL_TIME_WINDOW=300 +AUTO_BLOCK_CRITICAL=false +WHITELIST_IPS="127.0.0.1 10.0.0.0/8" +BLACKLIST_IPS="1.2.3.4" -Ubuntu -В файл /etc/pam.d/common-session добавляем следующую строку -> echo "session optional pam_exec.so type=open_session seteuid /usr/local/bin/ssh_login_info.sh" >> /etc/pam.d/common-session +Откройте файл /etc/pam.d/sshd в редакторе (например, nano или vim): -CentOS -В файл /etc/pam.d/sshd добавляем следующую строку -> echo "session optional pam_exec.so type=open_session seteuid /usr/local/bin/ssh_login_info.sh" >> /etc/pam.d/common-session +sudo nano /etc/pam.d/sshd +Добавьте в конец файла строку: + +session optional pam_exec.so /usr/local/bin/telegram-ssh-notify.sh #(обратите внимание: если вы переименовали скрипт, укажите правильное имя) + +Сохраните файл и закройте редактор. + +Перезапускать SSH не обязательно — PAM читает конфигурацию при каждой новой сессии. + +Создайте файл юнита (например, /etc/systemd/system/ssh-check.service): + +sudo nano /etc/systemd/system/ssh-check.service + +Вставьте следующее содержимое (подставьте правильный путь к вашему скрипту, если он называется иначе, например, telegram-ssh-notify.sh): + +[Unit] +Description=SSH Login Monitor for Telegram Notifications +After=network.target + +[Service] +ExecStart=/usr/local/bin/ваш_скрипт.sh monitor +Restart=always +User=root + +[Install] +WantedBy=multi-user.target + +Обратите внимание: в ExecStart нужно указать команду с аргументом monitor (как в вашем скрипте). +Если ваш скрипт называется telegram-ssh-notify.sh, то строка будет: + +ExecStart=/usr/local/bin/telegram-ssh-notify.sh monitor + +Перечитайте конфигурацию systemd и запустите сервис: + +bash +sudo systemctl daemon-reload +sudo systemctl enable ssh-check.service # автозапуск при загрузке +sudo systemctl start ssh-check.service + +Проверьте статус: + +bash +sudo systemctl status ssh-check.service + +Теперь ваш монитор будет работать как демон. \ No newline at end of file diff --git a/img/image copy.png b/img/image copy.png new file mode 100644 index 0000000..d25275f Binary files /dev/null and b/img/image copy.png differ diff --git a/img/image.png b/img/image.png new file mode 100644 index 0000000..a7dc271 Binary files /dev/null and b/img/image.png differ diff --git a/ssh_login_info.sh b/ssh_login_info.sh index d3e0992..a0b4ce7 100644 --- a/ssh_login_info.sh +++ b/ssh_login_info.sh @@ -1,19 +1,735 @@ -#!/bin/bash -# Telegram notification -# Send msg when your server load to high -token="123456:AasdE8asdaKNiradb1wRZT87pwErerc6biTsVcPE" # put your token here -chat_id="1234567" # your chat_id for sending notification -sendmsg="https://api.telegram.org/bot$token/sendMessage?parse_mode=markdown" # url for sending msg -sendfile="https://api.telegram.org/bot$token/sendDocument?parse_mode=markdown" # url for sending files -date="$(date "+%d-%b-%Y-%H:%M")" -caption_file=/tmp/ssh_caption_file.txt -msg=/tmp/ssh_msg_info.txt -curl http://ip-api.com/json/$PAM_RHOST -s -o $caption_file -country=$(cat $caption_file | jq '.country' | sed 's/"//g') -city=$(cat $caption_file | jq '.city' | sed 's/"//g') -org=$(cat $caption_file | jq '.as' | sed 's/"//g') -echo -e "📡New SSH login\n*🤖$PAM_USER* logged in on 🖥*$HOSTNAME* at $date from $PAM_RHOST\n🌎Country:*$country*\n🏙City=*$city*\n🕋Organisation=*$org*" > $msg -#curl -d text=$message -d chat_id=$chat_id $sendmsg -curl $sendmsg -d chat_id=$chat_id -d text="$(<$msg)" -rm /tmp/ssh_caption_file.txt -rm /tmp/ssh_msg_info.txt +#!/usr/bin/env bash +# Telegram SSH Notifications Script +# Version: 3.0.0 +# Author: System Administrator +# Description: Уведомления о SSH-входах и попытках взлома через Telegram + +set -euo pipefail +IFS=$'\n\t' + +# ============================================================================ +# CONSTANTS +# ============================================================================ +readonly SCRIPT_NAME="$(basename "${0}")" +readonly SCRIPT_VERSION="3.0.0" +readonly CONFIG_FILE="/etc/telegram-ssh-notify.conf" +readonly DEFAULT_LOG_FILE="/var/log/telegram-ssh-notify.log" +readonly FAILED_LOGINS_DIR="/var/log/ssh_failed_logins" +readonly IP_CACHE_DIR="/tmp/telegram-ssh-notify-cache" +readonly LOCK_FILE="/var/run/telegram-ssh-notify.lock" + +# ============================================================================ +# DEFAULT CONFIGURATION (может быть переопределено в файле или env) +# ============================================================================ +declare -gA CONFIG=( + [TELEGRAM_TOKEN]="" + [TELEGRAM_CHAT_ID]="" + [TELEGRAM_TOPIC_ID]="" + [MAX_ATTEMPTS_BEFORE_CRITICAL]="20" + [CRITICAL_TIME_WINDOW]="300" # секунд + [CLEANUP_DAYS]="7" + [IP_API_TIMEOUT]="3" + [ENABLE_GEOIP]="true" + [ENABLE_NOTIFICATIONS]="true" + [LOG_FILE]="${DEFAULT_LOG_FILE}" + [DEBUG]="false" + [AUTO_BLOCK_CRITICAL]="false" # автоматически блокировать IP при критической атаке + [BLOCK_COMMAND]="iptables -A INPUT -s {ip} -j DROP" + [WHITELIST_IPS]="" # разделённые пробелом IP или подсети + [BLACKLIST_IPS]="" # сразу блокировать эти IP + [RATE_LIMIT_SEC]="60" # минимальный интервал между одинаковыми уведомлениями + [USE_JOURNAL]="false" # использовать journalctl вместо файлов логов +) + +# Эмодзи для сообщений +declare -gA EMOJI=( + [SUCCESS]="✅" [INFO]="ℹ️" [WARNING]="⚠️" [DANGER]="🔴" [CRITICAL]="🚨" + [FIRE]="🔥" [LOCK]="🔒" [GLOBE]="🌍" [CITY]="🏙️" [BUILDING]="🏢" + [CLOCK]="🕐" [MAP]="🗺️" [SERVER]="🖥️" [USER]="👤" [IP]="🔗" + [DOOR]="🚪" [BELL]="🔔" [SHIELD]="🛡️" [MAGNIFYING_GLASS]="🔍" +) + +# Флаги стран (сокращённо) +declare -gA COUNTRY_FLAGS=( + [RU]="🇷🇺" [US]="🇺🇸" [DE]="🇩🇪" [CN]="🇨🇳" [UA]="🇺🇦" + [BY]="🇧🇾" [KZ]="🇰🇿" [FR]="🇫🇷" [GB]="🇬🇧" [JP]="🇯🇵" +) + +# ============================================================================ +# LOGGING +# ============================================================================ +log() { + local level="$1" + local message="$2" + local timestamp + timestamp="$(date '+%Y-%m-%d %H:%M:%S')" + local log_file="${CONFIG[LOG_FILE]}" + + # Создаём каталог для лога, если нужно + [[ -n "$log_file" ]] && mkdir -p "$(dirname "$log_file")" 2>/dev/null || true + + case "$level" in + DEBUG) + [[ "${CONFIG[DEBUG]}" == "true" ]] && echo "[$timestamp] [DEBUG] $message" >> "$log_file" + ;; + INFO) + echo "[$timestamp] [INFO] $message" >> "$log_file" + ;; + WARN) + echo "[$timestamp] [WARN] $message" >> "$log_file" + ;; + ERROR) + echo "[$timestamp] [ERROR] $message" >> "$log_file" + echo "[$timestamp] [ERROR] $message" >&2 + ;; + esac +} + +# ============================================================================ +# UTILITY FUNCTIONS +# ============================================================================ +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +validate_ip() { + local ip="$1" + # IPv4 + if [[ "$ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + local IFS='.' + read -r -a octets <<< "$ip" + for octet in "${octets[@]}"; do + if (( octet < 0 || octet > 255 )); then + return 1 + fi + done + return 0 + fi + # IPv6 просто проверяем формат + if [[ "$ip" =~ ^[0-9a-fA-F:]+$ ]]; then + return 0 + fi + return 1 +} + +is_private_ip() { + local ip="$1" + [[ "$ip" =~ ^10\. ]] && return 0 + [[ "$ip" =~ ^172\.(1[6-9]|2[0-9]|3[0-1])\. ]] && return 0 + [[ "$ip" =~ ^192\.168\. ]] && return 0 + [[ "$ip" =~ ^127\. ]] && return 0 + [[ "$ip" =~ ^169\.254\. ]] && return 0 + [[ "$ip" =~ ^fc[0-9a-f]{2}: ]] && return 0 + [[ "$ip" =~ ^fd[0-9a-f]{2}: ]] && return 0 + [[ "$ip" =~ ^fe80: ]] && return 0 + [[ "$ip" == "::1" ]] && return 0 + return 1 +} + +sanitize() { + echo "$1" | tr -d '\000-\037' | cut -c1-200 +} + +# Проверка, входит ли IP в список (поддерживаются CIDR) +ip_in_list() { + local ip="$1" + local list="$2" + [[ -z "$list" ]] && return 1 + for entry in $list; do + if [[ "$entry" == */* ]]; then + # CIDR + if command_exists ipcalc; then + if ipcalc -c "$ip" "$entry" >/dev/null 2>&1; then + return 0 + fi + else + # грубая проверка: если IP начинается с той же подсети + if [[ "$ip" == "${entry%/*}"* ]]; then + return 0 + fi + fi + else + [[ "$ip" == "$entry" ]] && return 0 + fi + done + return 1 +} + +# Rate limiting: проверяем, можно ли отправить уведомление по ключу (например, IP+user) +check_rate_limit() { + local key="$1" + local limit_sec="${CONFIG[RATE_LIMIT_SEC]}" + [[ "$limit_sec" -le 0 ]] && return 0 + local cache_file="${IP_CACHE_DIR}/rate_$(echo -n "$key" | md5sum | cut -d' ' -f1)" + mkdir -p "$IP_CACHE_DIR" + if [[ -f "$cache_file" ]]; then + local last + last="$(cat "$cache_file")" + local now + now="$(date +%s)" + if (( now - last < limit_sec )); then + return 1 + fi + fi + date +%s > "$cache_file" + return 0 +} + +# ============================================================================ +# CONFIGURATION LOADING +# ============================================================================ +load_config() { + # Сначала загружаем переменные окружения (приоритет выше) + local env_vars=( + TELEGRAM_TOKEN TELEGRAM_CHAT_ID TELEGRAM_TOPIC_ID + MAX_ATTEMPTS_BEFORE_CRITICAL CRITICAL_TIME_WINDOW CLEANUP_DAYS + IP_API_TIMEOUT ENABLE_GEOIP ENABLE_NOTIFICATIONS DEBUG + AUTO_BLOCK_CRITICAL WHITELIST_IPS BLACKLIST_IPS RATE_LIMIT_SEC + ) + for var in "${env_vars[@]}"; do + if [[ -n "${!var:-}" ]]; then + CONFIG["$var"]="${!var}" + log "DEBUG" "Loaded from env: $var=${!var}" + fi + done + + # Затем из конфигурационного файла (переопределяет, если нет в env) + if [[ -f "$CONFIG_FILE" ]]; then + log "INFO" "Loading configuration from $CONFIG_FILE" + local line key value + while IFS='=' read -r key value; do + key="${key// /}" + value="${value// /}" + [[ -z "$key" || "$key" =~ ^# ]] && continue + if [[ -n "${CONFIG[$key]:-}" ]]; then + CONFIG["$key"]="$value" + log "DEBUG" "Config: $key=$value" + fi + done < "$CONFIG_FILE" + else + log "WARN" "Configuration file not found, using defaults/env" + fi + + # Валидация обязательных параметров + if [[ -z "${CONFIG[TELEGRAM_TOKEN]}" || -z "${CONFIG[TELEGRAM_CHAT_ID]}" ]]; then + log "ERROR" "TELEGRAM_TOKEN and TELEGRAM_CHAT_ID must be set" + exit 1 + fi + + # Создаём необходимые каталоги + mkdir -p "$FAILED_LOGINS_DIR" "$IP_CACHE_DIR" "$(dirname "${CONFIG[LOG_FILE]}")" +} + +# ============================================================================ +# GEOIP FUNCTIONS +# ============================================================================ +get_ip_info() { + local ip="$1" + local cache_file="${IP_CACHE_DIR}/geo_$(echo -n "$ip" | md5sum | cut -d' ' -f1)" + local cache_age=3600 + + # Проверка кэша + if [[ -f "$cache_file" ]]; then + local file_time now + file_time="$(stat -c %Y "$cache_file" 2>/dev/null || stat -f %m "$cache_file")" + now="$(date +%s)" + if (( now - file_time < cache_age )); then + cat "$cache_file" + return 0 + fi + fi + + # Если GeoIP отключён + if [[ "${CONFIG[ENABLE_GEOIP]}" != "true" ]]; then + echo "🌐 *Страна:* отключено" + echo "🏙️ *Город:* отключено" + return 0 + fi + + # Приватный IP + if is_private_ip "$ip"; then + { + echo "🌐 *Страна:* частная сеть" + echo "🏙️ *Город:* локальный адрес" + } > "$cache_file" + cat "$cache_file" + return 0 + fi + + # Попытка использовать mmdblookup (MaxMind GeoIP2) если установлен + if command_exists mmdblookup && [[ -f /usr/share/GeoIP/GeoLite2-City.mmdb ]]; then + local country city + country="$(mmdblookup --file /usr/share/GeoIP/GeoLite2-City.mmdb --ip "$ip" country names en 2>/dev/null | head -1 | tr -d '"')" + city="$(mmdblookup --file /usr/share/GeoIP/GeoLite2-City.mmdb --ip "$ip" city names en 2>/dev/null | head -1 | tr -d '"')" + { + echo "🌐 *Страна:* ${country:-Неизвестно}" + echo "🏙️ *Город:* ${city:-Неизвестно}" + } > "$cache_file" + cat "$cache_file" + return 0 + fi + + # Резервный API ip-api.com + local response + response="$(curl -s --max-time "${CONFIG[IP_API_TIMEOUT]}" "http://ip-api.com/json/$ip?fields=status,country,city")" || true + if [[ -n "$response" ]] && command_exists jq; then + local status country city + status="$(jq -r '.status' <<<"$response")" + if [[ "$status" == "success" ]]; then + country="$(jq -r '.country' <<<"$response")" + city="$(jq -r '.city' <<<"$response")" + { + echo "🌐 *Страна:* ${country:-Неизвестно}" + echo "🏙️ *Город:* ${city:-Неизвестно}" + } > "$cache_file" + cat "$cache_file" + return 0 + fi + fi + + # Если ничего не сработало + echo "🌐 *Страна:* не определена" + echo "🏙️ *Город:* не определён" + return 1 +} + +# ============================================================================ +# IP FILTERING (WHITELIST/BLACKLIST) +# ============================================================================ +check_ip_filter() { + local ip="$1" + if ip_in_list "$ip" "${CONFIG[BLACKLIST_IPS]}"; then + log "INFO" "IP $ip is blacklisted, blocking immediately" + auto_block_ip "$ip" "blacklist" + return 1 + fi + if ip_in_list "$ip" "${CONFIG[WHITELIST_IPS]}"; then + log "DEBUG" "IP $ip is whitelisted, skipping notifications" + return 2 + fi + return 0 +} + +# ============================================================================ +# AUTO BLOCK IP +# ============================================================================ +auto_block_ip() { + local ip="$1" + local reason="$2" + if [[ "${CONFIG[AUTO_BLOCK_CRITICAL]}" != "true" ]]; then + return 0 + fi + local cmd="${CONFIG[BLOCK_COMMAND]//\{ip\}/$ip}" + log "WARN" "Auto-blocking IP $ip (reason: $reason) with command: $cmd" + eval "$cmd" || log "ERROR" "Failed to block IP $ip" +} + +# ============================================================================ +# FAILED LOGIN TRACKING +# ============================================================================ +track_failed_attempt() { + local ip="$1" + local user="$2" + local timestamp + timestamp="$(date +%s)" + local date_str + date_str="$(date '+%Y-%m-%d %H:%M:%S')" + local safe_ip + safe_ip="$(echo "$ip" | tr './:' '_')" + local ip_file="$FAILED_LOGINS_DIR/$safe_ip" + + # Читаем предыдущие данные + local attempts=1 last_attempt=0 old_user="$user" + if [[ -f "$ip_file" ]]; then + IFS='|' read -r attempts last_attempt old_user _ < "$ip_file" + attempts=$((attempts + 1)) + # Сбрасываем счётчик, если прошло много времени + if (( timestamp - last_attempt > 600 )); then + attempts=1 + fi + fi + + # Сохраняем + echo "$attempts|$timestamp|$user|$date_str" > "$ip_file" + log "INFO" "Failed attempt #$attempts from $ip as $user" + + # Проверяем условия для уведомления + local notify=false + local alert_level="info" + local time_window_diff=$((timestamp - last_attempt)) + + # Критическая атака + if (( attempts >= ${CONFIG[MAX_ATTEMPTS_BEFORE_CRITICAL]} )) && \ + (( time_window_diff < ${CONFIG[CRITICAL_TIME_WINDOW]} )); then + notify=true + alert_level="critical" + auto_block_ip "$ip" "critical_attack" + elif (( attempts == 1 || attempts == 3 || attempts == 5 || attempts % 10 == 0 )); then + notify=true + alert_level="warning" + fi + + if [[ "$notify" == "true" ]]; then + send_failed_login_alert "$ip" "$user" "$attempts" "$date_str" "$alert_level" + fi + + return "$attempts" +} + +# ============================================================================ +# TELEGRAM SENDER +# ============================================================================ +send_telegram_message() { + local message="$1" + local disable_notification="${2:-false}" + local parse_mode="${3:-markdown}" + + if [[ "${CONFIG[ENABLE_NOTIFICATIONS]}" != "true" ]]; then + log "INFO" "Notifications disabled, message not sent" + return 0 + fi + + local url="https://api.telegram.org/bot${CONFIG[TELEGRAM_TOKEN]}/sendMessage" + local response_file="/tmp/tg_response_$$.json" + + local curl_cmd=(curl -s -X POST "$url" -F "chat_id=${CONFIG[TELEGRAM_CHAT_ID]}" -F "text=$message" -F "parse_mode=$parse_mode" -F "disable_notification=$disable_notification") + if [[ -n "${CONFIG[TELEGRAM_TOPIC_ID]}" ]]; then + curl_cmd+=(-F "message_thread_id=${CONFIG[TELEGRAM_TOPIC_ID]}") + fi + + local http_code + http_code="$("${curl_cmd[@]}" -w "%{http_code}" -o "$response_file" 2>/dev/null || echo "000")" + + if [[ "$http_code" -eq 200 ]]; then + log "DEBUG" "Telegram message sent" + else + log "ERROR" "Telegram send failed (HTTP $http_code). Response: $(cat "$response_file" 2>/dev/null)" + fi + rm -f "$response_file" +} + +send_failed_login_alert() { + local ip="$1" + local user="$2" + local attempts="$3" + local date_str="$4" + local alert_level="$5" + + # Rate limiting + local rate_key="failed_${ip}_${user}" + check_rate_limit "$rate_key" || return 0 + + local ip_info + ip_info="$(get_ip_info "$ip")" + + local danger_emoji title + case "$alert_level" in + critical) + danger_emoji="${EMOJI[FIRE]}${EMOJI[CRITICAL]}" + title="*КРИТИЧЕСКАЯ АТАКА!*" + ;; + warning) + if (( attempts >= 10 )); then + danger_emoji="${EMOJI[DANGER]}⚠️" + title="*ВЫСОКАЯ АКТИВНОСТЬ*" + else + danger_emoji="${EMOJI[WARNING]}" + title="" + fi + ;; + *) + danger_emoji="❌" + title="" + ;; + esac + + local message + message="$(cat << EOF +${danger_emoji} *НЕУДАЧНАЯ ПОПЫТКА SSH* ${danger_emoji} +${title} + +⚠️ *Попытка:* #${attempts} +👤 *Пользователь:* \`${user}\` +🖥️ *Сервер:* \`${HOSTNAME:-$(hostname)}\` +🔗 *IP атакующего:* \`${ip}\` + +*Геоинформация:* +${ip_info} + +🕐 *Время:* ${date_str} +📊 *Всего попыток с IP:* ${attempts} + +#SSH #FailedLogin #SecurityAlert +EOF +)" + send_telegram_message "$message" "false" +} + +send_successful_login() { + local ip="$1" + local user="$2" + + local rate_key="success_${ip}_${user}" + check_rate_limit "$rate_key" || return 0 + + local ip_info + ip_info="$(get_ip_info "$ip")" + + local message + message="$(cat << EOF +✅ *УСПЕШНЫЙ ВХОД SSH* + +👤 *Пользователь:* \`${user}\` +🖥️ *Сервер:* \`${HOSTNAME:-$(hostname)}\` +🔗 *IP адрес:* \`${ip}\` + +*Информация о подключении:* +${ip_info} + +🕐 *Время входа:* $(date '+%d %b %Y, %H:%M:%S') +📡 *Сервис:* ${PAM_SERVICE:-ssh} + +#SSH #Login #Successful +EOF +)" + send_telegram_message "$message" "true" +} + +send_logout_notification() { + local ip="$1" + local user="$2" + local session_start="${3:-}" + + local duration="?" + if [[ -n "$session_start" ]]; then + local start_sec end_sec diff + start_sec="$(date -d "$session_start" +%s 2>/dev/null || echo 0)" + end_sec="$(date +%s)" + if (( start_sec > 0 )); then + diff=$((end_sec - start_sec)) + duration="$(printf '%02d:%02d:%02d' $((diff/3600)) $(((diff%3600)/60)) $((diff%60)))" + fi + fi + + local message + message="$(cat << EOF +🚪 *ВЫХОД ИЗ SSH СЕССИИ* + +👤 *Пользователь:* \`${user}\` +🖥️ *Сервер:* \`${HOSTNAME:-$(hostname)}\` +🔗 *IP адрес:* \`${ip}\` + +🕐 *Время выхода:* $(date '+%d %b %Y, %H:%M:%S') +⏱️ *Длительность:* $duration + +#SSH #Logout +EOF +)" + send_telegram_message "$message" "true" +} + +# ============================================================================ +# LOG MONITORING (режим демона) +# ============================================================================ +monitor_logs() { + log "INFO" "Starting log monitor" + + local use_journal="${CONFIG[USE_JOURNAL]}" + if [[ "$use_journal" == "true" ]] && command_exists journalctl; then + journalctl -f -n0 -u ssh.service -o cat | while read -r line; do + process_log_line "$line" + done + else + local auth_logs=("/var/log/auth.log" "/var/log/secure") + local log_file="" + for f in "${auth_logs[@]}"; do + if [[ -f "$f" ]]; then + log_file="$f" + break + fi + done + if [[ -z "$log_file" ]]; then + log "ERROR" "No auth log file found" + return 1 + fi + log "INFO" "Monitoring $log_file" + tail -Fn0 "$log_file" | while read -r line; do + process_log_line "$line" + done + fi +} + +process_log_line() { + local line="$1" + local ip user + + # Failed password + if [[ "$line" =~ Failed\ password\ for\ (.+)\ from\ ([0-9\.]+) ]]; then + user="${BASH_REMATCH[1]}" + ip="${BASH_REMATCH[2]}" + check_ip_filter "$ip" || return 0 + track_failed_attempt "$ip" "$user" + # Invalid user + elif [[ "$line" =~ Invalid\ user\ ([^\ ]+)\ from\ ([0-9\.]+) ]]; then + user="${BASH_REMATCH[1]}" + ip="${BASH_REMATCH[2]}" + check_ip_filter "$ip" || return 0 + track_failed_attempt "$ip" "$user" + # Successful login + elif [[ "$line" =~ Accepted\ password\ for\ ([^\ ]+)\ from\ ([0-9\.]+) ]]; then + user="${BASH_REMATCH[1]}" + ip="${BASH_REMATCH[2]}" + check_ip_filter "$ip" || return 0 + send_successful_login "$ip" "$user" + fi +} + +# ============================================================================ +# PAM HANDLER (вызывается из PAM) +# ============================================================================ +handle_pam() { + local pam_type="${PAM_TYPE:-}" + local pam_user="${PAM_USER:-}" + local pam_rhost="${PAM_RHOST:-}" + local pam_service="${PAM_SERVICE:-}" + + [[ -z "$pam_type" || -z "$pam_user" || -z "$pam_rhost" ]] && return 0 + + # Фильтрация + check_ip_filter "$pam_rhost" || return 0 + + log "INFO" "PAM event: $pam_type from $pam_rhost as $pam_user" + + case "$pam_type" in + open_session) + send_successful_login "$pam_rhost" "$pam_user" + ;; + close_session) + send_logout_notification "$pam_rhost" "$pam_user" + ;; + auth) + # Обрабатывается через логи, игнорируем + ;; + *) + log "DEBUG" "Unknown PAM type: $pam_type" + ;; + esac +} + +# ============================================================================ +# MAINTENANCE +# ============================================================================ +cleanup() { + log "INFO" "Cleaning up old files" + find "$FAILED_LOGINS_DIR" -type f -mtime "+${CONFIG[CLEANUP_DAYS]}" -delete + find "$IP_CACHE_DIR" -type f -mtime +1 -delete + log "INFO" "Cleanup done" +} + +status() { + echo "=== Telegram SSH Notifier Status ===" + echo "Version: $SCRIPT_VERSION" + echo "Log file: ${CONFIG[LOG_FILE]}" + echo "Failed logins dir: $FAILED_LOGINS_DIR" + echo "Notifications: ${CONFIG[ENABLE_NOTIFICATIONS]}" + echo "GeoIP: ${CONFIG[ENABLE_GEOIP]}" + echo "Auto-block: ${CONFIG[AUTO_BLOCK_CRITICAL]}" + echo "" + local count + count="$(find "$FAILED_LOGINS_DIR" -type f 2>/dev/null | wc -l)" + echo "Tracked IPs with failed attempts: $count" + echo "" + echo "Recent failed attempts (last 10):" + find "$FAILED_LOGINS_DIR" -type f -exec ls -lt {} + 2>/dev/null | head -10 | while read -r line; do + local file + file="$(echo "$line" | awk '{print $NF}')" + if [[ -f "$file" ]]; then + IFS='|' read -r attempts _ user date < "$file" + local ip_name + ip_name="$(basename "$file" | tr '_' '.')" + echo " $ip_name: $attempts attempts, last: $date, user: $user" + fi + done +} + +# ============================================================================ +# COMMAND LINE PARSING +# ============================================================================ +usage() { + cat << EOF +Telegram SSH Notifier v$SCRIPT_VERSION + +Использование: $SCRIPT_NAME [КОМАНДА] + +Команды: + monitor Запустить мониторинг логов (демон) + cleanup Очистить старые файлы и кэш + status Показать статус и статистику + test Отправить тестовое сообщение + --help, -h Показать эту справку + --version, -v Показать версию + +Без аргументов работает в режиме PAM (для интеграции с /etc/pam.d/sshd). + +Конфигурация: $CONFIG_FILE +Лог: ${CONFIG[LOG_FILE]} +EOF +} + +# ============================================================================ +# MAIN +# ============================================================================ +main() { + # Предотвращаем повторный запуск в режиме monitor + if [[ "${1:-}" == "monitor" ]]; then + if [[ -f "$LOCK_FILE" ]]; then + if kill -0 "$(cat "$LOCK_FILE")" 2>/dev/null; then + log "ERROR" "Another monitor process is already running (PID $(cat "$LOCK_FILE")). Exiting." + exit 1 + else + rm -f "$LOCK_FILE" + fi + fi + echo $$ > "$LOCK_FILE" + trap 'rm -f "$LOCK_FILE"' EXIT + fi + + load_config + + case "${1:-}" in + monitor) + monitor_logs + ;; + cleanup) + cleanup + ;; + status) + status + ;; + test) + send_telegram_message "*Тестовое сообщение от SSH Notifier*" "true" + echo "Тестовое сообщение отправлено." + ;; + --help|-h) + usage + ;; + --version|-v) + echo "$SCRIPT_VERSION" + ;; + "") + # PAM mode + handle_pam + ;; + *) + echo "Неизвестная команда: $1" + usage + exit 1 + ;; + esac +} + +# Защита от source +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + trap 'log "ERROR" "Script failed at line $LINENO"' ERR + main "$@" +fi