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

37 KiB
Raw Blame History

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):

{
  "timestamp": "2026-04-14 13:10:10",
  "status": {
    "91.206.126.83:443": {"endpoint": "...", "available": true, "config_file": "Client_12345"}
  }
}

Имена конфигов строятся из имени файла без расширения: Client_12345.confClient_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):

{
  "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):

{
  "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 через запятую:

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. Для каждого домена вызывает wcCheckDomainwcPrintResult
  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 — два запроса к БД:

-- 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 в терминале:

column -t -s',' /var/log/auspex/webcheck_14285_2026-04-29.csv

portsview.go — TUI просмотр портов

Точка входа: runPortsView()

Интерактивный TUI на базе Bubble Tea. Показывает занятые порты на хосте (ss -tlnp / netstat). Поддерживает фильтрацию, навигацию клавишами.


Pipeline доставки алертов

Все модули с алертами следуют единому паттерну:

// 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, иначе он может решить, что зависимость лишняя).