Added double check
This commit is contained in:
parent
cca899e659
commit
5dd61df087
16 changed files with 255 additions and 24 deletions
0
.gitignore
vendored
Normal file → Executable file
0
.gitignore
vendored
Normal file → Executable file
0
ARCHITECTURE.md
Normal file → Executable file
0
ARCHITECTURE.md
Normal file → Executable file
0
DATABASE_SCHEMA.md
Normal file → Executable file
0
DATABASE_SCHEMA.md
Normal file → Executable file
0
LICENSE
Normal file → Executable file
0
LICENSE
Normal file → Executable file
0
README.md
Normal file → Executable file
0
README.md
Normal file → Executable file
0
api.go
Normal file → Executable file
0
api.go
Normal file → Executable file
0
compare.go
Normal file → Executable file
0
compare.go
Normal file → Executable file
12
config.go
Normal file → Executable file
12
config.go
Normal file → Executable 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
159
database.go
Normal file → Executable 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
0
dns.go
Normal file → Executable file
0
git.go
Normal file → Executable file
0
git.go
Normal file → Executable file
0
go.mod
Normal file → Executable file
0
go.mod
Normal file → Executable file
0
go.sum
Normal file → Executable file
0
go.sum
Normal file → Executable file
11
main.go
Normal file → Executable file
11
main.go
Normal file → Executable 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
97
notify.go
Normal file → Executable 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
0
whitelist.go
Normal file → Executable file
Loading…
Add table
Reference in a new issue