auspex/ARCHITECTURE.md
2026-06-09 17:12:18 +03:00

492 lines
23 KiB
Markdown
Executable file
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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-форматирование (теги `<b>`, `<code>`, `<i>`)
- `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-теги (`<b>`, `<code>`, `<i>`) являются валидным HTML и отображаются в письме без конвертации
- `\n` заменяются на `<br>` для корректного отображения переносов строк
- Письмо оборачивается в 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 <token>`.
Успешный ответ: `201 Created` (не `200`, как в большинстве API).
**`mmHTMLToMarkdown(html)`** — конвертер форматирования:
| HTML (Telegram) | Markdown (Mattermost) |
|---|---|
| `<b>текст</b>` | `**текст**` |
| `<i>текст</i>` | `_текст_` |
| `<code>текст</code>` | `` `текст` `` |
| `<pre>текст</pre>` | ` ```текст``` ` |
| `━━━━━` | `---` |
| `&lt;` `&gt;` `&amp;` | `<` `>` `&` |
| Прочие теги | удаляются |
---
### `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 |
|---|---|
| 61120 | ⚠️ |
| 121240 | 🔥 |
| > 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 <name>` + `systemctl show <name> --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 <url>
```
- `-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` |