Privilegierte Benutzer-Accounts hinzufügen
All checks were successful
release-tag / release-image (push) Successful in 2m14s
All checks were successful
release-tag / release-image (push) Successful in 2m14s
This commit is contained in:
@@ -1768,4 +1768,36 @@ VALUES
|
||||
|
||||
UPDATE detection_rules
|
||||
SET enabled = 0
|
||||
WHERE name LIKE 'v1_%';
|
||||
WHERE name LIKE 'v1_%';
|
||||
|
||||
CREATE TABLE user_host_baseline (
|
||||
username VARCHAR(128),
|
||||
hostname VARCHAR(128),
|
||||
first_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (username, hostname)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_privilege_baseline (
|
||||
username VARCHAR(255) NOT NULL PRIMARY KEY,
|
||||
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
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS privileged_users (
|
||||
username VARCHAR(255) PRIMARY KEY,
|
||||
reason TEXT NULL,
|
||||
enabled TINYINT(1) NOT NULL DEFAULT 1,
|
||||
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_privileged_users_enabled
|
||||
ON privileged_users (enabled);
|
||||
|
||||
INSERT INTO privileged_users (username, reason, enabled)
|
||||
VALUES
|
||||
('administrator', 'Built-in Administrator', 1),
|
||||
('admin', 'Generic admin account', 1)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
reason = VALUES(reason);
|
||||
602
main.go
602
main.go
@@ -515,6 +515,7 @@ a:hover {
|
||||
<nav>
|
||||
<a href="/ui">Overview</a>
|
||||
<a href="/ui/soc">SOC</a>
|
||||
<a href="/ui/privileged-users">Privileged Users</a>
|
||||
<a href="/ui/agents">Agents</a>
|
||||
<a href="/ui/rules">Rules</a>
|
||||
<a href="/ui/baseline">Baseline</a>
|
||||
@@ -532,6 +533,72 @@ a:hover {
|
||||
</html>
|
||||
{{end}}
|
||||
|
||||
{{define "privileged_users"}}
|
||||
{{template "header" .}}
|
||||
<h1>{{.Title}}</h1>
|
||||
<p class="muted">Privilegierte Benutzer für UEBA-Regeln wie „Admin auf neuem Host“.</p>
|
||||
|
||||
<div class="card">
|
||||
<h2>Benutzer hinzufügen</h2>
|
||||
<form method="post" action="/ui/privileged-users/save">
|
||||
<div class="filters">
|
||||
<div>
|
||||
<label>Username</label>
|
||||
<input name="username" placeholder="adm.mustermann oder administrator" required>
|
||||
</div>
|
||||
<div>
|
||||
<label>Grund</label>
|
||||
<input name="reason" placeholder="Domain Admin, Server Admin, Helpdesk Admin">
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit">Speichern</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<h2>Privileged Users</h2>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Grund</th>
|
||||
<th>Status</th>
|
||||
<th>Created</th>
|
||||
<th>Updated</th>
|
||||
<th>Aktion</th>
|
||||
</tr>
|
||||
{{range .Users}}
|
||||
<tr>
|
||||
<td><span class="mono">{{.Username}}</span></td>
|
||||
<td>{{.Reason}}</td>
|
||||
<td>
|
||||
{{if .Enabled}}
|
||||
<span class="badge badge-on">aktiv</span>
|
||||
{{else}}
|
||||
<span class="badge badge-muted">inaktiv</span>
|
||||
{{end}}
|
||||
</td>
|
||||
<td>{{fmtTime .CreatedAt}}</td>
|
||||
<td>{{fmtTime .UpdatedAt}}</td>
|
||||
<td>
|
||||
<form method="post" action="/ui/privileged-users/toggle" class="inline-form">
|
||||
<input type="hidden" name="username" value="{{.Username}}">
|
||||
{{if .Enabled}}
|
||||
<input type="hidden" name="enabled" value="0">
|
||||
<button class="danger" type="submit">Deaktivieren</button>
|
||||
{{else}}
|
||||
<input type="hidden" name="enabled" value="1">
|
||||
<button class="success" type="submit">Aktivieren</button>
|
||||
{{end}}
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{{template "footer" .}}
|
||||
{{end}}
|
||||
|
||||
{{define "dashboard"}}
|
||||
{{template "header" .}}
|
||||
<h1>{{.Title}}</h1>
|
||||
@@ -1189,6 +1256,10 @@ type detector struct {
|
||||
baselineSamplesGauge *prometheus.GaugeVec
|
||||
|
||||
hostRiskScoreGauge *prometheus.GaugeVec
|
||||
|
||||
privilegedLogonsTotal *prometheus.CounterVec
|
||||
privilegedLogonFailuresTotal *prometheus.CounterVec
|
||||
privilegedNewHostTotal *prometheus.CounterVec
|
||||
}
|
||||
|
||||
type EventRow struct {
|
||||
@@ -1389,6 +1460,20 @@ type SOCPageData struct {
|
||||
RecentIncidents []SOCRecentIncidentRow
|
||||
}
|
||||
|
||||
type PrivilegedUserRow struct {
|
||||
Username string
|
||||
Reason string
|
||||
Enabled bool
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type PrivilegedUsersPageData struct {
|
||||
Title string
|
||||
Now time.Time
|
||||
Users []PrivilegedUserRow
|
||||
}
|
||||
|
||||
var (
|
||||
httpRequestsTotal = prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{Name: "eventcollector_http_requests_total", Help: "Total HTTP requests."},
|
||||
@@ -1526,6 +1611,27 @@ func main() {
|
||||
},
|
||||
[]string{"host", "severity"},
|
||||
),
|
||||
privilegedLogonsTotal: prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "siem_privileged_logons_total",
|
||||
Help: "Successful logons by privileged users.",
|
||||
},
|
||||
[]string{"user", "host"},
|
||||
),
|
||||
privilegedLogonFailuresTotal: prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "siem_privileged_logon_failures_total",
|
||||
Help: "Failed logons by privileged users.",
|
||||
},
|
||||
[]string{"user", "host"},
|
||||
),
|
||||
privilegedNewHostTotal: prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "siem_privileged_new_host_total",
|
||||
Help: "Privileged user logged on to a new host.",
|
||||
},
|
||||
[]string{"user", "host"},
|
||||
),
|
||||
}
|
||||
reg.MustRegister(
|
||||
d.lastSeenGauge,
|
||||
@@ -1540,6 +1646,9 @@ func main() {
|
||||
d.baselineStddevGauge,
|
||||
d.baselineSamplesGauge,
|
||||
d.hostRiskScoreGauge,
|
||||
d.privilegedLogonsTotal,
|
||||
d.privilegedLogonFailuresTotal,
|
||||
d.privilegedNewHostTotal,
|
||||
)
|
||||
|
||||
s := &server{
|
||||
@@ -1594,6 +1703,9 @@ func main() {
|
||||
mux.HandleFunc("/ui/baseline", s.handleUIBaseline)
|
||||
mux.HandleFunc("/ui/soc", s.handleUISOC)
|
||||
mux.HandleFunc("/ui/detections/batch-update", s.handleUIDetectionsBatchUpdate)
|
||||
mux.HandleFunc("/ui/privileged-users", s.handleUIPrivilegedUsers)
|
||||
mux.HandleFunc("/ui/privileged-users/save", s.handleUIPrivilegedUserSave)
|
||||
mux.HandleFunc("/ui/privileged-users/toggle", s.handleUIPrivilegedUserToggle)
|
||||
|
||||
httpSrv := &http.Server{
|
||||
Addr: cfg.ListenAddr,
|
||||
@@ -1627,6 +1739,128 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *server) listPrivilegedUsers(ctx context.Context) ([]PrivilegedUserRow, error) {
|
||||
rows, err := s.db.QueryContext(ctx, `
|
||||
SELECT username, COALESCE(reason, ''), enabled, created_at, updated_at
|
||||
FROM privileged_users
|
||||
ORDER BY username ASC
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var out []PrivilegedUserRow
|
||||
for rows.Next() {
|
||||
var u PrivilegedUserRow
|
||||
if err := rows.Scan(&u.Username, &u.Reason, &u.Enabled, &u.CreatedAt, &u.UpdatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, u)
|
||||
}
|
||||
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (s *server) handleUIPrivilegedUsers(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
users, err := s.listPrivilegedUsers(ctx)
|
||||
if err != nil {
|
||||
s.logger.Printf("privileged users: %v", err)
|
||||
writeError(w, http.StatusInternalServerError, "internal error")
|
||||
return
|
||||
}
|
||||
|
||||
s.renderTemplate(w, "privileged_users", PrivilegedUsersPageData{
|
||||
Title: "Privileged Users",
|
||||
Now: time.Now().In(s.location),
|
||||
Users: users,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *server) handleUIPrivilegedUserSave(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
if err := r.ParseForm(); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid form")
|
||||
return
|
||||
}
|
||||
|
||||
username := normalizeUsername(r.FormValue("username"))
|
||||
reason := strings.TrimSpace(r.FormValue("reason"))
|
||||
|
||||
if username == "" || strings.HasSuffix(username, "$") {
|
||||
writeError(w, http.StatusBadRequest, "invalid username")
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
_, err := s.db.ExecContext(ctx, `
|
||||
INSERT INTO privileged_users (username, reason, enabled)
|
||||
VALUES (?, ?, 1)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
reason = VALUES(reason),
|
||||
enabled = 1,
|
||||
updated_at = UTC_TIMESTAMP(6)
|
||||
`, username, reason)
|
||||
if err != nil {
|
||||
s.logger.Printf("save privileged user: %v", err)
|
||||
writeError(w, http.StatusInternalServerError, "internal error")
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/ui/privileged-users", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (s *server) handleUIPrivilegedUserToggle(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
if err := r.ParseForm(); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid form")
|
||||
return
|
||||
}
|
||||
|
||||
username := normalizeUsername(r.FormValue("username"))
|
||||
enabled := strings.TrimSpace(r.FormValue("enabled")) == "1"
|
||||
|
||||
if username == "" {
|
||||
writeError(w, http.StatusBadRequest, "invalid username")
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
_, err := s.db.ExecContext(ctx, `
|
||||
UPDATE privileged_users
|
||||
SET enabled = ?,
|
||||
updated_at = UTC_TIMESTAMP(6)
|
||||
WHERE username = ?
|
||||
`, enabled, username)
|
||||
if err != nil {
|
||||
s.logger.Printf("toggle privileged user: %v", err)
|
||||
writeError(w, http.StatusInternalServerError, "internal error")
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/ui/privileged-users", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (s *server) handleUIDetectionsBatchUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
@@ -2386,7 +2620,6 @@ func (s *server) handleUIRuleSave(w http.ResponseWriter, r *http.Request) {
|
||||
ThresholdCount: atoiDefault(r.FormValue("threshold_count"), 1),
|
||||
ThresholdWindowSeconds: atoiDefault(r.FormValue("threshold_window_seconds"), 0),
|
||||
SuppressForSeconds: atoiDefault(r.FormValue("suppress_for_seconds"), 3600),
|
||||
Enabled: true,
|
||||
}
|
||||
|
||||
if rule.Name == "" || rule.EventIDs == "" {
|
||||
@@ -3296,9 +3529,43 @@ INSERT INTO event_logs (
|
||||
|
||||
norm := NormalizeEventXML(item.Message)
|
||||
|
||||
realHost := firstNonEmpty(norm.Computer, item.Hostname)
|
||||
targetUser := normalizeUsername(norm.TargetUser)
|
||||
subjectUser := normalizeUsername(norm.SubjectUser)
|
||||
|
||||
// Erfolgreicher Logon
|
||||
if item.Channel == "Security" && item.EventID == 4624 && targetUser != "" {
|
||||
priv, err := s.detector.isPrivilegedUser(ctx, targetUser)
|
||||
if err != nil {
|
||||
s.logger.Printf("privileged user check failed for %s: %v", targetUser, err)
|
||||
} else if priv {
|
||||
s.detector.privilegedLogonsTotal.WithLabelValues(targetUser, realHost).Inc()
|
||||
}
|
||||
}
|
||||
|
||||
// Fehlgeschlagener Logon
|
||||
if item.Channel == "Security" && item.EventID == 4625 && targetUser != "" {
|
||||
priv, err := s.detector.isPrivilegedUser(ctx, targetUser)
|
||||
if err != nil {
|
||||
s.logger.Printf("privileged user check failed for %s: %v", targetUser, err)
|
||||
} else if priv {
|
||||
s.detector.privilegedLogonFailuresTotal.WithLabelValues(targetUser, realHost).Inc()
|
||||
}
|
||||
}
|
||||
|
||||
// Special Privileged Logon
|
||||
if item.Channel == "Security" && item.EventID == 4672 && subjectUser != "" {
|
||||
priv, err := s.detector.isPrivilegedUser(ctx, subjectUser)
|
||||
if err != nil {
|
||||
s.logger.Printf("privileged user check failed for %s: %v", subjectUser, err)
|
||||
} else if priv {
|
||||
s.detector.privilegedLogonsTotal.WithLabelValues(subjectUser, realHost).Inc()
|
||||
}
|
||||
}
|
||||
|
||||
args = append(args,
|
||||
agentID,
|
||||
item.Hostname,
|
||||
realHost,
|
||||
item.Channel,
|
||||
item.EventID,
|
||||
item.Source,
|
||||
@@ -4262,6 +4529,9 @@ func (s *server) runDetectionsOnce() {
|
||||
{"baseline_anomaly", s.detector.runBaselineAnomalyRule},
|
||||
{"baseline_update", s.detector.runBaselineUpdate},
|
||||
|
||||
{"ueba_admin_new_host", s.detector.runAdminNewHostRule},
|
||||
{"ueba_offhours_login", s.detector.runOffHoursLoginRule},
|
||||
{"ueba_first_privileged_use", s.detector.runFirstTimePrivilegedRule},
|
||||
{"ueba_new_user_context", s.detector.runUEBANewUserContextRule},
|
||||
{"ueba_update", s.detector.runUEBABaselineUpdate},
|
||||
{"host_risk_score", s.detector.runHostRiskScoreUpdate},
|
||||
@@ -5495,3 +5765,331 @@ func getenvDuration(key string, def time.Duration) time.Duration {
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *detector) runOffHoursLoginRule(ctx context.Context) error {
|
||||
if !d.cfg.UEBAEnabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
nowLocal := time.Now().Local()
|
||||
hour := nowLocal.Hour()
|
||||
|
||||
// Arbeitszeit: 6–20 Uhr
|
||||
if hour >= 6 && hour <= 20 {
|
||||
return nil
|
||||
}
|
||||
|
||||
windowEnd := time.Now().UTC()
|
||||
windowStart := windowEnd.Add(-5 * time.Minute)
|
||||
|
||||
const q = `
|
||||
SELECT hostname, target_user, COUNT(*) AS cnt
|
||||
FROM event_logs
|
||||
WHERE channel_name = 'Security'
|
||||
AND event_id = 4624
|
||||
AND ts >= ? AND ts < ?
|
||||
AND target_user <> ''
|
||||
AND target_user <> '-'
|
||||
AND target_user NOT LIKE '%$'
|
||||
GROUP BY hostname, target_user
|
||||
`
|
||||
|
||||
rows, err := d.db.QueryContext(ctx, q, windowStart, windowEnd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var host, user string
|
||||
var count int
|
||||
|
||||
if err := rows.Scan(&host, &user, &count); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user = normalizeUsername(user)
|
||||
if user == "" || isMachineAccount(user) {
|
||||
continue
|
||||
}
|
||||
|
||||
score := 2.0
|
||||
severity := "medium"
|
||||
|
||||
if count >= 5 {
|
||||
score = 4.0
|
||||
severity = "high"
|
||||
}
|
||||
|
||||
created, err := d.insertDetection(ctx, Detection{
|
||||
RuleName: "ueba_offhours_login",
|
||||
Severity: severity,
|
||||
Hostname: host,
|
||||
Channel: "Security",
|
||||
EventID: 4624,
|
||||
Score: score,
|
||||
Summary: fmt.Sprintf("UEBA: Login außerhalb der Arbeitszeit: Benutzer %s auf Host %s", user, host),
|
||||
WindowStart: windowStart,
|
||||
WindowEnd: windowEnd,
|
||||
Details: mustJSON(map[string]any{
|
||||
"user": user,
|
||||
"host": host,
|
||||
"local_hour": hour,
|
||||
"count": count,
|
||||
}),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if created {
|
||||
d.detectionHitsTotal.WithLabelValues("ueba_offhours_login", severity).Inc()
|
||||
d.anomalyScoreGauge.WithLabelValues(host, "ueba_offhours_login").Set(score)
|
||||
}
|
||||
}
|
||||
|
||||
return rows.Err()
|
||||
}
|
||||
|
||||
func (d *detector) runFirstTimePrivilegedRule(ctx context.Context) error {
|
||||
if !d.cfg.UEBAEnabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
windowEnd := time.Now().UTC()
|
||||
windowStart := windowEnd.Add(-10 * time.Minute)
|
||||
|
||||
const q = `
|
||||
SELECT hostname, subject_user, COUNT(*) AS cnt
|
||||
FROM event_logs
|
||||
WHERE channel_name = 'Security'
|
||||
AND event_id = 4672
|
||||
AND ts >= ? AND ts < ?
|
||||
AND subject_user <> ''
|
||||
AND subject_user <> '-'
|
||||
AND subject_user NOT LIKE '%$'
|
||||
GROUP BY hostname, subject_user
|
||||
`
|
||||
|
||||
rows, err := d.db.QueryContext(ctx, q, windowStart, windowEnd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var host, user string
|
||||
var count int
|
||||
|
||||
if err := rows.Scan(&host, &user, &count); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user = normalizeUsername(user)
|
||||
if user == "" || isMachineAccount(user) {
|
||||
continue
|
||||
}
|
||||
|
||||
var exists int
|
||||
err := d.db.QueryRowContext(ctx, `
|
||||
SELECT 1
|
||||
FROM user_privilege_baseline
|
||||
WHERE username = ?
|
||||
LIMIT 1
|
||||
`, user).Scan(&exists)
|
||||
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return err
|
||||
}
|
||||
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
created, err := d.insertDetection(ctx, Detection{
|
||||
RuleName: "ueba_first_privileged_use",
|
||||
Severity: "high",
|
||||
Hostname: host,
|
||||
Channel: "Security",
|
||||
EventID: 4672,
|
||||
Score: 6.0,
|
||||
Summary: fmt.Sprintf("UEBA: Benutzer %s nutzt erstmals privilegierte Rechte auf Host %s", user, host),
|
||||
WindowStart: windowStart,
|
||||
WindowEnd: windowEnd,
|
||||
Details: mustJSON(map[string]any{
|
||||
"user": user,
|
||||
"host": host,
|
||||
"count": count,
|
||||
}),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if created {
|
||||
d.detectionHitsTotal.WithLabelValues("ueba_first_privileged_use", "high").Inc()
|
||||
d.anomalyScoreGauge.WithLabelValues(host, "ueba_first_privileged_use").Set(6.0)
|
||||
}
|
||||
|
||||
_, err = d.db.ExecContext(ctx, `
|
||||
INSERT INTO user_privilege_baseline
|
||||
(username, first_seen, last_seen, seen_count)
|
||||
VALUES (?, UTC_TIMESTAMP(6), UTC_TIMESTAMP(6), ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
last_seen = UTC_TIMESTAMP(6),
|
||||
seen_count = seen_count + VALUES(seen_count)
|
||||
`, user, count)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
_, err = d.db.ExecContext(ctx, `
|
||||
UPDATE user_privilege_baseline
|
||||
SET last_seen = UTC_TIMESTAMP(6),
|
||||
seen_count = seen_count + ?
|
||||
WHERE username = ?
|
||||
`, count, user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return rows.Err()
|
||||
}
|
||||
|
||||
func (d *detector) isPrivilegedUser(ctx context.Context, username string) (bool, error) {
|
||||
u := normalizeUsername(username)
|
||||
if isMachineAccount(u) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if strings.HasPrefix(u, "adm-") || strings.HasSuffix(u, "-adm") || strings.HasSuffix(u, ".adm") {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
var count int
|
||||
err := d.db.QueryRowContext(ctx, `
|
||||
SELECT COUNT(*)
|
||||
FROM privileged_users
|
||||
WHERE enabled = 1
|
||||
AND LOWER(username) = ?
|
||||
`, u).Scan(&count)
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func normalizeUsername(username string) string {
|
||||
u := strings.ToLower(strings.TrimSpace(username))
|
||||
|
||||
if strings.Contains(u, `\`) {
|
||||
parts := strings.Split(u, `\`)
|
||||
u = parts[len(parts)-1]
|
||||
}
|
||||
|
||||
if strings.Contains(u, "@") {
|
||||
parts := strings.Split(u, "@")
|
||||
u = parts[0]
|
||||
}
|
||||
|
||||
return u
|
||||
}
|
||||
|
||||
func isMachineAccount(username string) bool {
|
||||
u := normalizeUsername(username)
|
||||
return u == "" || strings.HasSuffix(u, "$")
|
||||
}
|
||||
|
||||
func (d *detector) runAdminNewHostRule(ctx context.Context) error {
|
||||
if !d.cfg.UEBAEnabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
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, 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.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.last_seen >= ?
|
||||
AND b.first_seen < ?
|
||||
)
|
||||
GROUP BY e.hostname, e.target_user
|
||||
`, windowStart, windowEnd, lookbackStart, windowStart)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var host, user string
|
||||
var count int
|
||||
|
||||
if err := rows.Scan(&host, &user, &count); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
privileged, err := d.isPrivilegedUser(ctx, user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !privileged {
|
||||
continue
|
||||
}
|
||||
|
||||
score := 6.0
|
||||
severity := "high"
|
||||
|
||||
if count >= 3 {
|
||||
score = 9.0
|
||||
severity = "critical"
|
||||
}
|
||||
|
||||
created, err := d.insertDetection(ctx, Detection{
|
||||
RuleName: "ueba_admin_new_host",
|
||||
Severity: severity,
|
||||
Hostname: host,
|
||||
Channel: "Security",
|
||||
EventID: 4624,
|
||||
Score: score,
|
||||
WindowStart: windowStart,
|
||||
WindowEnd: windowEnd,
|
||||
Summary: fmt.Sprintf("UEBA: Privilegierter Benutzer %s meldet sich erstmals auf Host %s an", user, host),
|
||||
Details: mustJSON(map[string]any{
|
||||
"user": user,
|
||||
"host": host,
|
||||
"count": count,
|
||||
"lookback": d.cfg.UEBALookback.String(),
|
||||
"window": d.cfg.UEBANewContextWindow.String(),
|
||||
}),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if created {
|
||||
d.detectionHitsTotal.WithLabelValues("ueba_admin_new_host", severity).Inc()
|
||||
d.anomalyScoreGauge.WithLabelValues(host, "ueba_admin_new_host").Set(score)
|
||||
if d.privilegedNewHostTotal != nil {
|
||||
d.privilegedNewHostTotal.WithLabelValues(normalizeUsername(user), host).Inc()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rows.Err()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user