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
|
DETECTION_INTERVAL=1m
|
||||||
OFFLINE_AFTER=10m
|
OFFLINE_AFTER=10m
|
||||||
|
OFFLINE_ALERT_MAX=120m
|
||||||
FAILED_LOGON_WINDOW=5m
|
FAILED_LOGON_WINDOW=5m
|
||||||
FAILED_LOGON_THRESHOLD=25
|
FAILED_LOGON_THRESHOLD=25
|
||||||
REBOOT_WINDOW=15m
|
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; }
|
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; }
|
pre { white-space: pre-wrap; word-break: break-word; background: #111827; color: #e5e7eb; padding: 16px; border-radius: 10px; }
|
||||||
a { color: #2563eb; text-decoration: none; }
|
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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -63,6 +70,7 @@ const uiTemplates = `
|
|||||||
<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/agents">Agents</a>
|
||||||
<a href="/ui/detections">Detections</a>
|
<a href="/ui/detections">Detections</a>
|
||||||
<a href="/ui/events">Events</a>
|
<a href="/ui/events">Events</a>
|
||||||
<a href="/metrics">Metrics</a>
|
<a href="/metrics">Metrics</a>
|
||||||
@@ -189,6 +197,61 @@ const uiTemplates = `
|
|||||||
{{template "footer" .}}
|
{{template "footer" .}}
|
||||||
{{end}}
|
{{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"}}
|
{{define "event_detail"}}
|
||||||
{{template "header" .}}
|
{{template "header" .}}
|
||||||
<h1>{{.Title}}</h1>
|
<h1>{{.Title}}</h1>
|
||||||
@@ -226,6 +289,7 @@ type Config struct {
|
|||||||
DBConnMaxIdleTime time.Duration
|
DBConnMaxIdleTime time.Duration
|
||||||
DetectionInterval time.Duration
|
DetectionInterval time.Duration
|
||||||
OfflineAfter time.Duration
|
OfflineAfter time.Duration
|
||||||
|
OfflineAlertMax time.Duration
|
||||||
FailedLogonWindow time.Duration
|
FailedLogonWindow time.Duration
|
||||||
FailedLogonThreshold int
|
FailedLogonThreshold int
|
||||||
RebootWindow time.Duration
|
RebootWindow time.Duration
|
||||||
@@ -379,6 +443,23 @@ type EventDetailPageData struct {
|
|||||||
Event EventRow
|
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 (
|
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."},
|
||||||
@@ -526,6 +607,8 @@ func main() {
|
|||||||
mux.HandleFunc("/ui/detections", s.handleUIDetections)
|
mux.HandleFunc("/ui/detections", s.handleUIDetections)
|
||||||
mux.HandleFunc("/ui/events", s.handleUIEvents)
|
mux.HandleFunc("/ui/events", s.handleUIEvents)
|
||||||
mux.HandleFunc("/ui/event", s.handleUIEventDetail)
|
mux.HandleFunc("/ui/event", s.handleUIEventDetail)
|
||||||
|
mux.HandleFunc("/ui/agents", s.handleUIAgents)
|
||||||
|
mux.HandleFunc("/ui/agents/toggle", s.handleUIAgentToggle)
|
||||||
|
|
||||||
httpSrv := &http.Server{
|
httpSrv := &http.Server{
|
||||||
Addr: cfg.ListenAddr,
|
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) {
|
func (s *server) handleUIIndex(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodGet {
|
if r.Method != http.MethodGet {
|
||||||
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
|
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||||
@@ -930,6 +1120,7 @@ func loadConfig() Config {
|
|||||||
DBConnMaxIdleTime: getenvDuration("DB_CONN_MAX_IDLE_TIME", 1*time.Minute),
|
DBConnMaxIdleTime: getenvDuration("DB_CONN_MAX_IDLE_TIME", 1*time.Minute),
|
||||||
DetectionInterval: getenvDuration("DETECTION_INTERVAL", 1*time.Minute),
|
DetectionInterval: getenvDuration("DETECTION_INTERVAL", 1*time.Minute),
|
||||||
OfflineAfter: getenvDuration("OFFLINE_AFTER", 10*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),
|
FailedLogonWindow: getenvDuration("FAILED_LOGON_WINDOW", 5*time.Minute),
|
||||||
FailedLogonThreshold: getenvInt("FAILED_LOGON_THRESHOLD", 25),
|
FailedLogonThreshold: getenvInt("FAILED_LOGON_THRESHOLD", 25),
|
||||||
RebootWindow: getenvDuration("REBOOT_WINDOW", 15*time.Minute),
|
RebootWindow: getenvDuration("REBOOT_WINDOW", 15*time.Minute),
|
||||||
@@ -1382,15 +1573,18 @@ WHERE is_enabled = 1
|
|||||||
|
|
||||||
func (d *detector) runAgentOfflineRule(ctx context.Context) error {
|
func (d *detector) runAgentOfflineRule(ctx context.Context) error {
|
||||||
windowEnd := time.Now().UTC()
|
windowEnd := time.Now().UTC()
|
||||||
windowStart := windowEnd.Add(-d.cfg.OfflineAfter)
|
|
||||||
|
offlineAfterTime := windowEnd.Add(-d.cfg.OfflineAfter)
|
||||||
|
maxOfflineTime := windowEnd.Add(-d.cfg.OfflineAlertMax)
|
||||||
|
|
||||||
const q = `
|
const q = `
|
||||||
SELECT hostname, last_seen
|
SELECT hostname, last_seen
|
||||||
FROM agents
|
FROM agents
|
||||||
WHERE is_enabled = 1
|
WHERE is_enabled = 1
|
||||||
AND last_seen < ?
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1402,7 +1596,9 @@ WHERE is_enabled = 1
|
|||||||
if err := rows.Scan(&host, &lastSeen); err != nil {
|
if err := rows.Scan(&host, &lastSeen); err != nil {
|
||||||
return err
|
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())))
|
score := math.Max(1, float64(minutes)/float64(int(d.cfg.OfflineAfter.Minutes())))
|
||||||
severity := severityFromScore(score, 1.5, 3.0)
|
severity := severityFromScore(score, 1.5, 3.0)
|
||||||
|
|
||||||
@@ -1413,22 +1609,25 @@ WHERE is_enabled = 1
|
|||||||
Severity: severity,
|
Severity: severity,
|
||||||
Hostname: host,
|
Hostname: host,
|
||||||
Score: score,
|
Score: score,
|
||||||
WindowStart: windowStart,
|
WindowStart: offlineAfterTime,
|
||||||
WindowEnd: windowEnd,
|
WindowEnd: windowEnd,
|
||||||
Summary: fmt.Sprintf("Agent %s ist seit %d Minuten offline", host, minutes),
|
Summary: fmt.Sprintf("Agent %s ist seit %d Minuten offline", host, minutes),
|
||||||
Details: mustJSON(map[string]any{
|
Details: mustJSON(map[string]any{
|
||||||
"last_seen": lastSeen.UTC().Format(time.RFC3339Nano),
|
"last_seen": lastSeen.UTC().Format(time.RFC3339Nano),
|
||||||
"offline_minutes": minutes,
|
"offline_minutes": minutes,
|
||||||
"offline_after_min": int(d.cfg.OfflineAfter.Minutes()),
|
"offline_after_min": int(d.cfg.OfflineAfter.Minutes()),
|
||||||
|
"offline_alert_max_min": int(d.cfg.OfflineAlertMax.Minutes()),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if created {
|
if created {
|
||||||
d.detectionHitsTotal.WithLabelValues("agent_offline", severity).Inc()
|
d.detectionHitsTotal.WithLabelValues("agent_offline", severity).Inc()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return rows.Err()
|
return rows.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user