Batch-Update hinzugefügt, um Massen-Events bearbeiten zu können.
All checks were successful
release-tag / release-image (push) Successful in 2m10s
All checks were successful
release-tag / release-image (push) Successful in 2m10s
This commit is contained in:
184
main.go
184
main.go
@@ -486,6 +486,7 @@ a {
|
||||
<option value="acknowledged" {{if eq (index .Filters "status") "acknowledged"}}selected{{end}}>acknowledged</option>
|
||||
<option value="investigating" {{if eq (index .Filters "status") "investigating"}}selected{{end}}>investigating</option>
|
||||
<option value="legitimate" {{if eq (index .Filters "status") "legitimate"}}selected{{end}}>legitim</option>
|
||||
<option value="plausible" {{if eq .Status "plausible"}}selected{{end}}>plausibel</option>
|
||||
<option value="false_positive" {{if eq (index .Filters "status") "false_positive"}}selected{{end}}>false positive</option>
|
||||
<option value="resolved" {{if eq (index .Filters "status") "resolved"}}selected{{end}}>resolved</option>
|
||||
<option value="suppressed" {{if eq (index .Filters "status") "suppressed"}}selected{{end}}>suppressed</option>
|
||||
@@ -503,9 +504,46 @@ a {
|
||||
<a href="/ui/detections?status=investigating">Investigating</a>
|
||||
<a href="/ui/detections?status=false_positive">False Positives</a>
|
||||
<a href="/ui/detections?status=legitimate">Legitim</a>
|
||||
<a href="/ui/detections?status=plausible">Plausibel</a>
|
||||
<a href="/ui/detections?status=resolved">Resolved</a>
|
||||
<a href="/ui/detections?status=confirmed_incident">Confirmed Incidents</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Batch-Bewertung</h2>
|
||||
<p class="muted">Bearbeitet mehrere Detections anhand von Rule, Host, Channel, EventID oder Zeitfenster.</p>
|
||||
|
||||
<form method="post" action="/ui/detections/batch-update">
|
||||
<div class="filters">
|
||||
<div><label>Rule</label><input name="rule" value="{{index .Filters "rule"}}"></div>
|
||||
<div><label>Host</label><input name="host" value="{{index .Filters "host"}}"></div>
|
||||
<div><label>Channel</label><input name="channel"></div>
|
||||
<div><label>Event ID</label><input name="event_id"></div>
|
||||
<div><label>From RFC3339</label><input name="from" placeholder="2026-04-26T08:00:00+02:00"></div>
|
||||
<div><label>To RFC3339</label><input name="to" placeholder="2026-04-26T12:00:00+02:00"></div>
|
||||
<div><label>Limit</label><input name="limit" value="5000"></div>
|
||||
<div>
|
||||
<label>Status</label>
|
||||
<select name="status">
|
||||
<option value="plausible">plausibel</option>
|
||||
<option value="legitimate">legitim</option>
|
||||
<option value="false_positive">false positive</option>
|
||||
<option value="resolved">resolved</option>
|
||||
<option value="suppressed">suppressed</option>
|
||||
<option value="acknowledged">acknowledged</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label>Notiz</label>
|
||||
<textarea name="note" rows="3" placeholder="z. B. Patchday, Software-Rollout, Wartungsfenster"></textarea>
|
||||
|
||||
<p>
|
||||
<button type="submit">Batch anwenden</button>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<tr>
|
||||
@@ -543,6 +581,7 @@ a {
|
||||
<option value="acknowledged" {{if eq .Status "acknowledged"}}selected{{end}}>ack</option>
|
||||
<option value="investigating" {{if eq .Status "investigating"}}selected{{end}}>investigating</option>
|
||||
<option value="legitimate" {{if eq .Status "legitimate"}}selected{{end}}>legitim</option>
|
||||
<option value="plausible" {{if eq .Status "plausible"}}selected{{end}}>plausibel</option>
|
||||
<option value="false_positive" {{if eq .Status "false_positive"}}selected{{end}}>false positive</option>
|
||||
<option value="resolved" {{if eq .Status "resolved"}}selected{{end}}>resolved</option>
|
||||
<option value="suppressed" {{if eq .Status "suppressed"}}selected{{end}}>suppressed</option>
|
||||
@@ -573,6 +612,16 @@ a {
|
||||
<input name="note" placeholder="Notiz" value="{{.AnalystNote}}">
|
||||
<button type="submit">Speichern</button>
|
||||
</form>
|
||||
<form method="post" action="/ui/detections/batch-update" class="mini-form">
|
||||
<input type="hidden" name="rule" value="{{.RuleName}}">
|
||||
<input type="hidden" name="host" value="{{.Hostname}}">
|
||||
<input type="hidden" name="channel" value="{{.Channel}}">
|
||||
<input type="hidden" name="event_id" value="{{.EventID}}">
|
||||
<input type="hidden" name="status" value="plausible">
|
||||
<input type="hidden" name="note" value="Gleicher Typ wurde gesammelt als plausibel markiert">
|
||||
<input type="hidden" name="limit" value="5000">
|
||||
<button type="submit">Ähnliche plausibel</button>
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
<a href="/ui/events?host={{q .Hostname}}&rule={{q .RuleName}}&severity={{q .Severity}}">anzeigen</a>
|
||||
@@ -1429,6 +1478,7 @@ func main() {
|
||||
mux.HandleFunc("/ui/rules/toggle", s.handleUIRuleToggle)
|
||||
mux.HandleFunc("/ui/baseline", s.handleUIBaseline)
|
||||
mux.HandleFunc("/ui/soc", s.handleUISOC)
|
||||
mux.HandleFunc("/ui/detections/batch-update", s.handleUIDetectionsBatchUpdate)
|
||||
|
||||
httpSrv := &http.Server{
|
||||
Addr: cfg.ListenAddr,
|
||||
@@ -1462,6 +1512,136 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *server) handleUIDetectionsBatchUpdate(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
|
||||
}
|
||||
|
||||
status := strings.TrimSpace(r.FormValue("status"))
|
||||
note := strings.TrimSpace(r.FormValue("note"))
|
||||
reviewedBy := strings.TrimSpace(r.FormValue("reviewed_by"))
|
||||
|
||||
if reviewedBy == "" {
|
||||
reviewedBy = "ui-batch"
|
||||
}
|
||||
|
||||
switch status {
|
||||
case "open", "acknowledged", "investigating", "legitimate", "plausible", "false_positive", "resolved", "suppressed", "confirmed_incident":
|
||||
default:
|
||||
writeError(w, http.StatusBadRequest, "invalid status")
|
||||
return
|
||||
}
|
||||
|
||||
rule := strings.TrimSpace(r.FormValue("rule"))
|
||||
host := strings.TrimSpace(r.FormValue("host"))
|
||||
channel := strings.TrimSpace(r.FormValue("channel"))
|
||||
|
||||
var eventID uint32
|
||||
if v := strings.TrimSpace(r.FormValue("event_id")); v != "" {
|
||||
n, err := strconv.ParseUint(v, 10, 32)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid event_id")
|
||||
return
|
||||
}
|
||||
eventID = uint32(n)
|
||||
}
|
||||
|
||||
from := strings.TrimSpace(r.FormValue("from"))
|
||||
to := strings.TrimSpace(r.FormValue("to"))
|
||||
|
||||
limit := atoiDefault(r.FormValue("limit"), 5000)
|
||||
if limit <= 0 || limit > 50000 {
|
||||
limit = 5000
|
||||
}
|
||||
|
||||
if rule == "" && host == "" && channel == "" && eventID == 0 && from == "" && to == "" {
|
||||
writeError(w, http.StatusBadRequest, "at least one filter required")
|
||||
return
|
||||
}
|
||||
|
||||
isFalsePositive := status == "false_positive"
|
||||
isLegitimate := status == "legitimate" || status == "plausible"
|
||||
|
||||
query := `
|
||||
UPDATE detections
|
||||
SET status = ?,
|
||||
analyst_note = ?,
|
||||
reviewed_by = ?,
|
||||
reviewed_at = UTC_TIMESTAMP(6),
|
||||
is_false_positive = ?,
|
||||
is_legitimate = ?
|
||||
WHERE id IN (
|
||||
SELECT id FROM (
|
||||
SELECT id
|
||||
FROM detections
|
||||
WHERE 1=1
|
||||
`
|
||||
args := []any{
|
||||
status,
|
||||
note,
|
||||
reviewedBy,
|
||||
isFalsePositive,
|
||||
isLegitimate,
|
||||
}
|
||||
|
||||
if rule != "" {
|
||||
query += " AND rule_name = ?"
|
||||
args = append(args, rule)
|
||||
}
|
||||
if host != "" {
|
||||
query += " AND hostname = ?"
|
||||
args = append(args, host)
|
||||
}
|
||||
if channel != "" {
|
||||
query += " AND channel_name = ?"
|
||||
args = append(args, channel)
|
||||
}
|
||||
if eventID != 0 {
|
||||
query += " AND event_id = ?"
|
||||
args = append(args, eventID)
|
||||
}
|
||||
if from != "" {
|
||||
if t, err := parseUIRFC3339(from); err == nil {
|
||||
query += " AND created_at >= ?"
|
||||
args = append(args, t.UTC())
|
||||
}
|
||||
}
|
||||
if to != "" {
|
||||
if t, err := parseUIRFC3339(to); err == nil {
|
||||
query += " AND created_at <= ?"
|
||||
args = append(args, t.UTC())
|
||||
}
|
||||
}
|
||||
|
||||
query += `
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ?
|
||||
) x
|
||||
)
|
||||
`
|
||||
args = append(args, limit)
|
||||
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
res, err := s.db.ExecContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
s.logger.Printf("batch update detections: %v", err)
|
||||
writeError(w, http.StatusInternalServerError, "internal error")
|
||||
return
|
||||
}
|
||||
|
||||
affected, _ := res.RowsAffected()
|
||||
|
||||
http.Redirect(w, r, fmt.Sprintf("/ui/detections?status=%s&updated=%d", url.QueryEscape(status), affected), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (s *server) listSOCTopHosts(ctx context.Context, limit int) ([]SOCHostRiskRow, error) {
|
||||
rows, err := s.db.QueryContext(ctx, `
|
||||
SELECT hostname, risk_score, severity, open_detections,
|
||||
@@ -1609,7 +1789,7 @@ func (s *server) handleUIDetectionUpdate(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
switch status {
|
||||
case "open", "acknowledged", "investigating", "legitimate", "false_positive", "resolved", "suppressed", "confirmed_incident":
|
||||
case "open", "acknowledged", "investigating", "legitimate", "plausible", "false_positive", "resolved", "suppressed", "confirmed_incident":
|
||||
default:
|
||||
writeError(w, http.StatusBadRequest, "invalid status")
|
||||
return
|
||||
@@ -4188,7 +4368,7 @@ SELECT
|
||||
MAX(created_at) AS last_detection_at
|
||||
FROM detections
|
||||
WHERE created_at >= ?
|
||||
AND status NOT IN ('false_positive', 'suppressed', 'legitimate', 'resolved')
|
||||
AND status NOT IN ('false_positive', 'suppressed', 'legitimate', 'resolved', 'plausible')
|
||||
GROUP BY hostname, severity, status
|
||||
`, windowStart)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user