# 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 }