492 lines
23 KiB
Markdown
Executable file
492 lines
23 KiB
Markdown
Executable file
# 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>` | ` ```текст``` ` |
|
||
| `━━━━━` | `---` |
|
||
| `<` `>` `&` | `<` `>` `&` |
|
||
| Прочие теги | удаляются |
|
||
|
||
---
|
||
|
||
### `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 <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` |
|