feat: UseSAC sac-client.sh and --check-sac
This commit is contained in:
+209
@@ -0,0 +1,209 @@
|
|||||||
|
# SAC client for ssh-monitor (source from ssh-monitor)
|
||||||
|
# shellcheck shell=bash
|
||||||
|
|
||||||
|
sac_normalize_mode() {
|
||||||
|
printf '%s' "${1:-off}" | tr '[:upper:]' '[:lower:]'
|
||||||
|
}
|
||||||
|
|
||||||
|
sac_is_configured() {
|
||||||
|
[ -n "${SAC_URL:-}" ] && [ -n "${SAC_API_KEY:-}" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
sac_base_url() {
|
||||||
|
local url="${SAC_URL%/}"
|
||||||
|
case "$url" in
|
||||||
|
*/api/v1/events) url="${url%/api/v1/events}" ;;
|
||||||
|
esac
|
||||||
|
printf '%s' "${url%/}"
|
||||||
|
}
|
||||||
|
|
||||||
|
sac_agent_instance_id() {
|
||||||
|
local id_file="${SAC_AGENT_ID_FILE:-/var/lib/ssh-monitor/agent_instance_id}"
|
||||||
|
local dir
|
||||||
|
dir="$(dirname "$id_file")"
|
||||||
|
if [ ! -d "$dir" ]; then
|
||||||
|
mkdir -p "$dir" 2>/dev/null || id_file="/tmp/ssh-monitor-agent_instance_id"
|
||||||
|
fi
|
||||||
|
if [ -f "$id_file" ]; then
|
||||||
|
cat "$id_file"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
local new_id
|
||||||
|
new_id="$(python3 -c 'import uuid; print(uuid.uuid4())')"
|
||||||
|
printf '%s\n' "$new_id" >"$id_file" 2>/dev/null || true
|
||||||
|
printf '%s' "$new_id"
|
||||||
|
}
|
||||||
|
|
||||||
|
sac_check_health() {
|
||||||
|
local base code
|
||||||
|
base="$(sac_base_url)"
|
||||||
|
[ -n "$base" ] || return 1
|
||||||
|
if [ "${DRY_RUN:-0}" = "1" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
code="$(curl -sS -o /dev/null -w '%{http_code}' \
|
||||||
|
--connect-timeout "${SAC_TIMEOUT_SEC:-12}" \
|
||||||
|
--max-time "${SAC_TIMEOUT_SEC:-12}" \
|
||||||
|
"${base}/health" || echo 000)"
|
||||||
|
[ "$code" = "200" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
sac_spool_write() {
|
||||||
|
local event_id="$1"
|
||||||
|
local body="$2"
|
||||||
|
local dir="${SAC_SPOOL_DIR:-/var/lib/ssh-monitor/sac-spool}"
|
||||||
|
mkdir -p "$dir" 2>/dev/null || return 1
|
||||||
|
printf '%s' "$body" >"${dir}/${event_id}.json"
|
||||||
|
}
|
||||||
|
|
||||||
|
sac_spool_remove() {
|
||||||
|
local event_id="$1"
|
||||||
|
local dir="${SAC_SPOOL_DIR:-/var/lib/ssh-monitor/sac-spool}"
|
||||||
|
rm -f "${dir}/${event_id}.json"
|
||||||
|
}
|
||||||
|
|
||||||
|
# send_sac_event type severity title summary [details_json]
|
||||||
|
sac_send_event() {
|
||||||
|
local event_type="$1"
|
||||||
|
local severity="$2"
|
||||||
|
local title="$3"
|
||||||
|
local summary="$4"
|
||||||
|
local details_json="${5:-}"
|
||||||
|
|
||||||
|
if ! sac_is_configured; then
|
||||||
|
write_log "WARN: SAC не настроен (SAC_URL / SAC_API_KEY)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${DRY_RUN:-0}" = "1" ]; then
|
||||||
|
printf '[DRY-RUN SAC] %s | %s | %s\n' "$event_type" "$severity" "$title" >&2
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local payload http_code event_id
|
||||||
|
payload="$(SAC_EVENT_TYPE="$event_type" \
|
||||||
|
SAC_SEVERITY="$severity" \
|
||||||
|
SAC_TITLE="$title" \
|
||||||
|
SAC_SUMMARY="$summary" \
|
||||||
|
SAC_DETAILS_JSON="$details_json" \
|
||||||
|
SAC_AGENT_ID="$(sac_agent_instance_id)" \
|
||||||
|
SAC_PRODUCT_VERSION="${SSH_MONITOR_VERSION:-unknown}" \
|
||||||
|
python3 <<'PY'
|
||||||
|
import json, os, socket, uuid
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
def details():
|
||||||
|
raw = os.environ.get("SAC_DETAILS_JSON", "").strip()
|
||||||
|
if not raw:
|
||||||
|
return None
|
||||||
|
return json.loads(raw)
|
||||||
|
|
||||||
|
host = socket.gethostname()
|
||||||
|
payload = {
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"event_id": str(uuid.uuid4()),
|
||||||
|
"occurred_at": datetime.now(timezone.utc).astimezone().isoformat(timespec="seconds"),
|
||||||
|
"source": {
|
||||||
|
"product": "ssh-monitor",
|
||||||
|
"product_version": os.environ.get("SAC_PRODUCT_VERSION", "unknown"),
|
||||||
|
"agent_instance_id": os.environ["SAC_AGENT_ID"],
|
||||||
|
},
|
||||||
|
"host": {
|
||||||
|
"hostname": host,
|
||||||
|
"os_family": "linux",
|
||||||
|
},
|
||||||
|
"category": "agent",
|
||||||
|
"type": os.environ["SAC_EVENT_TYPE"],
|
||||||
|
"severity": os.environ["SAC_SEVERITY"],
|
||||||
|
"title": os.environ["SAC_TITLE"],
|
||||||
|
"summary": os.environ["SAC_SUMMARY"],
|
||||||
|
}
|
||||||
|
d = details()
|
||||||
|
if d is not None:
|
||||||
|
payload["details"] = d
|
||||||
|
print(json.dumps(payload, ensure_ascii=False))
|
||||||
|
PY
|
||||||
|
)" || return 1
|
||||||
|
|
||||||
|
event_id="$(printf '%s' "$payload" | python3 -c 'import json,sys; print(json.load(sys.stdin)["event_id"])')"
|
||||||
|
|
||||||
|
local tmp resp
|
||||||
|
tmp="$(mktemp)"
|
||||||
|
resp="$(mktemp)"
|
||||||
|
printf '%s' "$payload" >"$tmp"
|
||||||
|
http_code="$(curl -sS -o "$resp" -w '%{http_code}' \
|
||||||
|
-X POST "${SAC_URL}" \
|
||||||
|
-H "Authorization: Bearer ${SAC_API_KEY}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Idempotency-Key: ${event_id}" \
|
||||||
|
--connect-timeout "${SAC_TIMEOUT_SEC:-12}" \
|
||||||
|
--max-time "${SAC_TIMEOUT_SEC:-12}" \
|
||||||
|
-d @"$tmp" 2>/dev/null || echo 000)"
|
||||||
|
rm -f "$tmp"
|
||||||
|
|
||||||
|
if [ "$http_code" = "202" ]; then
|
||||||
|
sac_spool_remove "$event_id" 2>/dev/null || true
|
||||||
|
write_log "SAC: принято event_id=$event_id type=$event_type"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
write_log "WARN: SAC POST HTTP $http_code: $(tr '\n' ' ' <"$resp" | head -c 200)"
|
||||||
|
sac_spool_write "$event_id" "$payload"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# notify_or_sac type severity title summary telegram_message
|
||||||
|
notify_or_sac() {
|
||||||
|
local event_type="$1"
|
||||||
|
local severity="$2"
|
||||||
|
local title="$3"
|
||||||
|
local summary="$4"
|
||||||
|
local telegram_message="${5:-$summary}"
|
||||||
|
local mode
|
||||||
|
mode="$(sac_normalize_mode "${UseSAC:-off}")"
|
||||||
|
|
||||||
|
case "$mode" in
|
||||||
|
off)
|
||||||
|
notify_send "$telegram_message"
|
||||||
|
;;
|
||||||
|
exclusive)
|
||||||
|
sac_send_event "$event_type" "$severity" "$title" "$summary"
|
||||||
|
;;
|
||||||
|
dual)
|
||||||
|
sac_send_event "$event_type" "$severity" "$title" "$summary" || true
|
||||||
|
notify_send "$telegram_message"
|
||||||
|
;;
|
||||||
|
fallback)
|
||||||
|
if sac_send_event "$event_type" "$severity" "$title" "$summary"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
notify_send "$telegram_message"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
write_log "WARN: неизвестный UseSAC=$mode, только Telegram"
|
||||||
|
notify_send "$telegram_message"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
run_check_sac() {
|
||||||
|
printf 'Проверка SAC (ssh-monitor)\n'
|
||||||
|
printf 'UseSAC=%s\n' "${UseSAC:-off}"
|
||||||
|
printf 'SAC_URL=%s\n' "${SAC_URL:-}"
|
||||||
|
if ! sac_is_configured; then
|
||||||
|
printf 'SAC: SAC_URL или SAC_API_KEY не заданы\n' >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if sac_check_health; then
|
||||||
|
printf 'SAC health: OK\n'
|
||||||
|
else
|
||||||
|
printf 'SAC health: FAIL\n' >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if sac_send_event "agent.test" "info" "SAC test" "ssh-monitor --check-sac"; then
|
||||||
|
printf 'SAC ingest agent.test: OK (ожидается HTTP 202)\n'
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
printf 'SAC ingest agent.test: FAIL\n' >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
+63
-1
@@ -9,6 +9,18 @@ IFS=$'\n\t'
|
|||||||
CONFIG_FILE="/etc/ssh-monitor.conf"
|
CONFIG_FILE="/etc/ssh-monitor.conf"
|
||||||
SSH_MONITOR_VERSION="1.1.3-server-label"
|
SSH_MONITOR_VERSION="1.1.3-server-label"
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
if [ -f "$SCRIPT_DIR/sac-client.sh" ]; then
|
||||||
|
# shellcheck source=sac-client.sh
|
||||||
|
source "$SCRIPT_DIR/sac-client.sh"
|
||||||
|
else
|
||||||
|
sac_normalize_mode() { printf '%s' "${1:-off}" | tr '[:upper:]' '[:lower:]'; }
|
||||||
|
sac_is_configured() { return 1; }
|
||||||
|
sac_check_health() { return 1; }
|
||||||
|
notify_or_sac() { notify_send "${5:-$4}"; }
|
||||||
|
run_check_sac() { printf 'sac-client.sh не найден\n' >&2; return 1; }
|
||||||
|
fi
|
||||||
|
|
||||||
TELEGRAM_BOT_TOKEN=""
|
TELEGRAM_BOT_TOKEN=""
|
||||||
TELEGRAM_CHAT_ID=""
|
TELEGRAM_CHAT_ID=""
|
||||||
BACKUP_WEBHOOK_URL=""
|
BACKUP_WEBHOOK_URL=""
|
||||||
@@ -17,6 +29,15 @@ BACKUP_WEBHOOK_URL=""
|
|||||||
# Пусто = авто: только настроенные каналы, порядок telegram → email.
|
# Пусто = авто: только настроенные каналы, порядок telegram → email.
|
||||||
NOTIFY_ORDER=""
|
NOTIFY_ORDER=""
|
||||||
|
|
||||||
|
# Security Alert Center: off | exclusive | dual | fallback
|
||||||
|
UseSAC="off"
|
||||||
|
SAC_URL=""
|
||||||
|
SAC_API_KEY=""
|
||||||
|
SAC_SPOOL_DIR="/var/lib/ssh-monitor/sac-spool"
|
||||||
|
SAC_TIMEOUT_SEC="12"
|
||||||
|
SAC_SEND_HEARTBEAT="1"
|
||||||
|
SAC_FALLBACK_FAILURES="5"
|
||||||
|
|
||||||
# Почта (SMTP через python3). Нужны MAIL_SMTP_HOST, MAIL_FROM, MAIL_TO.
|
# Почта (SMTP через python3). Нужны MAIL_SMTP_HOST, MAIL_FROM, MAIL_TO.
|
||||||
MAIL_SMTP_HOST=""
|
MAIL_SMTP_HOST=""
|
||||||
MAIL_SMTP_PORT="587"
|
MAIL_SMTP_PORT="587"
|
||||||
@@ -192,6 +213,11 @@ load_config() {
|
|||||||
local cfg_whitelist_ips_csv="${default_whitelist_ips_csv}"
|
local cfg_whitelist_ips_csv="${default_whitelist_ips_csv}"
|
||||||
local cfg_whitelist_subnets_csv="${default_whitelist_subnets_csv}"
|
local cfg_whitelist_subnets_csv="${default_whitelist_subnets_csv}"
|
||||||
local cfg_notify_order="${NOTIFY_ORDER:-}"
|
local cfg_notify_order="${NOTIFY_ORDER:-}"
|
||||||
|
local cfg_use_sac="${UseSAC:-off}"
|
||||||
|
local cfg_sac_url="${SAC_URL:-}"
|
||||||
|
local cfg_sac_api_key="${SAC_API_KEY:-}"
|
||||||
|
local cfg_sac_spool_dir="${SAC_SPOOL_DIR:-/var/lib/ssh-monitor/sac-spool}"
|
||||||
|
local cfg_sac_timeout_sec="${SAC_TIMEOUT_SEC:-12}"
|
||||||
local cfg_mail_smtp_host="${MAIL_SMTP_HOST:-}"
|
local cfg_mail_smtp_host="${MAIL_SMTP_HOST:-}"
|
||||||
local cfg_mail_smtp_port="${MAIL_SMTP_PORT:-587}"
|
local cfg_mail_smtp_port="${MAIL_SMTP_PORT:-587}"
|
||||||
local cfg_mail_smtp_user="${MAIL_SMTP_USER:-}"
|
local cfg_mail_smtp_user="${MAIL_SMTP_USER:-}"
|
||||||
@@ -254,6 +280,11 @@ load_config() {
|
|||||||
csv_to_array "$whitelist_subnets_csv" WHITELIST_SUBNETS
|
csv_to_array "$whitelist_subnets_csv" WHITELIST_SUBNETS
|
||||||
|
|
||||||
NOTIFY_ORDER="$(trim_value "${NOTIFY_ORDER:-$cfg_notify_order}")"
|
NOTIFY_ORDER="$(trim_value "${NOTIFY_ORDER:-$cfg_notify_order}")"
|
||||||
|
UseSAC="$(trim_value "${UseSAC:-$cfg_use_sac}")"
|
||||||
|
SAC_URL="$(trim_value "${SAC_URL:-$cfg_sac_url}")"
|
||||||
|
SAC_API_KEY="$(trim_value "${SAC_API_KEY:-$cfg_sac_api_key}")"
|
||||||
|
SAC_SPOOL_DIR="$(trim_value "${SAC_SPOOL_DIR:-$cfg_sac_spool_dir}")"
|
||||||
|
SAC_TIMEOUT_SEC="$(trim_value "${SAC_TIMEOUT_SEC:-$cfg_sac_timeout_sec}")"
|
||||||
MAIL_SMTP_HOST="$(trim_value "${MAIL_SMTP_HOST:-$cfg_mail_smtp_host}")"
|
MAIL_SMTP_HOST="$(trim_value "${MAIL_SMTP_HOST:-$cfg_mail_smtp_host}")"
|
||||||
MAIL_SMTP_PORT="${MAIL_SMTP_PORT:-$cfg_mail_smtp_port}"
|
MAIL_SMTP_PORT="${MAIL_SMTP_PORT:-$cfg_mail_smtp_port}"
|
||||||
MAIL_SMTP_USER="$(trim_value "${MAIL_SMTP_USER:-$cfg_mail_smtp_user}")"
|
MAIL_SMTP_USER="$(trim_value "${MAIL_SMTP_USER:-$cfg_mail_smtp_user}")"
|
||||||
@@ -357,8 +388,31 @@ validate_config() {
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
local sac_mode
|
||||||
|
sac_mode="$(sac_normalize_mode "${UseSAC:-off}")"
|
||||||
|
case "$sac_mode" in
|
||||||
|
off) ;;
|
||||||
|
exclusive | dual | fallback)
|
||||||
|
if ! sac_is_configured; then
|
||||||
|
printf '%s\n' "UseSAC=${sac_mode}: задайте SAC_URL и SAC_API_KEY" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
printf '%s\n' "Неверный UseSAC=\"${UseSAC:-}\"" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
refresh_notify_chain
|
refresh_notify_chain
|
||||||
if [ "${#NOTIFY_CHAIN[@]}" -eq 0 ]; then
|
if [ "$sac_mode" = "exclusive" ]; then
|
||||||
|
if [ "${CHECK_CONFIG_ONLY:-0}" != "1" ] && [ "${CHECK_SAC_ONLY:-0}" != "1" ] && [ "${DRY_RUN:-0}" != "1" ]; then
|
||||||
|
if ! sac_check_health; then
|
||||||
|
printf '%s\n' 'UseSAC=exclusive: SAC /health недоступен' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
elif [ "${#NOTIFY_CHAIN[@]}" -eq 0 ]; then
|
||||||
printf '%s\n' 'Не настроен ни один канал отправки оповещений' >&2
|
printf '%s\n' 'Не настроен ни один канал отправки оповещений' >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -1868,6 +1922,8 @@ run_check_config() {
|
|||||||
"${MAIL_SMTP_STARTTLS:-}" "${MAIL_SMTP_SSL:-}"
|
"${MAIL_SMTP_STARTTLS:-}" "${MAIL_SMTP_SSL:-}"
|
||||||
printf 'MAIL_SMTP_USER: %s\n' "$([ -n "${MAIL_SMTP_USER:-}" ] && echo задан || echo не задан)"
|
printf 'MAIL_SMTP_USER: %s\n' "$([ -n "${MAIL_SMTP_USER:-}" ] && echo задан || echo не задан)"
|
||||||
printf 'MAIL_SMTP_PASSWORD: %s\n' "$([ -n "${MAIL_SMTP_PASSWORD:-}" ] && echo задан || echo не задан)"
|
printf 'MAIL_SMTP_PASSWORD: %s\n' "$([ -n "${MAIL_SMTP_PASSWORD:-}" ] && echo задан || echo не задан)"
|
||||||
|
printf 'UseSAC=%s SAC_URL=%s SAC_API_KEY=%s\n' "${UseSAC:-off}" "${SAC_URL:-}" \
|
||||||
|
"$([ -n "${SAC_API_KEY:-}" ] && echo задан || echo не задан)"
|
||||||
if bash -n "$0" 2>/dev/null; then
|
if bash -n "$0" 2>/dev/null; then
|
||||||
printf 'bash -n: OK\n'
|
printf 'bash -n: OK\n'
|
||||||
else
|
else
|
||||||
@@ -1881,6 +1937,7 @@ parse_args() {
|
|||||||
for a in "$@"; do
|
for a in "$@"; do
|
||||||
case "$a" in
|
case "$a" in
|
||||||
--check-config) CHECK_CONFIG_ONLY=1 ;;
|
--check-config) CHECK_CONFIG_ONLY=1 ;;
|
||||||
|
--check-sac) CHECK_SAC_ONLY=1 ;;
|
||||||
--dry-run) DRY_RUN=1 ;;
|
--dry-run) DRY_RUN=1 ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
@@ -1900,6 +1957,11 @@ if [ "$CHECK_CONFIG_ONLY" = "1" ]; then
|
|||||||
exit $?
|
exit $?
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ "${CHECK_SAC_ONLY:-0}" = "1" ]; then
|
||||||
|
run_check_sac
|
||||||
|
exit $?
|
||||||
|
fi
|
||||||
|
|
||||||
trap send_shutdown_notification EXIT INT TERM
|
trap send_shutdown_notification EXIT INT TERM
|
||||||
|
|
||||||
mkdir -p "$(dirname "$LOG_FILE")"
|
mkdir -p "$(dirname "$LOG_FILE")"
|
||||||
|
|||||||
@@ -25,6 +25,14 @@ BACKUP_WEBHOOK_URL=""
|
|||||||
# NOTIFY_ORDER="email"
|
# NOTIFY_ORDER="email"
|
||||||
NOTIFY_ORDER=""
|
NOTIFY_ORDER=""
|
||||||
|
|
||||||
|
# --- Security Alert Center (SAC) ---
|
||||||
|
# off | exclusive | dual | fallback — см. docs в security-alert-center (agent-integration.md)
|
||||||
|
UseSAC="off"
|
||||||
|
SAC_URL="https://sac.kalinamall.ru/api/v1/events"
|
||||||
|
SAC_API_KEY=""
|
||||||
|
SAC_SPOOL_DIR="/var/lib/ssh-monitor/sac-spool"
|
||||||
|
SAC_TIMEOUT_SEC="12"
|
||||||
|
|
||||||
# SMTP (python3). Для авто-режима почты нужны MAIL_SMTP_HOST, MAIL_FROM, MAIL_TO.
|
# SMTP (python3). Для авто-режима почты нужны MAIL_SMTP_HOST, MAIL_FROM, MAIL_TO.
|
||||||
MAIL_SMTP_HOST=""
|
MAIL_SMTP_HOST=""
|
||||||
MAIL_SMTP_PORT="587"
|
MAIL_SMTP_PORT="587"
|
||||||
|
|||||||
Reference in New Issue
Block a user