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:
@@ -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
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user