# Auspex — Architecture ## Обзор Auspex — монолитное CLI-приложение на Go. Все модули живут в одном пакете `main`, конфигурация глобальная, каналы доставки алертов shared. Такой подход выбран осознанно — утилита запускается по cron, живёт секунды, сложная изоляция не нужна. ``` ┌─────────────────────────────────────────────────────┐ │ auspex │ │ │ │ main.go ──► runByFlag() / showMenu() │ │ │ │ │ ┌──────────┼──────────────┐ │ │ ▼ ▼ ▼ │ │ certcheck portcheck logcheck ... │ │ │ │ │ │ │ └──────────┴──────────────┘ │ │ │ │ │ Alert Pipeline │ │ ┌──────────┼──────────────┐ │ │ ▼ ▼ ▼ │ │ telegram email mattermost │ └─────────────────────────────────────────────────────┘ ``` --- ## Файлы и их роли ### `main.go` — точка входа Отвечает за два режима запуска: **CLI-флаги** (`runByFlag`): прямой вызов нужного модуля по флагу (`-c`, `-p`, `-l`, `-sv`, `-d`, `-m`, `-ss`, `-w`). **Интерактивное меню** (`showMenu`): ASCII-баннер, numbered menu, `bufio.Reader` для ввода. Поддерживает как цифры (`1`-`8`), так и текстовые алиасы (`-c`, `c`). ``` auspex [флаг] [аргументы] │ ├── -ss → runPortsView() ├── -c → runCertCheck() ├── -p → runPortCheck() ├── -m → runMatchCheck(clientFilter) ├── -l → runLogCheck() ├── -sv → runServiceCheck() ├── -d → runDockerCheck() ├── -w → runWebCheck(sid) ├── -v → версия └── -h → usage ``` --- ### `config.go` — конфигурация Все настройки — глобальные переменные вида `cfgXxx`. Загрузка из `/etc/auspex/auspex.env` при старте через `loadConfig()` → `applyConfig()`. Формат `.env`: ``` KEY=VALUE # кавычки обрезаются автоматически # комментарии игнорируются ``` Если файл не найден — используются значения по умолчанию, вывод предупреждения в stdout. Программа продолжает работу. **Группы переменных:** - Telegram (токен, chat ID, прокси) - Mattermost (URL, токен бота, channel ID) - Email/SMTP (хост, порт, логин, пароль, from, to) - PostgreSQL (user, password, dbname, port, primary host, secondary host) - Пути (SSL dir, nginx glob, log dir, state files) - Пороги (cert days, log stale minutes) - Параметры проверки (timeout, retries, workers) - Список служб для проверки **Вспомогательные функции:** - `isTelegramConfigured()` — проверяет наличие токена и chat ID - `isEmailConfigured()` — проверяет host, from, to - `isMattermostConfigured()` — проверяет URL, токен, channel ID - `printConfigStatus()` — выводит статус всех каналов при запуске --- ### `telegram.go` — доставка в Telegram **Функции отправки:** - `telegramSendHTML(message)` — HTML-форматирование (теги ``, ``, ``) - `telegramSendPlainText(message)` — plain text без форматирования (используется в portcheck) **Цепочка прокси (`telegramHTTPClients`):** Возвращает срез `[]*http.Client` в порядке приоритета: ``` 1. HTTP прокси (если задан TELEGRAM_HTTP_PROXY) 2. SOCKS5 (если задан TELEGRAM_SOCKS5_PROXY) 3. Прямое (всегда, как последний fallback) ``` `telegramDoPost` и `telegramDoPostForm` перебирают клиентов по очереди. При ошибке одного — переходят к следующему с логом предупреждения. Успешный вариант логируется. **SOCKS5:** реализован через `golang.org/x/net/proxy`. Логин/пароль задаются отдельными переменными (не в URL) — это исключает проблемы со спецсимволами вроде `!` и `$` в паролях. **HTTP прокси:** реализован через стандартный `http.Transport{Proxy: http.ProxyURL(...)}`. --- ### `email.go` — доставка по Email **`emailSendHTML(subject, message)`** — отправка HTML-письма через SMTP. Особенности реализации: - Telegram-теги (``, ``, ``) являются валидным HTML и отображаются в письме без конвертации - `\n` заменяются на `
` для корректного отображения переносов строк - Письмо оборачивается в HTML-шаблон с CSS стилями (`emailBuildHTML`) - Поддерживается `AUTH LOGIN` (тип авторизации который использует Microsoft Exchange и многие корпоративные серверы) через кастомный `emailLoginAuth`, реализующий интерфейс `smtp.Auth` - Стандартный `smtp.PlainAuth` намеренно не используется — он несовместим с рядом серверов - Порт 587 + STARTTLS работает автоматически через `smtp.SendMail` - `EMAIL_TO` поддерживает несколько адресов через запятую **Диагностика при неполной конфигурации:** - Не задан HOST/FROM/TO → лог `[Email] Не настроен — пропускаем` - Не задан USER или PASSWORD → предупреждение, попытка без авторизации (для relay-серверов) - Ошибка SMTP → лог с кодом ответа и телом ошибки --- ### `mattermost.go` — доставка в Mattermost **`mattermostSend(message)`** — отправка через Bot API. Endpoint: `POST /api/v4/posts` с заголовком `Authorization: Bearer `. Успешный ответ: `201 Created` (не `200`, как в большинстве API). **`mmHTMLToMarkdown(html)`** — конвертер форматирования: | HTML (Telegram) | Markdown (Mattermost) | |---|---| | `текст` | `**текст**` | | `текст` | `_текст_` | | `текст` | `` `текст` `` | | `
текст
` | ` ```текст``` ` | | `━━━━━` | `---` | | `<` `>` `&` | `<` `>` `&` | | Прочие теги | удаляются | --- ### `certcheck.go` — проверка SSL сертификатов **Точка входа:** `runCertCheck()` **Алгоритм:** 1. `ccFindCerts(cfgSSLDir)` — рекурсивный обход директории, фильтрация по расширениям (`.crt`, `.pem`, `.cer`, `.cert`, `.der`, `.p7b`, `.p7c`, `.p7s`, `.csr`) 2. Параллельный парсинг через пул горутин (`cfgMaxWorkers`) 3. `ccParseCert(path)` — определение формата (PEM/DER), парсинг через `crypto/x509`, вычисление `daysLeft` 4. `ccCheckAndAlert(cert)` — сравнение с порогами и отправка алерта **Уровни алертов:** | Условие | Emoji | Уровень | |---|---|---| | `daysLeft < 0` | 💀💀💀 | Сертификат истёк | | `daysLeft <= CERT_CRITICAL_DAYS` | 🚨🚨🚨 | Критично | | `daysLeft <= CERT_WARNING_DAYS` | ⚠️⚠️ | Предупреждение | | `daysLeft <= CERT_INFO_DAYS` | ℹ️ | Информация | | Иначе | — | Алерт не нужен | --- ### `portcheck.go` — проверка доступности портов **Точка входа:** `runPortCheck()` **Алгоритм:** 1. Подключение к PostgreSQL (primary → secondary failover) 2. Загрузка карты `check_ports` из таблицы `apps_settings` — какие конфиги проверять 3. `pcParseNginxConfig(file)` — парсинг `upstream { server ip:port; }` блоков regex'ом 4. Дедупликация endpoint'ов по всем файлам 5. Параллельная TCP-проверка (`net.DialTimeout`) с повторами (`cfgMaxRetries`) 6. `pcCompareStates` — сравнение с предыдущим state-файлом 7. Отправка алерта только при изменениях **State-файл** (`/tmp/port_checker_state.json`): ```json { "timestamp": "2026-04-14 13:10:10", "status": { "91.206.126.83:443": {"endpoint": "...", "available": true, "config_file": "Client_12345"} } } ``` **Имена конфигов** строятся из имени файла без расширения: `Client_12345.conf` → `Client_12345`. --- ### `logcheck.go` — проверка свежести логов **Точка входа:** `runLogCheck()` **Алгоритм (4 шага через PostgreSQL):** 1. `lcGetInstancesByHostname` — инстансы для текущего хоста (`instances_new`) 2. `lcGetClientsByInstances` — клиенты для этих инстансов (`client_info`) 3. `lcGetAppsByClients` — приложения с `check_write_logs = true` (`apps_settings`) 4. `lcCheckLogFile` — поиск файла по паттерну `{LOG_DIR}/*/*/{l7resourceid}*_access.log`, проверка `ModTime` Файл считается устаревшим если `time.Since(ModTime) > cfgLogStaleMinutes`. **Уровни в алерте** по времени с последней записи: | Минут | Emoji | |---|---| | 61–120 | ⚠️ | | 121–240 | 🔥 | | > 240 | 💀 | --- ### `matchcheck.go` — проверка совпадений Angie ↔ Docker **Точка входа:** `runMatchCheck(clientFilter)` Проверяет соответствие между портами в конфигах Angie и запущенными Docker контейнерами. Поддерживает фильтрацию по имени клиента. **Алгоритм:** 1. Парсинг конфигов Angie из `cfgAngieConfDir` — извлечение портов на которых слушает Angie 2. `docker ps` — список запущенных контейнеров с их портами 3. Сопоставление: для каждого клиента проверяется есть ли соответствующий контейнер и совпадают ли порты 4. Алерт при расхождениях --- ### `servicecheck.go` — проверка служб systemd **Точка входа:** `runServiceCheck()` **Алгоритм:** 1. Параллельная проверка всех служб из `cfgServicesToCheck` через горутины 2. `scCheckService(name)` — `systemctl is-active ` + `systemctl show --property=SubState,Description` 3. Загрузка предыдущего state-файла 4. `scCompareStates` — сравнение текущих статусов с предыдущими 5. Алерт только при изменениях **State-файл** (`/tmp/auspex_service_state.json`): ```json { "timestamp": "2026-04-14 13:00:00", "services": { "angie": "active", "docker": "failed" } } ``` **Типы изменений в алерте:** | Изменение | Emoji | |---|---| | `active` → любой другой | ❌ | | любой → `active` | ✅ (восстановление) | | `failed` | 💀 | | `inactive` | 🔴 | | `activating/deactivating` | 🟡 | --- ### `dockercheck.go` — проверка Docker контейнеров **Точка входа:** `runDockerCheck()` **Алгоритм:** 1. `dcCheckDockerAvailable()` — `docker info` для проверки доступности демона 2. `dcGetContainers()` — `docker ps -a --format '{{.ID}}|{{.Names}}|{{.Status}}|{{.Image}}'` 3. `dcGetHealth(id)` — `docker inspect --format '{{if .State.Health}}...{{end}}'` для каждого контейнера отдельно (`.Health` недоступен в `docker ps` в ряде сборок) 4. Нормализация статуса: `"Up 2 hours"` → `"running"`, `"Exited (1) 3 min ago"` → `"exited"` 5. `dcCompareStates` — сравнение с предыдущим state-файлом 6. Алерт только при изменениях **Типы событий:** | Kind | Условие | Emoji | |---|---|---| | `appeared` | Новый контейнер (не при первом запуске) | 🆕 | | `disappeared` | Контейнер исчез из `docker ps -a` | 👻 | | `status_changed` → не running | Упал/завис | 🔴 💀 🔄 | | `status_changed` → running | Восстановился | ✅ | | `health_changed` → unhealthy | Нездоров | 🤒 | | `health_changed` → healthy | Оздоровился | ✅ | **State-файл** (`/tmp/auspex_docker_state.json`): ```json { "timestamp": "2026-04-14 13:00:00", "containers": { "portainer": {"status": "running", "health": "none"}, "zabbix-agent-7.0": {"status": "running", "health": "none"} } } ``` --- --- ### `webcheck.go` — проверка доступности сайтов **Точка входа:** `runWebCheck(sidArg string)` Проверяет HTTP-доступность доменов WAF-клиента через браузер **lynx**. Использование lynx позволяет обойти антибот-системы которые блокируют стандартные HTTP-клиенты (Go, Python, curl). **Аргумент:** один или несколько SID через запятую: ```bash auspex -w 14285 auspex -w 14285,11071,9823 ``` **Алгоритм (один SID):** 1. Проверяет наличие `lynx` через `exec.LookPath` — если нет, понятная ошибка с командой установки 2. Подключается к PostgreSQL (primary → secondary failover) — один раз для всех SID 3. `wcGetDomains` — запрашивает `domain_name` и `aliases` из `sp_info WHERE sid = $1`, затем `client_title` из `apps_settings WHERE l7resourceid = $1` 4. Парсит `aliases` из JSONB-массива (`[]string`) 5. Очищает домены через `wcCleanDomain` — убирает markdown-ссылки вида `[текст](url)`, протоколы, слеши 6. Для каждого домена вызывает `wcCheckDomain` → `wcPrintResult` 7. Выводит итоговую статистику через `wcPrintSummary` 8. Сохраняет CSV-файл **Алгоритм (несколько SID):** При передаче нескольких SID утилита спрашивает способ сохранения CSV: ``` Несколько SID (3). Сохранить результаты: 1) В один файл (с разделителями по SID) 2) В отдельные файлы для каждого SID ``` - Вариант **1** — один файл `webcheck_{sid1}_{sid2}_{date}.csv` с секциями: ``` === SID: 14285 | Название клиента === domain,protocol,http_code,available,checked_at ... === SID: 11071 | Другой клиент === ... ``` - Вариант **2** — отдельный файл для каждого SID как обычно БД и lynx инициализируются один раз независимо от количества SID. **`wcGetDomains`** — два запроса к БД: ```sql -- 1. Домены клиента SELECT domain_name, aliases FROM sp_info WHERE sid = $1 -- 2. Название клиента (fallback: sid если не найдено) SELECT client_title FROM apps_settings WHERE l7resourceid = $1 LIMIT 1 ``` **`wcCheckDomain`:** - Wildcard домены (`*.example.com`) — пропускаются немедленно с кодом `wildcard` - Пробует `https://` → при DNS-ошибке возвращает `dns_error` без попытки http - Пробует `http://` как fallback если https не дал HTTP-кода - Если оба протокола не дали результата → `unavailable` **`wcLynxGet(url)`** — ядро проверки: ``` lynx -head -dump -connect_timeout=10 -read_timeout=10 ``` - `-head` — HTTP HEAD запрос (только заголовки, без загрузки тела) - `-dump` — неинтерактивный режим, вывод в stdout - Парсит HTTP-код регулярным выражением `HTTP/[\d.]+ (\d{3})` - При редиректах берёт **последний** код — он актуальный - DNS-ошибка определяется по строкам `Unable to locate remote host` и `Can't access startfile` в выводе lynx **Коды результата:** | HTTPCode | Значение | |---|---| | `200`, `301`, `302`... | HTTP-код ответа сервера | | `dns_error` | Домен не резолвится | | `wildcard` | Wildcard домен, пропущен | | `error` | Оба протокола недоступны | **`wcIsSuccessCode`** — считает успешными коды `2xx` и `3xx`. **CSV-файл** сохраняется в `/var/log/auspex/` с UTF-8 BOM для корректного открытия в Excel: - Один SID: `webcheck_{sid}_{date}.csv` - Несколько SID (раздельно): `webcheck_{sid}_{date}.csv` для каждого - Несколько SID (объединённо): `webcheck_{sid1}_{sid2}_..._{date}.csv` **Просмотр CSV в терминале:** ```bash column -t -s',' /var/log/auspex/webcheck_14285_2026-04-29.csv ``` ### `portsview.go` — TUI просмотр портов **Точка входа:** `runPortsView()` Интерактивный TUI на базе [Bubble Tea](https://github.com/charmbracelet/bubbletea). Показывает занятые порты на хосте (`ss -tlnp` / `netstat`). Поддерживает фильтрацию, навигацию клавишами. --- ## Pipeline доставки алертов Все модули с алертами следуют единому паттерну: ```go // 1. Telegram if err := telegramSendHTML(message); err != nil { log.Printf("❌ Ошибка отправки в Telegram: %v", err) } else { log.Println("✅ Алерт успешно отправлен в Telegram") } // 2. Email if err := emailSendHTML("Тема", message); err != nil { log.Printf("❌ Ошибка отправки Email: %v", err) } else { log.Println("✅ Алерт успешно отправлен на Email") } // 3. Mattermost if err := mattermostSend(message); err != nil { log.Printf("❌ Ошибка отправки в Mattermost: %v", err) } else { log.Println("✅ Алерт успешно отправлен в Mattermost") } ``` Все три вызова независимы — ошибка одного не влияет на остальные. --- ## Паттерн state-файлов Модули `-p`, `-sv`, `-d` используют одинаковый механизм для предотвращения спама при запуске по cron: ``` Запуск N: Загрузить prev_state → Получить curr_state → Compare → Alert if changed → Save curr_state Запуск N+1: Загрузить prev_state → Получить curr_state → Compare → ... ``` **Первый запуск** (state-файл отсутствует): состояние сохраняется, алерты не отправляются. Исключение: в `dockercheck` появление нового контейнера (`dcKindAppeared`) тоже не алертится при первом запуске — флаг `isFirstRun` передаётся в `dcCompareStates`. --- ## Параллелизм | Модуль | Механизм | |---|---| | `certcheck` | Пул горутин (`cfgMaxWorkers`) через канал задач | | `servicecheck` | Горутина на каждую службу + `sync.WaitGroup` | | `dockercheck` | Последовательно (inspect на каждый контейнер отдельно) | | `portcheck` | Горутина на каждый endpoint | --- ## Работа с PostgreSQL Модули `portcheck` и `logcheck` используют PostgreSQL с failover: ``` primary (cfgPrimaryDBHost) → ping OK? → использовать → ping fail → secondary (cfgSecondaryDBHost) ``` Каждый модуль реализует свой `pcConnectDB()` / `lcConnectDB()` / `wcConnectDB()` — функции идентичны по логике, префиксы разные для избежания коллизий имён в пакете `main`. `webcheck` также использует PostgreSQL для получения доменов из таблицы `sp_info`. --- ## Соглашения по именованию Все идентификаторы в каждом модуле имеют уникальный префикс — это позволяет всем файлам жить в одном пакете `main` без конфликтов: | Модуль | Префикс | |---|---| | certcheck | `cc` | | portcheck | `pc` | | logcheck | `lc` | | matchcheck | `mc` | | servicecheck | `sc` | | dockercheck | `dc` | | email | `email` | | mattermost | `mm` | | webcheck | `wc` | | config | `cfg` |