feat: Rename WAF to Coraza in UI and update related tests

- Updated UI components to reflect the renaming of "WAF (Coraza)" to "Coraza".
- Removed WAF controls from the Security page and adjusted related tests.
- Verified that all frontend tests pass after updating assertions to match the new UI.
- Added a test script to package.json for running tests with Vitest.
- Adjusted imports for jest-dom to be compatible with Vitest.
- Updated TypeScript configuration to include Vitest types for testing.
This commit is contained in:
GitHub Actions
2025-12-12 03:19:27 +00:00
parent 8e09efe548
commit effed44ce8
12 changed files with 358 additions and 1289 deletions
+1 -1
View File
@@ -67,7 +67,7 @@ export default function Layout({ children }: LayoutProps) {
{ name: 'CrowdSec', path: '/security/crowdsec', icon: '🛡️' },
{ name: 'Access Lists', path: '/security/access-lists', icon: '🔒' },
{ name: 'Rate Limiting', path: '/security/rate-limiting', icon: '⚡' },
{ name: 'WAF (Coraza)', path: '/security/waf', icon: '🛡️' },
{ name: 'Coraza', path: '/security/waf', icon: '🛡️' },
]},
{ name: 'Notifications', path: '/notifications', icon: '🔔' },
// Import group moved under Tasks
+5 -43
View File
@@ -3,7 +3,7 @@ import { useState, useEffect } from 'react'
import { useNavigate, Outlet } from 'react-router-dom'
import { Shield, ShieldAlert, ShieldCheck, Lock, Activity, ExternalLink } from 'lucide-react'
import { getSecurityStatus, type SecurityStatus } from '../api/security'
import { useSecurityConfig, useUpdateSecurityConfig, useGenerateBreakGlassToken, useRuleSets } from '../hooks/useSecurity'
import { useSecurityConfig, useUpdateSecurityConfig, useGenerateBreakGlassToken } from '../hooks/useSecurity'
import { startCrowdsec, stopCrowdsec, statusCrowdsec } from '../api/crowdsec'
import { updateSetting } from '../api/settings'
import { Switch } from '../components/ui/Switch'
@@ -21,7 +21,6 @@ export default function Security() {
queryFn: getSecurityStatus,
})
const { data: securityConfig } = useSecurityConfig()
const { data: ruleSetsData } = useRuleSets()
const [adminWhitelist, setAdminWhitelist] = useState<string>('')
const [showNotificationSettings, setShowNotificationSettings] = useState(false)
useEffect(() => {
@@ -168,7 +167,7 @@ export default function Security() {
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">Cerberus Disabled</h2>
</div>
<p className="text-sm text-gray-500 dark:text-gray-400 max-w-lg">
Cerberus powers CrowdSec, WAF, ACLs, and Rate Limiting. Enable the Cerberus toggle in System Settings to awaken the guardian, then configure each head below.
Cerberus powers CrowdSec, Coraza, ACLs, and Rate Limiting. Enable the Cerberus toggle in System Settings to awaken the guardian, then configure each head below.
</p>
<Button
variant="primary"
@@ -314,11 +313,11 @@ export default function Security() {
</div>
</Card>
{/* WAF - Layer 3: Request Inspection */}
{/* Coraza - Layer 3: Request Inspection */}
<Card className={status.waf.enabled ? 'border-green-200 dark:border-green-900' : ''}>
<div className="text-xs text-gray-400 mb-2">🛡 Layer 3: Request Inspection</div>
<div className="flex flex-row items-center justify-between pb-2">
<h3 className="text-sm font-medium text-white">WAF (Coraza)</h3>
<h3 className="text-sm font-medium text-white">Coraza</h3>
<div className="flex items-center gap-3">
<Switch
checked={status.waf.enabled}
@@ -338,43 +337,6 @@ export default function Security() {
? `Protects against: SQL injection, XSS, RCE, zero-day exploits*`
: 'Web Application Firewall'}
</p>
{status.waf.enabled && (
<div className="mt-3 space-y-3">
<div>
<label className="text-xs text-gray-400 block mb-1">WAF Mode</label>
<select
value={securityConfig?.config?.waf_mode || 'block'}
onChange={(e) => updateSecurityConfigMutation.mutate({ name: 'default', waf_mode: e.target.value })}
className="w-full bg-gray-800 border border-gray-700 rounded px-2 py-1 text-sm text-white"
data-testid="waf-mode-select"
>
<option value="block">Block (deny malicious requests)</option>
<option value="monitor">Monitor (log only, don't block)</option>
</select>
</div>
<div>
<label className="text-xs text-gray-400 block mb-1">Active Rule Set</label>
<select
value={securityConfig?.config?.waf_rules_source || ''}
onChange={(e) => updateSecurityConfigMutation.mutate({ name: 'default', waf_rules_source: e.target.value || undefined })}
className="w-full bg-gray-800 border border-gray-700 rounded px-2 py-1 text-sm text-white"
data-testid="waf-ruleset-select"
>
<option value="">None (all rule sets)</option>
{ruleSetsData?.rulesets?.map((rs) => (
<option key={rs.id} value={rs.name}>
{rs.name} ({rs.mode === 'blocking' ? 'blocking' : 'detection'})
</option>
))}
</select>
{(!ruleSetsData?.rulesets || ruleSetsData.rulesets.length === 0) && (
<p className="text-xs text-yellow-500 mt-1">
No rule sets configured. Add one below.
</p>
)}
</div>
</div>
)}
<div className="mt-4">
<Button
variant="secondary"
@@ -382,7 +344,7 @@ export default function Security() {
className="w-full"
onClick={() => navigate('/security/waf')}
>
{status.waf.enabled ? 'Manage Rule Sets' : 'Configure'}
Configure
</Button>
</div>
</div>
@@ -1,6 +1,6 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen, waitFor, within } from '@testing-library/react'
import '@testing-library/jest-dom'
import '@testing-library/jest-dom/vitest'
import userEvent from '@testing-library/user-event'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import type { ProxyHost } from '../../api/proxyHosts'
@@ -284,7 +284,7 @@ describe('Security Page - QA Security Audit', () => {
// All 4 cards should be present
expect(screen.getByText('CrowdSec')).toBeInTheDocument()
expect(screen.getByText('Access Control')).toBeInTheDocument()
expect(screen.getByText('WAF (Coraza)')).toBeInTheDocument()
expect(screen.getByText('Coraza')).toBeInTheDocument()
expect(screen.getByText('Rate Limiting')).toBeInTheDocument()
})
})
@@ -303,17 +303,6 @@ describe('Security Page - QA Security Audit', () => {
expect(screen.getByTestId('toggle-rate-limit')).toBeInTheDocument()
})
it('WAF controls have proper test IDs when enabled', async () => {
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatus)
await renderSecurityPage()
await waitFor(() => screen.getByText(/Cerberus Dashboard/i))
expect(screen.getByTestId('waf-mode-select')).toBeInTheDocument()
expect(screen.getByTestId('waf-ruleset-select')).toBeInTheDocument()
})
it('CrowdSec controls surface primary actions when enabled', async () => {
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatus)
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: false })
@@ -341,7 +330,7 @@ describe('Security Page - QA Security Audit', () => {
const cardNames = cards.map(card => card.textContent)
// Spec requirement from current_spec.md plus Live Security Logs feature
expect(cardNames).toEqual(['CrowdSec', 'Access Control', 'WAF (Coraza)', 'Rate Limiting', 'Live Security Logs'])
expect(cardNames).toEqual(['CrowdSec', 'Access Control', 'Coraza', 'Rate Limiting', 'Live Security Logs'])
})
it('layer indicators match spec descriptions', async () => {
@@ -186,125 +186,6 @@ describe('Security page', () => {
expect(crowdsecToggle).toBeDisabled()
})
it('shows WAF mode selector when WAF is enabled', async () => {
const status: SecurityStatus = {
cerberus: { enabled: true },
crowdsec: { enabled: false, mode: 'disabled' as const, api_url: '' },
waf: { enabled: true, mode: 'enabled' as const },
rate_limit: { enabled: false },
acl: { enabled: false },
}
vi.mocked(api.getSecurityStatus).mockResolvedValue(status as SecurityStatus)
vi.mocked(api.getSecurityConfig).mockResolvedValue(mockSecurityConfig)
vi.mocked(api.getRuleSets).mockResolvedValue(mockRuleSets)
renderWithProviders(<Security />)
await waitFor(() => expect(screen.getByTestId('waf-mode-select')).toBeInTheDocument())
// Check mode selector is present with correct options
const modeSelect = screen.getByTestId('waf-mode-select')
expect(modeSelect).toBeInTheDocument()
expect(modeSelect).toHaveValue('block')
})
it('shows WAF ruleset selector with available rulesets', async () => {
const status: SecurityStatus = {
cerberus: { enabled: true },
crowdsec: { enabled: false, mode: 'disabled' as const, api_url: '' },
waf: { enabled: true, mode: 'enabled' as const },
rate_limit: { enabled: false },
acl: { enabled: false },
}
vi.mocked(api.getSecurityStatus).mockResolvedValue(status as SecurityStatus)
vi.mocked(api.getSecurityConfig).mockResolvedValue(mockSecurityConfig)
vi.mocked(api.getRuleSets).mockResolvedValue(mockRuleSets)
renderWithProviders(<Security />)
await waitFor(() => expect(screen.getByTestId('waf-ruleset-select')).toBeInTheDocument())
// Check ruleset selector shows available rulesets
const rulesetSelect = screen.getByTestId('waf-ruleset-select')
expect(rulesetSelect).toBeInTheDocument()
// Verify options are present
expect(screen.getByText('None (all rule sets)')).toBeInTheDocument()
expect(screen.getByText('OWASP CRS (blocking)')).toBeInTheDocument()
expect(screen.getByText('Custom Rules (detection)')).toBeInTheDocument()
})
it('calls updateSecurityConfig when WAF mode is changed', async () => {
const status: SecurityStatus = {
cerberus: { enabled: true },
crowdsec: { enabled: false, mode: 'disabled' as const, api_url: '' },
waf: { enabled: true, mode: 'enabled' as const },
rate_limit: { enabled: false },
acl: { enabled: false },
}
vi.mocked(api.getSecurityStatus).mockResolvedValue(status as SecurityStatus)
vi.mocked(api.getSecurityConfig).mockResolvedValue(mockSecurityConfig)
vi.mocked(api.getRuleSets).mockResolvedValue(mockRuleSets)
vi.mocked(api.updateSecurityConfig).mockResolvedValue({})
renderWithProviders(<Security />)
await waitFor(() => expect(screen.getByTestId('waf-mode-select')).toBeInTheDocument())
// Change mode to monitor
const modeSelect = screen.getByTestId('waf-mode-select')
await userEvent.selectOptions(modeSelect, 'monitor')
await waitFor(() => {
expect(api.updateSecurityConfig).toHaveBeenCalledWith(
expect.objectContaining({ waf_mode: 'monitor' })
)
})
})
it('calls updateSecurityConfig when WAF ruleset is changed', async () => {
const status: SecurityStatus = {
cerberus: { enabled: true },
crowdsec: { enabled: false, mode: 'disabled' as const, api_url: '' },
waf: { enabled: true, mode: 'enabled' as const },
rate_limit: { enabled: false },
acl: { enabled: false },
}
vi.mocked(api.getSecurityStatus).mockResolvedValue(status as SecurityStatus)
vi.mocked(api.getSecurityConfig).mockResolvedValue(mockSecurityConfig)
vi.mocked(api.getRuleSets).mockResolvedValue(mockRuleSets)
vi.mocked(api.updateSecurityConfig).mockResolvedValue({})
renderWithProviders(<Security />)
await waitFor(() => expect(screen.getByTestId('waf-ruleset-select')).toBeInTheDocument())
// Select a specific ruleset
const rulesetSelect = screen.getByTestId('waf-ruleset-select')
await userEvent.selectOptions(rulesetSelect, 'OWASP CRS')
await waitFor(() => {
expect(api.updateSecurityConfig).toHaveBeenCalledWith(
expect.objectContaining({ waf_rules_source: 'OWASP CRS' })
)
})
})
it('shows warning when no rulesets are configured', async () => {
const status: SecurityStatus = {
cerberus: { enabled: true },
crowdsec: { enabled: false, mode: 'disabled' as const, api_url: '' },
waf: { enabled: true, mode: 'enabled' as const },
rate_limit: { enabled: false },
acl: { enabled: false },
}
vi.mocked(api.getSecurityStatus).mockResolvedValue(status as SecurityStatus)
vi.mocked(api.getSecurityConfig).mockResolvedValue(mockSecurityConfig)
vi.mocked(api.getRuleSets).mockResolvedValue({ rulesets: [] })
renderWithProviders(<Security />)
await waitFor(() => expect(screen.getByTestId('waf-ruleset-select')).toBeInTheDocument())
// Should show warning about no rulesets
expect(screen.getByText('No rule sets configured. Add one below.')).toBeInTheDocument()
})
it('displays correct WAF threat protection summary when enabled', async () => {
const status: SecurityStatus = {
cerberus: { enabled: true },
@@ -323,24 +204,4 @@ describe('Security page', () => {
// WAF now shows threat protection summary instead of mode text
await waitFor(() => expect(screen.getByText(/SQL injection, XSS, RCE/i)).toBeInTheDocument())
})
it('does not show WAF controls when WAF is disabled', async () => {
const status: SecurityStatus = {
cerberus: { enabled: true },
crowdsec: { enabled: false, mode: 'disabled' as const, api_url: '' },
waf: { enabled: false, mode: 'disabled' as const },
rate_limit: { enabled: false },
acl: { enabled: false },
}
vi.mocked(api.getSecurityStatus).mockResolvedValue(status as SecurityStatus)
vi.mocked(api.getSecurityConfig).mockResolvedValue(mockSecurityConfig)
vi.mocked(api.getRuleSets).mockResolvedValue(mockRuleSets)
renderWithProviders(<Security />)
await waitFor(() => expect(screen.getByText('Cerberus Dashboard')).toBeInTheDocument())
// Mode selector and ruleset selector should not be visible
expect(screen.queryByTestId('waf-mode-select')).not.toBeInTheDocument()
expect(screen.queryByTestId('waf-ruleset-select')).not.toBeInTheDocument()
})
})
+3 -35
View File
@@ -232,39 +232,7 @@ describe('Security', () => {
})
describe('WAF Controls', () => {
it('should change WAF mode', async () => {
const user = userEvent.setup()
const { useUpdateSecurityConfig } = await import('../../hooks/useSecurity')
const mockMutate = vi.fn()
vi.mocked(useUpdateSecurityConfig).mockReturnValue({ mutate: mockMutate, isPending: false } as unknown as ReturnType<typeof useUpdateSecurityConfig>)
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatus)
await renderSecurityPage()
await waitFor(() => screen.getByTestId('waf-mode-select'))
const select = screen.getByTestId('waf-mode-select')
await user.selectOptions(select, 'monitor')
await waitFor(() => expect(mockMutate).toHaveBeenCalledWith({ name: 'default', waf_mode: 'monitor' }))
})
it('should change WAF ruleset', async () => {
const user = userEvent.setup()
const { useUpdateSecurityConfig } = await import('../../hooks/useSecurity')
const mockMutate = vi.fn()
vi.mocked(useUpdateSecurityConfig).mockReturnValue({ mutate: mockMutate, isPending: false } as unknown as ReturnType<typeof useUpdateSecurityConfig>)
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatus)
await renderSecurityPage()
await waitFor(() => screen.getByTestId('waf-ruleset-select'))
const select = screen.getByTestId('waf-ruleset-select')
await user.selectOptions(select, 'OWASP CRS')
await waitFor(() => expect(mockMutate).toHaveBeenCalledWith({ name: 'default', waf_rules_source: 'OWASP CRS' }))
})
})
// Note: WAF Controls tests removed - dropdowns moved to dedicated WAF config page (/security/waf)
describe('Card Order (Pipeline Sequence)', () => {
it('should render cards in correct pipeline order: CrowdSec → ACL → WAF → Rate Limiting', async () => {
@@ -277,8 +245,8 @@ describe('Security', () => {
const cards = screen.getAllByRole('heading', { level: 3 })
const cardNames = cards.map(card => card.textContent)
// Verify pipeline order: CrowdSec (Layer 1) → ACL (Layer 2) → WAF (Layer 3) → Rate Limiting (Layer 4) + Live Security Logs
expect(cardNames).toEqual(['CrowdSec', 'Access Control', 'WAF (Coraza)', 'Rate Limiting', 'Live Security Logs'])
// Verify pipeline order: CrowdSec (Layer 1) → ACL (Layer 2) → Coraza (Layer 3) → Rate Limiting (Layer 4) + Live Security Logs
expect(cardNames).toEqual(['CrowdSec', 'Access Control', 'Coraza', 'Rate Limiting', 'Live Security Logs'])
})
it('should display layer indicators on each card', async () => {
+1 -1
View File
@@ -4,7 +4,7 @@
declare global { var IS_REACT_ACT_ENVIRONMENT: boolean | undefined }
globalThis.IS_REACT_ACT_ENVIRONMENT = true
import '@testing-library/jest-dom'
import '@testing-library/jest-dom/vitest'
import { cleanup } from '@testing-library/react'
import { afterEach } from 'vitest'