feat(ui/waf): Regel-Ausnahmen direkt im Config-Dialog + durchsuchbarer Select — v1.3.14

Feedback: in der Regel-Ausnahmen-Sektion konnte man Ausnahmen nur SEHEN, nicht
hinzufügen (nur Hinweis auf den Alarme-Tab), und die Rule-ID war Freitext.

Jetzt: durchsuchbarer Select (aus CRS_RULES, filtert nach ID UND Beschreibung —
z.B. "941" oder "XSS") + optionale Notiz + Hinzufügen-Button direkt im Dialog.
Speichert sofort (wie der Entfernen-Button). Der Dropdown deckt alle 331
message-behafteten CRS-Regeln ab = genau die, die je in einem Alarm auftauchen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Debian
2026-08-03 11:41:49 +02:00
parent 235b5c3b9a
commit f0be5be496
4 changed files with 74 additions and 9 deletions

View File

@@ -1 +1 @@
1.3.13
1.3.14

View File

@@ -1947,7 +1947,11 @@
"saveFailed": "WAF-Konfiguration konnte nicht gespeichert werden.",
"noExclusions": "Noch keine Regelausnahmen.",
"noNote": "Keine Notiz",
"exclusionsAddHint": "Ausnahmen über den Alarme-Tab hinzufügen — \"Als Ausnahme\" auf einem Alarm klicken."
"exclusionsAddHint": "Regel oben suchen und hinzufügen — oder im Alarme-Tab per \"Als Ausnahme\" auf einem Alarm.",
"exclusionAddPlaceholder": "Regel suchen (ID oder Beschreibung)…",
"exclusionAddNote": "Notiz (optional)",
"exclusionAddNotFound": "Keine Regel gefunden",
"exclusionAddBtn": "Hinzufügen"
},
"tabs": {
"domains": "Domains",

View File

@@ -1947,7 +1947,11 @@
"saveFailed": "Failed to save WAF configuration.",
"noExclusions": "No rule exclusions yet.",
"noNote": "No note",
"exclusionsAddHint": "Add exceptions via the Alerts tab — click \"Add exception\" on an alert."
"exclusionsAddHint": "Search for a rule above and add it — or via the Alerts tab with \"Add exception\" on an alert.",
"exclusionAddPlaceholder": "Search rule (ID or description)…",
"exclusionAddNote": "Note (optional)",
"exclusionAddNotFound": "No rule found",
"exclusionAddBtn": "Add"
},
"tabs": {
"domains": "Domains",

View File

@@ -1,10 +1,10 @@
import { useState } from 'react'
import { useMemo, useState } from 'react'
import {
Alert, Button, Card, Col, Drawer, Form, Input, Modal, Popconfirm, Row,
Select, Space, Switch, Tabs, Tag, Tooltip, Typography, message,
} from 'antd'
import {
CheckCircleOutlined, CloseCircleOutlined, DeleteOutlined,
CheckCircleOutlined, CloseCircleOutlined, DeleteOutlined, PlusOutlined,
SafetyCertificateOutlined, SettingOutlined, WarningOutlined,
} from '@ant-design/icons'
import { useTranslation } from 'react-i18next'
@@ -14,7 +14,7 @@ import apiClient, { isEnvelope } from '../../api/client'
import PageHeader from '../../components/PageHeader'
import DataTable from '../../components/DataTable'
import { useAuthStore } from '../../stores/auth'
import { getRuleDescription } from './crsRules'
import { CRS_RULES, getRuleDescription } from './crsRules'
const { Text } = Typography
@@ -102,6 +102,14 @@ function ConfigDrawer({ domainName, domainId, onClose }: ConfigDrawerProps) {
const qc = useQueryClient()
const isViewer = useAuthStore((s) => s.user?.role) === 'viewer'
const [form] = Form.useForm<WafFormValues>()
const [addRuleId, setAddRuleId] = useState<string>('')
const [addNote, setAddNote] = useState<string>('')
// Durchsuchbare Optionen aus der CRS-Regel-Liste (ID — Beschreibung).
// filterOption unten matcht sowohl ID als auch Beschreibung.
const ruleOptions = useMemo(
() => Object.entries(CRS_RULES).map(([id, desc]) => ({ value: id, label: `${id}${desc}` })),
[],
)
const { data: cfg, isLoading } = useQuery({
queryKey: ['waf', 'config', domainId],
@@ -261,9 +269,58 @@ function ConfigDrawer({ domainName, domainId, onClose }: ConfigDrawerProps) {
))}
</Space>
)}
<div style={{ marginTop: 8 }}>
<Text type="secondary" style={{ fontSize: 11 }}>{t('waf.config.exclusionsAddHint')}</Text>
</div>
{!isViewer && (
<div style={{ marginTop: 10 }}>
<div style={{ display: 'flex', gap: 8 }}>
<Select
showSearch
value={addRuleId || undefined}
placeholder={t('waf.config.exclusionAddPlaceholder')}
style={{ flex: 1, minWidth: 0 }}
options={ruleOptions}
optionFilterProp="label"
notFoundContent={t('waf.config.exclusionAddNotFound')}
onChange={(v) => setAddRuleId(v)}
/>
<Input
placeholder={t('waf.config.exclusionAddNote')}
value={addNote}
onChange={(e) => setAddNote(e.target.value)}
style={{ width: 150 }}
/>
<Button
type="primary"
icon={<PlusOutlined />}
loading={save.isPending}
disabled={!addRuleId}
onClick={() => {
if (!addRuleId) return
const existing = cfg?.rule_exclusions ?? []
if (existing.includes(addRuleId)) { setAddRuleId(''); setAddNote(''); return }
const newNotes = { ...(cfg?.exclusion_notes ?? {}) }
if (addNote.trim()) newNotes[addRuleId] = addNote.trim()
save.mutate({
domain_id: domainId!,
enabled: cfg?.enabled ?? false,
mode: cfg?.mode ?? 'detection',
paranoia_level: cfg?.paranoia_level ?? 1,
rule_exclusions: [...existing, addRuleId],
crs_plugins: cfg?.crs_plugins ?? [],
exclusion_notes: newNotes,
trusted_proxies: cfg?.trusted_proxies ?? [],
custom_rules: cfg?.custom_rules ?? '',
})
setAddRuleId(''); setAddNote('')
}}
>
{t('waf.config.exclusionAddBtn')}
</Button>
</div>
<Text type="secondary" style={{ fontSize: 11, display: 'block', marginTop: 6 }}>
{t('waf.config.exclusionsAddHint')}
</Text>
</div>
)}
</Form.Item>
<Form.Item