import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Shield, Plus, Pencil, Trash2, ExternalLink, FileCode2, Sparkles } from 'lucide-react'
import { Button } from '../components/ui/Button'
import { Input } from '../components/ui/Input'
import { useRuleSets, useUpsertRuleSet, useDeleteRuleSet } from '../hooks/useSecurity'
import type { SecurityRuleSet, UpsertRuleSetPayload } from '../api/security'
import { ConfigReloadOverlay } from '../components/LoadingStates'
/**
* WAF Rule Presets for common security configurations
*/
const WAF_PRESETS = [
{
name: 'OWASP Core Rule Set',
url: 'https://github.com/coreruleset/coreruleset/archive/refs/tags/v3.3.5.tar.gz',
content: '',
description: 'Industry standard protection against OWASP Top 10 vulnerabilities.',
},
{
name: 'Basic SQL Injection Protection',
url: '',
content: `SecRule ARGS "@detectSQLi" "id:1001,phase:1,deny,status:403,msg:'SQLi Detected'"
SecRule REQUEST_BODY "@detectSQLi" "id:1002,phase:2,deny,status:403,msg:'SQLi in Body'"
SecRule REQUEST_COOKIES "@detectSQLi" "id:1003,phase:1,deny,status:403,msg:'SQLi in Cookies'"`,
description: 'Simple rules to block common SQL injection patterns.',
},
{
name: 'Basic XSS Protection',
url: '',
content: `SecRule ARGS "@detectXSS" "id:2001,phase:1,deny,status:403,msg:'XSS Detected'"
SecRule REQUEST_BODY "@detectXSS" "id:2002,phase:2,deny,status:403,msg:'XSS in Body'"`,
description: 'Rules to block common Cross-Site Scripting (XSS) attacks.',
},
{
name: 'Common Bad Bots',
url: '',
content: `SecRule REQUEST_HEADERS:User-Agent "@rx (?i)(curl|curl|python|scrapy|httpclient|libwww|nikto|sqlmap)" "id:3001,phase:1,deny,status:403,msg:'Bad Bot Detected'"
SecRule REQUEST_HEADERS:User-Agent "@streq -" "id:3002,phase:1,deny,status:403,msg:'Empty User-Agent'"`,
description: 'Block known malicious bots and scanners.',
},
] as const
/**
* Confirmation dialog for destructive actions
*/
function ConfirmDialog({
isOpen,
title,
message,
confirmLabel,
cancelLabel,
onConfirm,
onCancel,
isLoading,
deletingLabel,
}: {
isOpen: boolean
title: string
message: string
confirmLabel: string
cancelLabel: string
onConfirm: () => void
onCancel: () => void
isLoading?: boolean
deletingLabel: string
}) {
if (!isOpen) return null
return (
e.stopPropagation()}
>
{title}
{message}
)
}
/**
* Form for creating/editing a WAF rule set
*/
function RuleSetForm({
initialData,
onSubmit,
onCancel,
isLoading,
t,
}: {
initialData?: SecurityRuleSet
onSubmit: (data: UpsertRuleSetPayload) => void
onCancel: () => void
isLoading?: boolean
t: (key: string) => string
}) {
const [name, setName] = useState(initialData?.name || '')
const [sourceUrl, setSourceUrl] = useState(initialData?.source_url || '')
const [content, setContent] = useState(initialData?.content || '')
const [mode, setMode] = useState<'blocking' | 'detection'>(
initialData?.mode === 'detection' ? 'detection' : 'blocking'
)
const [selectedPreset, setSelectedPreset] = useState('')
const handlePresetChange = (presetName: string) => {
setSelectedPreset(presetName)
if (presetName === '') return
const preset = WAF_PRESETS.find((p) => p.name === presetName)
if (preset) {
setName(preset.name)
setSourceUrl(preset.url)
setContent(preset.content)
}
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
onSubmit({
id: initialData?.id,
name: name.trim(),
source_url: sourceUrl.trim() || undefined,
content: content.trim() || undefined,
mode,
})
}
const isValid = name.trim().length > 0 && (content.trim().length > 0 || sourceUrl.trim().length > 0)
return (
)
}
/**
* WAF Configuration Page - Manage Coraza rule sets
*/
export default function WafConfig() {
const { t } = useTranslation()
const { data: ruleSets, isLoading, error } = useRuleSets()
const upsertMutation = useUpsertRuleSet()
const deleteMutation = useDeleteRuleSet()
const [showCreateForm, setShowCreateForm] = useState(false)
const [editingRuleSet, setEditingRuleSet] = useState(null)
const [deleteConfirm, setDeleteConfirm] = useState(null)
// Determine if any security operation is in progress
const isApplyingConfig = upsertMutation.isPending || deleteMutation.isPending
// Determine contextual message based on operation
const getMessage = () => {
if (upsertMutation.isPending) {
return editingRuleSet
? { message: t('wafConfig.cerberusAwakens'), submessage: t('wafConfig.guardianStandsWatch') }
: { message: t('wafConfig.forgingDefenses'), submessage: t('wafConfig.rulesInscribing') }
}
if (deleteMutation.isPending) {
return { message: t('wafConfig.loweringBarrier'), submessage: t('wafConfig.defenseLayerRemoved') }
}
return { message: t('wafConfig.cerberusAwakens'), submessage: t('wafConfig.guardianStandsWatch') }
}
const { message, submessage } = getMessage()
const handleCreate = (data: UpsertRuleSetPayload) => {
upsertMutation.mutate(data, {
onSuccess: () => setShowCreateForm(false),
})
}
const handleUpdate = (data: UpsertRuleSetPayload) => {
upsertMutation.mutate(data, {
onSuccess: () => setEditingRuleSet(null),
})
}
const handleDelete = () => {
if (!deleteConfirm) return
deleteMutation.mutate(deleteConfirm.id, {
onSuccess: () => {
setDeleteConfirm(null)
setEditingRuleSet(null)
},
})
}
if (isLoading) {
return (
{t('wafConfig.loadingConfiguration')}
)
}
if (error) {
return (
{t('wafConfig.failedToLoad')}: {error instanceof Error ? error.message : t('common.unknownError')}
)
}
const ruleSetList = ruleSets?.rulesets || []
return (
<>
{isApplyingConfig && (
)}
{/* Header */}
{t('wafConfig.title')}
{t('wafConfig.description')}
{/* Info Banner */}
{t('wafConfig.aboutTitle')}
{t('wafConfig.aboutDescription')}
{/* Create Form */}
{showCreateForm && (
{t('wafConfig.createRuleSet')}
setShowCreateForm(false)}
isLoading={upsertMutation.isPending}
t={t}
/>
)}
{/* Edit Form */}
{editingRuleSet && (
{t('wafConfig.editRuleSet')}
setEditingRuleSet(null)}
isLoading={upsertMutation.isPending}
t={t}
/>
)}
{/* Delete Confirmation */}
setDeleteConfirm(null)}
isLoading={deleteMutation.isPending}
/>
{/* Empty State */}
{ruleSetList.length === 0 && !showCreateForm && !editingRuleSet && (
🛡️
{t('wafConfig.noRuleSets')}
{t('wafConfig.noRuleSetsDescription')}
)}
{/* Rule Sets Table */}
{ruleSetList.length > 0 && !showCreateForm && !editingRuleSet && (
|
{t('common.name')}
|
{t('wafConfig.mode')}
|
{t('wafConfig.source')}
|
{t('wafConfig.lastUpdated')}
|
{t('common.actions')}
|
{ruleSetList.map((rs) => (
|
{rs.name}
{rs.content && (
{t('wafConfig.ruleCount', { count: rs.content.split('\n').filter((l) => l.trim()).length })}
)}
|
{rs.mode === 'blocking' ? t('wafConfig.blocking') : t('wafConfig.detection')}
|
{rs.source_url ? (
{t('wafConfig.url')}
) : (
{t('wafConfig.inline')}
)}
|
{rs.last_updated
? new Date(rs.last_updated).toLocaleDateString()
: '-'}
|
|
))}
)}
>
)
}