Agent-Kontrolle und Offlien-Alarm auf 120 Minuten beschränkt
All checks were successful
release-tag / release-image (push) Successful in 2m41s
All checks were successful
release-tag / release-image (push) Successful in 2m41s
This commit is contained in:
1
dot_env
1
dot_env
@@ -15,6 +15,7 @@ HTTP_IDLE_TIMEOUT=60s
|
||||
|
||||
DETECTION_INTERVAL=1m
|
||||
OFFLINE_AFTER=10m
|
||||
OFFLINE_ALERT_MAX=120m
|
||||
FAILED_LOGON_WINDOW=5m
|
||||
FAILED_LOGON_THRESHOLD=25
|
||||
REBOOT_WINDOW=15m
|
||||
|
||||
213
main.go
213
main.go
@@ -56,6 +56,13 @@ const uiTemplates = `
|
||||
button { padding: 10px 14px; background: #2563eb; color: white; border: 0; border-radius: 8px; cursor: pointer; }
|
||||
pre { white-space: pre-wrap; word-break: break-word; background: #111827; color: #e5e7eb; padding: 16px; border-radius: 10px; }
|
||||
a { color: #2563eb; text-decoration: none; }
|
||||
.badge { display: inline-block; padding: 3px 8px; border-radius: 999px; font-size: 12px; font-weight: bold; }
|
||||
.badge-on { background: #dcfce7; color: #166534; }
|
||||
.badge-off { background: #fee2e2; color: #991b1b; }
|
||||
.badge-muted { background: #e5e7eb; color: #374151; }
|
||||
.inline-form { display: inline; }
|
||||
button.danger { background: #dc2626; }
|
||||
button.success { background: #16a34a; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -63,6 +70,7 @@ const uiTemplates = `
|
||||
<div><strong>SIEM-lite</strong></div>
|
||||
<nav>
|
||||
<a href="/ui">Dashboard</a>
|
||||
<a href="/ui/agents">Agents</a>
|
||||
<a href="/ui/detections">Detections</a>
|
||||
<a href="/ui/events">Events</a>
|
||||
<a href="/metrics">Metrics</a>
|
||||
@@ -189,6 +197,61 @@ const uiTemplates = `
|
||||
{{template "footer" .}}
|
||||
{{end}}
|
||||
|
||||
{{define "agents"}}
|
||||
{{template "header" .}}
|
||||
<h1>{{.Title}}</h1>
|
||||
<p class="muted">Stand: {{fmtTime .Now}}</p>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>Hostname</th>
|
||||
<th>Status</th>
|
||||
<th>Aktiviert</th>
|
||||
<th>First Seen</th>
|
||||
<th>Last Seen</th>
|
||||
<th>Offline Minuten</th>
|
||||
<th>Last IP</th>
|
||||
<th>Aktion</th>
|
||||
</tr>
|
||||
{{range .Agents}}
|
||||
<tr>
|
||||
<td><strong>{{.Hostname}}</strong></td>
|
||||
<td>
|
||||
{{if .IsOnline}}
|
||||
<span class="badge badge-on">online</span>
|
||||
{{else}}
|
||||
<span class="badge badge-off">offline</span>
|
||||
{{end}}
|
||||
</td>
|
||||
<td>
|
||||
{{if .IsEnabled}}
|
||||
<span class="badge badge-on">aktiv</span>
|
||||
{{else}}
|
||||
<span class="badge badge-muted">inaktiv</span>
|
||||
{{end}}
|
||||
</td>
|
||||
<td>{{fmtTime .FirstSeen}}</td>
|
||||
<td>{{fmtTime .LastSeen}}</td>
|
||||
<td>{{.OfflineMinutes}}</td>
|
||||
<td>{{.LastIP}}</td>
|
||||
<td>
|
||||
<form class="inline-form" method="post" action="/ui/agents/toggle">
|
||||
<input type="hidden" name="id" value="{{.ID}}">
|
||||
{{if .IsEnabled}}
|
||||
<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>
|
||||
{{template "footer" .}}
|
||||
{{end}}
|
||||
|
||||
{{define "event_detail"}}
|
||||
{{template "header" .}}
|
||||
<h1>{{.Title}}</h1>
|
||||
@@ -226,6 +289,7 @@ type Config struct {
|
||||
DBConnMaxIdleTime time.Duration
|
||||
DetectionInterval time.Duration
|
||||
OfflineAfter time.Duration
|
||||
OfflineAlertMax time.Duration
|
||||
FailedLogonWindow time.Duration
|
||||
FailedLogonThreshold int
|
||||
RebootWindow time.Duration
|
||||
@@ -379,6 +443,23 @@ type EventDetailPageData struct {
|
||||
Event EventRow
|
||||
}
|
||||
|
||||
type AgentRow struct {
|
||||
ID uint64
|
||||
Hostname string
|
||||
FirstSeen time.Time
|
||||
LastSeen time.Time
|
||||
LastIP string
|
||||
IsEnabled bool
|
||||
IsOnline bool
|
||||
OfflineMinutes int
|
||||
}
|
||||
|
||||
type AgentListPageData struct {
|
||||
Title string
|
||||
Now time.Time
|
||||
Agents []AgentRow
|
||||
}
|
||||
|
||||
var (
|
||||
httpRequestsTotal = prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{Name: "eventcollector_http_requests_total", Help: "Total HTTP requests."},
|
||||
@@ -526,6 +607,8 @@ func main() {
|
||||
mux.HandleFunc("/ui/detections", s.handleUIDetections)
|
||||
mux.HandleFunc("/ui/events", s.handleUIEvents)
|
||||
mux.HandleFunc("/ui/event", s.handleUIEventDetail)
|
||||
mux.HandleFunc("/ui/agents", s.handleUIAgents)
|
||||
mux.HandleFunc("/ui/agents/toggle", s.handleUIAgentToggle)
|
||||
|
||||
httpSrv := &http.Server{
|
||||
Addr: cfg.ListenAddr,
|
||||
@@ -559,6 +642,113 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *server) listAgents(ctx context.Context) ([]AgentRow, error) {
|
||||
const q = `
|
||||
SELECT id, hostname, first_seen, last_seen, last_ip, is_enabled
|
||||
FROM agents
|
||||
ORDER BY hostname ASC
|
||||
`
|
||||
|
||||
rows, err := s.db.QueryContext(ctx, q)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
now := time.Now().UTC()
|
||||
out := make([]AgentRow, 0)
|
||||
|
||||
for rows.Next() {
|
||||
var a AgentRow
|
||||
if err := rows.Scan(
|
||||
&a.ID,
|
||||
&a.Hostname,
|
||||
&a.FirstSeen,
|
||||
&a.LastSeen,
|
||||
&a.LastIP,
|
||||
&a.IsEnabled,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !a.LastSeen.IsZero() {
|
||||
a.OfflineMinutes = int(now.Sub(a.LastSeen.UTC()).Minutes())
|
||||
a.IsOnline = a.IsEnabled && now.Sub(a.LastSeen.UTC()) <= s.cfg.OfflineAfter
|
||||
}
|
||||
|
||||
out = append(out, a)
|
||||
}
|
||||
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (s *server) handleUIAgents(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()
|
||||
|
||||
agents, err := s.listAgents(ctx)
|
||||
if err != nil {
|
||||
s.logger.Printf("ui agents: %v", err)
|
||||
writeError(w, http.StatusInternalServerError, "internal error")
|
||||
return
|
||||
}
|
||||
|
||||
data := AgentListPageData{
|
||||
Title: "Agents",
|
||||
Now: time.Now(),
|
||||
Agents: agents,
|
||||
}
|
||||
|
||||
s.renderTemplate(w, "agents", data)
|
||||
}
|
||||
|
||||
func (s *server) handleUIAgentToggle(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
|
||||
}
|
||||
|
||||
id, err := strconv.ParseUint(strings.TrimSpace(r.FormValue("id")), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
writeError(w, http.StatusBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
|
||||
enabled := strings.TrimSpace(r.FormValue("enabled")) == "1"
|
||||
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
res, err := s.db.ExecContext(ctx, `
|
||||
UPDATE agents
|
||||
SET is_enabled = ?
|
||||
WHERE id = ?
|
||||
`, enabled, id)
|
||||
if err != nil {
|
||||
s.logger.Printf("toggle agent: %v", err)
|
||||
writeError(w, http.StatusInternalServerError, "internal error")
|
||||
return
|
||||
}
|
||||
|
||||
affected, _ := res.RowsAffected()
|
||||
if affected == 0 {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/ui/agents", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (s *server) handleUIIndex(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
@@ -930,6 +1120,7 @@ func loadConfig() Config {
|
||||
DBConnMaxIdleTime: getenvDuration("DB_CONN_MAX_IDLE_TIME", 1*time.Minute),
|
||||
DetectionInterval: getenvDuration("DETECTION_INTERVAL", 1*time.Minute),
|
||||
OfflineAfter: getenvDuration("OFFLINE_AFTER", 10*time.Minute),
|
||||
OfflineAlertMax: getenvDuration("OFFLINE_ALERT_MAX", 120*time.Minute),
|
||||
FailedLogonWindow: getenvDuration("FAILED_LOGON_WINDOW", 5*time.Minute),
|
||||
FailedLogonThreshold: getenvInt("FAILED_LOGON_THRESHOLD", 25),
|
||||
RebootWindow: getenvDuration("REBOOT_WINDOW", 15*time.Minute),
|
||||
@@ -1382,15 +1573,18 @@ WHERE is_enabled = 1
|
||||
|
||||
func (d *detector) runAgentOfflineRule(ctx context.Context) error {
|
||||
windowEnd := time.Now().UTC()
|
||||
windowStart := windowEnd.Add(-d.cfg.OfflineAfter)
|
||||
|
||||
offlineAfterTime := windowEnd.Add(-d.cfg.OfflineAfter)
|
||||
maxOfflineTime := windowEnd.Add(-d.cfg.OfflineAlertMax)
|
||||
|
||||
const q = `
|
||||
SELECT hostname, last_seen
|
||||
FROM agents
|
||||
WHERE is_enabled = 1
|
||||
AND last_seen < ?
|
||||
AND last_seen >= ?
|
||||
`
|
||||
rows, err := d.db.QueryContext(ctx, q, windowStart)
|
||||
rows, err := d.db.QueryContext(ctx, q, offlineAfterTime, maxOfflineTime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1402,7 +1596,9 @@ WHERE is_enabled = 1
|
||||
if err := rows.Scan(&host, &lastSeen); err != nil {
|
||||
return err
|
||||
}
|
||||
minutes := int(windowEnd.Sub(lastSeen).Minutes())
|
||||
|
||||
minutes := int(windowEnd.Sub(lastSeen.UTC()).Minutes())
|
||||
|
||||
score := math.Max(1, float64(minutes)/float64(int(d.cfg.OfflineAfter.Minutes())))
|
||||
severity := severityFromScore(score, 1.5, 3.0)
|
||||
|
||||
@@ -1413,22 +1609,25 @@ WHERE is_enabled = 1
|
||||
Severity: severity,
|
||||
Hostname: host,
|
||||
Score: score,
|
||||
WindowStart: windowStart,
|
||||
WindowStart: offlineAfterTime,
|
||||
WindowEnd: windowEnd,
|
||||
Summary: fmt.Sprintf("Agent %s ist seit %d Minuten offline", host, minutes),
|
||||
Details: mustJSON(map[string]any{
|
||||
"last_seen": lastSeen.UTC().Format(time.RFC3339Nano),
|
||||
"offline_minutes": minutes,
|
||||
"offline_after_min": int(d.cfg.OfflineAfter.Minutes()),
|
||||
"last_seen": lastSeen.UTC().Format(time.RFC3339Nano),
|
||||
"offline_minutes": minutes,
|
||||
"offline_after_min": int(d.cfg.OfflineAfter.Minutes()),
|
||||
"offline_alert_max_min": int(d.cfg.OfflineAlertMax.Minutes()),
|
||||
}),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if created {
|
||||
d.detectionHitsTotal.WithLabelValues("agent_offline", severity).Inc()
|
||||
}
|
||||
}
|
||||
|
||||
return rows.Err()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user