From 4cd9443468aad563563c7032b28ca89ae73e6b87 Mon Sep 17 00:00:00 2001 From: Evgeniy Date: Sun, 22 Feb 2026 20:16:52 +0300 Subject: [PATCH] version 3.0 --- README.md | 83 +++-- img/image copy.png | Bin 0 -> 28014 bytes img/image.png | Bin 0 -> 20162 bytes ssh_login_info.sh | 754 +++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 796 insertions(+), 41 deletions(-) create mode 100644 img/image copy.png create mode 100644 img/image.png 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 0000000000000000000000000000000000000000..d25275f475257022b8495c82e35cd79d077ebe9c GIT binary patch literal 28014 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 Gz<&UGbDwnp literal 0 HcmV?d00001 diff --git a/img/image.png b/img/image.png new file mode 100644 index 0000000000000000000000000000000000000000..a7dc271a974db6ba903a5e223c223e0bb6546419 GIT binary patch literal 20162 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 literal 0 HcmV?d00001 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