+ Failed to load WAF configuration: {error instanceof Error ? error.message : 'Unknown error'}
+
+ )
+ }
+
+ const ruleSetList = ruleSets?.rulesets || []
+
+ return (
+
+ {/* Header */}
+
+
+
+
+ WAF Configuration
+
+
+ Manage Coraza Web Application Firewall rule sets
+
+
+
+
+ window.open('https://coraza.io/docs/seclang/directives/', '_blank')
+ }
+ >
+
+ Rule Syntax
+
+
setShowCreateForm(true)} data-testid="create-ruleset-btn">
+
+ Add Rule Set
+
+
+
+
+ {/* Info Banner */}
+
+
+
+
+
+ About WAF Rule Sets
+
+
+ Rule sets define ModSecurity/Coraza rules that inspect and filter HTTP
+ requests. The WAF automatically enables SecRuleEngine On and{' '}
+ SecRequestBodyAccess On for your rules.
+
+
+
+
+
+ {/* Create Form */}
+ {showCreateForm && (
+
+
Create Rule Set
+ setShowCreateForm(false)}
+ isLoading={upsertMutation.isPending}
+ />
+
+ )}
+
+ {/* Edit Form */}
+ {editingRuleSet && (
+
+
+
Edit Rule Set
+ setDeleteConfirm(editingRuleSet)}
+ >
+
+ Delete
+
+
+
setEditingRuleSet(null)}
+ isLoading={upsertMutation.isPending}
+ />
+
+ )}
+
+ {/* Delete Confirmation */}
+
setDeleteConfirm(null)}
+ isLoading={deleteMutation.isPending}
+ />
+
+ {/* Empty State */}
+ {ruleSetList.length === 0 && !showCreateForm && !editingRuleSet && (
+
+
🛡️
+
No Rule Sets
+
+ Create your first WAF rule set to protect your services from web attacks
+
+
setShowCreateForm(true)}>
+
+ Create Rule Set
+
+
+ )}
+
+ {/* Rule Sets Table */}
+ {ruleSetList.length > 0 && !showCreateForm && !editingRuleSet && (
+
+
+
+
+
+ Name
+
+
+ Mode
+
+
+ Source
+
+
+ Last Updated
+
+
+ Actions
+
+
+
+
+ {ruleSetList.map((rs) => (
+
+
+ {rs.name}
+ {rs.content && (
+
+ {rs.content.split('\n').filter((l) => l.trim()).length} rule(s)
+
+ )}
+
+
+
+ {rs.mode === 'blocking' ? 'Blocking' : 'Detection'}
+
+
+
+ {rs.source_url ? (
+
+ URL
+
+
+ ) : (
+ Inline
+ )}
+
+
+ {rs.last_updated
+ ? new Date(rs.last_updated).toLocaleDateString()
+ : '-'}
+
+
+
+
setEditingRuleSet(rs)}
+ className="text-gray-400 hover:text-blue-400"
+ title="Edit"
+ data-testid={`edit-ruleset-${rs.id}`}
+ >
+
+
+
setDeleteConfirm(rs)}
+ className="text-gray-400 hover:text-red-400"
+ title="Delete"
+ data-testid={`delete-ruleset-${rs.id}`}
+ >
+
+
+
+
+
+ ))}
+
+
+
+ )}
+
+ )
+}
diff --git a/frontend/src/pages/__tests__/WafConfig.spec.tsx b/frontend/src/pages/__tests__/WafConfig.spec.tsx
new file mode 100644
index 00000000..7283000c
--- /dev/null
+++ b/frontend/src/pages/__tests__/WafConfig.spec.tsx
@@ -0,0 +1,464 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest'
+import { render, screen, waitFor } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+import { BrowserRouter } from 'react-router-dom'
+import WafConfig from '../WafConfig'
+import * as securityApi from '../../api/security'
+import type { SecurityRuleSet, RuleSetsResponse } from '../../api/security'
+
+vi.mock('../../api/security')
+
+const createQueryClient = () =>
+ new QueryClient({
+ defaultOptions: {
+ queries: { retry: false },
+ mutations: { retry: false },
+ },
+ })
+
+const renderWithProviders = (ui: React.ReactNode) => {
+ const qc = createQueryClient()
+ return render(
+