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
}