chore(home): mirror from kalinamall (9883e6a) with papatramp URLs

This commit is contained in:
2026-07-14 20:43:52 +10:00
commit ed4e78f6c3
312 changed files with 42790 additions and 0 deletions
+30
View File
@@ -0,0 +1,30 @@
# Только для Docker Compose (см. docs/install-ubuntu-24.04-docker.md).
# Production native: deploy/env.native.example → /opt/security-alert-center/config/sac-api.env
# PostgreSQL
POSTGRES_USER=sac
POSTGRES_PASSWORD=change-me-long-random
POSTGRES_DB=sac
# API (публичный URL для ссылок в ответах ingest)
SAC_PUBLIC_URL=https://sac.kalinamall.ru
SAC_API_PORT=8000
# JWT (UI — позже)
JWT_SECRET=change-me-openssl-rand-hex-32
# Первый API key для агентов (заголовок: Authorization: Bearer <ключ>)
# Сгенерировать: python -c "import secrets; print('sac_'+secrets.token_urlsafe(32))"
SAC_BOOTSTRAP_API_KEY=sac_replace_with_generated_key
# Admin UI (фаза 1C)
SAC_ADMIN_USERNAME=admin
SAC_ADMIN_PASSWORD=
# Telegram notifications from SAC (phase 1C.4)
TELEGRAM_ENABLED=false
TELEGRAM_BOT_TOKEN=
TELEGRAM_CHAT_ID=
TELEGRAM_MIN_SEVERITY=high
CORS_ORIGINS=*
+30
View File
@@ -0,0 +1,30 @@
# Deploy
## Production (native) — основной путь
1. [docs/install-ubuntu-24.04-native.md](../docs/install-ubuntu-24.04-native.md)
2. `deploy/env.native.example``/opt/security-alert-center/config/sac-api.env`
3. `deploy/systemd/sac-api.service``/etc/systemd/system/`
4. `deploy/nginx/sac.conf.tls.example``/etc/nginx/sites-available/sac` (wildcard TLS; только `sac.kalinamall.ru`, чужие Host → 444)
или `sac.conf.example` — только HTTP / отладка
5. `deploy/sac-deploy.sh``/opt/sac-deploy.sh` — полное обновление после `git pull`
```bash
sudo cp deploy/sac-deploy.sh /opt/sac-deploy.sh
sudo chmod 755 /opt/sac-deploy.sh
sudo /opt/sac-deploy.sh
```
Суточные отчёты: `sac-daily-report.service` + timer. Файл `sac-daily-report.timer` в git — шаблон;
на сервере timer **генерируется** из `SAC_DAILY_REPORT_HOUR` / `SAC_DAILY_REPORT_TIMEZONE`
(`deploy/systemd/render-sac-daily-report-timer.sh`, вызывается из `sac-deploy.sh`).
## Альтернатива: Docker
Только для стенда. См. [docs/install-ubuntu-24.04-docker.md](../docs/install-ubuntu-24.04-docker.md).
```bash
cp .env.example .env # docker-only
docker compose up -d --build
docker compose run --rm migrate
```
+51
View File
@@ -0,0 +1,51 @@
services:
postgres:
image: postgres:16-bookworm
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER:-sac}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB:-sac}
volumes:
- sac_pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-sac} -d ${POSTGRES_DB:-sac}"]
interval: 5s
timeout: 5s
retries: 10
api:
build:
context: ..
dockerfile: backend/Dockerfile
restart: unless-stopped
env_file:
- .env
environment:
DATABASE_URL: postgresql+psycopg2://${POSTGRES_USER:-sac}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-sac}
SAC_PUBLIC_URL: ${SAC_PUBLIC_URL:-http://localhost:8000}
JWT_SECRET: ${JWT_SECRET:?set JWT_SECRET}
SAC_BOOTSTRAP_API_KEY: ${SAC_BOOTSTRAP_API_KEY:?set SAC_BOOTSTRAP_API_KEY}
EVENT_SCHEMA_PATH: /app/schemas/event-schema-v1.json
CORS_ORIGINS: ${CORS_ORIGINS:-*}
ports:
- "${SAC_API_PORT:-8000}:8000"
depends_on:
postgres:
condition: service_healthy
migrate:
build:
context: ..
dockerfile: backend/Dockerfile
env_file:
- .env
environment:
DATABASE_URL: postgresql+psycopg2://${POSTGRES_USER:-sac}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-sac}
command: alembic upgrade head
depends_on:
postgres:
condition: service_healthy
volumes:
sac_pgdata:
+139
View File
@@ -0,0 +1,139 @@
# Native: /opt/security-alert-center/config/sac-api.env
# sudo chown sac:sac ... && sudo chmod 600 ...
# Пароль в URL — в двойных кавычках. Символ # в пароле допустим только внутри кавычек.
# systemd НЕ парсит этот файл — читает приложение (SAC_CONFIG_FILE).
DATABASE_URL=postgresql+psycopg2://sac:CHANGE_ME_POSTGRES_PASSWORD@127.0.0.1:5432/sac
# SQLAlchemy pool (uvicorn workers × concurrent ingest). Было по умолчанию 5+10 — мало для штурма 09:00.
SAC_DB_POOL_SIZE=15
SAC_DB_MAX_OVERFLOW=25
# Uvicorn workers (ingest burst). Читает deploy/systemd/sac-api-start.sh; при >1 отключите in-process host_silence scan ниже.
SAC_UVICORN_WORKERS=4
SAC_PUBLIC_URL=https://sac.kalinamall.ru
# Опционально: другой базовый URL для WinRM-скачивания RDP bundle с ПК (LAN / split-DNS).
# SAC_AGENT_BUNDLE_BASE_URL=https://192.168.x.x
JWT_SECRET=CHANGE_ME_openssl_rand_hex_32
SAC_SECURITY_ENFORCE=true
# API key для агентов: Authorization: Bearer <ключ>
# python3.12 -c "import secrets; print('sac_'+secrets.token_urlsafe(32))"
SAC_BOOTSTRAP_API_KEY=sac_replace_with_generated_key
EVENT_SCHEMA_PATH=/opt/security-alert-center/schemas/event-schema-v1.json
SAC_ADMIN_USERNAME=admin
SAC_ADMIN_PASSWORD=
# Telegram notifications from SAC (phase 1C.4)
# При UseSAC=exclusive на агентах — основной канал для оператора; см. docs/agent-integration.md §1.3.1
TELEGRAM_ENABLED=false
TELEGRAM_BOT_TOKEN=
TELEGRAM_CHAT_ID=
# exclusive: warning (failed login, problems); prod noise-only: high
TELEGRAM_MIN_SEVERITY=high
# Generic webhook (JSON POST), F-NOT-01
WEBHOOK_ENABLED=false
WEBHOOK_URL=
WEBHOOK_SECRET_HEADER=
WEBHOOK_SECRET=
WEBHOOK_MIN_SEVERITY=high
# SMTP email (F-NOT-01)
SMTP_ENABLED=false
SMTP_HOST=
SMTP_PORT=587
SMTP_USER=
SMTP_PASSWORD=
SMTP_FROM=
SMTP_TO=
SMTP_STARTTLS=true
SMTP_SSL=false
SMTP_MIN_SEVERITY=high
# Глобальное правило: severity ≥ NOTIFY_MIN_SEVERITY → каналы из NOTIFY_CHANNELS
NOTIFY_MIN_SEVERITY=warning
NOTIFY_CHANNELS=telegram,webhook,email
# Cooldown оповещений SAC (секунды; 0 = выкл. для kind)
SAC_NOTIFY_COOLDOWN_ENABLED=true
SAC_NOTIFY_EVENT_COOLDOWN_SEC=90
SAC_NOTIFY_PROBLEM_COOLDOWN_SEC=300
# Суточные отчёты из SAC (F-NOT-05; systemd sac-daily-report.timer)
SAC_DAILY_REPORT_ENABLED=true
SAC_DAILY_REPORT_HOUR=9
SAC_DAILY_REPORT_TIMEZONE=Europe/Moscow
SAC_DAILY_REPORT_SKIP_IF_AGENT_SENT=true
SAC_DAILY_REPORT_REQUIRE_ACTIVITY=true
# Статус хоста по agent.heartbeat
# Порог stale для agent.heartbeat (мин). При интервале агента ~4 ч — 300 (5 ч) даёт запас.
SAC_HEARTBEAT_STALE_MINUTES=300
# Проактивный алерт host_silence (без ожидания нового события с хоста)
# Если включён systemd timer sac-host-silence-scan.timer — лучше SAC_HOST_SILENCE_SCAN_ENABLED=false
# (иначе scan идёт параллельно из каждого uvicorn worker + timer).
SAC_HOST_SILENCE_SCAN_ENABLED=true
SAC_HOST_SILENCE_SCAN_INTERVAL_MINUTES=5
# Корреляция Problems: host + type + rule в одном open Problem (минуты)
SAC_PROBLEM_CORRELATION_WINDOW_MINUTES=60
# rule:brute_force_burst (ssh.login.failed / rdp.login.failed)
SAC_BRUTE_FORCE_WINDOW_MINUTES=15
SAC_BRUTE_FORCE_THRESHOLD=30
# rule:privilege_spike (privilege.sudo.command)
SAC_PRIVILEGE_SPIKE_WINDOW_MINUTES=10
SAC_PRIVILEGE_SPIKE_THRESHOLD=10
# rule:rdg_session_flap (302→303)
SAC_RDG_FLAP_WINDOW_MIN_SEC=1
SAC_RDG_FLAP_WINDOW_MAX_SEC=10
SAC_RDG_FLAP_DEDUP_SEC=30
# IP HAProxy (или другого RDG-прокси), через который виден вход: external_ip в событии 302/303
# SAC_RDG_HAPROXY_EXTERNAL_IPS=192.168.160.50,10.0.0.100
# qwinsta/logoff на Windows-хостах (доменный admin)
# SAC_WIN_ADMIN_USER=B26\\Administrator
# SAC_WIN_ADMIN_PASSWORD=
# SSH / update_ssh_monitor.sh на Linux-хостах
# SAC_LINUX_ADMIN_USER=root
# SAC_LINUX_ADMIN_PASSWORD=
# Retention (systemd sac-retention.timer)
SAC_EVENTS_RETENTION_DAYS=90
SAC_PROBLEMS_RETENTION_DAYS=180
# UI login: блокировка IP после N неудачных попыток (окно в минутах) + алерт в Telegram
SAC_LOGIN_MAX_FAILURES=3
SAC_LOGIN_FAILURE_WINDOW_MINUTES=15
SAC_LOGIN_ALERT_TELEGRAM=true
# IP через запятую — не блокируются в UI (дополняет список в Настройках SAC)
SAC_LOGIN_IP_WHITELIST=192.168.160.3
# fail2ban: синхронизация белого списка из UI (нужны права записи + reload)
SAC_FAIL2BAN_SYNC_ENABLED=false
SAC_FAIL2BAN_SSH_JAIL=sshd
SAC_FAIL2BAN_IGNOREIP_FILE=/etc/fail2ban/jail.d/sac-login-whitelist.local
# Seaca mobile push (FCM HTTP v1)
SAC_FCM_ENABLED=false
SAC_FCM_PROJECT_ID=
# Путь к JSON service account Firebase (chmod 600, владелец sac)
SAC_FCM_SERVICE_ACCOUNT_JSON=
SAC_MOBILE_REFRESH_EXPIRE_DAYS=90
CORS_ORIGINS=https://sac.kalinamall.ru
# SSH: verify host keys (RejectPolicy). Add keys to file before remote actions.
# Комментарий — только отдельной строкой с #. Нельзя: SAC_SSH_AUTO_ADD_HOST_KEY=false ← текст
# SAC_SSH_KNOWN_HOSTS_FILE=/opt/security-alert-center/config/ssh_known_hosts
# SAC_SSH_AUTO_ADD_HOST_KEY=false
# WinRM: enable HTTPS listener on clients (5986) before turning on.
# SAC_WINRM_USE_HTTPS=false
# SAC_WINRM_SERVER_CERT_VALIDATION=validate
+69
View File
@@ -0,0 +1,69 @@
# /etc/nginx/sites-available/sac
# Production FQDN: sac.kalinamall.ru
# API: 127.0.0.1:8000 (systemd sac-api)
#
# Hostname: только sac.kalinamall.ru; остальные Host → 444 (см. default_server ниже).
#
# Порядок: 1) этот конфиг (только :80) → nginx -t → reload
# 2) certbot --nginx -d sac.kalinamall.ru (добавит :443 и сертификаты)
upstream sac_api {
server 127.0.0.1:8000;
keepalive 8;
}
# --- default: отклонить чужие Host (не sac.kalinamall.ru) ---
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
return 444;
}
server {
listen 80;
listen [::]:80;
server_name sac.kalinamall.ru;
location /.well-known/acme-challenge/ {
root /var/www/html;
}
client_max_body_size 2m;
# SSH/WinRM и git-probe: backend ждёт до 900s
location ~ ^/api/v1/(hosts/[0-9]+/actions/(agent-update|agent-update-fallback|ssh-test|winrm-test)|settings/agent-updates/test-git) {
proxy_pass http://sac_api;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 30s;
proxy_send_timeout 960s;
proxy_read_timeout 960s;
}
# Ingest burst: lifecycle/auth deferred в API, но POST всё равно может ждать notify_event/problem.
location = /api/v1/events {
proxy_pass http://sac_api;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 30s;
proxy_send_timeout 120s;
proxy_read_timeout 120s;
}
location / {
proxy_pass http://sac_api;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 60s;
}
}
+94
View File
@@ -0,0 +1,94 @@
# /etc/nginx/sites-available/sac
# HTTPS с корпоративным wildcard-сертификатом (например VeriSign / DigiCert).
# Покрывает sac.kalinamall.ru при CN/SAN *.kalinamall.ru или sac.kalinamall.ru
#
# Hostname: только sac.kalinamall.ru. Любой другой Host (например ext.kalinamall.ru
# при ошибке DNS/маршрутизации на IP SAC) получает 444 / reject handshake —
# форма входа SAC не отдаётся на «чужом» имени.
#
# Перед включением:
# 1) Положите fullchain и ключ в /etc/ssl/sac/ (см. docs/ssl-certificate.md)
# 2) Подставьте пути в ssl_certificate / ssl_certificate_key ниже
# 3) sudo nginx -t && sudo systemctl reload nginx
upstream sac_api {
server 127.0.0.1:8000;
keepalive 8;
}
# --- default: отклонить чужие Host на :80 ---
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
return 444;
}
# --- default: отклонить чужой SNI/Host на :443 (nginx >= 1.19.5) ---
server {
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
server_name _;
ssl_reject_handshake on;
}
# Редирект HTTP → HTTPS (только sac.kalinamall.ru)
server {
listen 80;
listen [::]:80;
server_name sac.kalinamall.ru;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name sac.kalinamall.ru;
# --- замените пути на ваши файлы wildcard ---
ssl_certificate /etc/ssl/sac/fullchain.pem;
ssl_certificate_key /etc/ssl/sac/privkey.pem;
ssl_session_timeout 1d;
ssl_session_cache shared:SAC_SSL:10m;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
client_max_body_size 2m;
# SSH/WinRM и git-probe: backend ждёт до 900s
location ~ ^/api/v1/(hosts/[0-9]+/actions/(agent-update|agent-update-fallback|ssh-test|winrm-test)|settings/agent-updates/test-git) {
proxy_pass http://sac_api;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 30s;
proxy_send_timeout 960s;
proxy_read_timeout 960s;
}
# Ingest burst: lifecycle/auth deferred в API, но POST всё равно может ждать notify_event/problem.
location = /api/v1/events {
proxy_pass http://sac_api;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 30s;
proxy_send_timeout 120s;
proxy_read_timeout 120s;
}
location / {
proxy_pass http://sac_api;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 60s;
}
}
+198
View File
@@ -0,0 +1,198 @@
#!/usr/bin/env bash
# Полное обновление SAC на сервере (native).
# Установка: sudo cp /opt/security-alert-center/deploy/sac-deploy.sh /opt/sac-deploy.sh
# sudo chmod 755 /opt/sac-deploy.sh
# Запуск: sudo /opt/sac-deploy.sh
#
# Выполняет от пользователя sac: git pull, pip, alembic, npm build.
# От root: systemctl restart sac-api.
set -euo pipefail
APP_ROOT="${SAC_APP_ROOT:-/opt/security-alert-center}"
APP_USER="${SAC_APP_USER:-sac}"
SERVICE_NAME="${SAC_SERVICE:-sac-api}"
CONFIG_FILE="${SAC_CONFIG_FILE:-${APP_ROOT}/config/sac-api.env}"
log() { printf '[sac-deploy] %s\n' "$*"; }
die() { printf '[sac-deploy] ERROR: %s\n' "$*" >&2; exit 1; }
if [ "$(id -u)" -ne 0 ]; then
die "Запускайте от root: sudo /opt/sac-deploy.sh"
fi
id "$APP_USER" &>/dev/null || die "Пользователь ${APP_USER} не существует"
if [ ! -d "${APP_ROOT}/.git" ]; then
die "Не найден git-репозиторий: ${APP_ROOT}"
fi
log "Выравнивание владельца ${APP_ROOT}${APP_USER}:${APP_USER}"
chown -R "${APP_USER}:${APP_USER}" "${APP_ROOT}"
log "git fetch + sync"
sudo -u "${APP_USER}" git -C "${APP_ROOT}" fetch --prune
if sudo -u "${APP_USER}" git -C "${APP_ROOT}" pull --ff-only 2>/dev/null; then
log "git: fast-forward OK"
else
UPSTREAM="$(sudo -u "${APP_USER}" git -C "${APP_ROOT}" rev-parse --abbrev-ref '@{u}' 2>/dev/null || true)"
if [ -z "${UPSTREAM}" ]; then
UPSTREAM="origin/main"
fi
log "WARN: fast-forward невозможен (часто после force-push на kalinamall) — reset --hard ${UPSTREAM}"
sudo -u "${APP_USER}" git -C "${APP_ROOT}" reset --hard "${UPSTREAM}"
fi
if [ ! -f "${CONFIG_FILE}" ]; then
die "Нет конфига: ${CONFIG_FILE}"
fi
VENV="${APP_ROOT}/backend/.venv"
if [ ! -x "${VENV}/bin/pip" ]; then
die "Нет venv: ${VENV} — сначала установите по install-ubuntu-24.04-native.md"
fi
log "pip install -r requirements.txt"
sudo -u "${APP_USER}" "${VENV}/bin/pip" install -q -r "${APP_ROOT}/backend/requirements.txt"
log "Проверка ${CONFIG_FILE} (pydantic; без bash source — inline-комментарии в значениях ломают deploy)"
sudo -u "${APP_USER}" bash -c "
export SAC_CONFIG_FILE='${CONFIG_FILE}'
cd '${APP_ROOT}/backend'
'${VENV}/bin/python' -c 'from app.config import get_settings; get_settings(); print(\"config: OK\")'
" || die "Неверный ${CONFIG_FILE}: одна переменная = одна строка, комментарии только отдельной строкой с # (не «false ← …» после значения)"
log "alembic upgrade head"
sudo -u "${APP_USER}" bash -c "
export SAC_CONFIG_FILE='${CONFIG_FILE}'
cd '${APP_ROOT}/backend' && '${VENV}/bin/alembic' upgrade head
"
if [ -f "${APP_ROOT}/frontend/package.json" ]; then
if ! command -v npm >/dev/null 2>&1; then
log "npm не найден — пропуск сборки UI (apt install nodejs npm)"
else
log "frontend: npm ci / build"
sudo -u "${APP_USER}" bash -c "
cd '${APP_ROOT}/frontend'
if [ -f package-lock.json ]; then npm ci; else npm install; fi
npm run build
"
if [ ! -f "${APP_ROOT}/frontend/dist/index.html" ]; then
die "Сборка UI не создала frontend/dist/index.html"
fi
fi
fi
if [ -f "${APP_ROOT}/deploy/systemd/${SERVICE_NAME}.service" ]; then
SYSTEMD_DST="/etc/systemd/system/${SERVICE_NAME}.service"
if [ ! -f "${SYSTEMD_DST}" ] || ! cmp -s "${APP_ROOT}/deploy/systemd/${SERVICE_NAME}.service" "${SYSTEMD_DST}"; then
log "Обновление systemd unit → ${SYSTEMD_DST}"
cp "${APP_ROOT}/deploy/systemd/${SERVICE_NAME}.service" "${SYSTEMD_DST}"
systemctl daemon-reload
fi
fi
START_SH="${APP_ROOT}/deploy/systemd/sac-api-start.sh"
if [ -f "${START_SH}" ]; then
sed -i 's/\r$//' "${START_SH}"
chmod 755 "${START_SH}"
fi
for _sh in "${APP_ROOT}"/deploy/sac-deploy.sh "${APP_ROOT}"/deploy/systemd/*.sh; do
[ -f "${_sh}" ] || continue
sed -i 's/\r$//' "${_sh}"
chmod 755 "${_sh}" 2>/dev/null || true
done
log "Проверка DATABASE_URL (как у uvicorn через SAC_CONFIG_FILE)"
sudo -u "${APP_USER}" bash -c "
export SAC_CONFIG_FILE='${CONFIG_FILE}'
cd '${APP_ROOT}/backend'
'${VENV}/bin/python' -c \"
from sqlalchemy import create_engine, text
from app.config import get_settings
engine = create_engine(get_settings().database_url, pool_pre_ping=True)
with engine.connect() as conn:
conn.execute(text('SELECT 1'))
print('db: OK')
\"
" || die "PostgreSQL: проверьте DATABASE_URL в ${CONFIG_FILE} и systemctl status postgresql"
log "systemctl restart ${SERVICE_NAME} (краткий 502 в UI возможен ~10 с)"
log "Сброс зависших remote_action (running без worker после restart)"
sudo -u "${APP_USER}" bash -c "
export SAC_CONFIG_FILE='${CONFIG_FILE}'
cd '${APP_ROOT}/backend'
'${VENV}/bin/python' -c \"
from app.database import SessionLocal
from app.services.host_remote_actions import clear_stale_running_remote_actions
session = SessionLocal()
try:
names = clear_stale_running_remote_actions(session)
for name in names:
print(f'stale remote_action reset: {name}')
finally:
session.close()
\"
"
systemctl restart "${SERVICE_NAME}"
sleep 2
for _ in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15; do
if systemctl is-active --quiet "${SERVICE_NAME}"; then
break
fi
sleep 1
done
systemctl is-active --quiet "${SERVICE_NAME}" || die "${SERVICE_NAME} не active — journalctl -u ${SERVICE_NAME} -n 40 --no-pager"
HEALTH_OK=0
for _ in 1 2 3 4 5 6; do
sleep 2
if curl -sf "http://127.0.0.1:8000/health" >/dev/null 2>&1; then
HEALTH_OK=1
break
fi
done
if [ "${HEALTH_OK}" -eq 1 ]; then
log "health: OK (http://127.0.0.1:8000/health)"
else
die "${SERVICE_NAME} запущен, но /health не ответил — journalctl -u ${SERVICE_NAME} -n 80 --no-pager"
fi
RENDER_DAILY_TIMER="${APP_ROOT}/deploy/systemd/render-sac-daily-report-timer.sh"
if [ -f "${RENDER_DAILY_TIMER}" ]; then
chmod +x "${RENDER_DAILY_TIMER}" 2>/dev/null || true
fi
for aux in sac-retention sac-daily-report; do
for suffix in service timer; do
src="${APP_ROOT}/deploy/systemd/${aux}.${suffix}"
dst="/etc/systemd/system/${aux}.${suffix}"
if [ "${aux}" = "sac-daily-report" ] && [ "${suffix}" = "timer" ]; then
if [ -x "${RENDER_DAILY_TIMER}" ]; then
log "Генерация ${dst} из ${CONFIG_FILE} (SAC_DAILY_REPORT_HOUR/TIMEZONE)"
bash "${RENDER_DAILY_TIMER}" "${CONFIG_FILE}" "${dst}"
systemctl daemon-reload
systemctl restart "${aux}.timer" 2>/dev/null || systemctl start "${aux}.timer" 2>/dev/null || true
elif [ -f "${src}" ]; then
log "WARN: ${RENDER_DAILY_TIMER} не найден — копируем статический timer"
cp "${src}" "${dst}"
systemctl daemon-reload
fi
continue
fi
if [ -f "${src}" ]; then
if [ ! -f "${dst}" ] || ! cmp -s "${src}" "${dst}"; then
log "Обновление ${dst}"
cp "${src}" "${dst}"
systemctl daemon-reload
fi
fi
done
if [ -f "/etc/systemd/system/${aux}.timer" ]; then
systemctl enable "${aux}.timer" 2>/dev/null || true
systemctl start "${aux}.timer" 2>/dev/null || true
fi
done
log "Готово."
@@ -0,0 +1,44 @@
#!/usr/bin/env bash
# Генерирует sac-daily-report.timer из SAC_DAILY_REPORT_HOUR и SAC_DAILY_REPORT_TIMEZONE.
# Usage: sudo deploy/systemd/render-sac-daily-report-timer.sh [config.env] [output.timer]
set -euo pipefail
CONFIG_FILE="${1:-/opt/security-alert-center/config/sac-api.env}"
OUTPUT="${2:-/etc/systemd/system/sac-daily-report.timer}"
HOUR=9
TZ_NAME="Europe/Moscow"
if [ -f "${CONFIG_FILE}" ]; then
set -a
# shellcheck source=/dev/null
source "${CONFIG_FILE}"
set +a
fi
HOUR="${SAC_DAILY_REPORT_HOUR:-9}"
TZ_NAME="${SAC_DAILY_REPORT_TIMEZONE:-Europe/Moscow}"
if ! [[ "${HOUR}" =~ ^[0-9]+$ ]] || [ "${HOUR}" -lt 0 ] || [ "${HOUR}" -gt 23 ]; then
printf 'render-sac-daily-report-timer: invalid SAC_DAILY_REPORT_HOUR=%s\n' "${HOUR}" >&2
exit 1
fi
HOUR_PADDED="$(printf '%02d' "${HOUR}")"
cat >"${OUTPUT}" <<EOF
[Unit]
Description=Daily SAC report aggregation and notify
[Timer]
OnCalendar=*-*-* ${HOUR_PADDED}:00:00
Timezone=${TZ_NAME}
Persistent=true
[Install]
WantedBy=timers.target
EOF
printf 'render-sac-daily-report-timer: wrote %s (OnCalendar=%s:00:00 Timezone=%s)\n' \
"${OUTPUT}" "${HOUR_PADDED}" "${TZ_NAME}"
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
# Uvicorn launcher: SAC_UVICORN_WORKERS из sac-api.env (без systemd EnvironmentFile).
set -euo pipefail
APP_ROOT="${SAC_APP_ROOT:-/opt/security-alert-center}"
CONFIG_FILE="${SAC_CONFIG_FILE:-${APP_ROOT}/config/sac-api.env}"
VENV="${APP_ROOT}/backend/.venv"
HOST="127.0.0.1"
PORT="8000"
WORKERS=4
_read_env_int() {
local key="$1" default="$2" line val
[ -f "$CONFIG_FILE" ] || {
printf '%s\n' "$default"
return 0
}
line="$(grep -E "^[[:space:]]*${key}=" "$CONFIG_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%\'}"
if [[ "$val" =~ ^[0-9]+$ ]] && [ "$val" -ge 1 ]; then
printf '%s\n' "$val"
else
printf '%s\n' "$default"
fi
}
WORKERS="$(_read_env_int SAC_UVICORN_WORKERS 4)"
exec "${VENV}/bin/uvicorn" app.main:app \
--host "$HOST" \
--port "$PORT" \
--workers "$WORKERS" \
--timeout-graceful-shutdown 30
+24
View File
@@ -0,0 +1,24 @@
[Unit]
Description=Security Alert Center API (FastAPI)
Documentation=https://git.papatramp.ru/PapaTramp/security-alert-center
After=network-online.target postgresql.service
Wants=network-online.target
Requires=postgresql.service
[Service]
Type=simple
User=sac
Group=sac
WorkingDirectory=/opt/security-alert-center/backend
# Конфиг читает само приложение (pydantic), не systemd — иначе ломаются пароли с #, $ и т.д.
Environment=SAC_CONFIG_FILE=/opt/security-alert-center/config/sac-api.env
Environment=PYTHONPATH=/opt/security-alert-center/backend
ExecStart=/usr/bin/bash /opt/security-alert-center/deploy/systemd/sac-api-start.sh
Restart=on-failure
RestartSec=5
TimeoutStopSec=20
TimeoutStartSec=30
NoNewPrivileges=true
[Install]
WantedBy=multi-user.target
+16
View File
@@ -0,0 +1,16 @@
[Unit]
Description=Security Alert Center daily report generation (F-NOT-05)
After=network-online.target postgresql.service
Wants=network-online.target
[Service]
Type=oneshot
User=sac
Group=sac
WorkingDirectory=/opt/security-alert-center/backend
EnvironmentFile=/opt/security-alert-center/config/sac-api.env
ExecStart=/opt/security-alert-center/backend/.venv/bin/python -m app.jobs.daily_report
Nice=10
[Install]
WantedBy=multi-user.target
+12
View File
@@ -0,0 +1,12 @@
[Unit]
Description=Daily SAC report aggregation and notify
[Timer]
# Шаблон по умолчанию (hour=9, Europe/Moscow). На prod timer перегенерируется
# из config/sac-api.env: deploy/systemd/render-sac-daily-report-timer.sh или sac-deploy.sh
OnCalendar=*-*-* 09:00:00
Timezone=Europe/Moscow
Persistent=true
[Install]
WantedBy=timers.target
@@ -0,0 +1,16 @@
[Unit]
Description=Security Alert Center host silence scan (stale heartbeat)
After=network-online.target postgresql.service
Wants=network-online.target
[Service]
Type=oneshot
User=sac
Group=sac
WorkingDirectory=/opt/security-alert-center/backend
EnvironmentFile=/opt/security-alert-center/config/sac-api.env
ExecStart=/opt/security-alert-center/backend/.venv/bin/python -m app.jobs.host_silence_scan
Nice=10
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,11 @@
[Unit]
Description=Periodic SAC host silence scan (stale agent.heartbeat)
[Timer]
OnBootSec=3min
OnUnitActiveSec=5min
RandomizedDelaySec=30
Persistent=true
[Install]
WantedBy=timers.target
+16
View File
@@ -0,0 +1,16 @@
[Unit]
Description=Security Alert Center data retention
After=network-online.target postgresql.service
Wants=network-online.target
[Service]
Type=oneshot
User=sac
Group=sac
WorkingDirectory=/opt/security-alert-center/backend
EnvironmentFile=/opt/security-alert-center/config/sac-api.env
ExecStart=/opt/security-alert-center/backend/.venv/bin/python -m app.jobs.retention
Nice=10
[Install]
WantedBy=multi-user.target
+10
View File
@@ -0,0 +1,10 @@
[Unit]
Description=Daily SAC retention purge
[Timer]
OnCalendar=daily
RandomizedDelaySec=900
Persistent=true
[Install]
WantedBy=timers.target