auspex/ARCHITECTURE.md
2026-08-26 12:58:09 +03:00

619 lines
No EOL
37 KiB
Markdown
Raw 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`, `-fd`, `-dt`).
**Интерактивное меню** (`showMenu`): ASCII-баннер, numbered menu, `bufio.Reader` для ввода. Поддерживает как цифры (`1`-`10`), так и текстовые алиасы (`-c`, `c`).
```
auspex [флаг] [аргументы]
├── -ss → runPortsView()
├── -c → runCertCheck()
├── -p → runPortCheck() [WAF, build tag]
├── -m → runMatchCheck(clientFilter) [WAF, build tag]
├── -l → runLogCheck() [WAF, build tag]
├── -sv → runServiceCheck()
├── -d → runDockerCheck()
├── -w → runWebCheck(sid) [WAF, build tag]
├── -fd → runFDCheck()
├── -dt → runDockerTUI()
├── -fw → runFirewallTUI()
├── -au → runAudit(category)
├── -v → версия (+ вариант сборки: full/generic)
└── -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"}
}
}
```
---
---
### `dockerview.go` — Docker Live TUI (аналог lazydocker)
**Точка входа:** `runDockerTUI()`
В отличие от `dockercheck.go` — это интерактивный инструмент прямого действия, а не cron-снапшот с диффом. Переиспользует `dcNormalizeStatus`/`dcGetHealth`/`dcStatusEmoji`/`dcCheckDockerAvailable` из `dockercheck.go` (тот же пакет `main`), не дублируя парсинг статуса.
**Реализация:** `bubbletea`/`bubbles`/`lipgloss` (те же зависимости, что и в `portsview.go`, новых пакетов в `go.mod` не добавляет — `viewport` уже часть модуля `bubbles`).
**Автообновление:** список контейнеров (`docker ps -a` + `docker inspect` на health) обновляется раз в 3 сек, если включено (`a`).
**Действия:**
| Клавиша | Действие | Механизм |
|---|---|---|
| `l` | Логи | `docker logs -f --tail 200 <name>`, терминал передаётся через `tea.ExecProcess` |
| `e` | Шелл в контейнер | `docker exec -it <name> sh -c 'exec bash \|\| exec sh'`, тоже через `tea.ExecProcess` |
| `i` | `docker inspect` | JSON форматируется через `encoding/json.Indent`, показывается в прокручиваемом `viewport` |
| `s`/`t`/`R`/`d` | stop/start/restart/remove (`rm -f`) | Через диалог подтверждения (`y`/`n`), показывающий точную команду |
| `r` / `a` | Ручной рефреш / автообновление | — |
`tea.ExecProcess` — ключевой механизм для логов и шелла: приостанавливает рендеринг Bubble Tea, отдаёт реальный терминал дочернему процессу (полноценный интерактивный сеанс — ввод, TTY), и восстанавливает TUI после его завершения. Работает только в интерактивном терминале (не через cron/pipe).
---
### `fdcheck.go` — удалённые-но-открытые файловые дескрипторы
**Точка входа:** `runFDCheck()`
Находит ситуации "место не освобождается после `rm`, потому что кто-то держит файл открытым".
**Алгоритм:**
1. Обход `/proc/[pid]/fd/*` по всем процессам, `os.Readlink` на каждый — ищем таргеты с суффиксом `" (deleted)"`
2. Фильтрация шума: memfd, anon_inode, сокеты/пайпы, всё вне обычной ФС — не считаются утечкой места
3. Размер — через `os.Stat` на `/proc/PID/fd/N` (следует по magic-symlink к живому, хоть и удалённому, инoду)
4. Таблица отсортирована по размеру, интерактивный CLI-выбор записи → меню действий
**Действия:**
| Действие | Механизм | Примечание |
|---|---|---|
| Recover | `io.Copy` из `/proc/PID/fd/N` в новый файл | Снимок данных, места не освобождает |
| Reopen (SIGHUP/SIGUSR1) | `syscall.Kill(pid, sig)` | Работает только если демон сам поддерживает reopen (nginx/angie/rsyslog) |
| Restart | `systemctl restart <unit>` | `unit` определяется через regex по `/proc/PID/cgroup` |
| Kill | SIGTERM → (3 сек) → SIGKILL | Освобождает место немедленно |
| Truncate на месте | `os.OpenFile(fdPath, O_WRONLY).Truncate(0)` | Тот же приём, что `logrotate copytruncate`; безопасно только для `O_APPEND`-файлов — режим открытия проверяется через `/proc/PID/fdinfo/N` (`flags:`, бит `O_APPEND`) |
Truncate — единственное действие, которое освобождает место **без рестарта демона**: `ftruncate` меняет размер inode, а не конкретный fd, поэтому воздействует на все файловые описания, ссылающиеся на тот же inode, включая fd исходного процесса.
---
### `firewall.go` + `firewall_tui.go` — управление ufw / nftables / iptables
**Точка входа:** `runFirewallTUI()`
**Автоопределение бэкенда** (`fwDetectBackend`), по приоритету:
1. `ufw` — если `ufw status` сообщает `active`
2. `nftables` — если `nft list ruleset` возвращает непустой вывод
3. `iptables` — fallback
**Унифицированный интерфейс** `fwBackend` (`Name`, `ListRules`, `Rule`, `PortForward`, `RateLimit`, `DeleteRule`) реализован тремя структурами (`fwUFWBackend`, `fwNFTBackend`, `fwIPTBackend`) — TUI-слой работает с интерфейсом, не зная деталей конкретного бэкенда.
**Тегирование управляемых правил:** каждое новое правило получает уникальный комментарий `auspex-XXXXXXXX` (`fwNewTag`). После применения модуль ищет это правило в свежем списке — так он получает handle (nftables) / номер строки (iptables) / номер (ufw) для последующего удаления, не полагаясь на порядок вставки. **Удаление через `x` разрешено только для правил с этим тегом** — сторонние/системные правила видны в списке, но не редактируются.
**Защита от самоблокировки (ключевая часть дизайна):** после применения любого нового правила запускается обратный отсчёт `cfgFWRollbackSeconds` (по умолчанию 25 сек, `FW_ROLLBACK_SECONDS`). Если пользователь не подтвердил (`y`) — правило автоматически откатывается через тот же `undo()`, что вернула соответствующая функция интерфейса. Работает одинаково для всех трёх бэкендов, поскольку `undo` — часть возвращаемого значения каждого метода интерфейса, а не отдельная логика в TUI.
**Dry-run (`D`):** переключаемый режим — экран подтверждения показывает точную команду, но `apply()` не вызывается. Рекомендуется включать при первом знакомстве с инструментом на новом сервере — команды для nftables/iptables не тестировались на живой системе в песочнице, где писался этот код.
**Формы (wizard):** пошаговый ввод через один переиспользуемый `textinput.Model` — за раз один вопрос с валидацией (`fwValidatePort`, `fwValidateProto`, `fwValidateSource`, `fwValidateRequiredIP`, `fwValidateIntDefault`), в конце — экран предпросмотра команды перед реальным применением.
**Специфика по действиям:**
| Действие | ufw | nftables | iptables |
|---|---|---|---|
| Allow/Deny порт+прото/подсеть | `ufw allow/deny ...` | своя таблица `inet auspex`, чейн `input` (создаётся лениво) | `-I INPUT 1 ... -j ACCEPT/DROP` |
| Port forward / NAT | raw `iptables -t nat` (ufw не даёт CLI для DNAT) — **не персистентно через конфиг ufw**, нужно вручную дописать в `/etc/ufw/before.rules` | своя таблица `ip auspex_nat`, чейн `prerouting` (nat hook) | `-t nat -I PREROUTING` + `-I FORWARD` |
| Rate limit (защита от brute-force) | нативный `ufw limit` (фиксированный порог ~6/30 сек, кастомные значения игнорируются) | `limit rate over N/minute drop`, вставляется в начало чейна (порядок важен!) | классическая пара правил на модуле `recent` (`--set` / `--update --hitcount`) |
**Персистентность** (`fwPersistenceHint`) — модуль НЕ пишет системные конфиги автоматически: ufw сохраняет правила сам, для nftables/iptables пользователю показывается подсказка (`netfilter-persistent save` / ручное добавление в `/etc/nftables.conf`).
---
### `audit.go` — комплексный аудит-скан (read-only)
**Точка входа:** `runAudit(category string)`
Не проверяет "что-то одно", а прогоняет 7 категорий и печатает единый отчёт, отсортированный по severity (critical → warning → info). Полностью read-only — ничего не чинит и не меняет, только собирает находки. Задуман как первый шаг при поиске бага: что на тренировочном стенде (не связанном с WAF), что на реальном сервере.
**Категории:** `systemd`, `nginx` (детектит и nginx, и angie), `docker`, `cert`, `disk`, `network`, `firewall` — можно запустить одну через `auspex -au <категория>` (есть алиасы: `svc`→systemd, `angie`/`proxy`→nginx, `tls`/`ssl`→cert, `logs`→disk, `dns`→network, `iptables`/`ufw`→firewall).
**Переиспользует существующие модули напрямую** (не дублирует парсинг):
- `ccFindCerts`/`ccParseCert` (certcheck.go) — сроки сертификатов
- `dcCheckDockerAvailable`/`dcGetContainers` (dockercheck.go) — статус контейнеров
- `fdScan`/`fdHumanSize` (fdcheck.go) — удалённые-но-открытые файлы как признак утечки места
- `fwDetectBackend`/`ListRules` (firewall.go) — список firewall-правил
**Кросс-проверки между категориями — самая ценная часть:**
| Проверка | Как | Что выявляет |
|---|---|---|
| nginx `listen` ↔ реальные сокеты | парсит `nginx/angie -T`, сверяет с `ss -tlnp`/`-ulnp` (`auListeningPorts`) | конфиг применён, но процесс не слушает порт (не перечитал конфиг / упал) |
| nginx upstream ↔ доступность | парсит `server host:port;` из `-T`, TCP dial с таймаутом 2 сек | бэкенд не отвечает или неверный адрес в upstream |
| сертификат на диске ↔ то, что реально отдаёт TLS | `ccParseCert` против `tls.Dial` на `:443` | сервис не перечитал сертификат после обновления (самый частый "необъяснимый" баг с TLS) |
| firewall-правила ↔ слушающие порты | пересечение `ListRules()` и `auListeningPorts()` в обе стороны | правило разрешает порт, где никто не слушает (устарело) — и наоборот, порт слушает без явного allow (может блокироваться default policy) |
Кросс-проверки по firewall и network — эвристические (не учитывают, например, ACCEPT default policy), поэтому такие находки идут с severity `warning`, а не `critical`, и в конце отчёта есть явная оговорка об этом.
---
### `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` |
| dockerview | `dv` |
| fdcheck | `fd` |
| firewall | `fw` |
| audit | `au` |
| email | `email` |
| mattermost | `mm` |
| webcheck | `wc` |
| config | `cfg` |
---
## Build tags: full vs generic
`logcheck.go`, `webcheck.go`, `portcheck.go`, `matchcheck.go` требуют БД `waf_info` и/или структуру каталогов PT AF — они помечены `//go:build waf` и компилируются только при `go build -tags waf .`.
Для generic-сборки (`go build .`, без тега) их заменяют файлы `*_stub.go` с обратным ограничением `//go:build !waf` — те же имена и сигнатуры функций (`runLogCheck`, `runWebCheck`, `runPortCheck`, `runMatchCheck`), но вместо логики — `fmt.Errorf` с подсказкой пересобрать с нужным тегом. Благодаря этому `main.go` не содержит условной компиляции и одинаково линкуется в обоих вариантах.
`buildinfo_waf.go` / `buildinfo_generic.go` по той же схеме задают константу `buildVariant` (`"full"` / `"generic"`), которую показывают баннер меню и `auspex -v`.
`lib/pq` используется только в трёх WAF-тегированных файлах — в generic-сборке он просто не компилируется (в `go.mod` остаётся объявленным, это нормально для двух конфигураций одного модуля; не стоит гонять `go mod tidy` без тега `waf`, иначе он может решить, что зависимость лишняя).