From 5dd61df0876f5d45cd5c4a2413ab5d5331751a50 Mon Sep 17 00:00:00 2001 From: Magnus Root Date: Wed, 10 Jun 2026 16:00:37 +0300 Subject: [PATCH] Added double check --- .gitignore | 0 ARCHITECTURE.md | 0 DATABASE_SCHEMA.md | 0 LICENSE | 0 README.md | 0 api.go | 0 compare.go | 0 config.go | 12 +++- database.go | 159 ++++++++++++++++++++++++++++++++++++++++----- dns.go | 0 git.go | 0 go.mod | 0 go.sum | 0 main.go | 11 ++++ notify.go | 97 +++++++++++++++++++++++++-- whitelist.go | 0 16 files changed, 255 insertions(+), 24 deletions(-) mode change 100644 => 100755 .gitignore mode change 100644 => 100755 ARCHITECTURE.md mode change 100644 => 100755 DATABASE_SCHEMA.md mode change 100644 => 100755 LICENSE mode change 100644 => 100755 README.md mode change 100644 => 100755 api.go mode change 100644 => 100755 compare.go mode change 100644 => 100755 config.go mode change 100644 => 100755 database.go mode change 100644 => 100755 dns.go mode change 100644 => 100755 git.go mode change 100644 => 100755 go.mod mode change 100644 => 100755 go.sum mode change 100644 => 100755 main.go mode change 100644 => 100755 notify.go mode change 100644 => 100755 whitelist.go diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md old mode 100644 new mode 100755 diff --git a/DATABASE_SCHEMA.md b/DATABASE_SCHEMA.md old mode 100644 new mode 100755 diff --git a/LICENSE b/LICENSE old mode 100644 new mode 100755 diff --git a/README.md b/README.md old mode 100644 new mode 100755 diff --git a/api.go b/api.go old mode 100644 new mode 100755 diff --git a/compare.go b/compare.go old mode 100644 new mode 100755 diff --git a/config.go b/config.go old mode 100644 new mode 100755 index e4f2ece..6f79262 --- a/config.go +++ b/config.go @@ -13,9 +13,9 @@ import ( var ( // API bearerToken string - originURL = "https://api.servicepipe.ru/api/v1/l7/origin/global?limit=1000&l7ResourceId=" - aliasURL = "https://api.servicepipe.ru/api/v1/l7/alias/global?limit=1000&l7ResourceId=" - whoisURL = "https://api.servicepipe.ru/api/v1/l7/resource/" + originURL = "https://api.test.ru/api/v1/l7/origin/global?limit=1000&l7ResourceId=" + aliasURL = "https://api.test.ru/api/v1/l7/alias/global?limit=1000&l7ResourceId=" + whoisURL = "https://api.test.ru/api/v1/l7/resource/" // DB dbHost string @@ -46,6 +46,7 @@ var ( smtpPassword string smtpFrom string smtpTo string + smtpSkipTLS bool // Git gitRepoURL string @@ -54,6 +55,9 @@ var ( // Concurrency maxConcurrentWorkers int + + // Features + checkDuplicates bool ) // Загружаем переменные из sp_sync.env при старте @@ -88,10 +92,12 @@ func init() { smtpPassword = getEnv("EMAIL_SMTP_PASSWORD", "") smtpFrom = getEnv("EMAIL_FROM", "") smtpTo = getEnv("EMAIL_TO", "") + smtpSkipTLS = getEnv("EMAIL_SKIP_TLS_VERIFY", "") == "true" gitRepoURL = getEnv("GIT_REPO_URL", "https://token@svc-git.cirex.ru/wmx/waf_whitelist.git") gitRepoPath = getEnv("GIT_REPO_PATH", "/home/install/waf_whitelist") whitelistFile = getEnv("WHITELIST_FILE", "whitelist_ptaf.txt") maxConcurrentWorkers = getEnvAsInt("MAX_CONCURRENT_WORKERS", 5) + checkDuplicates = getEnv("CHECK_DUPLICATES", "true") != "false" // Отладка загрузки переменных log.Printf("[Config] DB_USER loaded: '%s'", dbUser) diff --git a/database.go b/database.go old mode 100644 new mode 100755 index c015286..6646fd9 --- a/database.go +++ b/database.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "log" + "sort" "strings" "time" ) @@ -156,6 +157,21 @@ func upsertSPInfo( wafVendor string, instanceSP string, ) error { + // Получаем client_title из apps_settings и waf_provider из client_info + var clientTitle string + db.QueryRow(`SELECT client_title FROM apps_settings WHERE l7resourceid = $1 LIMIT 1`, sid).Scan(&clientTitle) + if clientTitle == "" { + clientTitle = "—" + } + + var wafProviderClient string + if clientTitle != "—" { + db.QueryRow(`SELECT waf_provider FROM client_info WHERE client_title = $1`, clientTitle).Scan(&wafProviderClient) + } + if wafProviderClient == "" { + wafProviderClient = "—" + } + // Проверяем существующие данные var existingDomain string var existingOriginsJSON, existingAliasesJSON []byte @@ -192,7 +208,7 @@ func upsertSPInfo( hasChanges = true needsPTAFUpdate = true changeDetails = append(changeDetails, - fmt.Sprintf("🔄 Домен изменён:\n Было: %s\n Стало: %s", existingDomain, domain)) + fmt.Sprintf("Домен изменён: %s -> %s", existingDomain, domain)) } originChanges := compareOrigins(existingOrigins, newOrigins) @@ -225,7 +241,7 @@ func upsertSPInfo( if oldWafEnabled != wafEnabledSP { hasWAFChanges = true wafChangeDetails = append(wafChangeDetails, - fmt.Sprintf("WAF Enabled:\n Было: %d\n Стало: %d", oldWafEnabled, wafEnabledSP)) + fmt.Sprintf("WAF Enabled: %d -> %d", oldWafEnabled, wafEnabledSP)) } if oldWafVendor != wafVendor { @@ -239,7 +255,7 @@ func upsertSPInfo( newDisplay = "(не указан)" } wafChangeDetails = append(wafChangeDetails, - fmt.Sprintf("WAF Provider:\n Было: %s\n Стало: %s", oldDisplay, newDisplay)) + fmt.Sprintf("WAF Provider: %s -> %s", oldDisplay, newDisplay)) } if oldInstanceSP != instanceSP { @@ -253,7 +269,7 @@ func upsertSPInfo( newDisplay = "(не указан)" } wafChangeDetails = append(wafChangeDetails, - fmt.Sprintf("WAF Instance:\n Было: %s\n Стало: %s", oldDisplay, newDisplay)) + fmt.Sprintf("WAF Instance: %s -> %s", oldDisplay, newDisplay)) } } @@ -300,18 +316,22 @@ func upsertSPInfo( if hasChanges { ptafWarning := "" if needsPTAFUpdate { - ptafWarning = "\n\n⚠️ ВНИМАНИЕ: Необходимо внести изменения в кабинете PT AF!" + ptafWarning = "\n⚠️ ВНИМАНИЕ: Необходимо внести изменения в кабинете " + wafVendorName(wafVendor) + "!" } + now := time.Now().In(time.FixedZone("UTC+3", 3*60*60)) message := fmt.Sprintf( - "🔔 Обновление WAF Info\n\n"+ - "SID: %d\n"+ - "Домен: %s\n\n"+ - "Изменения:\n\n%s%s", + "🟡 Обновление WAF Info\n\n"+ + "SID: %d Домен: %s TENANT: %s WAF provider: %s\n\n"+ + "Изменения:\n%s%s\n\n"+ + "Время проверки: %s", sid, domain, - strings.Join(changeDetails, "\n\n"), + clientTitle, + wafProviderClient, + strings.Join(changeDetails, "\n"), ptafWarning, + now.Format("2006-01-02 15:04:05 UTC+3"), ) if err := sendAlert(message); err != nil { @@ -323,14 +343,17 @@ func upsertSPInfo( // Отправляем отдельный алерт для изменений WAF настроек if hasWAFChanges { + now := time.Now().In(time.FixedZone("UTC+3", 3*60*60)) wafMessage := fmt.Sprintf( - "⚙️ Изменения настроек инстанса на стороне SP\n\n"+ - "SID: %d\n"+ - "Домен: %s\n\n"+ - "Изменения:\n\n%s", + "⚪ Изменения настроек инстанса на стороне SP\n\n"+ + "SID: %d Домен: %s TENANT: %s\n\n"+ + "Изменения:\n%s\n\n"+ + "Время проверки: %s", sid, domain, - strings.Join(wafChangeDetails, "\n\n"), + clientTitle, + strings.Join(wafChangeDetails, "\n"), + now.Format("2006-01-02 15:04:05 UTC+3"), ) if err := sendAlert(wafMessage); err != nil { @@ -343,6 +366,19 @@ func upsertSPInfo( return nil } +func wafVendorName(vendor string) string { + switch strings.ToLower(vendor) { + case "ptaf": + return "PT AF" + case "sw": + return "SW" + case "wmx": + return "WMX" + default: + return "WAF" + } +} + func wafVendorWarning(vendor string) string { switch strings.ToLower(vendor) { case "ptaf": @@ -438,3 +474,96 @@ func cleanupRemovedSIDs(db *sql.DB, activeSIDs []int64) error { return nil } + +func checkDuplicateDomains(db *sql.DB) error { + rows, err := db.Query(`SELECT sid, domain_name, aliases FROM sp_info`) + if err != nil { + return fmt.Errorf("failed to query sp_info: %w", err) + } + defer rows.Close() + + // Единая карта: домен/алиас -> список SID где встречается + valueToSIDs := make(map[string][]int64) + + for rows.Next() { + var sid int64 + var domain string + var aliasesJSON []byte + if err := rows.Scan(&sid, &domain, &aliasesJSON); err != nil { + return err + } + + if domain != "" { + valueToSIDs[domain] = append(valueToSIDs[domain], sid) + } + + var aliases []string + if err := json.Unmarshal(aliasesJSON, &aliases); err == nil { + for _, alias := range aliases { + // Добавляем только если этот SID ещё не учтён для данного значения + alreadyAdded := false + for _, s := range valueToSIDs[alias] { + if s == sid { + alreadyAdded = true + break + } + } + if !alreadyAdded { + valueToSIDs[alias] = append(valueToSIDs[alias], sid) + } + } + } + } + + // Собираем дубли — значения встречающиеся у более чем одного SID + type duplicate struct { + value string + sids []int64 + } + var dups []duplicate + + for value, sids := range valueToSIDs { + if len(sids) > 1 { + dups = append(dups, duplicate{value, sids}) + } + } + + if len(dups) == 0 { + log.Println("[Duplicate Check] No duplicates found") + return nil + } + + // Сортируем для стабильного вывода + sort.Slice(dups, func(i, j int) bool { + return dups[i].value < dups[j].value + }) + + log.Printf("[Duplicate Check] Found %d duplicate(s)", len(dups)) + for _, d := range dups { + var sidStrs []string + for _, sid := range d.sids { + sidStrs = append(sidStrs, fmt.Sprintf("%d", sid)) + } + log.Printf("[Duplicate Check] %s → SID: %s", d.value, strings.Join(sidStrs, ", ")) + } + + var msg strings.Builder + msg.WriteString("🔴 Обнаружены дублирующиеся домены/алиасы\n\n") + + for _, d := range dups { + var sidStrs []string + for _, sid := range d.sids { + sidStrs = append(sidStrs, fmt.Sprintf("%d", sid)) + } + msg.WriteString(fmt.Sprintf(" %s → SID: %s\n", d.value, strings.Join(sidStrs, ", "))) + } + + 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"))) + + if err := sendAlert(msg.String()); err != nil { + log.Printf("[Duplicate Check] Failed to send alert: %v", err) + } + + return nil +} diff --git a/dns.go b/dns.go old mode 100644 new mode 100755 diff --git a/git.go b/git.go old mode 100644 new mode 100755 diff --git a/go.mod b/go.mod old mode 100644 new mode 100755 diff --git a/go.sum b/go.sum old mode 100644 new mode 100755 diff --git a/main.go b/main.go old mode 100644 new mode 100755 index 8632c3d..12720ca --- a/main.go +++ b/main.go @@ -71,6 +71,17 @@ func main() { } log.Println("Finish cleanup of removed SIDs") + // Проверка дублирующихся domain_name и aliases между разными SID + if checkDuplicates { + log.Println("Start duplicate domain/alias check") + if err := checkDuplicateDomains(db); err != nil { + log.Printf("Duplicate check error: %v", err) + } + log.Println("Finish duplicate domain/alias check") + } else { + log.Println("Duplicate domain/alias check is disabled") + } + // Обновление WAF whitelist log.Println("Start WAF whitelist update") if err := updateWAFWhitelist(db); err != nil { diff --git a/notify.go b/notify.go old mode 100644 new mode 100755 index cb8e73b..79f7ffc --- a/notify.go +++ b/notify.go @@ -3,6 +3,7 @@ package main import ( "bytes" "context" + "crypto/tls" "encoding/json" "fmt" "log" @@ -190,20 +191,75 @@ func sendEmailAlert(message string) error { body := fmt.Sprintf("From: %s\r\nTo: %s\r\nSubject: %s\r\nContent-Type: text/plain; charset=UTF-8\r\n\r\n%s", smtpFrom, smtpTo, subject, message) - var auth smtp.Auth - if smtpUser != "" && smtpPassword != "" { - auth = smtp.PlainAuth("", smtpUser, smtpPassword, smtpHost) - } - to := strings.Split(smtpTo, ",") for i := range to { to[i] = strings.TrimSpace(to[i]) } + if smtpSkipTLS { + // STARTTLS с отключённой проверкой сертификата + tlsConfig := &tls.Config{ + InsecureSkipVerify: true, + ServerName: smtpHost, + } + client, err := smtp.Dial(addr) + if err != nil { + return fmt.Errorf("smtp.Dial: %w", err) + } + defer client.Close() + + if err := client.StartTLS(tlsConfig); err != nil { + return fmt.Errorf("smtp.StartTLS: %w", err) + } + + if smtpUser != "" && smtpPassword != "" { + var authMech smtp.Auth + + if ok, ext := client.Extension("AUTH"); ok { + log.Printf("[Email] Server supported AUTH mechanisms: %s", ext) + switch { + case strings.Contains(ext, "LOGIN"): + log.Printf("[Email] Using LOGIN auth") + authMech = loginAuth(smtpUser, smtpPassword) + default: + log.Printf("[Email] Using PLAIN auth") + authMech = smtp.PlainAuth("", smtpUser, smtpPassword, smtpHost) + } + } else { + authMech = smtp.PlainAuth("", smtpUser, smtpPassword, smtpHost) + } + + if err := client.Auth(authMech); err != nil { + return fmt.Errorf("smtp.Auth: %w", err) + } + } + + if err := client.Mail(smtpFrom); err != nil { + return fmt.Errorf("smtp.Mail: %w", err) + } + for _, recipient := range to { + if err := client.Rcpt(recipient); err != nil { + return fmt.Errorf("smtp.Rcpt: %w", err) + } + } + w, err := client.Data() + if err != nil { + return fmt.Errorf("smtp.Data: %w", err) + } + if _, err := w.Write([]byte(body)); err != nil { + return fmt.Errorf("smtp write: %w", err) + } + return w.Close() + } + + // Стандартная отправка с проверкой сертификата + var auth smtp.Auth + if smtpUser != "" && smtpPassword != "" { + auth = smtp.PlainAuth("", smtpUser, smtpPassword, smtpHost) + } if err := smtp.SendMail(addr, auth, smtpFrom, to, []byte(body)); err != nil { return fmt.Errorf("smtp.SendMail: %w", err) } - return nil } @@ -313,3 +369,32 @@ func formatCommitMessage(added, removed []string) string { return msg } + +/* LOGIN AUTH */ + +// loginAuth реализует механизм аутентификации LOGIN для SMTP +type loginAuthType struct { + username, password string +} + +func loginAuth(username, password string) smtp.Auth { + return &loginAuthType{username, password} +} + +func (a *loginAuthType) Start(server *smtp.ServerInfo) (string, []byte, error) { + return "LOGIN", nil, nil +} + +func (a *loginAuthType) Next(fromServer []byte, more bool) ([]byte, error) { + if more { + switch string(fromServer) { + case "Username:": + return []byte(a.username), nil + case "Password:": + return []byte(a.password), nil + default: + return nil, fmt.Errorf("unexpected server challenge: %s", fromServer) + } + } + return nil, nil +} diff --git a/whitelist.go b/whitelist.go old mode 100644 new mode 100755