211 lines
5.5 KiB
Go
211 lines
5.5 KiB
Go
package analysis
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/config"
|
|
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/log/analysis/alert_group"
|
|
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/geoip"
|
|
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/notifications"
|
|
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/i18n"
|
|
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
|
|
)
|
|
|
|
type Alert interface {
|
|
Analyze(entry *Entry)
|
|
ClearDBData() error
|
|
}
|
|
|
|
type alert struct {
|
|
rulesIndex *RulesIndex
|
|
alertGroupService alert_group.Group
|
|
logger log.Logger
|
|
notify notifications.Notifications
|
|
ipInfo geoip.Info
|
|
}
|
|
|
|
type alertAnalyzeRuleReturn struct {
|
|
found bool
|
|
fields []*regexField
|
|
partition *string
|
|
}
|
|
|
|
type alertNotify struct {
|
|
rule *config.AlertRule
|
|
messages []string
|
|
alertNumber uint64
|
|
time time.Time
|
|
fields []*regexField
|
|
}
|
|
|
|
func NewAlert(
|
|
rulesIndex *RulesIndex,
|
|
alertGroupService alert_group.Group,
|
|
logger log.Logger,
|
|
notify notifications.Notifications,
|
|
ipInfo geoip.Info,
|
|
) Alert {
|
|
return &alert{
|
|
rulesIndex: rulesIndex,
|
|
alertGroupService: alertGroupService,
|
|
logger: logger,
|
|
notify: notify,
|
|
ipInfo: ipInfo,
|
|
}
|
|
}
|
|
|
|
func (a *alert) Analyze(entry *Entry) {
|
|
rules, err := a.rulesIndex.Alerts(entry)
|
|
if err != nil {
|
|
a.logger.Error(fmt.Sprintf("Failed to get alert rules: %s", err))
|
|
}
|
|
for _, rule := range rules {
|
|
result := a.analyzeRule(rule, entry.Message)
|
|
if !result.found {
|
|
continue
|
|
}
|
|
groupName := ""
|
|
alertNumber := uint64(0)
|
|
messages := []string{}
|
|
if rule.Group != nil {
|
|
alertGroup, err := a.alertGroupService.Analyze(rule.Group, entry.Time, entry.Message, result.partition)
|
|
if err != nil {
|
|
a.logger.Error(fmt.Sprintf("Failed to analyze alert group: %s", err))
|
|
continue
|
|
}
|
|
if !alertGroup.Alerted {
|
|
continue
|
|
}
|
|
|
|
groupName = rule.Group.Name
|
|
for _, lastLog := range alertGroup.LastLogs {
|
|
messages = append(messages, lastLog)
|
|
}
|
|
alertNumber = alertGroup.AlertNumber
|
|
} else {
|
|
messages = append(messages, entry.Message)
|
|
}
|
|
a.logger.Info(fmt.Sprintf("Alert detected (%s) (group:%s): %s", rule.Name, groupName, entry.Message))
|
|
a.sendNotify(&alertNotify{
|
|
rule: rule,
|
|
messages: messages,
|
|
alertNumber: alertNumber,
|
|
time: entry.Time,
|
|
fields: result.fields,
|
|
})
|
|
}
|
|
}
|
|
|
|
func (a *alert) ClearDBData() error {
|
|
return a.alertGroupService.ClearDBData()
|
|
}
|
|
|
|
func (a *alert) analyzeRule(rule *config.AlertRule, message string) alertAnalyzeRuleReturn {
|
|
result := alertAnalyzeRuleReturn{
|
|
found: false,
|
|
fields: []*regexField{},
|
|
partition: nil,
|
|
}
|
|
|
|
for _, pattern := range rule.Patterns {
|
|
re, err := pattern.Regexp.Get()
|
|
if err != nil {
|
|
a.logger.Error(fmt.Sprintf("Failed to compile regexp: %s", err))
|
|
continue
|
|
}
|
|
|
|
idx := re.FindStringSubmatchIndex(message)
|
|
|
|
if idx != nil {
|
|
if pattern.Partition != nil {
|
|
start, end, err := getValueStartEndByRegexIndex(int(pattern.Partition.Value), idx)
|
|
if err != nil {
|
|
a.logger.Error(fmt.Sprintf("alert. Failed to get partition value: %s", err))
|
|
continue
|
|
}
|
|
partition := pattern.Partition.Normalize(message[start:end])
|
|
if pattern.Partition.Type != nil {
|
|
if accepts, partitionCode := pattern.Partition.Type.Accepts(partition); !accepts {
|
|
continue
|
|
} else {
|
|
partition = partitionCode
|
|
}
|
|
if pattern.Partition.Type.IsPartitioned() {
|
|
result.partition = &partition
|
|
}
|
|
} else {
|
|
result.partition = &partition
|
|
}
|
|
}
|
|
|
|
for _, value := range pattern.Values {
|
|
start, end, err := getValueStartEndByRegexIndex(int(value.Value), idx)
|
|
if err != nil {
|
|
result.fields = append(result.fields, ®exField{name: value.Name, value: i18n.Lang.T("unknown")})
|
|
continue
|
|
}
|
|
result.fields = append(result.fields, ®exField{name: value.Name, value: message[start:end], typeValue: value.Type})
|
|
}
|
|
|
|
if len(pattern.Values) != len(result.fields) {
|
|
a.logger.Error(fmt.Sprintf("alert. analyzeRule len(pattern.Values) != len(result.fields): %d != %d", len(pattern.Values), len(result.fields)))
|
|
result.fields = []*regexField{}
|
|
result.partition = nil
|
|
continue
|
|
}
|
|
|
|
result.found = true
|
|
return result
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func (a *alert) sendNotify(notify *alertNotify) {
|
|
if !notify.rule.IsNotification {
|
|
return
|
|
}
|
|
|
|
groupName := ""
|
|
groupMessage := ""
|
|
if notify.rule.Group != nil {
|
|
groupName = notify.rule.Group.Name
|
|
groupMessage = notify.rule.Group.Message + "\n\n"
|
|
}
|
|
|
|
subject := i18n.Lang.T("alert.subject", map[string]any{
|
|
"Name": notify.rule.Name,
|
|
"GroupName": groupName,
|
|
})
|
|
text := subject + "\n\n" + groupMessage + notify.rule.Message + "\n\n"
|
|
text += i18n.Lang.T("time", map[string]any{
|
|
"Time": notify.time,
|
|
}) + "\n"
|
|
|
|
for _, field := range notify.fields {
|
|
v := field.value
|
|
if field.typeValue == config.PatternValueIP {
|
|
if ipInfo, err := a.ipInfo(field.value); err != nil {
|
|
a.logger.Error(fmt.Sprintf("Failed to get geoip info for ip %s: %s", v, err))
|
|
} else {
|
|
v = ipInfo
|
|
}
|
|
}
|
|
text += fmt.Sprintf("%s: %s\n", field.name, v)
|
|
}
|
|
if notify.alertNumber > 0 {
|
|
text += i18n.Lang.T("alertNumber", map[string]any{
|
|
"Count": notify.alertNumber,
|
|
}) + "\n"
|
|
}
|
|
text += "\n" + i18n.Lang.T("log", map[string]any{
|
|
"Count": len(notify.messages),
|
|
}) + "\n"
|
|
for _, message := range notify.messages {
|
|
text += message + "\n\n"
|
|
}
|
|
a.notify.SendAsync(notifications.Message{Subject: subject, Body: text})
|
|
}
|