package main import ( "database/sql" "encoding/json" "fmt" "log" "sort" "strings" "time" ) /* DATABASE */ func dbConnect() (*sql.DB, error) { dsn := fmt.Sprintf( "host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", dbHost, dbPort, dbUser, dbPassword, dbName, ) return sql.Open("postgres", dsn) } func loadClients(db *sql.DB) ([]string, error) { rows, err := db.Query(`SELECT client_title FROM client_info`) if err != nil { return nil, err } defer rows.Close() var result []string for rows.Next() { var c string if err := rows.Scan(&c); err != nil { return nil, err } result = append(result, c) } return result, nil } func loadL7IDs(db *sql.DB, client string) ([]int64, error) { rows, err := db.Query( `SELECT l7resourceid FROM apps_settings WHERE client_title = $1 AND mode = 'auto'`, client, ) if err != nil { return nil, err } defer rows.Close() var ids []int64 for rows.Next() { var id int64 if err := rows.Scan(&id); err != nil { return nil, err } ids = append(ids, id) } return ids, nil } func processSID(db *sql.DB, sid int64) error { // WHOIS var whois whoisResponse if err := apiGet(whoisURL+fmt.Sprint(sid)+"/global", &whois); err != nil { return fmt.Errorf("whois: %w", err) } // ORIGINS var originsResp apiList[originItem] if err := apiGet(originURL+fmt.Sprint(sid), &originsResp); err != nil { return fmt.Errorf("origins: %w", err) } // ALIASES var aliasResp apiList[aliasItem] if err := apiGet(aliasURL+fmt.Sprint(sid), &aliasResp); err != nil { return fmt.Errorf("aliases: %w", err) } originsJSON, _ := json.Marshal(originsResp.Data.Result.Items) var aliases []string var latestModifiedAt int64 = 0 for _, a := range aliasResp.Data.Result.Items { aliases = append(aliases, a.Domain) if a.ModifiedAt > latestModifiedAt { latestModifiedAt = a.ModifiedAt } } aliasesJSON, _ := json.Marshal(aliases) // Проверяем AntiDDOS antiddosEnabled := false protectedIP := whois.Data.Result.ProtectedIp var resolvedIP string if protectedIP != "" && whois.Data.Result.L7ResourceName != "" { // Резолвим домен resolvedIPAddr, err := resolveDomain(whois.Data.Result.L7ResourceName) if err != nil { log.Printf("[AntiDDOS Check] Failed to resolve %s: %v", whois.Data.Result.L7ResourceName, err) } else { resolvedIP = resolvedIPAddr log.Printf("[AntiDDOS Check] Domain: %s, Resolved: %s, Protected: %s", whois.Data.Result.L7ResourceName, resolvedIP, protectedIP) if resolvedIP == protectedIP { antiddosEnabled = true log.Printf("[AntiDDOS Check] ✅ AntiDDOS is ENABLED for %s", whois.Data.Result.L7ResourceName) } else { log.Printf("[AntiDDOS Check] ❌ AntiDDOS is DISABLED for %s", whois.Data.Result.L7ResourceName) } } } // Извлекаем WAF данные из API wafEnabledSP := whois.Data.Result.WafEnabled wafVendor := whois.Data.Result.WafProvider instanceSP := whois.Data.Result.WafInstance log.Printf("[WAF Info] SID: %d, WAF Enabled: %d, Vendor: %s, Instance: %s", sid, wafEnabledSP, wafVendor, instanceSP) return upsertSPInfo( db, sid, whois.Data.Result.L7ResourceName, originsResp.Data.Result.Items, aliases, originsJSON, aliasesJSON, latestModifiedAt, protectedIP, resolvedIP, antiddosEnabled, wafEnabledSP, wafVendor, instanceSP, ) } func upsertSPInfo( db *sql.DB, sid int64, domain string, newOrigins []originItem, newAliases []string, originsJSON []byte, aliasesJSON []byte, aliasesModifiedAt int64, protectedIP string, resolvedIP string, antiddosEnabled bool, wafEnabledSP int, 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 var existingWafEnabled sql.NullInt64 var existingWafVendor, existingInstanceSP sql.NullString err := db.QueryRow(` SELECT domain_name, origins, aliases, waf_enabled_sp, waf_vendor, instance_sp FROM sp_info WHERE sid = $1 `, sid).Scan(&existingDomain, &existingOriginsJSON, &existingAliasesJSON, &existingWafEnabled, &existingWafVendor, &existingInstanceSP) isNewRecord := err == sql.ErrNoRows if err != nil && !isNewRecord { return err } hasChanges := false var changeDetails []string needsPTAFUpdate := false hasWAFChanges := false var wafChangeDetails []string if !isNewRecord { var existingOrigins []originItem var existingAliases []string json.Unmarshal(existingOriginsJSON, &existingOrigins) json.Unmarshal(existingAliasesJSON, &existingAliases) if existingDomain != domain { hasChanges = true needsPTAFUpdate = true changeDetails = append(changeDetails, fmt.Sprintf("Домен изменён: %s -> %s", existingDomain, domain)) } originChanges := compareOrigins(existingOrigins, newOrigins) if originChanges != "" { hasChanges = true changeDetails = append(changeDetails, originChanges) } aliasChanges, aliasesChanged := compareAliases(existingAliases, newAliases) if aliasesChanged { hasChanges = true needsPTAFUpdate = true changeDetails = append(changeDetails, aliasChanges) } // Проверяем изменения WAF настроек oldWafEnabled := 0 if existingWafEnabled.Valid { oldWafEnabled = int(existingWafEnabled.Int64) } oldWafVendor := "" if existingWafVendor.Valid { oldWafVendor = existingWafVendor.String } oldInstanceSP := "" if existingInstanceSP.Valid { oldInstanceSP = existingInstanceSP.String } if oldWafEnabled != wafEnabledSP { hasWAFChanges = true wafChangeDetails = append(wafChangeDetails, fmt.Sprintf("WAF Enabled: %d -> %d", oldWafEnabled, wafEnabledSP)) } if oldWafVendor != wafVendor { hasWAFChanges = true oldDisplay := oldWafVendor if oldDisplay == "" { oldDisplay = "(не указан)" } newDisplay := wafVendor if newDisplay == "" { newDisplay = "(не указан)" } wafChangeDetails = append(wafChangeDetails, fmt.Sprintf("WAF Provider: %s -> %s", oldDisplay, newDisplay)) } if oldInstanceSP != instanceSP { hasWAFChanges = true oldDisplay := oldInstanceSP if oldDisplay == "" { oldDisplay = "(не указан)" } newDisplay := instanceSP if newDisplay == "" { newDisplay = "(не указан)" } wafChangeDetails = append(wafChangeDetails, fmt.Sprintf("WAF Instance: %s -> %s", oldDisplay, newDisplay)) } } var aliasesModifiedTime *time.Time if aliasesModifiedAt > 0 { t := time.Unix(aliasesModifiedAt, 0) aliasesModifiedTime = &t } _, err = db.Exec(` INSERT INTO sp_info (sid, domain_name, origins, aliases, aliaces_in_sp_modified, protected_ip, resolved_ip, antiddos_enable, waf_enabled_sp, waf_vendor, instance_sp) VALUES ($1, $2, $3::jsonb, $4::jsonb, $5, $6, $7, $8, $9, $10, $11) ON CONFLICT (sid) DO UPDATE SET domain_name = EXCLUDED.domain_name, origins = EXCLUDED.origins, aliases = EXCLUDED.aliases, aliaces_in_sp_modified = EXCLUDED.aliaces_in_sp_modified, protected_ip = EXCLUDED.protected_ip, resolved_ip = EXCLUDED.resolved_ip, antiddos_enable = EXCLUDED.antiddos_enable, waf_enabled_sp = EXCLUDED.waf_enabled_sp, waf_vendor = EXCLUDED.waf_vendor, instance_sp = EXCLUDED.instance_sp, updated_at = now() `, sid, domain, originsJSON, aliasesJSON, aliasesModifiedTime, protectedIP, resolvedIP, antiddosEnabled, wafEnabledSP, wafVendor, instanceSP, ) if err != nil { return err } // Отправляем алерт для обычных изменений if hasChanges { ptafWarning := "" if needsPTAFUpdate { ptafWarning = "\n⚠️ ВНИМАНИЕ: Необходимо внести изменения в кабинете " + wafVendorName(wafVendor) + "!" } now := time.Now().In(time.FixedZone("UTC+3", 3*60*60)) message := fmt.Sprintf( "🟡 Обновление WAF Info\n\n"+ "SID: %d Домен: %s TENANT: %s WAF provider: %s\n\n"+ "Изменения:\n%s%s\n\n"+ "Время проверки: %s", sid, domain, clientTitle, wafProviderClient, strings.Join(changeDetails, "\n"), ptafWarning, now.Format("2006-01-02 15:04:05")+" UTC+3", ) if err := sendAlert(message); err != nil { log.Printf("Failed to send Telegram alert for SID %d: %v", sid, err) } else { log.Printf("Alert sent for SID %d", sid) } } // Отправляем отдельный алерт для изменений WAF настроек if hasWAFChanges { now := time.Now().In(time.FixedZone("UTC+3", 3*60*60)) wafMessage := fmt.Sprintf( "⚪ Изменения настроек инстанса на стороне SP\n\n"+ "SID: %d Домен: %s TENANT: %s\n\n"+ "Изменения:\n%s\n\n"+ "Время проверки: %s", sid, domain, clientTitle, strings.Join(wafChangeDetails, "\n"), now.Format("2006-01-02 15:04:05")+" UTC+3", ) if err := sendAlert(wafMessage); err != nil { log.Printf("Failed to send WAF settings alert for SID %d: %v", sid, err) } else { log.Printf("WAF settings alert sent for SID %d", sid) } } return nil } func checkSIDAvailability(sid int64) error { var whois whoisResponse if err := apiGet(whoisURL+fmt.Sprint(sid)+"/global", &whois); err != nil { return fmt.Errorf("whois: %w", err) } var originsResp apiList[originItem] if err := apiGet(originURL+fmt.Sprint(sid), &originsResp); err != nil { return fmt.Errorf("origins: %w", err) } var aliasResp apiList[aliasItem] if err := apiGet(aliasURL+fmt.Sprint(sid), &aliasResp); err != nil { return fmt.Errorf("aliases: %w", err) } return nil } func sendAPIErrorsAlert(errors map[int64]string) { if len(errors) == 0 { return } // Сортируем SID для стабильного вывода sids := make([]int64, 0, len(errors)) for sid := range errors { sids = append(sids, sid) } sort.Slice(sids, func(i, j int) bool { return sids[i] < sids[j] }) now := time.Now().In(time.FixedZone("UTC+3", 3*60*60)) var msg strings.Builder msg.WriteString(fmt.Sprintf("🔴 Ошибки запросов к API ServicePipe (%d)\n\n", len(errors))) for _, sid := range sids { msg.WriteString(fmt.Sprintf("SID %d: %s\n", sid, errors[sid])) } 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("[API Errors Alert] Failed to send alert: %v", err) } } 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": return "⚠️ Проверьте, не нужно ли внести изменения в PT AF!" case "sw": return "⚠️ Проверьте, не нужно ли внести изменения в SW!" case "wmx": return "⚠️ Проверьте, не нужно ли внести изменения в WMX!" default: return "⚠️ Проверьте, не нужно ли внести изменения в WAF!" } } func cleanupRemovedSIDs(db *sql.DB, activeSIDs []int64) error { if len(activeSIDs) == 0 { log.Println("[Cleanup] No active SIDs provided, skipping cleanup to avoid accidental data loss") return nil } rows, err := db.Query(` SELECT s.sid, s.domain_name, a.waf_vendor FROM sp_info s LEFT JOIN apps_settings a ON a.l7resourceid = s.sid `) if err != nil { return fmt.Errorf("failed to query sp_info: %w", err) } defer rows.Close() activeSet := make(map[int64]bool, len(activeSIDs)) for _, sid := range activeSIDs { activeSet[sid] = true } type staleRecord struct { sid int64 domain string wafVendor string } var stale []staleRecord for rows.Next() { var sid int64 var domain string var wafVendor sql.NullString if err := rows.Scan(&sid, &domain, &wafVendor); err != nil { return err } if !activeSet[sid] { vendor := "" if wafVendor.Valid { vendor = wafVendor.String } stale = append(stale, staleRecord{sid, domain, vendor}) } } if len(stale) == 0 { log.Println("[Cleanup] No stale SIDs found in sp_info") return nil } log.Printf("[Cleanup] Found %d stale SID(s) to remove", len(stale)) for _, rec := range stale { log.Printf("[Cleanup] Removing SID %d (%s) from sp_info", rec.sid, rec.domain) _, err := db.Exec(`DELETE FROM sp_info WHERE sid = $1`, rec.sid) if err != nil { log.Printf("[Cleanup] Failed to delete SID %d: %v", rec.sid, err) continue } wafWarning := wafVendorWarning(rec.wafVendor) message := fmt.Sprintf( "🗑 Ресурс удалён из мониторинга\n\n"+ "SID: %d\n"+ "Домен: %s\n\n"+ "Ресурс отсутствует в apps_settings и был удалён из sp_info.\n"+ "%s", rec.sid, rec.domain, wafWarning, ) if err := sendAlert(message); err != nil { log.Printf("[Cleanup] Failed to send Telegram alert for SID %d: %v", rec.sid, err) } else { log.Printf("[Cleanup] Alert sent for removed SID %d", rec.sid) } } 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 }