Noise-Canceling UEBA
All checks were successful
release-tag / release-image (push) Successful in 2m11s

This commit is contained in:
2026-04-27 10:18:23 +02:00
parent 7dd03a00ce
commit aff9a0dc3f
2 changed files with 91 additions and 27 deletions

View File

@@ -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

105
main.go
View File

@@ -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