337 lines
8.7 KiB
Go
337 lines
8.7 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"time"
|
|
)
|
|
|
|
type State struct {
|
|
LastRun time.Time `json:"last_run"`
|
|
TemplateCommit string `json:"template_commit"`
|
|
TemplateVersion string `json:"template_version"`
|
|
ClientsHash string `json:"clients_hash"`
|
|
ClientCount int `json:"client_count"`
|
|
DomainCount int `json:"domain_count"`
|
|
SIDs []string `json:"sids"`
|
|
LastChangeReason string `json:"last_change_reason,omitempty"`
|
|
AlertsHash string `json:"alerts_hash,omitempty"` // Хэш лимитов алертов из БД
|
|
AlertTemplateHash string `json:"alert_template_hash,omitempty"` // Хэш файла alert_rules_template.json
|
|
}
|
|
|
|
type StateManager struct {
|
|
stateFile string
|
|
state *State
|
|
}
|
|
|
|
func NewStateManager(stateFile string) *StateManager {
|
|
return &StateManager{
|
|
stateFile: stateFile,
|
|
}
|
|
}
|
|
|
|
func (sm *StateManager) Load() error {
|
|
data, err := os.ReadFile(sm.stateFile)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
// First run - no state file
|
|
sm.state = &State{}
|
|
return nil
|
|
}
|
|
return fmt.Errorf("failed to read state file: %w", err)
|
|
}
|
|
|
|
sm.state = &State{}
|
|
if err := json.Unmarshal(data, sm.state); err != nil {
|
|
return fmt.Errorf("failed to parse state file: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (sm *StateManager) Save() error {
|
|
sm.state.LastRun = time.Now()
|
|
|
|
// Create directory if it doesn't exist
|
|
dir := filepath.Dir(sm.stateFile)
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
return fmt.Errorf("failed to create state directory: %w", err)
|
|
}
|
|
|
|
data, err := json.MarshalIndent(sm.state, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal state: %w", err)
|
|
}
|
|
|
|
if err := os.WriteFile(sm.stateFile, data, 0644); err != nil {
|
|
return fmt.Errorf("failed to write state file: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (sm *StateManager) CheckChanges(templateCommit, templateVersion string, clients map[string]ClientData) (bool, string) {
|
|
if sm.state == nil {
|
|
return true, "First run - no previous state"
|
|
}
|
|
|
|
reasons := []string{}
|
|
|
|
// Check template commit
|
|
if sm.state.TemplateCommit != templateCommit {
|
|
reasons = append(reasons, fmt.Sprintf("Template commit changed: %s -> %s",
|
|
truncateHash(sm.state.TemplateCommit), truncateHash(templateCommit)))
|
|
}
|
|
|
|
// Check template version
|
|
if sm.state.TemplateVersion != templateVersion {
|
|
reasons = append(reasons, fmt.Sprintf("Template version changed: %s -> %s",
|
|
sm.state.TemplateVersion, templateVersion))
|
|
}
|
|
|
|
// Calculate new hash
|
|
newHash := calculateClientsHash(clients)
|
|
if sm.state.ClientsHash != newHash {
|
|
reasons = append(reasons, "Database content changed")
|
|
|
|
// Get details about what changed
|
|
newSIDs, removedSIDs, totalNew := compareSIDs(sm.state.SIDs, extractSIDs(clients))
|
|
|
|
if len(newSIDs) > 0 {
|
|
reasons = append(reasons, fmt.Sprintf(" - New SIDs: %v", newSIDs))
|
|
}
|
|
if len(removedSIDs) > 0 {
|
|
reasons = append(reasons, fmt.Sprintf(" - Removed SIDs: %v", removedSIDs))
|
|
}
|
|
if totalNew == 0 && len(newSIDs) == 0 && len(removedSIDs) == 0 {
|
|
reasons = append(reasons, " - Data modified (domains, aliases, etc.)")
|
|
}
|
|
}
|
|
|
|
// Check client count
|
|
newClientCount := len(clients)
|
|
if sm.state.ClientCount != newClientCount {
|
|
reasons = append(reasons, fmt.Sprintf("Client count changed: %d -> %d",
|
|
sm.state.ClientCount, newClientCount))
|
|
}
|
|
|
|
// Check domain count
|
|
newDomainCount := countDomains(clients)
|
|
if sm.state.DomainCount != newDomainCount {
|
|
reasons = append(reasons, fmt.Sprintf("Domain count changed: %d -> %d",
|
|
sm.state.DomainCount, newDomainCount))
|
|
}
|
|
|
|
hasChanges := len(reasons) > 0
|
|
changeReason := ""
|
|
if hasChanges {
|
|
changeReason = fmt.Sprintf("Changes detected:\n %s", joinReasons(reasons))
|
|
}
|
|
|
|
return hasChanges, changeReason
|
|
}
|
|
|
|
func (sm *StateManager) Update(templateCommit, templateVersion string, clients map[string]ClientData, changeReason string) {
|
|
if sm.state == nil {
|
|
sm.state = &State{}
|
|
}
|
|
|
|
sm.state.TemplateCommit = templateCommit
|
|
sm.state.TemplateVersion = templateVersion
|
|
sm.state.ClientsHash = calculateClientsHash(clients)
|
|
sm.state.ClientCount = len(clients)
|
|
sm.state.DomainCount = countDomains(clients)
|
|
sm.state.SIDs = extractSIDs(clients)
|
|
sm.state.LastChangeReason = changeReason
|
|
}
|
|
|
|
func calculateClientsHash(clients map[string]ClientData) string {
|
|
// Create a deterministic representation
|
|
type hashData struct {
|
|
ClientTitle string
|
|
RPSLimit int
|
|
Domains []struct {
|
|
SID string
|
|
DomainName string
|
|
Aliases string
|
|
}
|
|
}
|
|
|
|
var data []hashData
|
|
for _, client := range clients {
|
|
hd := hashData{ClientTitle: client.ClientTitle, RPSLimit: client.RPSLimit}
|
|
for _, domain := range client.Domains {
|
|
hd.Domains = append(hd.Domains, struct {
|
|
SID string
|
|
DomainName string
|
|
Aliases string
|
|
}{
|
|
SID: domain.SID,
|
|
DomainName: domain.DomainName,
|
|
Aliases: domain.Aliases,
|
|
})
|
|
}
|
|
// Sort domains for consistent hash
|
|
sort.Slice(hd.Domains, func(i, j int) bool {
|
|
return hd.Domains[i].SID < hd.Domains[j].SID
|
|
})
|
|
data = append(data, hd)
|
|
}
|
|
|
|
// Sort by client title for consistent hash
|
|
sort.Slice(data, func(i, j int) bool {
|
|
return data[i].ClientTitle < data[j].ClientTitle
|
|
})
|
|
|
|
// Calculate hash
|
|
jsonData, _ := json.Marshal(data)
|
|
hash := sha256.Sum256(jsonData)
|
|
return fmt.Sprintf("%x", hash)
|
|
}
|
|
|
|
// calculateAlertsHash считает хэш от данных нужных для алертов (rps_limit + пороги per-SID)
|
|
func calculateAlertsHash(clients map[string]ClientData) string {
|
|
type domainAlert struct {
|
|
SID string
|
|
Limit4xx int
|
|
Limit5xx int
|
|
}
|
|
type alertData struct {
|
|
ClientTitle string
|
|
RPSLimit int
|
|
Domains []domainAlert
|
|
}
|
|
var data []alertData
|
|
for _, client := range clients {
|
|
var domains []domainAlert
|
|
for _, d := range client.Domains {
|
|
domains = append(domains, domainAlert{
|
|
SID: d.SID,
|
|
Limit4xx: d.Limit4xx,
|
|
Limit5xx: d.Limit5xx,
|
|
})
|
|
}
|
|
if client.RPSLimit > 0 || len(domains) > 0 {
|
|
data = append(data, alertData{
|
|
ClientTitle: client.ClientTitle,
|
|
RPSLimit: client.RPSLimit,
|
|
Domains: domains,
|
|
})
|
|
}
|
|
}
|
|
sort.Slice(data, func(i, j int) bool {
|
|
return data[i].ClientTitle < data[j].ClientTitle
|
|
})
|
|
jsonData, _ := json.Marshal(data)
|
|
hash := sha256.Sum256(jsonData)
|
|
return fmt.Sprintf("%x", hash)
|
|
}
|
|
|
|
// AlertTemplateChanged проверяет изменился ли файл alert_rules_template.json
|
|
func (sm *StateManager) AlertTemplateChanged(templateHash string) bool {
|
|
if sm.state == nil {
|
|
return true
|
|
}
|
|
return sm.state.AlertTemplateHash != templateHash
|
|
}
|
|
|
|
// UpdateAlertTemplateHash сохраняет хэш шаблона алертов
|
|
func (sm *StateManager) UpdateAlertTemplateHash(templateHash string) {
|
|
if sm.state == nil {
|
|
sm.state = &State{}
|
|
}
|
|
sm.state.AlertTemplateHash = templateHash
|
|
}
|
|
|
|
// AlertsNeedUpdate проверяет изменился ли хэш алертов
|
|
func (sm *StateManager) AlertsNeedUpdate(clients map[string]ClientData) bool {
|
|
if sm.state == nil {
|
|
return true
|
|
}
|
|
return sm.state.AlertsHash != calculateAlertsHash(clients)
|
|
}
|
|
|
|
// UpdateAlertsHash сохраняет хэш алертов в state
|
|
func (sm *StateManager) UpdateAlertsHash(clients map[string]ClientData) {
|
|
if sm.state == nil {
|
|
sm.state = &State{}
|
|
}
|
|
sm.state.AlertsHash = calculateAlertsHash(clients)
|
|
}
|
|
|
|
func extractSIDs(clients map[string]ClientData) []string {
|
|
sidMap := make(map[string]bool)
|
|
for _, client := range clients {
|
|
for _, domain := range client.Domains {
|
|
sidMap[domain.SID] = true
|
|
}
|
|
}
|
|
|
|
sids := make([]string, 0, len(sidMap))
|
|
for sid := range sidMap {
|
|
sids = append(sids, sid)
|
|
}
|
|
sort.Strings(sids)
|
|
return sids
|
|
}
|
|
|
|
func compareSIDs(oldSIDs, newSIDs []string) (added, removed []string, total int) {
|
|
oldMap := make(map[string]bool)
|
|
for _, sid := range oldSIDs {
|
|
oldMap[sid] = true
|
|
}
|
|
|
|
newMap := make(map[string]bool)
|
|
for _, sid := range newSIDs {
|
|
newMap[sid] = true
|
|
if !oldMap[sid] {
|
|
added = append(added, sid)
|
|
}
|
|
}
|
|
|
|
for _, sid := range oldSIDs {
|
|
if !newMap[sid] {
|
|
removed = append(removed, sid)
|
|
}
|
|
}
|
|
|
|
total = len(added) + len(removed)
|
|
return
|
|
}
|
|
|
|
func countDomains(clients map[string]ClientData) int {
|
|
count := 0
|
|
for _, client := range clients {
|
|
count += len(client.Domains)
|
|
}
|
|
return count
|
|
}
|
|
|
|
func joinReasons(reasons []string) string {
|
|
result := ""
|
|
for i, reason := range reasons {
|
|
if i > 0 {
|
|
result += "\n "
|
|
}
|
|
result += reason
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (sm *StateManager) GetLastRun() time.Time {
|
|
if sm.state == nil {
|
|
return time.Time{}
|
|
}
|
|
return sm.state.LastRun
|
|
}
|
|
|
|
func (sm *StateManager) GetStats() (clientCount, domainCount, sidCount int) {
|
|
if sm.state == nil {
|
|
return 0, 0, 0
|
|
}
|
|
return sm.state.ClientCount, sm.state.DomainCount, len(sm.state.SIDs)
|
|
}
|