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