This commit is contained in:
@@ -1396,3 +1396,33 @@ CREATE TABLE baseline_exclusions (
|
|||||||
|
|
||||||
CREATE INDEX idx_baseline_exclusions_lookup
|
CREATE INDEX idx_baseline_exclusions_lookup
|
||||||
ON baseline_exclusions (enabled, hostname, channel_name, event_id, expires_at);
|
ON baseline_exclusions (enabled, hostname, channel_name, event_id, expires_at);
|
||||||
|
|
||||||
|
CREATE TABLE host_risk_scores (
|
||||||
|
hostname VARCHAR(255) PRIMARY KEY,
|
||||||
|
risk_score DOUBLE NOT NULL DEFAULT 0,
|
||||||
|
severity VARCHAR(16) NOT NULL DEFAULT 'info',
|
||||||
|
open_detections INT NOT NULL DEFAULT 0,
|
||||||
|
high_detections INT NOT NULL DEFAULT 0,
|
||||||
|
critical_detections INT NOT NULL DEFAULT 0,
|
||||||
|
confirmed_incidents INT NOT NULL DEFAULT 0,
|
||||||
|
last_detection_at TIMESTAMP(6) NULL,
|
||||||
|
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,
|
||||||
|
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),
|
||||||
|
seen_count BIGINT NOT NULL DEFAULT 1,
|
||||||
|
UNIQUE KEY uniq_user_context (username, hostname, src_ip, workstation)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_ueba_user_baseline_user
|
||||||
|
ON ueba_user_baseline (username, last_seen);
|
||||||
|
|
||||||
|
CREATE INDEX idx_host_risk_score
|
||||||
|
ON host_risk_scores (risk_score, severity);
|
||||||
502
main.go
502
main.go
@@ -399,6 +399,7 @@ a {
|
|||||||
<div><strong>SIEM-lite</strong></div>
|
<div><strong>SIEM-lite</strong></div>
|
||||||
<nav>
|
<nav>
|
||||||
<a href="/ui">Dashboard</a>
|
<a href="/ui">Dashboard</a>
|
||||||
|
<a href="/ui/soc">SOC</a>
|
||||||
<a href="/ui/agents">Agents</a>
|
<a href="/ui/agents">Agents</a>
|
||||||
<a href="/ui/rules">Rules</a>
|
<a href="/ui/rules">Rules</a>
|
||||||
<a href="/ui/baseline">Baseline</a>
|
<a href="/ui/baseline">Baseline</a>
|
||||||
@@ -659,6 +660,66 @@ a {
|
|||||||
{{template "footer" .}}
|
{{template "footer" .}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
{{define "soc"}}
|
||||||
|
{{template "header" .}}
|
||||||
|
<h1>{{.Title}}</h1>
|
||||||
|
<p class="muted">Stand: {{fmtTime .Now}}</p>
|
||||||
|
|
||||||
|
<h2>Top Host Risk Scores</h2>
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>Host</th>
|
||||||
|
<th>Risk</th>
|
||||||
|
<th>Severity</th>
|
||||||
|
<th>Open</th>
|
||||||
|
<th>High</th>
|
||||||
|
<th>Critical</th>
|
||||||
|
<th>Confirmed</th>
|
||||||
|
<th>Last Detection</th>
|
||||||
|
</tr>
|
||||||
|
{{range .TopHosts}}
|
||||||
|
<tr>
|
||||||
|
<td><strong><a href="/ui/detections?host={{q .Hostname}}">{{.Hostname}}</a></strong></td>
|
||||||
|
<td><strong>{{printf "%.1f" .RiskScore}}</strong></td>
|
||||||
|
<td><span class="badge sev-{{.Severity}}">{{.Severity}}</span></td>
|
||||||
|
<td>{{.OpenDetections}}</td>
|
||||||
|
<td>{{.HighDetections}}</td>
|
||||||
|
<td>{{.CriticalDetections}}</td>
|
||||||
|
<td>{{.ConfirmedIncidents}}</td>
|
||||||
|
<td>{{if .LastDetectionAt.Valid}}{{fmtTime .LastDetectionAt.Time}}{{end}}</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Recent SOC Relevant Detections</h2>
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>Zeit</th>
|
||||||
|
<th>Rule</th>
|
||||||
|
<th>Severity</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Host</th>
|
||||||
|
<th>Summary</th>
|
||||||
|
</tr>
|
||||||
|
{{range .RecentIncidents}}
|
||||||
|
<tr>
|
||||||
|
<td>{{fmtTime .CreatedAt}}</td>
|
||||||
|
<td><span class="mono">{{.RuleName}}</span></td>
|
||||||
|
<td><span class="badge sev-{{.Severity}}">{{.Severity}}</span></td>
|
||||||
|
<td><span class="badge status-{{.Status}}">{{.Status}}</span></td>
|
||||||
|
<td><a href="/ui/detections?host={{q .Hostname}}">{{.Hostname}}</a></td>
|
||||||
|
<td class="wrap">{{.Summary}}</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{template "footer" .}}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
{{define "baseline"}}
|
{{define "baseline"}}
|
||||||
{{template "header" .}}
|
{{template "header" .}}
|
||||||
<h1>{{.Title}}</h1>
|
<h1>{{.Title}}</h1>
|
||||||
@@ -866,6 +927,11 @@ type Config struct {
|
|||||||
BaselineSuppressFor time.Duration
|
BaselineSuppressFor time.Duration
|
||||||
|
|
||||||
Timezone string
|
Timezone string
|
||||||
|
|
||||||
|
UEBAEnabled bool
|
||||||
|
UEBALookback time.Duration
|
||||||
|
UEBANewContextWindow time.Duration
|
||||||
|
RiskScoreWindow time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
type LogPayload struct {
|
type LogPayload struct {
|
||||||
@@ -957,6 +1023,8 @@ type detector struct {
|
|||||||
baselineAverageGauge *prometheus.GaugeVec
|
baselineAverageGauge *prometheus.GaugeVec
|
||||||
baselineStddevGauge *prometheus.GaugeVec
|
baselineStddevGauge *prometheus.GaugeVec
|
||||||
baselineSamplesGauge *prometheus.GaugeVec
|
baselineSamplesGauge *prometheus.GaugeVec
|
||||||
|
|
||||||
|
hostRiskScoreGauge *prometheus.GaugeVec
|
||||||
}
|
}
|
||||||
|
|
||||||
type EventRow struct {
|
type EventRow struct {
|
||||||
@@ -1128,6 +1196,35 @@ type baselineDetailsJSON struct {
|
|||||||
WindowMinutes int `json:"window_minutes"`
|
WindowMinutes int `json:"window_minutes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SOCHostRiskRow struct {
|
||||||
|
Hostname string
|
||||||
|
RiskScore float64
|
||||||
|
Severity string
|
||||||
|
OpenDetections int
|
||||||
|
HighDetections int
|
||||||
|
CriticalDetections int
|
||||||
|
ConfirmedIncidents int
|
||||||
|
LastDetectionAt sql.NullTime
|
||||||
|
UpdatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type SOCRecentIncidentRow struct {
|
||||||
|
ID uint64
|
||||||
|
CreatedAt time.Time
|
||||||
|
RuleName string
|
||||||
|
Severity string
|
||||||
|
Status string
|
||||||
|
Hostname string
|
||||||
|
Summary string
|
||||||
|
}
|
||||||
|
|
||||||
|
type SOCPageData struct {
|
||||||
|
Title string
|
||||||
|
Now time.Time
|
||||||
|
TopHosts []SOCHostRiskRow
|
||||||
|
RecentIncidents []SOCRecentIncidentRow
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
httpRequestsTotal = prometheus.NewCounterVec(
|
httpRequestsTotal = prometheus.NewCounterVec(
|
||||||
prometheus.CounterOpts{Name: "eventcollector_http_requests_total", Help: "Total HTTP requests."},
|
prometheus.CounterOpts{Name: "eventcollector_http_requests_total", Help: "Total HTTP requests."},
|
||||||
@@ -1258,6 +1355,13 @@ func main() {
|
|||||||
},
|
},
|
||||||
[]string{"host", "channel", "event_id"},
|
[]string{"host", "channel", "event_id"},
|
||||||
),
|
),
|
||||||
|
hostRiskScoreGauge: prometheus.NewGaugeVec(
|
||||||
|
prometheus.GaugeOpts{
|
||||||
|
Name: "eventcollector_host_risk_score",
|
||||||
|
Help: "Calculated risk score per host.",
|
||||||
|
},
|
||||||
|
[]string{"host", "severity"},
|
||||||
|
),
|
||||||
}
|
}
|
||||||
reg.MustRegister(
|
reg.MustRegister(
|
||||||
d.lastSeenGauge,
|
d.lastSeenGauge,
|
||||||
@@ -1271,6 +1375,7 @@ func main() {
|
|||||||
d.baselineAverageGauge,
|
d.baselineAverageGauge,
|
||||||
d.baselineStddevGauge,
|
d.baselineStddevGauge,
|
||||||
d.baselineSamplesGauge,
|
d.baselineSamplesGauge,
|
||||||
|
d.hostRiskScoreGauge,
|
||||||
)
|
)
|
||||||
|
|
||||||
s := &server{
|
s := &server{
|
||||||
@@ -1323,6 +1428,7 @@ func main() {
|
|||||||
mux.HandleFunc("/ui/rules/save", s.handleUIRuleSave)
|
mux.HandleFunc("/ui/rules/save", s.handleUIRuleSave)
|
||||||
mux.HandleFunc("/ui/rules/toggle", s.handleUIRuleToggle)
|
mux.HandleFunc("/ui/rules/toggle", s.handleUIRuleToggle)
|
||||||
mux.HandleFunc("/ui/baseline", s.handleUIBaseline)
|
mux.HandleFunc("/ui/baseline", s.handleUIBaseline)
|
||||||
|
mux.HandleFunc("/ui/soc", s.handleUISOC)
|
||||||
|
|
||||||
httpSrv := &http.Server{
|
httpSrv := &http.Server{
|
||||||
Addr: cfg.ListenAddr,
|
Addr: cfg.ListenAddr,
|
||||||
@@ -1356,6 +1462,105 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *server) listSOCTopHosts(ctx context.Context, limit int) ([]SOCHostRiskRow, error) {
|
||||||
|
rows, err := s.db.QueryContext(ctx, `
|
||||||
|
SELECT hostname, risk_score, severity, open_detections,
|
||||||
|
high_detections, critical_detections, confirmed_incidents,
|
||||||
|
last_detection_at, updated_at
|
||||||
|
FROM host_risk_scores
|
||||||
|
ORDER BY risk_score DESC
|
||||||
|
LIMIT ?
|
||||||
|
`, limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var out []SOCHostRiskRow
|
||||||
|
for rows.Next() {
|
||||||
|
var r SOCHostRiskRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&r.Hostname,
|
||||||
|
&r.RiskScore,
|
||||||
|
&r.Severity,
|
||||||
|
&r.OpenDetections,
|
||||||
|
&r.HighDetections,
|
||||||
|
&r.CriticalDetections,
|
||||||
|
&r.ConfirmedIncidents,
|
||||||
|
&r.LastDetectionAt,
|
||||||
|
&r.UpdatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out = append(out, r)
|
||||||
|
}
|
||||||
|
return out, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) listSOCRecentIncidents(ctx context.Context, limit int) ([]SOCRecentIncidentRow, error) {
|
||||||
|
rows, err := s.db.QueryContext(ctx, `
|
||||||
|
SELECT id, created_at, rule_name, severity, status, hostname, summary
|
||||||
|
FROM detections
|
||||||
|
WHERE status IN ('open', 'investigating', 'confirmed_incident')
|
||||||
|
OR severity IN ('high', 'critical')
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT ?
|
||||||
|
`, limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var out []SOCRecentIncidentRow
|
||||||
|
for rows.Next() {
|
||||||
|
var r SOCRecentIncidentRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&r.ID,
|
||||||
|
&r.CreatedAt,
|
||||||
|
&r.RuleName,
|
||||||
|
&r.Severity,
|
||||||
|
&r.Status,
|
||||||
|
&r.Hostname,
|
||||||
|
&r.Summary,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out = append(out, r)
|
||||||
|
}
|
||||||
|
return out, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) handleUISOC(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()
|
||||||
|
|
||||||
|
topHosts, err := s.listSOCTopHosts(ctx, 20)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Printf("soc top hosts: %v", err)
|
||||||
|
writeError(w, http.StatusInternalServerError, "internal error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
incidents, err := s.listSOCRecentIncidents(ctx, 50)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Printf("soc recent incidents: %v", err)
|
||||||
|
writeError(w, http.StatusInternalServerError, "internal error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.renderTemplate(w, "soc", SOCPageData{
|
||||||
|
Title: "SOC Dashboard",
|
||||||
|
Now: time.Now(),
|
||||||
|
TopHosts: topHosts,
|
||||||
|
RecentIncidents: incidents,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (s *server) createBaselineExclusionFromDetection(ctx context.Context, det Detection, reason, createdBy string, hours int) error {
|
func (s *server) createBaselineExclusionFromDetection(ctx context.Context, det Detection, reason, createdBy string, hours int) error {
|
||||||
var expiresAt any = nil
|
var expiresAt any = nil
|
||||||
if hours > 0 {
|
if hours > 0 {
|
||||||
@@ -2493,6 +2698,11 @@ func loadConfig() Config {
|
|||||||
BaselineSuppressFor: getenvDuration("BASELINE_SUPPRESS_FOR", 1*time.Hour),
|
BaselineSuppressFor: getenvDuration("BASELINE_SUPPRESS_FOR", 1*time.Hour),
|
||||||
|
|
||||||
Timezone: getenv("TZ", "Europe/Berlin"),
|
Timezone: getenv("TZ", "Europe/Berlin"),
|
||||||
|
|
||||||
|
UEBAEnabled: getenvBool("UEBA_ENABLED", true),
|
||||||
|
UEBALookback: getenvDuration("UEBA_LOOKBACK", 30*24*time.Hour),
|
||||||
|
UEBANewContextWindow: getenvDuration("UEBA_NEW_CONTEXT_WINDOW", 10*time.Minute),
|
||||||
|
RiskScoreWindow: getenvDuration("RISK_SCORE_WINDOW", 24*time.Hour),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3756,6 +3966,10 @@ func (s *server) runDetectionsOnce() {
|
|||||||
|
|
||||||
{"baseline_anomaly", s.detector.runBaselineAnomalyRule},
|
{"baseline_anomaly", s.detector.runBaselineAnomalyRule},
|
||||||
{"baseline_update", s.detector.runBaselineUpdate},
|
{"baseline_update", s.detector.runBaselineUpdate},
|
||||||
|
|
||||||
|
{"ueba_new_user_context", s.detector.runUEBANewUserContextRule},
|
||||||
|
{"ueba_update", s.detector.runUEBABaselineUpdate},
|
||||||
|
{"host_risk_score", s.detector.runHostRiskScoreUpdate},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, rule := range rules {
|
for _, rule := range rules {
|
||||||
@@ -3770,6 +3984,294 @@ func (s *server) runDetectionsOnce() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *detector) runUEBABaselineUpdate(ctx context.Context) error {
|
||||||
|
if !d.cfg.UEBAEnabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
windowEnd := time.Now().UTC()
|
||||||
|
windowStart := windowEnd.Add(-d.cfg.UEBANewContextWindow)
|
||||||
|
|
||||||
|
rows, err := d.db.QueryContext(ctx, `
|
||||||
|
SELECT target_user, hostname, src_ip, workstation, COUNT(*)
|
||||||
|
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 target_user, hostname, src_ip, workstation
|
||||||
|
`, windowStart, windowEnd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var user, host, srcIP, workstation string
|
||||||
|
var count int
|
||||||
|
|
||||||
|
if err := rows.Scan(&user, &host, &srcIP, &workstation, &count); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := d.db.ExecContext(ctx, `
|
||||||
|
INSERT INTO ueba_user_baseline
|
||||||
|
(username, hostname, src_ip, workstation, 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,
|
||||||
|
host,
|
||||||
|
srcIP,
|
||||||
|
workstation,
|
||||||
|
count,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *detector) runUEBANewUserContextRule(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, e.src_ip, e.workstation, 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.src_ip = e.src_ip
|
||||||
|
AND b.workstation = e.workstation
|
||||||
|
AND b.first_seen < ?
|
||||||
|
AND b.last_seen >= ?
|
||||||
|
)
|
||||||
|
GROUP BY e.hostname, e.target_user, e.src_ip, e.workstation
|
||||||
|
`,
|
||||||
|
windowStart,
|
||||||
|
windowEnd,
|
||||||
|
windowStart,
|
||||||
|
lookbackStart,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var host, user, srcIP, workstation string
|
||||||
|
var count int
|
||||||
|
|
||||||
|
if err := rows.Scan(&host, &user, &srcIP, &workstation, &count); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
score := 2.0
|
||||||
|
severity := "medium"
|
||||||
|
|
||||||
|
if count >= 5 {
|
||||||
|
score = 4.0
|
||||||
|
severity = "high"
|
||||||
|
}
|
||||||
|
|
||||||
|
created, err := d.insertDetection(ctx, Detection{
|
||||||
|
RuleName: "ueba_new_user_context",
|
||||||
|
Severity: severity,
|
||||||
|
Hostname: host,
|
||||||
|
Channel: "Security",
|
||||||
|
EventID: 4624,
|
||||||
|
Score: score,
|
||||||
|
WindowStart: windowStart,
|
||||||
|
WindowEnd: windowEnd,
|
||||||
|
Summary: fmt.Sprintf(
|
||||||
|
"UEBA: Benutzer %s meldet sich in neuem Kontext an: Host=%s IP=%s Workstation=%s",
|
||||||
|
user,
|
||||||
|
host,
|
||||||
|
srcIP,
|
||||||
|
workstation,
|
||||||
|
),
|
||||||
|
Details: mustJSON(map[string]any{
|
||||||
|
"user": user,
|
||||||
|
"src_ip": srcIP,
|
||||||
|
"workstation": workstation,
|
||||||
|
"count": count,
|
||||||
|
"lookback": d.cfg.UEBALookback.String(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if created {
|
||||||
|
d.detectionHitsTotal.WithLabelValues("ueba_new_user_context", severity).Inc()
|
||||||
|
d.anomalyScoreGauge.WithLabelValues(host, "ueba_new_user_context").Set(score)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func riskWeight(severity string) float64 {
|
||||||
|
switch severity {
|
||||||
|
case "critical":
|
||||||
|
return 40
|
||||||
|
case "high":
|
||||||
|
return 15
|
||||||
|
case "medium":
|
||||||
|
return 5
|
||||||
|
case "low":
|
||||||
|
return 1
|
||||||
|
case "info":
|
||||||
|
return 0.5
|
||||||
|
default:
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func severityFromRisk(score float64) string {
|
||||||
|
switch {
|
||||||
|
case score >= 80:
|
||||||
|
return "critical"
|
||||||
|
case score >= 40:
|
||||||
|
return "high"
|
||||||
|
case score >= 15:
|
||||||
|
return "medium"
|
||||||
|
case score >= 5:
|
||||||
|
return "low"
|
||||||
|
default:
|
||||||
|
return "info"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *detector) runHostRiskScoreUpdate(ctx context.Context) error {
|
||||||
|
windowStart := time.Now().UTC().Add(-d.cfg.RiskScoreWindow)
|
||||||
|
|
||||||
|
rows, err := d.db.QueryContext(ctx, `
|
||||||
|
SELECT
|
||||||
|
hostname,
|
||||||
|
severity,
|
||||||
|
status,
|
||||||
|
COUNT(*) AS cnt,
|
||||||
|
MAX(created_at) AS last_detection_at
|
||||||
|
FROM detections
|
||||||
|
WHERE created_at >= ?
|
||||||
|
AND status NOT IN ('false_positive', 'suppressed')
|
||||||
|
GROUP BY hostname, severity, status
|
||||||
|
`, windowStart)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
type agg struct {
|
||||||
|
score float64
|
||||||
|
open int
|
||||||
|
high int
|
||||||
|
critical int
|
||||||
|
confirmedIncidents int
|
||||||
|
last time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
stats := map[string]*agg{}
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var host, sev, status string
|
||||||
|
var count int
|
||||||
|
var last time.Time
|
||||||
|
|
||||||
|
if err := rows.Scan(&host, &sev, &status, &count, &last); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := stats[host]; !ok {
|
||||||
|
stats[host] = &agg{}
|
||||||
|
}
|
||||||
|
|
||||||
|
a := stats[host]
|
||||||
|
|
||||||
|
w := riskWeight(sev)
|
||||||
|
if status == "confirmed_incident" {
|
||||||
|
w += 50
|
||||||
|
a.confirmedIncidents += count
|
||||||
|
}
|
||||||
|
|
||||||
|
if status == "open" || status == "investigating" || status == "acknowledged" {
|
||||||
|
a.open += count
|
||||||
|
}
|
||||||
|
|
||||||
|
if sev == "high" {
|
||||||
|
a.high += count
|
||||||
|
}
|
||||||
|
if sev == "critical" {
|
||||||
|
a.critical += count
|
||||||
|
}
|
||||||
|
|
||||||
|
a.score += w * float64(count)
|
||||||
|
|
||||||
|
if last.After(a.last) {
|
||||||
|
a.last = last
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for host, a := range stats {
|
||||||
|
sev := severityFromRisk(a.score)
|
||||||
|
|
||||||
|
_, err := d.db.ExecContext(ctx, `
|
||||||
|
INSERT INTO host_risk_scores
|
||||||
|
(hostname, risk_score, severity, open_detections, high_detections, critical_detections, confirmed_incidents, last_detection_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
risk_score = VALUES(risk_score),
|
||||||
|
severity = VALUES(severity),
|
||||||
|
open_detections = VALUES(open_detections),
|
||||||
|
high_detections = VALUES(high_detections),
|
||||||
|
critical_detections = VALUES(critical_detections),
|
||||||
|
confirmed_incidents = VALUES(confirmed_incidents),
|
||||||
|
last_detection_at = VALUES(last_detection_at),
|
||||||
|
updated_at = UTC_TIMESTAMP(6)
|
||||||
|
`,
|
||||||
|
host,
|
||||||
|
a.score,
|
||||||
|
sev,
|
||||||
|
a.open,
|
||||||
|
a.high,
|
||||||
|
a.critical,
|
||||||
|
a.confirmedIncidents,
|
||||||
|
a.last,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
d.hostRiskScoreGauge.WithLabelValues(host, sev).Set(a.score)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (d *detector) updateAgentMetrics(ctx context.Context) error {
|
func (d *detector) updateAgentMetrics(ctx context.Context) error {
|
||||||
const q = `
|
const q = `
|
||||||
SELECT hostname, last_seen
|
SELECT hostname, last_seen
|
||||||
|
|||||||
Reference in New Issue
Block a user