Funktionsrollback
release-tag / release-image (push) Failing after 1m8s

This commit is contained in:
2026-07-24 07:17:38 +02:00
parent a4ff984914
commit e9d9583f28
38 changed files with 3243 additions and 364 deletions
+64 -61
View File
@@ -13,22 +13,21 @@ import (
"example.com/siem-greenfield/internal/clickhouse"
"example.com/siem-greenfield/internal/config"
"example.com/siem-greenfield/internal/postgres"
"example.com/siem-greenfield/internal/rules"
)
type rule struct {
name, severity string
eventCode uint32
score float64
query func(time.Time, time.Time, string) string
summary func(map[string]any) string
}
func Run(ctx context.Context, cfg config.Config) error {
pg, e := postgres.Open(ctx, cfg.PostgresURL)
if e != nil {
return e
}
defer pg.Close()
if e := syncBuiltins(ctx, cfg, pg); e != nil {
return fmt.Errorf("sync built-in rules: %w", e)
}
if e := pg.EnsureCustomRuleSet(ctx, cfg.TenantID); e != nil {
return e
}
ch := clickhouse.New(cfg)
ticker := time.NewTicker(cfg.DetectorInterval)
defer ticker.Stop()
@@ -47,69 +46,79 @@ func Run(ctx context.Context, cfg config.Config) error {
}
}
}
func syncBuiltins(ctx context.Context, cfg config.Config, pg *postgres.Store) error {
sets, e := rules.LoadDir(cfg.RulesDir)
if e != nil {
return e
}
keep := make([]string, 0, len(sets))
for _, rs := range sets {
keep = append(keep, rs.ID)
if e := pg.SyncRuleSet(ctx, cfg.TenantID, rs); e != nil {
return fmt.Errorf("%s: %w", rs.ID, e)
}
}
if e := pg.PruneBuiltinRuleSets(ctx, cfg.TenantID, keep); e != nil {
return fmt.Errorf("prune built-in rule sets: %w", e)
}
log.Printf("rule engine: synchronized %d built-in rule sets", len(sets))
return nil
}
func runAll(ctx context.Context, cfg config.Config, pg *postgres.Store, ch *clickhouse.Client) error {
enabled, e := pg.ListRules(ctx, cfg.TenantID, true)
if e != nil {
return e
}
end := time.Now().UTC()
start := end.Add(-cfg.DetectorLookback)
for _, r := range rules(cfg.ClickHouseDB) {
q := r.query(start, end, cfg.TenantID)
rows, e := ch.QueryJSON(ctx, q)
for _, sr := range enabled {
q, _, e := rules.Compile(sr.Rule, cfg.ClickHouseDB, cfg.TenantID, end)
if e != nil {
return fmt.Errorf("%s: %w", r.name, e)
log.Printf("rule %s compile: %v", sr.ID, e)
continue
}
qctx, cancel := context.WithTimeout(ctx, 20*time.Second)
rows, e := ch.QueryJSON(qctx, q)
cancel()
if e != nil {
log.Printf("rule %s query: %v", sr.ID, e)
continue
}
for _, row := range rows {
host := str(row["host_name"])
user := str(row["user_name"])
ip := str(row["source_ip"])
workstation := str(row["workstation"])
count := int64(num(row["cnt"]))
ws := timeVal(row["window_start"], start)
ws := timeVal(row["window_start"], end.Add(-time.Duration(sr.WindowSeconds)*time.Second))
we := timeVal(row["window_end"], end)
fp := fingerprint(r.name, host, user, ip, workstation, strconv.FormatInt(ws.Unix()/300, 10))
d := postgres.Detection{Fingerprint: fp, RuleName: r.name, Severity: r.severity, Hostname: host, UserName: user, SourceIP: ip, Workstation: workstation, EventCode: r.eventCode, Score: r.score, WindowStart: ws, WindowEnd: we, Summary: r.summary(row), Count: max64(1, count)}
suppressed, e := pg.IsSuppressed(ctx, cfg.TenantID, sr.ID, host, user, ip, we)
if e != nil {
log.Printf("rule %s suppression: %v", sr.ID, e)
continue
}
if suppressed {
continue
}
bucket := sr.SuppressSeconds
if bucket <= 0 {
bucket = sr.WindowSeconds
}
if bucket < 60 {
bucket = 60
}
fp := fingerprint(sr.ID, host, user, ip, workstation, strconv.FormatInt(ws.Unix()/int64(bucket), 10))
count := int64(num(row["cnt"]))
eventCode := uint32(num(row["event_code"]))
d := postgres.Detection{Fingerprint: fp, RuleID: sr.ID, RuleSetID: sr.RuleSetID, RuleName: sr.Title, Severity: sr.Severity, Hostname: host, UserName: user, SourceIP: ip, Workstation: workstation, EventCode: eventCode, Score: sr.Score, WindowStart: ws, WindowEnd: we, Summary: rules.RenderSummary(sr.Summary, row), Count: max64(1, count), Tags: sr.Tags, MITRE: sr.MITRE}
if e := pg.UpsertDetection(ctx, d, cfg.TenantID); e != nil {
return e
log.Printf("rule %s detection: %v", sr.ID, e)
}
}
}
return nil
}
func rules(db string) []rule {
table := clickhouse.Ident(db) + ".events"
return []rule{
{name: "audit_log_cleared", severity: "critical", eventCode: 1102, score: 9.8, query: simpleEvent(table, 1102, 1), summary: func(m map[string]any) string {
return fmt.Sprintf("Audit-Log auf %s wurde gelöscht", str(m["host_name"]))
}},
{name: "service_installed", severity: "high", eventCode: 7045, score: 8.0, query: simpleEvent(table, 7045, 1), summary: func(m map[string]any) string {
return fmt.Sprintf("Neuer Dienst auf %s installiert", str(m["host_name"]))
}},
{name: "account_lockout", severity: "medium", eventCode: 4740, score: 5.5, query: func(s, e time.Time, t string) string {
return fmt.Sprintf(`SELECT host_name, target_user AS user_name, '' AS source_ip, workstation, uniqExact(event_uid) cnt, min(event_time) window_start, max(event_time) window_end FROM %s WHERE tenant_id=%s AND event_time>=%s AND event_time<%s AND event_code=4740 GROUP BY host_name,user_name,workstation HAVING cnt>=1`, table, clickhouse.Q(t), clickhouse.Q(ts(s)), clickhouse.Q(ts(e)))
}, summary: func(m map[string]any) string {
return fmt.Sprintf("Account-Lockout: %s; Caller %s; DC/Host %s (%d×)", str(m["user_name"]), fallback(str(m["workstation"]), "unbekannt"), str(m["host_name"]), int64(num(m["cnt"])))
}},
{name: "failed_logon_burst", severity: "high", eventCode: 4625, score: 7.5, query: func(s, e time.Time, t string) string {
return fmt.Sprintf(`SELECT host_name, target_user AS user_name, source_ip, workstation, uniqExact(event_uid) cnt, min(event_time) window_start, max(event_time) window_end FROM %s WHERE tenant_id=%s AND event_time>=%s AND event_time<%s AND event_code=4625 AND target_user!='' GROUP BY host_name,user_name,source_ip,workstation HAVING cnt>=20`, table, clickhouse.Q(t), clickhouse.Q(ts(s)), clickhouse.Q(ts(e)))
}, summary: func(m map[string]any) string {
return fmt.Sprintf("%d fehlgeschlagene Logons für %s auf %s", int64(num(m["cnt"])), str(m["user_name"]), str(m["host_name"]))
}},
{name: "password_spray", severity: "high", eventCode: 4625, score: 8.5, query: func(s, e time.Time, t string) string {
return fmt.Sprintf(`SELECT '' AS host_name, '' AS user_name, source_ip, '' AS workstation, uniqExact(target_user) users, uniqExact(event_uid) cnt, min(event_time) window_start, max(event_time) window_end FROM %s WHERE tenant_id=%s AND event_time>=%s AND event_time<%s AND event_code=4625 AND source_ip!='' AND target_user!='' GROUP BY source_ip HAVING users>=10 AND cnt>=20`, table, clickhouse.Q(t), clickhouse.Q(ts(s)), clickhouse.Q(ts(e)))
}, summary: func(m map[string]any) string {
return fmt.Sprintf("Password-Spray von %s gegen %.0f Benutzer (%d Versuche)", str(m["source_ip"]), num(m["users"]), int64(num(m["cnt"])))
}},
{name: "privileged_group_change", severity: "critical", eventCode: 4728, score: 9.2, query: func(s, e time.Time, t string) string {
return fmt.Sprintf(`SELECT host_name, target_user AS user_name, '' AS source_ip, workstation, uniqExact(event_uid) cnt, min(event_time) window_start, max(event_time) window_end FROM %s WHERE tenant_id=%s AND event_time>=%s AND event_time<%s AND event_code IN (4728,4732,4756) GROUP BY host_name,user_name,workstation HAVING cnt>=1`, table, clickhouse.Q(t), clickhouse.Q(ts(s)), clickhouse.Q(ts(e)))
}, summary: func(m map[string]any) string {
return fmt.Sprintf("Privilegierte Gruppenmitgliedschaft geändert: %s auf %s", str(m["user_name"]), str(m["host_name"]))
}},
}
}
func simpleEvent(table string, id uint32, min int) func(time.Time, time.Time, string) string {
return func(s, e time.Time, t string) string {
return fmt.Sprintf(`SELECT host_name, '' AS user_name, '' AS source_ip, '' AS workstation, uniqExact(event_uid) cnt, min(event_time) window_start, max(event_time) window_end FROM %s WHERE tenant_id=%s AND event_time>=%s AND event_time<%s AND event_code=%d GROUP BY host_name HAVING cnt>=%d`, table, clickhouse.Q(t), clickhouse.Q(ts(s)), clickhouse.Q(ts(e)), id, min)
}
}
func ts(t time.Time) string { return t.UTC().Format("2006-01-02 15:04:05.000") }
func str(v any) string {
if v == nil {
return ""
@@ -144,12 +153,6 @@ func fingerprint(v ...string) string {
h := sha256.Sum256([]byte(strings.Join(v, "|")))
return hex.EncodeToString(h[:])
}
func fallback(v, d string) string {
if strings.TrimSpace(v) == "" {
return d
}
return v
}
func max64(a, b int64) int64 {
if a > b {
return a