This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user