Files

133 lines
5.8 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}