Added double check

This commit is contained in:
Magnus Root 2026-06-10 16:00:37 +03:00
parent cca899e659
commit 5dd61df087
16 changed files with 255 additions and 24 deletions

0
.gitignore vendored Normal file → Executable file
View file

0
ARCHITECTURE.md Normal file → Executable file
View file

0
DATABASE_SCHEMA.md Normal file → Executable file
View file

0
LICENSE Normal file → Executable file
View file

0
README.md Normal file → Executable file
View file

0
api.go Normal file → Executable file
View file

0
compare.go Normal file → Executable file
View file

12
config.go Normal file → Executable file
View file

@ -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)

159
database.go Normal file → Executable file
View file

@ -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("<b>🔄 Домен изменён:</b>\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("<b>WAF Enabled:</b>\n <b>Было:</b> %d\n <b>Стало:</b> %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("<b>WAF Provider:</b>\n <b>Было:</b> %s\n <b>Стало:</b> %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("<b>WAF Instance:</b>\n <b>Было:</b> %s\n <b>Стало:</b> %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<b>⚠️ ВНИМАНИЕ: Необходимо внести изменения в кабинете PT AF!</b>"
ptafWarning = "\n<b>⚠️ ВНИМАНИЕ: Необходимо внести изменения в кабинете " + wafVendorName(wafVendor) + "!</b>"
}
now := time.Now().In(time.FixedZone("UTC+3", 3*60*60))
message := fmt.Sprintf(
"<b>🔔 Обновление WAF Info</b>\n\n"+
"<b>SID:</b> %d\n"+
"<b>Домен:</b> %s\n\n"+
"<b>Изменения:</b>\n\n%s%s",
"🟡 <b>Обновление WAF Info</b>\n\n"+
"<b>SID:</b> %d <b>Домен:</b> %s <b>TENANT:</b> %s <b>WAF provider:</b> %s\n\n"+
"<b>Изменения:</b>\n%s%s\n\n"+
"<b>Время проверки:</b> %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(
"<b>⚙️ Изменения настроек инстанса на стороне SP</b>\n\n"+
"<b>SID:</b> %d\n"+
"<b>Домен:</b> %s\n\n"+
"<b>Изменения:</b>\n\n%s",
"<b>Изменения настроек инстанса на стороне SP</b>\n\n"+
"<b>SID:</b> %d <b>Домен:</b> %s <b>TENANT:</b> %s\n\n"+
"<b>Изменения:</b>\n%s\n\n"+
"<b>Время проверки:</b> %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("🔴 <b>Обнаружены дублирующиеся домены/алиасы</b>\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<b>Время проверки:</b> %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
}

0
dns.go Normal file → Executable file
View file

0
git.go Normal file → Executable file
View file

0
go.mod Normal file → Executable file
View file

0
go.sum Normal file → Executable file
View file

11
main.go Normal file → Executable file
View file

@ -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 {

97
notify.go Normal file → Executable file
View file

@ -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
}

0
whitelist.go Normal file → Executable file
View file