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:
@@ -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;
|
||||||
@@ -77,6 +77,7 @@ type upsertBody struct {
|
|||||||
Mode string `json:"mode"`
|
Mode string `json:"mode"`
|
||||||
ParanoiaLevel int `json:"paranoia_level"`
|
ParanoiaLevel int `json:"paranoia_level"`
|
||||||
RuleExclusions []string `json:"rule_exclusions"`
|
RuleExclusions []string `json:"rule_exclusions"`
|
||||||
|
ExclusionNotes map[string]string `json:"exclusion_notes"`
|
||||||
TrustedProxies []string `json:"trusted_proxies"`
|
TrustedProxies []string `json:"trusted_proxies"`
|
||||||
CustomRules string `json:"custom_rules"`
|
CustomRules string `json:"custom_rules"`
|
||||||
}
|
}
|
||||||
@@ -106,12 +107,16 @@ func (h *WafHandler) Upsert(c *gin.Context) {
|
|||||||
body.TrustedProxies = []string{}
|
body.TrustedProxies = []string{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if body.ExclusionNotes == nil {
|
||||||
|
body.ExclusionNotes = map[string]string{}
|
||||||
|
}
|
||||||
cfg := models.WafConfig{
|
cfg := models.WafConfig{
|
||||||
DomainID: domainID,
|
DomainID: domainID,
|
||||||
Enabled: body.Enabled,
|
Enabled: body.Enabled,
|
||||||
Mode: body.Mode,
|
Mode: body.Mode,
|
||||||
ParanoiaLevel: body.ParanoiaLevel,
|
ParanoiaLevel: body.ParanoiaLevel,
|
||||||
RuleExclusions: body.RuleExclusions,
|
RuleExclusions: body.RuleExclusions,
|
||||||
|
ExclusionNotes: body.ExclusionNotes,
|
||||||
TrustedProxies: body.TrustedProxies,
|
TrustedProxies: body.TrustedProxies,
|
||||||
CustomRules: body.CustomRules,
|
CustomRules: body.CustomRules,
|
||||||
}
|
}
|
||||||
@@ -187,6 +192,7 @@ func defaultConfig(domainID int64) models.WafConfig {
|
|||||||
Mode: "detection",
|
Mode: "detection",
|
||||||
ParanoiaLevel: 1,
|
ParanoiaLevel: 1,
|
||||||
RuleExclusions: []string{},
|
RuleExclusions: []string{},
|
||||||
|
ExclusionNotes: map[string]string{},
|
||||||
TrustedProxies: []string{},
|
TrustedProxies: []string{},
|
||||||
CustomRules: "",
|
CustomRules: "",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ type WafConfig struct {
|
|||||||
Mode string `gorm:"column:mode" json:"mode"` // "detection" | "blocking"
|
Mode string `gorm:"column:mode" json:"mode"` // "detection" | "blocking"
|
||||||
ParanoiaLevel int `gorm:"column:paranoia_level" json:"paranoia_level"` // 1–4
|
ParanoiaLevel int `gorm:"column:paranoia_level" json:"paranoia_level"` // 1–4
|
||||||
RuleExclusions []string `gorm:"column:rule_exclusions;type:text[]" json:"rule_exclusions"`
|
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"`
|
TrustedProxies []string `gorm:"column:trusted_proxies;type:text[]" json:"trusted_proxies"`
|
||||||
CustomRules string `gorm:"column:custom_rules" json:"custom_rules"`
|
CustomRules string `gorm:"column:custom_rules" json:"custom_rules"`
|
||||||
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
|
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ func New(pool *pgxpool.Pool) *Repo { return &Repo{Pool: pool} }
|
|||||||
|
|
||||||
const baseSelect = `
|
const baseSelect = `
|
||||||
SELECT id, domain_id, enabled, mode, paranoia_level,
|
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
|
FROM waf_configs
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -30,11 +30,14 @@ func scan(row pgx.Row) (*models.WafConfig, error) {
|
|||||||
var c models.WafConfig
|
var c models.WafConfig
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&c.ID, &c.DomainID, &c.Enabled, &c.Mode, &c.ParanoiaLevel,
|
&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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if c.ExclusionNotes == nil {
|
||||||
|
c.ExclusionNotes = map[string]string{}
|
||||||
|
}
|
||||||
return &c, nil
|
return &c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,24 +76,28 @@ func (r *Repo) GetByDomain(ctx context.Context, domainID int64) (*models.WafConf
|
|||||||
// Returns the resulting row.
|
// Returns the resulting row.
|
||||||
func (r *Repo) Upsert(ctx context.Context, c models.WafConfig) (*models.WafConfig, error) {
|
func (r *Repo) Upsert(ctx context.Context, c models.WafConfig) (*models.WafConfig, error) {
|
||||||
c.UpdatedAt = time.Now()
|
c.UpdatedAt = time.Now()
|
||||||
|
if c.ExclusionNotes == nil {
|
||||||
|
c.ExclusionNotes = map[string]string{}
|
||||||
|
}
|
||||||
row := r.Pool.QueryRow(ctx, `
|
row := r.Pool.QueryRow(ctx, `
|
||||||
INSERT INTO waf_configs
|
INSERT INTO waf_configs
|
||||||
(domain_id, enabled, mode, paranoia_level,
|
(domain_id, enabled, mode, paranoia_level,
|
||||||
rule_exclusions, trusted_proxies, custom_rules, updated_at)
|
rule_exclusions, exclusion_notes, trusted_proxies, custom_rules, updated_at)
|
||||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8)
|
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)
|
||||||
ON CONFLICT (domain_id) DO UPDATE SET
|
ON CONFLICT (domain_id) DO UPDATE SET
|
||||||
enabled = EXCLUDED.enabled,
|
enabled = EXCLUDED.enabled,
|
||||||
mode = EXCLUDED.mode,
|
mode = EXCLUDED.mode,
|
||||||
paranoia_level = EXCLUDED.paranoia_level,
|
paranoia_level = EXCLUDED.paranoia_level,
|
||||||
rule_exclusions = EXCLUDED.rule_exclusions,
|
rule_exclusions = EXCLUDED.rule_exclusions,
|
||||||
|
exclusion_notes = EXCLUDED.exclusion_notes,
|
||||||
trusted_proxies = EXCLUDED.trusted_proxies,
|
trusted_proxies = EXCLUDED.trusted_proxies,
|
||||||
custom_rules = EXCLUDED.custom_rules,
|
custom_rules = EXCLUDED.custom_rules,
|
||||||
updated_at = EXCLUDED.updated_at
|
updated_at = EXCLUDED.updated_at
|
||||||
RETURNING id, domain_id, enabled, mode, paranoia_level,
|
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.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)
|
return scan(row)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1799,7 +1799,13 @@
|
|||||||
"customRules": "Eigene SecRules",
|
"customRules": "Eigene SecRules",
|
||||||
"customRulesHint": "Rohe SecRule-Direktiven die nach dem CRS eingefügt werden. Können CRS-Regeln überschreiben.",
|
"customRulesHint": "Rohe SecRule-Direktiven die nach dem CRS eingefügt werden. Können CRS-Regeln überschreiben.",
|
||||||
"defaultHint": "Standard: Nur-Erkennung, Paranoia-Level 1. Erst auf Blocking wechseln, nachdem Alerts geprüft wurden.",
|
"defaultHint": "Standard: Nur-Erkennung, Paranoia-Level 1. Erst auf Blocking wechseln, nachdem Alerts geprüft wurden.",
|
||||||
"saveFailed": "WAF-Konfiguration konnte nicht gespeichert werden."
|
"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.",
|
||||||
|
"exceptionModalTitle": "Ausnahme für Regel {{rule}} hinzufügen",
|
||||||
|
"exceptionModalHint": "Optional: Begründung warum diese Regel ein False Positive für diese Domain ist.",
|
||||||
|
"exceptionNotePlaceholder": "z.B. Unsere API verwendet nicht-standardisierte Header die diese Regel auslösen."
|
||||||
},
|
},
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"domains": "Domains",
|
"domains": "Domains",
|
||||||
|
|||||||
@@ -1799,7 +1799,13 @@
|
|||||||
"customRules": "Custom SecRules",
|
"customRules": "Custom SecRules",
|
||||||
"customRulesHint": "Raw SecRule directives appended after the CRS. Applied last, can override CRS rules.",
|
"customRulesHint": "Raw SecRule directives appended after the CRS. Applied last, can override CRS rules.",
|
||||||
"defaultHint": "Default: Detection-Only, Paranoia Level 1. Switch to Blocking only after reviewing alerts.",
|
"defaultHint": "Default: Detection-Only, Paranoia Level 1. Switch to Blocking only after reviewing alerts.",
|
||||||
"saveFailed": "Failed to save WAF configuration."
|
"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.",
|
||||||
|
"exceptionModalTitle": "Add exception for rule {{rule}}",
|
||||||
|
"exceptionModalHint": "Optional: describe why this rule is a false positive for this domain.",
|
||||||
|
"exceptionNotePlaceholder": "e.g. Our custom API uses non-standard headers that trigger this rule."
|
||||||
},
|
},
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"domains": "Domains",
|
"domains": "Domains",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import {
|
import {
|
||||||
Alert, Button, Card, Col, Drawer, Form, Input, Popconfirm, Row,
|
Alert, Button, Card, Col, Drawer, Form, Input, Modal, Popconfirm, Row,
|
||||||
Select, Space, Switch, Tabs, Tag, Tooltip, Typography, message,
|
Select, Space, Switch, Tabs, Tag, Tooltip, Typography, message,
|
||||||
} from 'antd'
|
} from 'antd'
|
||||||
import {
|
import {
|
||||||
@@ -32,6 +32,7 @@ interface WafConfig {
|
|||||||
mode: 'detection' | 'blocking'
|
mode: 'detection' | 'blocking'
|
||||||
paranoia_level: number
|
paranoia_level: number
|
||||||
rule_exclusions: string[]
|
rule_exclusions: string[]
|
||||||
|
exclusion_notes: Record<string, string>
|
||||||
trusted_proxies: string[]
|
trusted_proxies: string[]
|
||||||
custom_rules: string
|
custom_rules: string
|
||||||
}
|
}
|
||||||
@@ -63,6 +64,7 @@ function defaultConfig(domainId: number): WafConfig {
|
|||||||
mode: 'detection',
|
mode: 'detection',
|
||||||
paranoia_level: 1,
|
paranoia_level: 1,
|
||||||
rule_exclusions: [],
|
rule_exclusions: [],
|
||||||
|
exclusion_notes: {},
|
||||||
trusted_proxies: [],
|
trusted_proxies: [],
|
||||||
custom_rules: '',
|
custom_rules: '',
|
||||||
}
|
}
|
||||||
@@ -72,7 +74,6 @@ interface WafFormValues {
|
|||||||
enabled: boolean
|
enabled: boolean
|
||||||
mode: 'detection' | 'blocking'
|
mode: 'detection' | 'blocking'
|
||||||
paranoia_level: number
|
paranoia_level: number
|
||||||
rule_exclusions_str: string
|
|
||||||
trusted_proxies_str: string
|
trusted_proxies_str: string
|
||||||
custom_rules: string
|
custom_rules: string
|
||||||
}
|
}
|
||||||
@@ -134,12 +135,9 @@ function ConfigDrawer({ domainName, domainId, onClose }: ConfigDrawerProps) {
|
|||||||
layout="vertical"
|
layout="vertical"
|
||||||
initialValues={{
|
initialValues={{
|
||||||
...cfg,
|
...cfg,
|
||||||
rule_exclusions_str: (cfg.rule_exclusions ?? []).join(', '),
|
|
||||||
trusted_proxies_str: (cfg.trusted_proxies ?? []).join(', '),
|
trusted_proxies_str: (cfg.trusted_proxies ?? []).join(', '),
|
||||||
}}
|
}}
|
||||||
onFinish={(vals) => {
|
onFinish={(vals) => {
|
||||||
const exclusions = (vals.rule_exclusions_str ?? '')
|
|
||||||
.split(',').map((s: string) => s.trim()).filter(Boolean)
|
|
||||||
const proxies = (vals.trusted_proxies_str ?? '')
|
const proxies = (vals.trusted_proxies_str ?? '')
|
||||||
.split(',').map((s: string) => s.trim()).filter(Boolean)
|
.split(',').map((s: string) => s.trim()).filter(Boolean)
|
||||||
save.mutate({
|
save.mutate({
|
||||||
@@ -147,7 +145,8 @@ function ConfigDrawer({ domainName, domainId, onClose }: ConfigDrawerProps) {
|
|||||||
enabled: vals.enabled,
|
enabled: vals.enabled,
|
||||||
mode: vals.mode,
|
mode: vals.mode,
|
||||||
paranoia_level: vals.paranoia_level,
|
paranoia_level: vals.paranoia_level,
|
||||||
rule_exclusions: exclusions,
|
rule_exclusions: cfg?.rule_exclusions ?? [],
|
||||||
|
exclusion_notes: cfg?.exclusion_notes ?? {},
|
||||||
trusted_proxies: proxies,
|
trusted_proxies: proxies,
|
||||||
custom_rules: vals.custom_rules ?? '',
|
custom_rules: vals.custom_rules ?? '',
|
||||||
})
|
})
|
||||||
@@ -190,15 +189,52 @@ function ConfigDrawer({ domainName, domainId, onClose }: ConfigDrawerProps) {
|
|||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
{/* Exclusions list — shows existing exclusions with notes + remove button */}
|
||||||
label={t('waf.config.exclusions')}
|
<Form.Item label={t('waf.config.exclusions')}>
|
||||||
name="rule_exclusions_str"
|
{(cfg?.rule_exclusions ?? []).length === 0 ? (
|
||||||
help={t('waf.config.exclusionsHint')}
|
<Text type="secondary" style={{ fontSize: 12 }}>{t('waf.config.noExclusions')}</Text>
|
||||||
>
|
) : (
|
||||||
<Input
|
<Space direction="vertical" style={{ width: '100%' }} size={4}>
|
||||||
disabled={isViewer}
|
{(cfg?.rule_exclusions ?? []).map(ruleId => (
|
||||||
placeholder="920350, 941130"
|
<div key={ruleId} style={{
|
||||||
|
display: 'flex', alignItems: 'flex-start', gap: 8,
|
||||||
|
padding: '6px 10px', background: '#F8FAFC',
|
||||||
|
border: '1px solid #E2E8F0', borderRadius: 6,
|
||||||
|
}}>
|
||||||
|
<Tag style={{ fontFamily: 'monospace', flexShrink: 0 }}>{ruleId}</Tag>
|
||||||
|
<Text style={{ fontSize: 12, color: '#475569', flex: 1 }}>
|
||||||
|
{cfg?.exclusion_notes?.[ruleId] || <em style={{ color: '#94A3B8' }}>{t('waf.config.noNote')}</em>}
|
||||||
|
</Text>
|
||||||
|
{!isViewer && (
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
danger
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
onClick={() => {
|
||||||
|
const newExclusions = (cfg?.rule_exclusions ?? []).filter(r => r !== ruleId)
|
||||||
|
const newNotes = { ...(cfg?.exclusion_notes ?? {}) }
|
||||||
|
delete newNotes[ruleId]
|
||||||
|
save.mutate({
|
||||||
|
domain_id: domainId!,
|
||||||
|
enabled: cfg?.enabled ?? false,
|
||||||
|
mode: cfg?.mode ?? 'detection',
|
||||||
|
paranoia_level: cfg?.paranoia_level ?? 1,
|
||||||
|
rule_exclusions: newExclusions,
|
||||||
|
exclusion_notes: newNotes,
|
||||||
|
trusted_proxies: cfg?.trusted_proxies ?? [],
|
||||||
|
custom_rules: cfg?.custom_rules ?? '',
|
||||||
|
})
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
|
<div style={{ marginTop: 8 }}>
|
||||||
|
<Text type="secondary" style={{ fontSize: 11 }}>{t('waf.config.exclusionsAddHint')}</Text>
|
||||||
|
</div>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
@@ -257,6 +293,8 @@ function AlertsTab({ domainId }: { domainId?: number }) {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
const isViewer = useAuthStore((s) => s.user?.role) === 'viewer'
|
const isViewer = useAuthStore((s) => s.user?.role) === 'viewer'
|
||||||
|
const [exceptionModal, setExceptionModal] = useState<{ domainId: number; ruleId: number } | null>(null)
|
||||||
|
const [exceptionNote, setExceptionNote] = useState('')
|
||||||
|
|
||||||
const { data: alerts, isLoading } = useQuery({
|
const { data: alerts, isLoading } = useQuery({
|
||||||
queryKey: ['waf', 'alerts', domainId ?? 'all'],
|
queryKey: ['waf', 'alerts', domainId ?? 'all'],
|
||||||
@@ -273,19 +311,22 @@ function AlertsTab({ domainId }: { domainId?: number }) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const addException = useMutation({
|
const addException = useMutation({
|
||||||
mutationFn: async ({ domainId, ruleId }: { domainId: number; ruleId: number }) => {
|
mutationFn: async ({ domainId, ruleId, note }: { domainId: number; ruleId: number; note: string }) => {
|
||||||
// Fetch current config (returns default if none exists yet)
|
|
||||||
const r = await apiClient.get(`/waf/configs/${domainId}`)
|
const r = await apiClient.get(`/waf/configs/${domainId}`)
|
||||||
const cfg: WafConfig = isEnvelope(r.data)
|
const cfg: WafConfig = isEnvelope(r.data)
|
||||||
? (r.data.data as { config: WafConfig }).config
|
? (r.data.data as { config: WafConfig }).config
|
||||||
: defaultConfig(domainId)
|
: defaultConfig(domainId)
|
||||||
const exclusions = [...(cfg.rule_exclusions ?? [])]
|
const exclusions = [...(cfg.rule_exclusions ?? [])]
|
||||||
|
const notes = { ...(cfg.exclusion_notes ?? {}) }
|
||||||
const ruleStr = String(ruleId)
|
const ruleStr = String(ruleId)
|
||||||
if (!exclusions.includes(ruleStr)) exclusions.push(ruleStr)
|
if (!exclusions.includes(ruleStr)) exclusions.push(ruleStr)
|
||||||
return apiClient.put(`/waf/configs/${domainId}`, { ...cfg, rule_exclusions: exclusions })
|
if (note.trim()) notes[ruleStr] = note.trim()
|
||||||
|
return apiClient.put(`/waf/configs/${domainId}`, { ...cfg, rule_exclusions: exclusions, exclusion_notes: notes })
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
message.success(t('waf.alerts.exceptionAdded'))
|
message.success(t('waf.alerts.exceptionAdded'))
|
||||||
|
setExceptionModal(null)
|
||||||
|
setExceptionNote('')
|
||||||
void qc.invalidateQueries({ queryKey: ['waf'] })
|
void qc.invalidateQueries({ queryKey: ['waf'] })
|
||||||
},
|
},
|
||||||
onError: () => message.error(t('waf.alerts.exceptionFailed')),
|
onError: () => message.error(t('waf.alerts.exceptionFailed')),
|
||||||
@@ -341,19 +382,16 @@ function AlertsTab({ domainId }: { domainId?: number }) {
|
|||||||
width: 130,
|
width: 130,
|
||||||
render: (_: unknown, row: WafAlert) => {
|
render: (_: unknown, row: WafAlert) => {
|
||||||
const canExclude = !!row.domain_id && row.rule_id > 0 && !isViewer
|
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 (
|
return (
|
||||||
<Tooltip title={!row.domain_id ? t('waf.alerts.noDomain') : t('waf.alerts.addException')}>
|
<Tooltip title={!row.domain_id ? t('waf.alerts.noDomain') : undefined}>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
disabled={!canExclude}
|
disabled={!canExclude}
|
||||||
loading={isPending}
|
onClick={() => {
|
||||||
onClick={() => row.domain_id && addException.mutate({
|
if (!row.domain_id) return
|
||||||
domainId: row.domain_id,
|
setExceptionModal({ domainId: row.domain_id, ruleId: row.rule_id })
|
||||||
ruleId: row.rule_id,
|
setExceptionNote('')
|
||||||
})}
|
}}
|
||||||
>
|
>
|
||||||
{t('waf.alerts.addException')}
|
{t('waf.alerts.addException')}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -392,6 +430,31 @@ function AlertsTab({ domainId }: { domainId?: number }) {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Exception note modal */}
|
||||||
|
<Modal
|
||||||
|
title={t('waf.alerts.exceptionModalTitle', { rule: exceptionModal?.ruleId })}
|
||||||
|
open={exceptionModal !== null}
|
||||||
|
onCancel={() => { setExceptionModal(null); setExceptionNote('') }}
|
||||||
|
onOk={() => exceptionModal && addException.mutate({
|
||||||
|
domainId: exceptionModal.domainId,
|
||||||
|
ruleId: exceptionModal.ruleId,
|
||||||
|
note: exceptionNote,
|
||||||
|
})}
|
||||||
|
confirmLoading={addException.isPending}
|
||||||
|
okText={t('waf.alerts.addException')}
|
||||||
|
>
|
||||||
|
<p style={{ marginBottom: 12, color: '#475569', fontSize: 13 }}>
|
||||||
|
{t('waf.alerts.exceptionModalHint')}
|
||||||
|
</p>
|
||||||
|
<Input.TextArea
|
||||||
|
rows={3}
|
||||||
|
placeholder={t('waf.alerts.exceptionNotePlaceholder')}
|
||||||
|
value={exceptionNote}
|
||||||
|
onChange={e => setExceptionNote(e.target.value)}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user