#!/bin/bash # Path permission helpers for ssh-monitor, sac-client, watchdog, updater. # shellcheck shell=bash ssh_monitor_config_peek() { local key="$1" file="$2" default="${3:-}" local line val [ -f "$file" ] || { printf '%s\n' "$default" return 0 } line="$(grep -E "^[[:space:]]*${key}=" "$file" 2>/dev/null | tail -1)" || { printf '%s\n' "$default" return 0 } val="${line#*=}" val="${val#"${val%%[![:space:]]*}"}" val="${val%"${val##*[![:space:]]}"}" val="${val#\"}" val="${val%\"}" val="${val#\'}" val="${val%\'}" printf '%s\n' "$val" } ssh_monitor_truthy() { case "${1,,}" in 1 | yes | true | on) return 0 ;; esac return 1 } ssh_monitor_path_mode() { local path="$1" [ -e "$path" ] || return 1 stat -c '%a' "$path" 2>/dev/null || stat -f '%OLp' "$path" 2>/dev/null } ssh_monitor_path_owner() { local path="$1" [ -e "$path" ] || return 1 stat -c '%U:%G' "$path" 2>/dev/null } ssh_monitor_is_root_owned() { local path="$1" own [ -e "$path" ] || return 0 own="$(ssh_monitor_path_owner "$path" 2>/dev/null || true)" [ "$own" = "root:root" ] } ssh_monitor_secure_dir() { local dir="$1" [ -n "$dir" ] || return 1 mkdir -p "$dir" 2>/dev/null || return 1 chmod 700 "$dir" 2>/dev/null || true chown root:root "$dir" 2>/dev/null || true } ssh_monitor_secure_file() { local file="$1" local dir [ -n "$file" ] || return 1 dir="$(dirname "$file")" ssh_monitor_secure_dir "$dir" || true if [ ! -f "$file" ]; then : >"$file" 2>/dev/null || touch "$file" 2>/dev/null || return 1 fi chmod 600 "$file" 2>/dev/null || true chown root:root "$file" 2>/dev/null || true } # Существующий файл или каталог: 600/700 root:root; для отсутствующего пути — только родительский каталог. ssh_monitor_harden_path() { local path="$1" [ -n "$path" ] || return 0 if [ -d "$path" ]; then ssh_monitor_secure_dir "$path" elif [ -f "$path" ] || [ -L "$path" ]; then ssh_monitor_secure_file "$path" else local parent parent="$(dirname "$path")" [ -n "$parent" ] && [ "$parent" != "." ] && ssh_monitor_secure_dir "$parent" fi } ssh_monitor_warn_path_perms() { local path="$1" kind="$2" max_mode="${3:-600}" local mode own msg [ -e "$path" ] || return 0 if ! ssh_monitor_is_root_owned "$path"; then own="$(ssh_monitor_path_owner "$path" 2>/dev/null || echo '?')" msg="WARN: ${kind} ${path}: владелец ${own}, ожидается root:root" printf '%s\n' "$msg" >&2 [ -n "${write_log:-}" ] && write_log "$msg" || true fi mode="$(ssh_monitor_path_mode "$path" 2>/dev/null || echo "")" if [ -n "$mode" ] && [ "$((10#$mode))" -gt "$((10#$max_mode))" ]; then msg="WARN: ${kind} ${path}: mode ${mode}, рекомендуется не шире ${max_mode}" printf '%s\n' "$msg" >&2 [ -n "${write_log:-}" ] && write_log "$msg" || true fi } ssh_monitor_check_config_file_perms() { local file="${1:-/etc/ssh-monitor.conf}" local strict="${2:-0}" local mode msg [ -f "$file" ] || return 0 if ! ssh_monitor_is_root_owned "$file"; then msg="WARN: конфиг ${file}: владелец $(ssh_monitor_path_owner "$file" 2>/dev/null || echo '?'), ожидается root:root" printf '%s\n' "$msg" >&2 [ -n "${write_log:-}" ] && write_log "$msg" || true if ssh_monitor_truthy "$strict"; then printf '%s\n' "CONFIG_STRICT_PERMS=1: исправьте владельца конфига (chown root:root ${file})" >&2 return 1 fi fi mode="$(ssh_monitor_path_mode "$file" 2>/dev/null || echo "")" case "$mode" in 600 | 400) ;; *) msg="WARN: конфиг ${file}: mode ${mode:-?}, рекомендуется 600 или 400 (chmod 600 ${file})" printf '%s\n' "$msg" >&2 [ -n "${write_log:-}" ] && write_log "$msg" || true if ssh_monitor_truthy "$strict"; then printf '%s\n' "CONFIG_STRICT_PERMS=1: ослабленные права конфига недопустимы" >&2 return 1 fi ;; esac return 0 } # Подпись сервера в Telegram/webhook (как в ssh-monitor: SERVER_DISPLAY_NAME или hostname + IPv4). ssh_monitor_server_display_name_with_ip() { local hn ip hn="${SERVER_DISPLAY_NAME:-}" if [ -z "$hn" ]; then hn="$(hostname 2>/dev/null | tr -d '\r\n')" [ -z "$hn" ] && hn="(unknown)" fi ip="${SERVER_IPV4:-}" if [ -z "$ip" ] && command -v ip >/dev/null 2>&1; then ip="$(ip -4 route get 1.1.1.1 2>/dev/null | awk '/\bsrc\b/ {for (i = 1; i < NF; i++) if ($i == "src") { print $(i + 1); exit }}')" [ -z "$ip" ] && ip="$(ip -4 route get 8.8.8.8 2>/dev/null | awk '/\bsrc\b/ {for (i = 1; i < NF; i++) if ($i == "src") { print $(i + 1); exit }}')" fi if [ -z "$ip" ] && command -v hostname >/dev/null 2>&1 && hostname -I >/dev/null 2>&1; then ip="$(hostname -I 2>/dev/null | tr ' ' '\n' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | grep -Ev '^(127\.|0\.0\.0\.0)$' | head -n1)" fi if [ -n "$ip" ]; then printf '%s (%s)' "$hn" "$ip" else printf '%s' "$hn" fi } ssh_monitor_message_ensure_server_line() { local msg="$1" if [[ "$msg" == *'🖥️ Сервер:'* ]]; then printf '%s' "$msg" return 0 fi local first rest first="${msg%%$'\n'*}" if [ "$first" = "$msg" ]; then printf '%s\n🖥️ Сервер: %s' "$first" "$(ssh_monitor_server_display_name_with_ip)" else rest="${msg#*$'\n'}" printf '%s\n🖥️ Сервер: %s\n%s' "$first" "$(ssh_monitor_server_display_name_with_ip)" "$rest" fi } # Флаг штатного SAC/cron-update: подавляет lifecycle/watchdog Telegram на время прогона updater. SSH_MONITOR_UPDATE_STATE_DEFAULT="/var/lib/ssh-monitor/agent-update-in-progress" SSH_MONITOR_UPDATE_STATE_MAX_AGE_SEC="${SSH_MONITOR_UPDATE_STATE_MAX_AGE_SEC:-900}" ssh_monitor_update_state_file() { printf '%s\n' "${SSH_MONITOR_UPDATE_STATE_FILE:-$SSH_MONITOR_UPDATE_STATE_DEFAULT}" } ssh_monitor_update_in_progress() { local f ts now age f="$(ssh_monitor_update_state_file)" [ -f "$f" ] || return 1 ts="$(tr -d '\r\n' <"$f" 2>/dev/null || true)" [[ "$ts" =~ ^[0-9]+$ ]] || return 1 now=$(date '+%s') age=$((now - ts)) [ "$age" -le "$SSH_MONITOR_UPDATE_STATE_MAX_AGE_SEC" ] } ssh_monitor_update_mark_begin() { local f dir f="$(ssh_monitor_update_state_file)" dir="$(dirname "$f")" ssh_monitor_secure_dir "$dir" 2>/dev/null || mkdir -p "$dir" 2>/dev/null || true date '+%s' >"$f" ssh_monitor_secure_file "$f" 2>/dev/null || chmod 600 "$f" 2>/dev/null || true } ssh_monitor_update_mark_end() { local f f="$(ssh_monitor_update_state_file)" rm -f "$f" 2>/dev/null || true }