feat(waf): Ausnahmen mit Notiz + Ausnahmen-Liste im Drawer — v1.2.79

- Migration 0039: exclusion_notes JSONB in waf_configs (rule_id → note)
- Model/Service/Handler: exclusion_notes in Upsert + GET durchgereicht
- Alerts-Tab: "Als Ausnahme"-Button öffnet Modal mit Notiz-Textarea;
  Notiz wird in exclusion_notes gespeichert
- Config-Drawer: Ausnahmen als Liste (Rule-ID + Notiz + Entfernen-Button)
  statt rohem Textfeld; Ausnahmen nur noch via Alert-Tab hinzufügbar
- i18n EN + DE

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Debian
2026-06-03 11:31:49 +02:00
parent 801fa26da7
commit 8a4fefee73
8 changed files with 153 additions and 58 deletions

View File

@@ -0,0 +1,6 @@
-- +goose Up
ALTER TABLE waf_configs
ADD COLUMN IF NOT EXISTS exclusion_notes JSONB NOT NULL DEFAULT '{}';
-- +goose Down
ALTER TABLE waf_configs DROP COLUMN IF EXISTS exclusion_notes;

View File

@@ -73,12 +73,13 @@ func (h *WafHandler) Get(c *gin.Context) {
// upsertBody is the accepted JSON for PUT /waf/configs/:domain_id.
type upsertBody struct {
Enabled bool `json:"enabled"`
Mode string `json:"mode"`
ParanoiaLevel int `json:"paranoia_level"`
RuleExclusions []string `json:"rule_exclusions"`
TrustedProxies []string `json:"trusted_proxies"`
CustomRules string `json:"custom_rules"`
Enabled bool `json:"enabled"`
Mode string `json:"mode"`
ParanoiaLevel int `json:"paranoia_level"`
RuleExclusions []string `json:"rule_exclusions"`
ExclusionNotes map[string]string `json:"exclusion_notes"`
TrustedProxies []string `json:"trusted_proxies"`
CustomRules string `json:"custom_rules"`
}
// Upsert creates or updates the WAF config for a domain.
@@ -106,12 +107,16 @@ func (h *WafHandler) Upsert(c *gin.Context) {
body.TrustedProxies = []string{}
}
if body.ExclusionNotes == nil {
body.ExclusionNotes = map[string]string{}
}
cfg := models.WafConfig{
DomainID: domainID,
Enabled: body.Enabled,
Mode: body.Mode,
ParanoiaLevel: body.ParanoiaLevel,
RuleExclusions: body.RuleExclusions,
ExclusionNotes: body.ExclusionNotes,
TrustedProxies: body.TrustedProxies,
CustomRules: body.CustomRules,
}
@@ -187,6 +192,7 @@ func defaultConfig(domainID int64) models.WafConfig {
Mode: "detection",
ParanoiaLevel: 1,
RuleExclusions: []string{},
ExclusionNotes: map[string]string{},
TrustedProxies: []string{},
CustomRules: "",
}

View File

@@ -5,15 +5,16 @@ import "time"
// WafConfig holds the per-domain WAF policy.
// Default on creation: enabled=false, mode=detection, paranoia_level=1.
type WafConfig struct {
ID int64 `gorm:"primaryKey" json:"id"`
DomainID int64 `gorm:"column:domain_id;uniqueIndex" json:"domain_id"`
Enabled bool `gorm:"column:enabled" json:"enabled"`
Mode string `gorm:"column:mode" json:"mode"` // "detection" | "blocking"
ParanoiaLevel int `gorm:"column:paranoia_level" json:"paranoia_level"` // 14
RuleExclusions []string `gorm:"column:rule_exclusions;type:text[]" json:"rule_exclusions"`
TrustedProxies []string `gorm:"column:trusted_proxies;type:text[]" json:"trusted_proxies"`
CustomRules string `gorm:"column:custom_rules" json:"custom_rules"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
ID int64 `gorm:"primaryKey" json:"id"`
DomainID int64 `gorm:"column:domain_id;uniqueIndex" json:"domain_id"`
Enabled bool `gorm:"column:enabled" json:"enabled"`
Mode string `gorm:"column:mode" json:"mode"` // "detection" | "blocking"
ParanoiaLevel int `gorm:"column:paranoia_level" json:"paranoia_level"` // 14
RuleExclusions []string `gorm:"column:rule_exclusions;type:text[]" json:"rule_exclusions"`
ExclusionNotes map[string]string `gorm:"column:exclusion_notes;type:jsonb" json:"exclusion_notes"` // rule_id → note
TrustedProxies []string `gorm:"column:trusted_proxies;type:text[]" json:"trusted_proxies"`
CustomRules string `gorm:"column:custom_rules" json:"custom_rules"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
}
func (WafConfig) TableName() string { return "waf_configs" }

View File

@@ -22,7 +22,7 @@ func New(pool *pgxpool.Pool) *Repo { return &Repo{Pool: pool} }
const baseSelect = `
SELECT id, domain_id, enabled, mode, paranoia_level,
rule_exclusions, trusted_proxies, custom_rules, updated_at
rule_exclusions, exclusion_notes, trusted_proxies, custom_rules, updated_at
FROM waf_configs
`
@@ -30,11 +30,14 @@ func scan(row pgx.Row) (*models.WafConfig, error) {
var c models.WafConfig
err := row.Scan(
&c.ID, &c.DomainID, &c.Enabled, &c.Mode, &c.ParanoiaLevel,
&c.RuleExclusions, &c.TrustedProxies, &c.CustomRules, &c.UpdatedAt,
&c.RuleExclusions, &c.ExclusionNotes, &c.TrustedProxies, &c.CustomRules, &c.UpdatedAt,
)
if err != nil {
return nil, err
}
if c.ExclusionNotes == nil {
c.ExclusionNotes = map[string]string{}
}
return &c, nil
}
@@ -73,24 +76,28 @@ func (r *Repo) GetByDomain(ctx context.Context, domainID int64) (*models.WafConf
// Returns the resulting row.
func (r *Repo) Upsert(ctx context.Context, c models.WafConfig) (*models.WafConfig, error) {
c.UpdatedAt = time.Now()
if c.ExclusionNotes == nil {
c.ExclusionNotes = map[string]string{}
}
row := r.Pool.QueryRow(ctx, `
INSERT INTO waf_configs
(domain_id, enabled, mode, paranoia_level,
rule_exclusions, trusted_proxies, custom_rules, updated_at)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8)
rule_exclusions, exclusion_notes, trusted_proxies, custom_rules, updated_at)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)
ON CONFLICT (domain_id) DO UPDATE SET
enabled = EXCLUDED.enabled,
mode = EXCLUDED.mode,
paranoia_level = EXCLUDED.paranoia_level,
rule_exclusions = EXCLUDED.rule_exclusions,
trusted_proxies = EXCLUDED.trusted_proxies,
custom_rules = EXCLUDED.custom_rules,
updated_at = EXCLUDED.updated_at
enabled = EXCLUDED.enabled,
mode = EXCLUDED.mode,
paranoia_level = EXCLUDED.paranoia_level,
rule_exclusions = EXCLUDED.rule_exclusions,
exclusion_notes = EXCLUDED.exclusion_notes,
trusted_proxies = EXCLUDED.trusted_proxies,
custom_rules = EXCLUDED.custom_rules,
updated_at = EXCLUDED.updated_at
RETURNING id, domain_id, enabled, mode, paranoia_level,
rule_exclusions, trusted_proxies, custom_rules, updated_at
rule_exclusions, exclusion_notes, trusted_proxies, custom_rules, updated_at
`,
c.DomainID, c.Enabled, c.Mode, c.ParanoiaLevel,
c.RuleExclusions, c.TrustedProxies, c.CustomRules, c.UpdatedAt,
c.RuleExclusions, c.ExclusionNotes, c.TrustedProxies, c.CustomRules, c.UpdatedAt,
)
return scan(row)
}