Anpassung Noise-Canceling new_source_ip_for_user
All checks were successful
release-tag / release-image (push) Successful in 2m38s

This commit is contained in:
2026-04-27 10:00:32 +02:00
parent 54aad0bdf6
commit 7dd03a00ce
2 changed files with 91 additions and 19 deletions

View File

@@ -1800,4 +1800,14 @@ VALUES
('administrator', 'Built-in Administrator', 1),
('admin', 'Generic admin account', 1)
ON DUPLICATE KEY UPDATE
reason = VALUES(reason);
reason = VALUES(reason);
CREATE TABLE IF NOT EXISTS user_source_ip_seen (
username VARCHAR(255) NOT NULL,
src_ip VARCHAR(64) NOT NULL,
hostname VARCHAR(255) NOT NULL,
first_seen DATETIME(6) NOT NULL,
last_seen DATETIME(6) NOT NULL,
seen_count BIGINT NOT NULL DEFAULT 1,
PRIMARY KEY (username, src_ip, hostname)
);

98
main.go
View File

@@ -5278,29 +5278,31 @@ GROUP BY s.hostname, s.target_user, s.src_ip
func (d *detector) runNewSourceIPForUserRule(ctx context.Context) error {
windowEnd := time.Now().UTC()
windowStart := windowEnd.Add(-d.cfg.NewSourceIPWindow)
lookbackStart := windowStart.Add(-d.cfg.NewSourceIPLookback)
const q = `
SELECT e.hostname, e.target_user, e.src_ip, COUNT(*) AS cnt
SELECT e.hostname, e.target_user, e.src_ip, MIN(e.ts) AS first_seen, COUNT(*) AS cnt
FROM event_logs e
WHERE e.channel_name = 'Security'
AND e.event_id = 4624
AND e.ts >= ? AND e.ts < ?
AND e.target_user <> '' AND e.target_user <> '-'
AND e.src_ip <> '' AND e.src_ip <> '-' AND e.src_ip <> '::1' AND e.src_ip <> '127.0.0.1'
AND NOT EXISTS (
SELECT 1
FROM event_logs old
WHERE old.hostname = e.hostname
AND old.channel_name = 'Security'
AND old.event_id = 4624
AND old.target_user = e.target_user
AND old.src_ip = e.src_ip
AND old.ts >= ? AND old.ts < ?
AND e.target_user <> ''
AND e.target_user <> '-'
AND e.target_user NOT LIKE '%$'
AND e.src_ip <> ''
AND e.src_ip <> '-'
AND e.src_ip <> '::1'
AND e.src_ip <> '127.0.0.1'
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
`
rows, err := d.db.QueryContext(ctx, q, windowStart, windowEnd, lookbackStart, windowStart)
rows, err := d.db.QueryContext(ctx, q, windowStart, windowEnd)
if err != nil {
return err
}
@@ -5308,17 +5310,33 @@ GROUP BY e.hostname, e.target_user, e.src_ip
for rows.Next() {
var host, user, srcIP string
var firstSeen time.Time
var cnt int
if err := rows.Scan(&host, &user, &srcIP, &cnt); err != nil {
if err := rows.Scan(&host, &user, &srcIP, &firstSeen, &cnt); err != nil {
return err
}
user = normalizeUsername(user)
if isNoiseAccount(user) {
continue
}
isNew, err := d.touchUserSourceIPSeen(ctx, user, srcIP, host, firstSeen, cnt)
if err != nil {
return err
}
if !isNew {
continue
}
score := 1.5 + math.Log10(float64(cnt)+1)
severity := "medium"
if cnt >= 5 {
severity = "high"
}
d.anomalyScoreGauge.WithLabelValues(host, "new_source_ip_for_user").Set(score)
created, err := d.insertDetection(ctx, Detection{
RuleName: "new_source_ip_for_user",
@@ -5329,23 +5347,27 @@ GROUP BY e.hostname, e.target_user, e.src_ip
Score: score,
WindowStart: windowStart,
WindowEnd: windowEnd,
Summary: fmt.Sprintf("Benutzer %s meldet sich auf %s von neuer Quell-IP %s an", user, host, srcIP),
Summary: fmt.Sprintf("Benutzer %s meldet sich auf %s erstmals von Quell-IP %s an", user, host, srcIP),
Details: mustJSON(map[string]any{
"user": user,
"src_ip": srcIP,
"host": host,
"count": cnt,
"first_seen": firstSeen.UTC().Format(time.RFC3339Nano),
"window_minutes": int(d.cfg.NewSourceIPWindow.Minutes()),
"lookback_hours": int(d.cfg.NewSourceIPLookback.Hours()),
"event_id": 4624,
}),
})
if err != nil {
return err
}
if created {
d.detectionHitsTotal.WithLabelValues("new_source_ip_for_user", severity).Inc()
d.anomalyScoreGauge.WithLabelValues(host, "new_source_ip_for_user").Set(score)
}
}
return rows.Err()
}
@@ -5859,6 +5881,46 @@ GROUP BY hostname, target_user
return rows.Err()
}
func (d *detector) touchUserSourceIPSeen(ctx context.Context, username, srcIP, hostname string, firstSeen time.Time, count int) (bool, error) {
username = normalizeUsername(username)
hostname = strings.TrimSpace(hostname)
srcIP = strings.TrimSpace(srcIP)
if username == "" || srcIP == "" || hostname == "" {
return false, nil
}
res, err := d.db.ExecContext(ctx, `
INSERT INTO user_source_ip_seen
(username, src_ip, hostname, first_seen, last_seen, seen_count)
VALUES (?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
last_seen = VALUES(last_seen),
seen_count = seen_count + VALUES(seen_count)
`,
username,
srcIP,
hostname,
firstSeen.UTC(),
firstSeen.UTC(),
count,
)
if err != nil {
return false, err
}
affected, err := res.RowsAffected()
if err != nil {
return false, err
}
// MySQL:
// 1 = Insert
// 2 = Update durch ON DUPLICATE KEY UPDATE
// 0 = kein effektives Update, je nach Client/Settings möglich
return affected == 1, nil
}
func (d *detector) runFirstTimePrivilegedRule(ctx context.Context) error {
if !d.cfg.UEBAEnabled {
return nil