feat(waf): Regel direkt aus Alert als Ausnahme hinzufügen — v1.2.78

Jede Alert-Zeile hat jetzt einen "Als Ausnahme"-Button. Klick:
1. Lädt aktuelle WAF-Config der betroffenen Domain
2. Fügt die Rule-ID zu rule_exclusions hinzu (dedupliziert)
3. Speichert via PUT /waf/configs/:domain_id → triggert HAProxy-Reload
4. Erfolgsmeldung + WAF-Config-Query invalidiert

Button disabled wenn domain_id fehlt (Domain nicht aufgelöst) oder Viewer-Rolle.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Debian
2026-06-03 11:20:52 +02:00
parent 220d9d7050
commit 801fa26da7
4 changed files with 56 additions and 3 deletions

View File

@@ -1823,7 +1823,11 @@
"ruleId": "Regel-ID",
"severity": "Schwere",
"msg": "Meldung"
}
},
"addException": "Als Ausnahme",
"exceptionAdded": "Regel als Ausnahme für diese Domain hinzugefügt.",
"exceptionFailed": "Ausnahme konnte nicht gespeichert werden.",
"noDomain": "Domain nicht gefunden — Ausnahme manuell konfigurieren."
}
}
}

View File

@@ -1823,7 +1823,11 @@
"ruleId": "Rule ID",
"severity": "Severity",
"msg": "Message"
}
},
"addException": "Add exception",
"exceptionAdded": "Rule added as exception for this domain.",
"exceptionFailed": "Failed to add exception.",
"noDomain": "Domain not found — configure exception manually."
}
}
}

View File

@@ -272,6 +272,25 @@ function AlertsTab({ domainId }: { domainId?: number }) {
},
})
const addException = useMutation({
mutationFn: async ({ domainId, ruleId }: { domainId: number; ruleId: number }) => {
// Fetch current config (returns default if none exists yet)
const r = await apiClient.get(`/waf/configs/${domainId}`)
const cfg: WafConfig = isEnvelope(r.data)
? (r.data.data as { config: WafConfig }).config
: defaultConfig(domainId)
const exclusions = [...(cfg.rule_exclusions ?? [])]
const ruleStr = String(ruleId)
if (!exclusions.includes(ruleStr)) exclusions.push(ruleStr)
return apiClient.put(`/waf/configs/${domainId}`, { ...cfg, rule_exclusions: exclusions })
},
onSuccess: () => {
message.success(t('waf.alerts.exceptionAdded'))
void qc.invalidateQueries({ queryKey: ['waf'] })
},
onError: () => message.error(t('waf.alerts.exceptionFailed')),
})
const severityColor = (s: string) => {
switch (s?.toLowerCase()) {
case 'critical': return 'red'
@@ -316,6 +335,32 @@ function AlertsTab({ domainId }: { domainId?: number }) {
render: (v: string) => <Tag color={severityColor(v)}>{v || '—'}</Tag> },
{ title: t('waf.alerts.col.msg'), dataIndex: 'rule_msg', key: 'rule_msg', ellipsis: true,
render: (v: string) => <Text style={{ fontSize: 11, color: '#64748B' }}>{v || '—'}</Text> },
{
title: '',
key: 'exception',
width: 130,
render: (_: unknown, row: WafAlert) => {
const canExclude = !!row.domain_id && row.rule_id > 0 && !isViewer
const isPending = addException.isPending &&
addException.variables?.domainId === row.domain_id &&
addException.variables?.ruleId === row.rule_id
return (
<Tooltip title={!row.domain_id ? t('waf.alerts.noDomain') : t('waf.alerts.addException')}>
<Button
size="small"
disabled={!canExclude}
loading={isPending}
onClick={() => row.domain_id && addException.mutate({
domainId: row.domain_id,
ruleId: row.rule_id,
})}
>
{t('waf.alerts.addException')}
</Button>
</Tooltip>
)
},
},
]
return (