version 3.0

This commit is contained in:
2026-02-22 20:16:52 +03:00
parent dc00c87209
commit 4cd9443468
4 changed files with 796 additions and 41 deletions

View File

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