diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index bd97662..0eb0c0f 100755 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -2,12 +2,33 @@ ## Обзор -API Sync — однопроходная утилита (не демон), запускаемая по cron. При каждом запуске выполняет три последовательных этапа: синхронизацию данных из API в PostgreSQL, очистку устаревших записей и обновление whitelist-файла в Git-репозитории. +API Sync — однопроходная утилита (не демон), запускаемая по cron. Поддерживает два режима запуска: основной и проверка дублей по ключу `--cd`. + +--- + +## Режимы запуска + +```bash +./API_sync # основной запуск +./API_sync --cd # только проверка дублирующихся доменов/алиасов +``` + +Пример cron-конфигурации: + +``` +# Основной запуск каждые 15 минут +*/15 * * * * install cd /home/install && /home/install/API_sync >> /var/log/API_sync.log 2>&1 + +# Проверка дублей каждый час +0 * * * * install cd /home/install && /home/install/API_sync --cd >> /var/log/API_sync.log 2>&1 +``` --- ## Поток выполнения +### Основной режим + ``` main() │ @@ -19,33 +40,49 @@ main() │ ├─ 4. Параллельная обработка SID (worker pool) │ └─ processSID() для каждого SID - │ ├─ GET /l7/resource/{sid}/global → whois - │ ├─ GET /l7/origin/global?l7ResourceId={sid} → origins - │ ├─ GET /l7/alias/global?l7ResourceId={sid} → aliases + │ ├─ GET /l7/resource/{sid}/global → whois + │ │ └─ ошибка → алерт 🔴 + следующий SID + │ ├─ GET /l7/origin/global?l7ResourceId={sid} → origins + │ │ └─ ошибка → алерт 🔴 + следующий SID + │ ├─ GET /l7/alias/global?l7ResourceId={sid} → aliases + │ │ └─ ошибка → алерт 🔴 + следующий SID │ ├─ DNS резолв домена → проверка AntiDDOS │ └─ upsertAPIInfo() → сравнение + запись в API_info + алерты │ ├─ 5. Очистка устаревших SID из API_info (cleanupRemovedSIDs) │ - └─ 6. Обновление WAF whitelist (updateWAFWhitelist) + ├─ 6. Проверка дублирующихся доменов/алиасов (если CHECK_DUPLICATES=true) + │ └─ checkDuplicateDomains() → алерт 🔴 если найдены дубли + │ + └─ 7. Обновление WAF whitelist (updateWAFWhitelist) ├─ git pull - ├─ Загрузка IP из API_info (auto) - ├─ Загрузка IP из manual_info (ручные) + ├─ Загрузка IP из API_info (auto-ресурсы) + ├─ Загрузка IP из manual_info (ручные ресурсы) + ├─ Объединение и дедупликация IP ├─ Фильтрация WAF-сетей (таблица ips) ├─ Сравнение с текущим whitelist-файлом ├─ Запись файла + git commit + git push └─ Отправка алерта об изменениях ``` +### Режим --cd + +``` +main() --cd + │ + ├─ 1. Подключение к PostgreSQL + └─ 2. checkDuplicateDomains() → алерт 🔴 если найдены дубли +``` + --- ## Модули ### main.go — точка входа и worker pool -Инициализирует подключение к БД, собирает все SID для обработки и запускает пул горутин. Количество воркеров задаётся через `MAX_CONCURRENT_WORKERS` (по умолчанию 5). SID передаются воркерам через буферизированный канал. +При запуске с ключом `--cd` выполняет только проверку дублей и завершается. В основном режиме инициализирует подключение к БД, собирает все SID и запускает пул горутин. Количество воркеров задаётся через `MAX_CONCURRENT_WORKERS` (по умолчанию 5). SID передаются воркерам через буферизированный канал. Все воркеры работают параллельно — DNS-резолв, запросы к API и запись в БД выполняются одновременно для разных SID. -После завершения всех воркеров последовательно запускаются очистка устаревших записей и обновление whitelist. +После завершения всех воркеров последовательно запускаются: очистка устаревших записей, проверка дублей (если не отключена через `CHECK_DUPLICATES=false`) и обновление whitelist. ### config.go — конфигурация @@ -53,7 +90,7 @@ main() ### api.go — HTTP-клиент и структуры API -Содержит единственный HTTP-клиент с таймаутом 20 секунд и функцию `apiGet()`, которая выполняет GET-запрос к API API с Bearer-авторизацией и десериализует JSON-ответ в переданную структуру. +Содержит единственный HTTP-клиент с таймаутом 20 секунд и функцию `apiGet()`, которая выполняет GET-запрос к API ServicePipe с Bearer-авторизацией и десериализует JSON-ответ в переданную структуру. Определены структуры ответов API: `originItem`, `aliasItem`, `whoisReAPIonse`, `apiList`. @@ -63,25 +100,31 @@ main() **`loadL7IDs()`** — загружает SID из `apps_settings` для конкретного клиента с `mode = 'auto'`. -**`processSID()`** — оркестрирует обработку одного SID: делает три запроса к API, резолвит домен для проверки AntiDDOS, вызывает `upsertAPIInfo()`. +**`processSID()`** — оркестрирует обработку одного SID: делает три запроса к API, резолвит домен для проверки AntiDDOS, вызывает `upsertAPIInfo()`. При ошибке любого API-запроса отправляет алерт через `sendAPIErrorAlert()` с указанием SID, endpoint и текста ошибки. -**`upsertAPIInfo()`** — сравнивает новые данные со старыми из БД и при наличии изменений формирует алерты: -- изменение домена → алерт + флаг необходимости обновления PT AF -- изменение origins (IP, mode, weight) — через `compareOrigins()` -- изменение aliases — через `compareAliases()` +**`sendAPIErrorAlert()`** — формирует и отправляет алерт 🔴 при ошибке запроса к API ServicePipe. Указывает SID, endpoint (`whois`, `origins`, `aliases`) и текст ошибки. + +**`upsertAPIInfo()`** — в начале подтягивает из БД `client_title` (из `apps_settings`) и `waf_provider` (из `client_info`) для использования в алертах. Затем сравнивает новые данные со старыми и при наличии изменений формирует алерты: +- изменение домена → строка `Домен изменён: old -> new` + флаг необходимости обновления WAF +- изменение origins — через `compareOrigins()` +- изменение aliases — через `compareAliases()` + флаг обновления WAF - изменение WAF-настроек (vendor, instance, enabled) — отдельный алерт Сохранение выполняется через `INSERT ... ON CONFLICT DO UPDATE` (upsert по `sid`). **`cleanupRemovedSIDs()`** — сравнивает все SID в `API_info` с актуальным списком активных SID. Записи, которых нет в активном списке (ресурс удалён из `apps_settings` или сменил `mode`), удаляются из `API_info`. По каждому удалённому ресурсу отправляется алерт с указанием WAF-вендора (берётся из `apps_settings` через LEFT JOIN). -**`wafVendorWarning()`** — формирует строку предупреждения в зависимости от вендора: `ptaf` → PT AF, `sw` → SW, `wmx` → WMX. +**`checkDuplicateDomains()`** — загружает все `domain_name` и `aliases` из `API_info` в единую карту. Если одно и то же значение встречается у двух и более разных SID в любой комбинации полей — считается дублем. При наличии дублей отправляется алерт 🔴 с перечислением всех совпадений и их SID. В основном режиме управляется флагом `CHECK_DUPLICATES`, при запуске с `--cd` выполняется всегда. + +**`wafVendorName()`** — возвращает читаемое название вендора: `ptaf` → `PT AF`, `sw` → `SW`, `wmx` → `WMX`. + +**`wafVendorWarning()`** — возвращает строку предупреждения для алерта об удалении ресурса с указанием конкретного WAF-вендора. ### compare.go — функции сравнения -**`compareOrigins()`** — сравнивает два списка origins по IP-адресу. Определяет добавленные, удалённые и изменённые (mode/weight) записи. Возвращает отформатированную строку для алерта с текущим состоянием origins. +**`compareOrigins()`** — сравнивает два списка origins по IP-адресу. Определяет добавленные, удалённые и изменённые (mode/weight) записи. Возвращает отформатированную строку с изменениями и текущим состоянием origins. -**`compareAliases()`** — сравнивает два отсортированных списка доменов. Возвращает строку с изменениями и булев флаг наличия изменений. +**`compareAliases()`** — сравнивает два отсортированных списка доменов. Добавленные aliases оформляются как HTML-ссылки. Возвращает строку с изменениями и булев флаг наличия изменений. ### dns.go — DNS-резолвер @@ -100,7 +143,7 @@ main() **Mattermost** — POST на `/api/v4/posts` с Bearer-токеном. HTML-теги Telegram конвертируются в Markdown через `htmlToMarkdown()`. Отправляется только если заполнены все три переменные: `MATTERMOST_URL`, `MATTERMOST_BOT_TOKEN`, `MATTERMOST_CHANNEL_ID`. -**Email** — через стандартный `net/smtp`. Поддерживает несколько адресов получателей через запятую в `EMAIL_TO`. Авторизация опциональна — если `EMAIL_SMTP_USER` пуст, отправка идёт без аутентификации (relay). HTML-теги Telegram удаляются через `stripHTMLTags()`. Отправляется только если заполнены `EMAIL_SMTP_HOST`, `EMAIL_FROM`, `EMAIL_TO`. +**Email** — через стандартный `net/smtp`. Поддерживает несколько адресов получателей через запятую в `EMAIL_TO`. При `EMAIL_SKIP_TLS_VERIFY=true` использует STARTTLS с отключённой проверкой сертификата. Механизм аутентификации выбирается автоматически по возможностям сервера: если сервер поддерживает `LOGIN` — используется он, иначе `PLAIN`. Реализован собственный `loginAuth` для совместимости с корпоративными SMTP (Exchange, Outlook). Отправляется только если заполнены `EMAIL_SMTP_HOST`, `EMAIL_FROM`, `EMAIL_TO`. ### git.go — Git-операции @@ -118,6 +161,8 @@ main() 5. Сравнивает с текущим содержимым файла (`compareWhitelists`) 6. При наличии изменений — перезаписывает файл, коммитит и пушит, отправляет алерт +**`loadAllOrigins()`** — загружает все origin IP из `API_info` (auto-ресурсы). + **`loadManualOrigins()`** — загружает все записи из `manual_info` как есть, без дополнительных условий. Таблица заполняется администратором вручную и программой не изменяется. **`filterNonWAFOrigins()`** — исключает из списка IP-адреса, которые принадлежат WAF-сетям из таблицы `ips`. Такие адреса являются узлами самого WAF и не должны попадать в whitelist. @@ -128,14 +173,50 @@ main() Все алерты отправляются через единую функцию `sendAlert()`. Типы алертов: -| Событие | Содержание | -|---|---| -| Изменение origins | Добавленные/удалённые/изменённые backend IP + текущее состояние | -| Изменение aliases | Добавленные/удалённые домены | -| Изменение домена | Старое и новое значение + предупреждение об обновлении WAF | -| Изменение WAF-настроек | Изменения waf_enabled, waf_vendor, waf_instance | -| Удаление ресурса | SID, домен, предупреждение с указанием конкретного WAF-вендора | -| Обновление whitelist | Добавленные/удалённые IP с привязкой к SID и домену, итоговое количество | +| Событие | Эмодзи | Содержание | +|---|---|---| +| Изменение origins/aliases/домена | 🟡 | SID, домен, TENANT, WAF provider, детали изменений, предупреждение об обновлении WAF, время | +| Изменение WAF-настроек инстанса | ⚪ | SID, домен, TENANT, изменения waf_enabled/vendor/instance в формате `было -> стало`, время | +| Удаление ресурса из мониторинга | 🗑 | SID, домен, предупреждение с указанием конкретного WAF-вендора | +| Ошибка запроса к API ServicePipe | 🔴 | SID, endpoint, текст ошибки, время | +| Дублирующиеся домены/алиасы | 🔴 | Список дублей с указанием SID, время проверки | +| Обновление whitelist | 🔄 | Добавленные/удалённые IP с привязкой к SID и домену, итоговое количество | + +Предупреждение об обновлении WAF формируется динамически в зависимости от вендора: `ptaf` → PT AF, `sw` → SW, `wmx` → WMX. + +--- + +## Переменные окружения + +| Переменная | По умолчанию | Описание | +|---|---|---| +| `BEARER_TOKEN` | — | Токен авторизации API ServicePipe | +| `DB_HOST` | localhost | Хост PostgreSQL | +| `DB_PORT` | 5432 | Порт PostgreSQL | +| `DB_USER` | — | Пользователь БД | +| `DB_PASSWORD` | — | Пароль БД | +| `DB_NAME` | waf_info | Имя БД | +| `TELEGRAM_BOT_TOKEN` | — | Токен Telegram-бота | +| `TELEGRAM_CHAT_ID` | — | ID чата для алертов | +| `TELEGRAM_HTTP_PROXY` | — | HTTP-прокси для Telegram | +| `TELEGRAM_SOCKS5_PROXY` | — | SOCKS5-прокси для Telegram | +| `TELEGRAM_SOCKS5_USER` | — | Логин SOCKS5-прокси | +| `TELEGRAM_SOCKS5_PASSWORD` | — | Пароль SOCKS5-прокси | +| `MATTERMOST_URL` | — | URL Mattermost-сервера | +| `MATTERMOST_BOT_TOKEN` | — | Токен Mattermost-бота | +| `MATTERMOST_CHANNEL_ID` | — | ID канала Mattermost | +| `EMAIL_SMTP_HOST` | — | SMTP-сервер | +| `EMAIL_SMTP_PORT` | 587 | SMTP-порт | +| `EMAIL_SMTP_USER` | — | Логин SMTP | +| `EMAIL_SMTP_PASSWORD` | — | Пароль SMTP | +| `EMAIL_FROM` | — | Адрес отправителя | +| `EMAIL_TO` | — | Адреса получателей (через запятую) | +| `EMAIL_SKIP_TLS_VERIFY` | false | Отключить проверку TLS-сертификата SMTP | +| `GIT_REPO_URL` | — | URL Git-репозитория whitelist | +| `GIT_REPO_PATH` | — | Локальный путь к репозиторию | +| `WHITELIST_FILE` | whitelist_ptaf.txt | Имя файла whitelist | +| `MAX_CONCURRENT_WORKERS` | 5 | Количество параллельных воркеров | +| `CHECK_DUPLICATES` | true | Включить проверку дублей в основном режиме | --- @@ -147,3 +228,4 @@ main() | `github.com/joho/godotenv` | Загрузка `.env` файла | | `golang.org/x/net` | SOCKS5 прокси для Telegram | | `net/smtp` | Отправка email (стандартная библиотека) | +| `crypto/tls` | TLS для SMTP с отключённой проверкой сертификата | \ No newline at end of file diff --git a/README.md b/README.md index f0a039a..87f65c4 100755 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ ## Структура проекта ``` -├── main.go # Точка входа, worker pool +├── main.go # Точка входа, worker pool, режим --cd ├── config.go # Конфигурация и переменные окружения ├── api.go # HTTP клиент и API структуры ├── database.go # Работа с PostgreSQL @@ -37,8 +37,6 @@ go build -o API_sync go build -ldflags="-s -w" -o API_sync ``` -Go автоматически найдёт все `.go` файлы в директории и скомпилирует их в один бинарник. - ## Конфигурация ### Шаг 1: Создать файл конфигурации @@ -83,14 +81,16 @@ EMAIL_SMTP_USER=user@example.com EMAIL_SMTP_PASSWORD=your_password EMAIL_FROM=API_sync@example.com EMAIL_TO=admin@example.com,team@example.com +EMAIL_SKIP_TLS_VERIFY=false # Git репозиторий whitelist -GIT_REPO_URL=https://token@svc-git.test.ru/wmx/waf_whitelist.git +GIT_REPO_URL=https://token@svc-git.cirex.ru/wmx/waf_whitelist.git GIT_REPO_PATH=/home/install/waf_whitelist WHITELIST_FILE=whitelist_ptaf.txt -# Параллельность +# Параллельность и функции MAX_CONCURRENT_WORKERS=5 +CHECK_DUPLICATES=true ``` ### Шаг 3: Защитить файл конфигурации @@ -104,14 +104,23 @@ chmod 600 API_sync.env ### Запуск вручную ```bash +# Основной запуск ./API_sync + +# Только проверка дублирующихся доменов/алиасов +./API_sync --cd ``` ### Запуск через cron ```bash # /etc/cron.d/API_sync + +# Основной запуск каждые 15 минут */15 * * * * install cd /home/install && /home/install/API_sync >> /var/log/API_sync.log 2>&1 + +# Проверка дублей каждый час +0 * * * * install cd /home/install && /home/install/API_sync --cd >> /var/log/API_sync.log 2>&1 ``` ## Возможности @@ -132,6 +141,10 @@ chmod 600 API_sync.env ✅ **Очистка устаревших данных** — автоматическое удаление из API_info ресурсов, пропавших из apps_settings +✅ **Алерты об ошибках API** — уведомление при недоступности API API для конкретного SID + +✅ **Проверка дублей** — поиск совпадающих доменов/алиасов между разными SID, запускается в основном режиме (управляется `CHECK_DUPLICATES`) или отдельно через `--cd` + ✅ **Режим auto/manual** — только ресурсы с `mode = 'auto'` синхронизируются через API; ресурсы из manual_info используются как дополнительный источник IP для whitelist ## База данных @@ -173,32 +186,34 @@ CREATE TABLE manual_info ( origins JSONB ); --- Добавить столбец mode в apps_settings (если ещё не добавлен) +-- Добавить столбцы в apps_settings (если ещё не добавлены) ALTER TABLE apps_settings ADD COLUMN mode VARCHAR(10) DEFAULT 'auto'; - --- Добавить столбец waf_vendor в apps_settings (если ещё не добавлен) ALTER TABLE apps_settings ADD COLUMN waf_vendor VARCHAR(50); + +-- Добавить столбец в client_info (если ещё не добавлен) +ALTER TABLE client_info ADD COLUMN waf_provider VARCHAR(50); ``` ## Логи -Программа выводит детальные логи: - ``` -2025/12/26 10:00:00 Start sync API_info -2025/12/26 10:00:00 Total SIDs to process: 25 -2025/12/26 10:00:00 Using 5 concurrent workers -2025/12/26 10:00:01 [Worker 0] Processing SID 10307 -2025/12/26 10:00:01 [WAF Info] SID: 10307, WAF Enabled: 1, Vendor: ptaf, Instance: PTAFd_02_03 -2025/12/26 10:00:01 [AntiDDOS Check] ✅ AntiDDOS is ENABLED for nationallottery.ru -2025/12/26 10:00:01 [Telegram] Using SOCKS5 proxy: proxy.example.com:1080 -2025/12/26 10:00:15 Finish sync API_info -2025/12/26 10:00:15 Start cleanup of removed SIDs -2025/12/26 10:00:15 [Cleanup] No stale SIDs found in API_info -2025/12/26 10:00:15 Finish cleanup of removed SIDs -2025/12/26 10:00:15 Start WAF whitelist update -2025/12/26 10:00:16 [WAF Whitelist] No changes needed -2025/12/26 10:00:16 Finish WAF whitelist update +2026/06/10 10:00:00 Start sync API_info +2026/06/10 10:00:00 Total SIDs to process: 25 +2026/06/10 10:00:00 Using 5 concurrent workers +2026/06/10 10:00:01 [Worker 0] Processing SID 10307 +2026/06/10 10:00:01 [WAF Info] SID: 10307, WAF Enabled: 1, Vendor: ptaf, Instance: PTAFd_02_03 +2026/06/10 10:00:01 [AntiDDOS Check] ✅ AntiDDOS is ENABLED for nationallottery.ru +2026/06/10 10:00:01 [Telegram] Using SOCKS5 proxy: proxy.example.com:1080 +2026/06/10 10:00:15 Finish sync API_info +2026/06/10 10:00:15 Start cleanup of removed SIDs +2026/06/10 10:00:15 [Cleanup] No stale SIDs found in API_info +2026/06/10 10:00:15 Finish cleanup of removed SIDs +2026/06/10 10:00:15 Start duplicate domain/alias check +2026/06/10 10:00:15 [Duplicate Check] No duplicates found +2026/06/10 10:00:15 Finish duplicate domain/alias check +2026/06/10 10:00:15 Start WAF whitelist update +2026/06/10 10:00:16 [WAF Whitelist] No changes needed +2026/06/10 10:00:16 Finish WAF whitelist update ``` ## Troubleshooting @@ -228,3 +243,11 @@ MAX_CONCURRENT_WORKERS=10 ### Telegram не доступен напрямую Заполните переменные прокси в конфигурации. Утилита автоматически попробует HTTP-прокси, затем SOCKS5, и только если оба недоступны — прямое подключение. + +### Ошибка TLS при отправке email + +Если сертификат SMTP-сервера просрочен или невалиден: + +```env +EMAIL_SKIP_TLS_VERIFY=true +``` \ No newline at end of file diff --git a/database.go b/database.go index 6646fd9..ab276f0 100755 --- a/database.go +++ b/database.go @@ -63,18 +63,21 @@ func processSID(db *sql.DB, sid int64) error { // WHOIS var whois whoisResponse if err := apiGet(whoisURL+fmt.Sprint(sid)+"/global", &whois); err != nil { + sendAPIErrorAlert(sid, "whois", err) return err } // ORIGINS var originsResp apiList[originItem] if err := apiGet(originURL+fmt.Sprint(sid), &originsResp); err != nil { + sendAPIErrorAlert(sid, "origins", err) return err } // ALIASES var aliasResp apiList[aliasItem] if err := apiGet(aliasURL+fmt.Sprint(sid), &aliasResp); err != nil { + sendAPIErrorAlert(sid, "aliases", err) return err } @@ -331,7 +334,7 @@ func upsertSPInfo( wafProviderClient, strings.Join(changeDetails, "\n"), ptafWarning, - now.Format("2006-01-02 15:04:05 UTC+3"), + now.Format("2006-01-02 15:04:05")+" UTC+3", ) if err := sendAlert(message); err != nil { @@ -353,7 +356,7 @@ func upsertSPInfo( domain, clientTitle, strings.Join(wafChangeDetails, "\n"), - now.Format("2006-01-02 15:04:05 UTC+3"), + now.Format("2006-01-02 15:04:05")+" UTC+3", ) if err := sendAlert(wafMessage); err != nil { @@ -366,6 +369,24 @@ func upsertSPInfo( return nil } +func sendAPIErrorAlert(sid int64, endpoint string, err error) { + now := time.Now().In(time.FixedZone("UTC+3", 3*60*60)) + message := fmt.Sprintf( + "🔴 Ошибка запроса к API ServicePipe\n\n"+ + "SID: %d\n"+ + "Endpoint: %s\n"+ + "Ошибка: %s\n\n"+ + "Время: %s", + sid, + endpoint, + err.Error(), + now.Format("2006-01-02 15:04:05")+" UTC+3", + ) + if alertErr := sendAlert(message); alertErr != nil { + log.Printf("[API Error Alert] Failed to send alert for SID %d: %v", sid, alertErr) + } +} + func wafVendorName(vendor string) string { switch strings.ToLower(vendor) { case "ptaf": @@ -559,7 +580,7 @@ func checkDuplicateDomains(db *sql.DB) error { } now := time.Now().In(time.FixedZone("UTC+3", 3*60*60)) - msg.WriteString(fmt.Sprintf("\nВремя проверки: %s", now.Format("2006-01-02 15:04:05 UTC+3"))) + msg.WriteString(fmt.Sprintf("\nВремя проверки: %s", now.Format("2006-01-02 15:04:05")+" UTC+3")) if err := sendAlert(msg.String()); err != nil { log.Printf("[Duplicate Check] Failed to send alert: %v", err) diff --git a/main.go b/main.go index 12720ca..a0ad793 100755 --- a/main.go +++ b/main.go @@ -2,12 +2,28 @@ package main import ( "log" + "os" "sync" _ "github.com/lib/pq" // PostgreSQL driver ) func main() { + // Режим --cd: только проверка дублей + if len(os.Args) > 1 && os.Args[1] == "--cd" { + log.Println("Running duplicate domain/alias check only") + + db, err := dbConnect() + if err != nil { + log.Fatal(err) + } + defer db.Close() + + if err := checkDuplicateDomains(db); err != nil { + log.Printf("Duplicate check error: %v", err) + } + return + } log.Println("Start sync sp_info") db, err := dbConnect()