From aff9a0dc3f6dde2f7c117301c35da253f336254b Mon Sep 17 00:00:00 2001 From: jbergner Date: Mon, 27 Apr 2026 10:18:23 +0200 Subject: [PATCH] Noise-Canceling UEBA --- deploy/mariadb/init/001-schema.sql | 13 ++-- main.go | 105 +++++++++++++++++++++++------ 2 files changed, 91 insertions(+), 27 deletions(-) diff --git a/deploy/mariadb/init/001-schema.sql b/deploy/mariadb/init/001-schema.sql index 009afed..0a706a9 100644 --- a/deploy/mariadb/init/001-schema.sql +++ b/deploy/mariadb/init/001-schema.sql @@ -1409,16 +1409,15 @@ CREATE TABLE host_risk_scores ( updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6) ); -CREATE TABLE ueba_user_baseline ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, +CREATE TABLE IF NOT EXISTS ueba_user_baseline ( username VARCHAR(255) NOT NULL, hostname VARCHAR(255) NOT NULL, - src_ip VARCHAR(255) NOT NULL DEFAULT '', - workstation VARCHAR(255) NOT NULL DEFAULT '', - first_seen TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), - last_seen TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + src_ip VARCHAR(64) NOT NULL, + workstation VARCHAR(255) NOT NULL, + first_seen DATETIME(6) NOT NULL DEFAULT UTC_TIMESTAMP(6), + last_seen DATETIME(6) NOT NULL DEFAULT UTC_TIMESTAMP(6), seen_count BIGINT NOT NULL DEFAULT 1, - UNIQUE KEY uniq_user_context (username, hostname, src_ip, workstation) + PRIMARY KEY (username, hostname, src_ip, workstation) ); CREATE INDEX idx_ueba_user_baseline_user diff --git a/main.go b/main.go index 966b4cd..95fc4f9 100644 --- a/main.go +++ b/main.go @@ -4610,10 +4610,15 @@ func (d *detector) runUEBANewUserContextRule(ctx context.Context) error { windowEnd := time.Now().UTC() windowStart := windowEnd.Add(-d.cfg.UEBANewContextWindow) - lookbackStart := windowEnd.Add(-d.cfg.UEBALookback) - rows, err := d.db.QueryContext(ctx, ` -SELECT e.hostname, e.target_user, e.src_ip, e.workstation, COUNT(*) AS cnt + const q = ` +SELECT + e.hostname, + e.target_user, + e.src_ip, + e.workstation, + MIN(e.ts) AS first_seen, + COUNT(*) AS cnt FROM event_logs e WHERE e.channel_name = 'Security' AND e.event_id = 4624 @@ -4621,23 +4626,17 @@ WHERE e.channel_name = 'Security' AND e.target_user <> '' AND e.target_user <> '-' AND e.target_user NOT LIKE '%$' - AND NOT EXISTS ( - SELECT 1 - FROM ueba_user_baseline b - WHERE b.username = e.target_user - AND b.hostname = e.hostname - AND b.src_ip = e.src_ip - AND b.workstation = e.workstation - AND b.first_seen < ? - AND b.last_seen >= ? + AND LOWER(e.target_user) NOT IN ( + 'system', + 'localsystem', + 'local service', + 'network service', + 'anonymous logon' ) GROUP BY e.hostname, e.target_user, e.src_ip, e.workstation -`, - windowStart, - windowEnd, - windowStart, - lookbackStart, - ) +` + + rows, err := d.db.QueryContext(ctx, q, windowStart, windowEnd) if err != nil { return err } @@ -4645,12 +4644,27 @@ GROUP BY e.hostname, e.target_user, e.src_ip, e.workstation for rows.Next() { var host, user, srcIP, workstation string + var firstSeen time.Time var count int - if err := rows.Scan(&host, &user, &srcIP, &workstation, &count); err != nil { + if err := rows.Scan(&host, &user, &srcIP, &workstation, &firstSeen, &count); err != nil { return err } + user = normalizeUsername(user) + if isNoiseAccount(user) { + continue + } + + isNew, err := d.touchUEBAUserContext(ctx, user, host, srcIP, workstation, firstSeen, count) + if err != nil { + return err + } + + if !isNew { + continue + } + score := 2.0 severity := "medium" @@ -4679,8 +4693,10 @@ GROUP BY e.hostname, e.target_user, e.src_ip, e.workstation "user": user, "src_ip": srcIP, "workstation": workstation, + "host": host, "count": count, - "lookback": d.cfg.UEBALookback.String(), + "first_seen": firstSeen.UTC().Format(time.RFC3339Nano), + "window": d.cfg.UEBANewContextWindow.String(), }), }) if err != nil { @@ -5371,6 +5387,55 @@ GROUP BY e.hostname, e.target_user, e.src_ip return rows.Err() } +func (d *detector) touchUEBAUserContext(ctx context.Context, username, hostname, srcIP, workstation string, firstSeen time.Time, count int) (bool, error) { + username = normalizeUsername(username) + hostname = strings.TrimSpace(hostname) + srcIP = strings.TrimSpace(srcIP) + workstation = strings.TrimSpace(workstation) + + if username == "" || hostname == "" { + return false, nil + } + + if srcIP == "" { + srcIP = "-" + } + if workstation == "" { + workstation = "-" + } + + res, err := d.db.ExecContext(ctx, ` +INSERT INTO ueba_user_baseline +(username, hostname, src_ip, workstation, first_seen, last_seen, seen_count) +VALUES (?, ?, ?, ?, ?, ?, ?) +ON DUPLICATE KEY UPDATE +last_seen = VALUES(last_seen), +seen_count = seen_count + VALUES(seen_count) +`, + username, + hostname, + srcIP, + workstation, + firstSeen.UTC(), + firstSeen.UTC(), + count, + ) + if err != nil { + return false, err + } + + affected, err := res.RowsAffected() + if err != nil { + return false, err + } + + // MySQL: + // 1 = neuer Insert + // 2 = Update wegen ON DUPLICATE KEY + // 0 = kein effektives Update, abhängig von Client/Settings möglich + return affected == 1, nil +} + func (d *detector) isDetectionSuppressed(ctx context.Context, det Detection) (bool, error) { var count int