Files
notification_ssh/ssh_login_info.sh
2026-02-22 20:16:52 +03:00

736 lines
24 KiB
Bash
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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