feat(waf): benutzerdefinierte App-Profile + Fix: CRS-Plugins wurden im Agent nie geladen — v1.3.17
Neu: eigene WAF-App-Profile (benannte, wiederverwendbare Rule-ID-Ausnahme- Bündel) — zentrale Bibliothek im UI (eigener Tab), pro Domain zuweisbar, Built-in-OWASP-Plugins bleiben read-only + als Vorlage klonbar. Nur reine Rule-IDs/Ranges (keine SecLang-Ausführung, injektionssicher). - Migration 0047: Tabelle waf_app_profiles (repliziert via reconcile) + waf_configs.app_profiles. - Service/Handler: CRUD (/waf/profiles), Built-ins geschützt (builtin=false-Gate). - Agent-Loader: app_profiles → in effektive rule_exclusions gemerged; ihr updated_at hebt das effektive updated_at der Domain → Engine-Rebuild bei Profil-Edit. - UI: Profile-Tab (Liste/Editor mit durchsuchbaren Rule-IDs) + Multi-Select im Domain-Drawer. FIX (wichtig): ListAllWithDomain — der EINZIGE Loader des laufenden WAF-Agents — selektierte crs_plugins nie. Dadurch war cfg.CRSPlugins im Agent immer leer und KEIN Built-in-CRS-Plugin (Nextcloud/WordPress/Drupal) wurde je in die Engine inkludiert. Jetzt geladen (+ app_profiles). Die per-Domain-Plugin-Wahl wirkt damit erstmals tatsächlich. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1880,7 +1880,11 @@
|
||||
"deleteFailed": "Löschen fehlgeschlagen",
|
||||
"secretSet": "Gespeichert — leer lassen, um es unverändert zu lassen.",
|
||||
"secretUnset": "Noch nichts gespeichert.",
|
||||
"tabs": { "settings": "Einstellungen", "clients": "Clients (NAS)", "users": "Benutzer" },
|
||||
"tabs": {
|
||||
"settings": "Einstellungen",
|
||||
"clients": "Clients (NAS)",
|
||||
"users": "Benutzer"
|
||||
},
|
||||
"settings": {
|
||||
"enabled": "RADIUS auf dieser Node aktiv",
|
||||
"listen": "Listen-Adressen"
|
||||
@@ -1951,11 +1955,15 @@
|
||||
"exclusionAddPlaceholder": "Regel suchen (ID oder Beschreibung)…",
|
||||
"exclusionAddNote": "Notiz (optional)",
|
||||
"exclusionAddNotFound": "Keine Regel gefunden",
|
||||
"exclusionAddBtn": "Hinzufügen"
|
||||
"exclusionAddBtn": "Hinzufügen",
|
||||
"appProfiles": "Eigene App-Profile",
|
||||
"appProfilesHint": "Wiederverwendbare, selbst gepflegte Profile (Regel-ID-Bündel) — zentral unter „App-Profile“ anlegen und hier pro Domain zuweisen. Ergänzt die OWASP-Plugins oben.",
|
||||
"appProfilesPlaceholder": "Eigene Profile wählen (optional)"
|
||||
},
|
||||
"tabs": {
|
||||
"domains": "Domains",
|
||||
"alerts": "Alarme"
|
||||
"alerts": "Alarme",
|
||||
"profiles": "App-Profile"
|
||||
},
|
||||
"alerts": {
|
||||
"total": "Einträge",
|
||||
@@ -1984,6 +1992,39 @@
|
||||
"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.",
|
||||
"alreadyExcluded": "Bereits Ausnahme"
|
||||
},
|
||||
"profiles": {
|
||||
"intro": "Wiederverwendbare Ausnahme-Profile (Bündel von CRS-Regel-IDs). Einmal anlegen, pro Domain zuweisen. Die eingebauten OWASP-Plugins sind read-only und werden pro Domain gewählt.",
|
||||
"new": "Neues Profil",
|
||||
"empty": "Noch keine eigenen Profile. Lege eins an, um Regel-Ausnahmen wiederzuverwenden.",
|
||||
"builtinInfo": "Eingebaute OWASP-CRS-Plugins (dateibasiert, gepflegt). Read-only — pro Domain im WAF-Drawer wählbar. „Als Vorlage“ erstellt daraus ein leeres eigenes Profil zum Befüllen.",
|
||||
"typeBuiltin": "OWASP",
|
||||
"typeCustom": "Eigen",
|
||||
"col": {
|
||||
"name": "Name",
|
||||
"type": "Typ",
|
||||
"description": "Beschreibung",
|
||||
"rules": "Ausnahmen"
|
||||
},
|
||||
"edit": "Bearbeiten",
|
||||
"delete": "Löschen",
|
||||
"clone": "Klonen",
|
||||
"asTemplate": "Als Vorlage",
|
||||
"deleteConfirm": "Profil wirklich löschen? Zuweisungen an Domains verlieren dann diese Ausnahmen.",
|
||||
"createTitle": "Neues App-Profil",
|
||||
"editTitle": "App-Profil bearbeiten",
|
||||
"name": "Name",
|
||||
"namePlaceholder": "z.B. Meine WebApp",
|
||||
"description": "Beschreibung",
|
||||
"descriptionPlaceholder": "Wofür ist dieses Profil? (optional)",
|
||||
"rules": "Regel-Ausnahmen",
|
||||
"rulesHint": "CRS-Regel-IDs die für zugewiesene Domains deaktiviert werden. Durchsuchbar nach ID oder Beschreibung.",
|
||||
"rulesPlaceholder": "Regel-IDs suchen und hinzufügen…",
|
||||
"saved": "Profil gespeichert.",
|
||||
"saveFailed": "Profil konnte nicht gespeichert werden.",
|
||||
"deleted": "Profil gelöscht.",
|
||||
"deleteFailed": "Profil konnte nicht gelöscht werden.",
|
||||
"cloneSuffix": "Kopie"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1880,7 +1880,11 @@
|
||||
"deleteFailed": "Delete failed",
|
||||
"secretSet": "Stored — leave empty to keep unchanged.",
|
||||
"secretUnset": "Nothing stored yet.",
|
||||
"tabs": { "settings": "Settings", "clients": "Clients (NAS)", "users": "Users" },
|
||||
"tabs": {
|
||||
"settings": "Settings",
|
||||
"clients": "Clients (NAS)",
|
||||
"users": "Users"
|
||||
},
|
||||
"settings": {
|
||||
"enabled": "RADIUS active on this node",
|
||||
"listen": "Listen addresses"
|
||||
@@ -1951,11 +1955,15 @@
|
||||
"exclusionAddPlaceholder": "Search rule (ID or description)…",
|
||||
"exclusionAddNote": "Note (optional)",
|
||||
"exclusionAddNotFound": "No rule found",
|
||||
"exclusionAddBtn": "Add"
|
||||
"exclusionAddBtn": "Add",
|
||||
"appProfiles": "Custom App Profiles",
|
||||
"appProfilesHint": "Reusable, self-maintained profiles (bundles of rule IDs) — create them centrally under “App Profiles” and assign them here per domain. Complements the OWASP plugins above.",
|
||||
"appProfilesPlaceholder": "Select custom profiles (optional)"
|
||||
},
|
||||
"tabs": {
|
||||
"domains": "Domains",
|
||||
"alerts": "Alerts"
|
||||
"alerts": "Alerts",
|
||||
"profiles": "App Profiles"
|
||||
},
|
||||
"alerts": {
|
||||
"total": "entries",
|
||||
@@ -1984,6 +1992,39 @@
|
||||
"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.",
|
||||
"alreadyExcluded": "Already excluded"
|
||||
},
|
||||
"profiles": {
|
||||
"intro": "Reusable exclusion profiles (bundles of CRS rule IDs). Create once, assign per domain. The built-in OWASP plugins are read-only and selected per domain.",
|
||||
"new": "New Profile",
|
||||
"empty": "No custom profiles yet. Create one to reuse rule exclusions.",
|
||||
"builtinInfo": "Built-in OWASP CRS plugins (file-based, maintained). Read-only — selectable per domain in the WAF drawer. “Use as template” creates an empty custom profile from it to fill in.",
|
||||
"typeBuiltin": "OWASP",
|
||||
"typeCustom": "Custom",
|
||||
"col": {
|
||||
"name": "Name",
|
||||
"type": "Type",
|
||||
"description": "Description",
|
||||
"rules": "Exclusions"
|
||||
},
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"clone": "Clone",
|
||||
"asTemplate": "Use as template",
|
||||
"deleteConfirm": "Really delete this profile? Domains using it will lose these exclusions.",
|
||||
"createTitle": "New App Profile",
|
||||
"editTitle": "Edit App Profile",
|
||||
"name": "Name",
|
||||
"namePlaceholder": "e.g. My WebApp",
|
||||
"description": "Description",
|
||||
"descriptionPlaceholder": "What is this profile for? (optional)",
|
||||
"rules": "Rule Exclusions",
|
||||
"rulesHint": "CRS rule IDs disabled for assigned domains. Searchable by ID or description.",
|
||||
"rulesPlaceholder": "Search and add rule IDs…",
|
||||
"saved": "Profile saved.",
|
||||
"saveFailed": "Could not save profile.",
|
||||
"deleted": "Profile deleted.",
|
||||
"deleteFailed": "Could not delete profile.",
|
||||
"cloneSuffix": "Copy"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useEffect, 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, PlusOutlined,
|
||||
SafetyCertificateOutlined, SettingOutlined, WarningOutlined,
|
||||
CheckCircleOutlined, CloseCircleOutlined, CopyOutlined, DeleteOutlined,
|
||||
EditOutlined, PlusOutlined, SafetyCertificateOutlined, SettingOutlined,
|
||||
WarningOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
@@ -34,11 +35,22 @@ interface WafConfig {
|
||||
paranoia_level: number
|
||||
rule_exclusions: string[]
|
||||
crs_plugins: string[]
|
||||
app_profiles: string[]
|
||||
exclusion_notes: Record<string, string>
|
||||
trusted_proxies: string[]
|
||||
custom_rules: string
|
||||
}
|
||||
|
||||
interface WafProfile {
|
||||
id: number
|
||||
name: string
|
||||
description: string
|
||||
rule_exclusions: string[]
|
||||
builtin: boolean
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
|
||||
// CRS-App-Exclusion-Plugins — muss zur Backend-Whitelist (KnownCRSPlugins) passen.
|
||||
const CRS_PLUGIN_OPTIONS = [
|
||||
{ value: 'nextcloud', label: 'Nextcloud' },
|
||||
@@ -74,17 +86,25 @@ function defaultConfig(domainId: number): WafConfig {
|
||||
paranoia_level: 1,
|
||||
rule_exclusions: [],
|
||||
crs_plugins: [],
|
||||
app_profiles: [],
|
||||
exclusion_notes: {},
|
||||
trusted_proxies: [],
|
||||
custom_rules: '',
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchProfiles(): Promise<WafProfile[]> {
|
||||
const r = await apiClient.get('/waf/profiles')
|
||||
if (!isEnvelope(r.data)) return []
|
||||
return (r.data.data as { profiles?: WafProfile[] }).profiles ?? []
|
||||
}
|
||||
|
||||
interface WafFormValues {
|
||||
enabled: boolean
|
||||
mode: 'detection' | 'blocking'
|
||||
paranoia_level: number
|
||||
crs_plugins: string[]
|
||||
app_profiles: string[]
|
||||
trusted_proxies_str: string
|
||||
custom_rules: string
|
||||
}
|
||||
@@ -117,6 +137,13 @@ function ConfigDrawer({ domainName, domainId, onClose }: ConfigDrawerProps) {
|
||||
enabled: domainId !== null,
|
||||
})
|
||||
|
||||
// Eigene App-Profile (nur benutzerdefinierte, nicht built-in) als Optionen.
|
||||
const { data: profiles } = useQuery({ queryKey: ['waf', 'profiles'], queryFn: fetchProfiles })
|
||||
const profileOptions = useMemo(
|
||||
() => (profiles ?? []).filter(p => !p.builtin).map(p => ({ value: p.name, label: p.name })),
|
||||
[profiles],
|
||||
)
|
||||
|
||||
const save = useMutation({
|
||||
mutationFn: (values: WafConfig) =>
|
||||
apiClient.put(`/waf/configs/${domainId}`, values),
|
||||
@@ -154,6 +181,7 @@ function ConfigDrawer({ domainName, domainId, onClose }: ConfigDrawerProps) {
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
...cfg,
|
||||
app_profiles: cfg.app_profiles ?? [],
|
||||
trusted_proxies_str: (cfg.trusted_proxies ?? []).join(', '),
|
||||
}}
|
||||
onFinish={(vals) => {
|
||||
@@ -166,6 +194,7 @@ function ConfigDrawer({ domainName, domainId, onClose }: ConfigDrawerProps) {
|
||||
paranoia_level: vals.paranoia_level,
|
||||
rule_exclusions: cfg?.rule_exclusions ?? [],
|
||||
crs_plugins: vals.crs_plugins ?? [],
|
||||
app_profiles: vals.app_profiles ?? [],
|
||||
exclusion_notes: cfg?.exclusion_notes ?? {},
|
||||
trusted_proxies: proxies,
|
||||
custom_rules: vals.custom_rules ?? '',
|
||||
@@ -223,6 +252,21 @@ function ConfigDrawer({ domainName, domainId, onClose }: ConfigDrawerProps) {
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('waf.config.appProfiles')}
|
||||
name="app_profiles"
|
||||
help={t('waf.config.appProfilesHint')}
|
||||
>
|
||||
<Select
|
||||
mode="multiple"
|
||||
allowClear
|
||||
disabled={isViewer}
|
||||
placeholder={t('waf.config.appProfilesPlaceholder')}
|
||||
options={profileOptions}
|
||||
notFoundContent={t('waf.profiles.empty')}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* Exclusions list — shows existing exclusions with notes + remove button */}
|
||||
<Form.Item label={t('waf.config.exclusions')}>
|
||||
{(cfg?.rule_exclusions ?? []).length === 0 ? (
|
||||
@@ -258,6 +302,7 @@ function ConfigDrawer({ domainName, domainId, onClose }: ConfigDrawerProps) {
|
||||
paranoia_level: cfg?.paranoia_level ?? 1,
|
||||
rule_exclusions: newExclusions,
|
||||
crs_plugins: cfg?.crs_plugins ?? [],
|
||||
app_profiles: cfg?.app_profiles ?? [],
|
||||
exclusion_notes: newNotes,
|
||||
trusted_proxies: cfg?.trusted_proxies ?? [],
|
||||
custom_rules: cfg?.custom_rules ?? '',
|
||||
@@ -306,6 +351,7 @@ function ConfigDrawer({ domainName, domainId, onClose }: ConfigDrawerProps) {
|
||||
paranoia_level: cfg?.paranoia_level ?? 1,
|
||||
rule_exclusions: [...existing, addRuleId],
|
||||
crs_plugins: cfg?.crs_plugins ?? [],
|
||||
app_profiles: cfg?.app_profiles ?? [],
|
||||
exclusion_notes: newNotes,
|
||||
trusted_proxies: cfg?.trusted_proxies ?? [],
|
||||
custom_rules: cfg?.custom_rules ?? '',
|
||||
@@ -562,6 +608,220 @@ function AlertsTab({ domainId, configMap }: { domainId?: number; configMap: Map<
|
||||
)
|
||||
}
|
||||
|
||||
// ---------- Profiles tab ----------------------------------------------------
|
||||
|
||||
interface ProfileFormValues {
|
||||
name: string
|
||||
description: string
|
||||
rule_exclusions: string[]
|
||||
}
|
||||
|
||||
function ProfileEditor({ profile, onClose }: { profile: WafProfile | null; onClose: () => void }) {
|
||||
const { t } = useTranslation()
|
||||
const qc = useQueryClient()
|
||||
const [form] = Form.useForm<ProfileFormValues>()
|
||||
const isCreate = profile !== null && profile.id === 0
|
||||
const ruleOptions = useMemo(
|
||||
() => Object.entries(CRS_RULES).map(([id, desc]) => ({ value: id, label: `${id} — ${desc}` })),
|
||||
[],
|
||||
)
|
||||
|
||||
// Formular bei jedem Öffnen/Wechsel neu befüllen.
|
||||
useEffect(() => {
|
||||
if (profile) {
|
||||
form.setFieldsValue({
|
||||
name: profile.name,
|
||||
description: profile.description,
|
||||
rule_exclusions: profile.rule_exclusions ?? [],
|
||||
})
|
||||
}
|
||||
}, [profile, form])
|
||||
|
||||
const save = useMutation({
|
||||
mutationFn: (vals: ProfileFormValues) =>
|
||||
isCreate
|
||||
? apiClient.post('/waf/profiles', vals)
|
||||
: apiClient.put(`/waf/profiles/${profile!.id}`, vals),
|
||||
onSuccess: () => {
|
||||
message.success(t('waf.profiles.saved'))
|
||||
void qc.invalidateQueries({ queryKey: ['waf'] })
|
||||
onClose()
|
||||
},
|
||||
onError: () => message.error(t('waf.profiles.saveFailed')),
|
||||
})
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
title={isCreate ? t('waf.profiles.createTitle') : t('waf.profiles.editTitle')}
|
||||
open={profile !== null}
|
||||
onClose={onClose}
|
||||
width={520}
|
||||
footer={
|
||||
<Space style={{ justifyContent: 'flex-end', width: '100%' }}>
|
||||
<Button onClick={onClose}>{t('common.cancel')}</Button>
|
||||
<Button type="primary" loading={save.isPending} onClick={() => form.submit()}>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={(vals) => save.mutate({
|
||||
name: vals.name,
|
||||
description: vals.description ?? '',
|
||||
rule_exclusions: vals.rule_exclusions ?? [],
|
||||
})}
|
||||
>
|
||||
<Form.Item
|
||||
label={t('waf.profiles.name')}
|
||||
name="name"
|
||||
rules={[{ required: true, max: 60 }]}
|
||||
>
|
||||
<Input placeholder={t('waf.profiles.namePlaceholder')} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t('waf.profiles.description')} name="description">
|
||||
<Input placeholder={t('waf.profiles.descriptionPlaceholder')} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('waf.profiles.rules')}
|
||||
name="rule_exclusions"
|
||||
help={t('waf.profiles.rulesHint')}
|
||||
>
|
||||
<Select
|
||||
mode="multiple"
|
||||
showSearch
|
||||
allowClear
|
||||
placeholder={t('waf.profiles.rulesPlaceholder')}
|
||||
options={ruleOptions}
|
||||
optionFilterProp="label"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
function ProfilesTab() {
|
||||
const { t } = useTranslation()
|
||||
const qc = useQueryClient()
|
||||
const isViewer = useAuthStore((s) => s.user?.role) === 'viewer'
|
||||
const [editing, setEditing] = useState<WafProfile | null>(null)
|
||||
|
||||
const { data: profiles, isLoading } = useQuery({ queryKey: ['waf', 'profiles'], queryFn: fetchProfiles })
|
||||
const custom = (profiles ?? []).filter(p => !p.builtin)
|
||||
|
||||
const del = useMutation({
|
||||
mutationFn: (id: number) => apiClient.delete(`/waf/profiles/${id}`),
|
||||
onSuccess: () => {
|
||||
message.success(t('waf.profiles.deleted'))
|
||||
void qc.invalidateQueries({ queryKey: ['waf'] })
|
||||
},
|
||||
onError: () => message.error(t('waf.profiles.deleteFailed')),
|
||||
})
|
||||
|
||||
// openCreate(seed) öffnet den Editor im Create-Modus (id=0) mit Startwerten.
|
||||
const openCreate = (seed?: Partial<WafProfile>) => setEditing({
|
||||
id: 0,
|
||||
name: seed?.name ?? '',
|
||||
description: seed?.description ?? '',
|
||||
rule_exclusions: seed?.rule_exclusions ?? [],
|
||||
builtin: false,
|
||||
})
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t('waf.profiles.col.name'),
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
render: (v: string) => <Text strong style={{ fontSize: 13 }}>{v}</Text>,
|
||||
},
|
||||
{
|
||||
title: t('waf.profiles.col.description'),
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
ellipsis: true,
|
||||
render: (v: string) => <Text type="secondary" style={{ fontSize: 12 }}>{v || '—'}</Text>,
|
||||
},
|
||||
{
|
||||
title: t('waf.profiles.col.rules'),
|
||||
key: 'rules',
|
||||
width: 90,
|
||||
render: (_: unknown, row: WafProfile) => <Tag>{(row.rule_exclusions ?? []).length}</Tag>,
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
key: 'actions',
|
||||
width: 210,
|
||||
render: (_: unknown, row: WafProfile) => (
|
||||
<Space size={4}>
|
||||
<Button size="small" icon={<EditOutlined />} disabled={isViewer} onClick={() => setEditing(row)}>
|
||||
{t('waf.profiles.edit')}
|
||||
</Button>
|
||||
<Tooltip title={t('waf.profiles.clone')}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
disabled={isViewer}
|
||||
onClick={() => openCreate({
|
||||
name: `${row.name} ${t('waf.profiles.cloneSuffix')}`,
|
||||
description: row.description,
|
||||
rule_exclusions: row.rule_exclusions,
|
||||
})}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Popconfirm title={t('waf.profiles.deleteConfirm')} onConfirm={() => del.mutate(row.id)} disabled={isViewer}>
|
||||
<Button size="small" danger icon={<DeleteOutlined />} disabled={isViewer} />
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="mt-2">
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
className="mb-16"
|
||||
message={t('waf.profiles.intro')}
|
||||
description={
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Text style={{ fontSize: 12 }}>{t('waf.profiles.builtinInfo')}</Text>
|
||||
<div style={{ marginTop: 8, display: 'flex', gap: 12, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
{CRS_PLUGIN_OPTIONS.map(p => (
|
||||
<Space key={p.value} size={2}>
|
||||
<Tag icon={<SafetyCertificateOutlined />}>{p.label}</Tag>
|
||||
{!isViewer && (
|
||||
<Button size="small" type="link" onClick={() => openCreate({ name: `${p.value}-custom` })}>
|
||||
{t('waf.profiles.asTemplate')}
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<div className="flex-between mb-12">
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{custom.length} {t('waf.profiles.typeCustom')}
|
||||
</Text>
|
||||
<Button type="primary" icon={<PlusOutlined />} disabled={isViewer} onClick={() => openCreate()}>
|
||||
{t('waf.profiles.new')}
|
||||
</Button>
|
||||
</div>
|
||||
{custom.length === 0 && !isLoading ? (
|
||||
<Alert type="info" showIcon message={t('waf.profiles.empty')} />
|
||||
) : (
|
||||
<DataTable rowKey="id" loading={isLoading} dataSource={custom} columns={columns} />
|
||||
)}
|
||||
<ProfileEditor profile={editing} onClose={() => setEditing(null)} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ---------- Page ------------------------------------------------------------
|
||||
|
||||
export default function WAFPage() {
|
||||
@@ -735,6 +995,11 @@ export default function WAFPage() {
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'profiles',
|
||||
label: t('waf.tabs.profiles'),
|
||||
children: <ProfilesTab />,
|
||||
},
|
||||
{
|
||||
key: 'alerts',
|
||||
label: t('waf.tabs.alerts'),
|
||||
|
||||
Reference in New Issue
Block a user