feat(waf): Alerts — Regelübereinstimmungen in DB + UI — v1.2.77
- Migration 0038: waf_alerts-Tabelle - AlertWriter (Buffered-Channel → async DB-Write) - SPOE: MatchedRules → sendAlert() nach ProcessRequestHeaders() - API: GET /waf/alerts + DELETE /waf/alerts - WAF-Page: Tabs Domains | Alarme; Alarme-Tabelle mit Rule-ID, Severity, Aktion (Detected/Blocked), URI, Client-IP + Purge-Button Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -113,6 +113,73 @@ func (r *Repo) ListEnabled(ctx context.Context) ([]models.WafConfig, error) {
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// WafAlert mirrors the waf_alerts DB row.
|
||||
type WafAlert struct {
|
||||
ID int64 `json:"id"`
|
||||
DomainID *int64 `json:"domain_id,omitempty"`
|
||||
Hostname string `json:"hostname"`
|
||||
ClientIP string `json:"client_ip"`
|
||||
Method string `json:"method"`
|
||||
URI string `json:"uri"`
|
||||
RuleID int `json:"rule_id"`
|
||||
RuleMsg string `json:"rule_msg"`
|
||||
Severity string `json:"severity"`
|
||||
Action string `json:"action"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// ListAlerts returns recent WAF alerts, optionally filtered by domain_id.
|
||||
func (r *Repo) ListAlerts(ctx context.Context, domainID *int64, limit int) ([]WafAlert, error) {
|
||||
if limit <= 0 || limit > 1000 {
|
||||
limit = 200
|
||||
}
|
||||
var rows interface{ Next() bool; Scan(...any) error; Close(); Err() error }
|
||||
var err error
|
||||
if domainID != nil {
|
||||
rows2, e := r.Pool.Query(ctx, `
|
||||
SELECT id, domain_id, hostname, client_ip, method, uri,
|
||||
rule_id, rule_msg, severity, action, created_at
|
||||
FROM waf_alerts
|
||||
WHERE domain_id = $1
|
||||
ORDER BY created_at DESC LIMIT $2
|
||||
`, *domainID, limit)
|
||||
rows, err = rows2, e
|
||||
} else {
|
||||
rows2, e := r.Pool.Query(ctx, `
|
||||
SELECT id, domain_id, hostname, client_ip, method, uri,
|
||||
rule_id, rule_msg, severity, action, created_at
|
||||
FROM waf_alerts
|
||||
ORDER BY created_at DESC LIMIT $1
|
||||
`, limit)
|
||||
rows, err = rows2, e
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
out := make([]WafAlert, 0, limit)
|
||||
for rows.Next() {
|
||||
var a WafAlert
|
||||
if err := rows.Scan(
|
||||
&a.ID, &a.DomainID, &a.Hostname, &a.ClientIP, &a.Method, &a.URI,
|
||||
&a.RuleID, &a.RuleMsg, &a.Severity, &a.Action, &a.CreatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, a)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// PurgeAlerts removes alerts older than the given number of days.
|
||||
func (r *Repo) PurgeAlerts(ctx context.Context, olderThanDays int) error {
|
||||
_, err := r.Pool.Exec(ctx,
|
||||
`DELETE FROM waf_alerts WHERE created_at < NOW() - ($1 || ' days')::interval`,
|
||||
olderThanDays,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// DomainConfigPair combines a domain hostname with its WAF config.
|
||||
type DomainConfigPair struct {
|
||||
Hostname string
|
||||
|
||||
Reference in New Issue
Block a user