133 lines
5.8 KiB
Go
133 lines
5.8 KiB
Go
package rule
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"strings"
|
||
|
||
"git.ilapage.cn/ila/yovision/Bell/server/app/event"
|
||
"github.com/jackc/pgx/v5"
|
||
"github.com/jackc/pgx/v5/pgxpool"
|
||
)
|
||
|
||
var ErrInvalid = errors.New("规则字段无效")
|
||
|
||
type Service struct{ DB *pgxpool.Pool }
|
||
|
||
func validSeverity(value string) bool {
|
||
return value == "low" || value == "medium" || value == "high" || value == "critical"
|
||
}
|
||
func severityRank(value string) int {
|
||
return map[string]int{"low": 1, "medium": 2, "high": 3, "critical": 4}[value]
|
||
}
|
||
func (s Service) Create(ctx context.Context, input CreateInput) (Rule, error) {
|
||
input.Code = strings.ToLower(strings.TrimSpace(input.Code))
|
||
input.Name = strings.TrimSpace(input.Name)
|
||
input.MinimumSeverity = strings.ToLower(strings.TrimSpace(input.MinimumSeverity))
|
||
if input.Code == "" || input.Name == "" || !validSeverity(input.MinimumSeverity) {
|
||
return Rule{}, ErrInvalid
|
||
}
|
||
if input.EventType != nil {
|
||
v := strings.TrimSpace(*input.EventType)
|
||
if v == "" {
|
||
input.EventType = nil
|
||
} else {
|
||
input.EventType = &v
|
||
}
|
||
}
|
||
if input.LocationContains != nil {
|
||
v := strings.TrimSpace(*input.LocationContains)
|
||
if v == "" {
|
||
input.LocationContains = nil
|
||
} else {
|
||
input.LocationContains = &v
|
||
}
|
||
}
|
||
var result Rule
|
||
err := s.DB.QueryRow(ctx, `INSERT INTO bell_rules(code,name,event_type,minimum_severity,location_contains) VALUES($1,$2,$3,$4,$5) RETURNING id::text,code,name,enabled,event_type,minimum_severity,location_contains,version,created_at::text,updated_at::text`, input.Code, input.Name, input.EventType, input.MinimumSeverity, input.LocationContains).Scan(&result.ID, &result.Code, &result.Name, &result.Enabled, &result.EventType, &result.MinimumSeverity, &result.LocationContains, &result.Version, &result.CreatedAt, &result.UpdatedAt)
|
||
return result, err
|
||
}
|
||
func (s Service) SetEnabled(ctx context.Context, id string, enabled bool) (Rule, error) {
|
||
var result Rule
|
||
err := s.DB.QueryRow(ctx, `UPDATE bell_rules SET enabled=$2,version=version+1,updated_at=now() WHERE id=$1 RETURNING id::text,code,name,enabled,event_type,minimum_severity,location_contains,version,created_at::text,updated_at::text`, id, enabled).Scan(&result.ID, &result.Code, &result.Name, &result.Enabled, &result.EventType, &result.MinimumSeverity, &result.LocationContains, &result.Version, &result.CreatedAt, &result.UpdatedAt)
|
||
return result, err
|
||
}
|
||
func (s Service) List(ctx context.Context) ([]Rule, error) {
|
||
rows, err := s.DB.Query(ctx, `SELECT id::text,code,name,enabled,event_type,minimum_severity,location_contains,version,created_at::text,updated_at::text FROM bell_rules ORDER BY created_at,id LIMIT 200`)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
items := []Rule{}
|
||
for rows.Next() {
|
||
var item Rule
|
||
if err := rows.Scan(&item.ID, &item.Code, &item.Name, &item.Enabled, &item.EventType, &item.MinimumSeverity, &item.LocationContains, &item.Version, &item.CreatedAt, &item.UpdatedAt); err != nil {
|
||
return nil, err
|
||
}
|
||
items = append(items, item)
|
||
}
|
||
return items, rows.Err()
|
||
}
|
||
|
||
func (s Service) Match(ctx context.Context, tx pgx.Tx, evt event.Event) error {
|
||
rows, err := tx.Query(ctx, `SELECT id::text,code,name,enabled,event_type,minimum_severity,location_contains,version,created_at::text,updated_at::text FROM bell_rules WHERE enabled=true ORDER BY id`)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
rules := []Rule{}
|
||
for rows.Next() {
|
||
var item Rule
|
||
if err := rows.Scan(&item.ID, &item.Code, &item.Name, &item.Enabled, &item.EventType, &item.MinimumSeverity, &item.LocationContains, &item.Version, &item.CreatedAt, &item.UpdatedAt); err != nil {
|
||
rows.Close()
|
||
return err
|
||
}
|
||
rules = append(rules, item)
|
||
}
|
||
rows.Close()
|
||
if err := rows.Err(); err != nil {
|
||
return err
|
||
}
|
||
for _, current := range rules {
|
||
matched := true
|
||
reasons := []string{}
|
||
if current.EventType != nil && *current.EventType != evt.EventType {
|
||
matched = false
|
||
reasons = append(reasons, "事件类型不匹配")
|
||
}
|
||
if severityRank(evt.Severity) < severityRank(current.MinimumSeverity) {
|
||
matched = false
|
||
reasons = append(reasons, "风险等级低于阈值")
|
||
}
|
||
if current.LocationContains != nil && !strings.Contains(strings.ToLower(evt.Location), strings.ToLower(*current.LocationContains)) {
|
||
matched = false
|
||
reasons = append(reasons, "地点条件不匹配")
|
||
}
|
||
explanation := "全部条件命中"
|
||
if !matched {
|
||
explanation = strings.Join(reasons, ";")
|
||
}
|
||
if _, err := tx.Exec(ctx, `INSERT INTO bell_rule_evaluations(event_id,rule_id,rule_version,matched,explanation) VALUES($1,$2,$3,$4,$5) ON CONFLICT DO NOTHING`, evt.ID, current.ID, current.Version, matched, explanation); err != nil {
|
||
return err
|
||
}
|
||
if !matched {
|
||
continue
|
||
}
|
||
snapshot, _ := json.Marshal(current)
|
||
summary := fmt.Sprintf("%s:%s", current.Name, evt.EventType)
|
||
var alertID string
|
||
err := tx.QueryRow(ctx, `INSERT INTO bell_alerts(primary_rule_id,correlation_key,severity,summary,location) VALUES($1,$2,$3,$4,$5) ON CONFLICT(primary_rule_id,correlation_key) WHERE status='open' DO UPDATE SET updated_at=now(),severity=CASE WHEN array_position(ARRAY['low','medium','high','critical'],EXCLUDED.severity)>array_position(ARRAY['low','medium','high','critical'],bell_alerts.severity) THEN EXCLUDED.severity ELSE bell_alerts.severity END RETURNING id::text`, current.ID, strings.ToLower(evt.Location), evt.Severity, summary, evt.Location).Scan(&alertID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if _, err := tx.Exec(ctx, `INSERT INTO bell_alert_events(alert_id,event_id) VALUES($1,$2) ON CONFLICT DO NOTHING`, alertID, evt.ID); err != nil {
|
||
return err
|
||
}
|
||
if _, err := tx.Exec(ctx, `INSERT INTO bell_rule_matches(alert_id,event_id,rule_id,rule_version,rule_snapshot,explanation) VALUES($1,$2,$3,$4,$5,$6) ON CONFLICT DO NOTHING`, alertID, evt.ID, current.ID, current.Version, snapshot, explanation); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
return nil
|
||
}
|