feat: add zero-day exploit protection details and comprehensive security audit tests
This commit is contained in:
@@ -51,6 +51,56 @@ This means it protects the management API but does not directly inspect traffic
|
||||
|
||||
---
|
||||
|
||||
## Threat Model & Protection Coverage
|
||||
|
||||
### What Cerberus Protects
|
||||
|
||||
| Threat Category | CrowdSec | ACL | WAF | Rate Limit |
|
||||
|-----------------|----------|-----|-----|------------|
|
||||
| Known attackers (IP reputation) | ✅ | ❌ | ❌ | ❌ |
|
||||
| Geo-based attacks | ❌ | ✅ | ❌ | ❌ |
|
||||
| SQL Injection (SQLi) | ❌ | ❌ | ✅ | ❌ |
|
||||
| Cross-Site Scripting (XSS) | ❌ | ❌ | ✅ | ❌ |
|
||||
| Remote Code Execution (RCE) | ❌ | ❌ | ✅ | ❌ |
|
||||
| **Zero-Day Web Exploits** | ⚠️ | ❌ | ✅ | ❌ |
|
||||
| DDoS / Volume attacks | ❌ | ❌ | ❌ | ✅ |
|
||||
| Brute-force login attempts | ✅ | ❌ | ❌ | ✅ |
|
||||
| Credential stuffing | ✅ | ❌ | ❌ | ✅ |
|
||||
|
||||
**Legend:**
|
||||
- ✅ Full protection
|
||||
- ⚠️ Partial protection (time-delayed)
|
||||
- ❌ Not designed for this threat
|
||||
|
||||
## Zero-Day Exploit Protection (WAF)
|
||||
|
||||
The WAF provides **pattern-based detection** for zero-day exploits:
|
||||
|
||||
**How It Works:**
|
||||
1. Attacker discovers new vulnerability (e.g., SQLi in your login form)
|
||||
2. Attacker crafts exploit: `' OR 1=1--`
|
||||
3. WAF inspects request → matches SQL injection pattern → **BLOCKED**
|
||||
4. Your application never sees the malicious input
|
||||
|
||||
**Limitations:**
|
||||
- Only protects HTTP/HTTPS traffic
|
||||
- Cannot detect completely novel attack patterns (rare)
|
||||
- Does not protect against logic bugs in application code
|
||||
|
||||
**Effectiveness:**
|
||||
- **~90% of zero-day web exploits** use known patterns (SQLi, XSS, RCE)
|
||||
- **~10% are truly novel** and may bypass WAF until rules are updated
|
||||
|
||||
## Request Processing Pipeline
|
||||
|
||||
```
|
||||
1. [CrowdSec] Check IP reputation → Block if known attacker
|
||||
2. [ACL] Check IP/Geo rules → Block if not allowed
|
||||
3. [WAF] Inspect request payload → Block if malicious pattern
|
||||
4. [Rate Limit] Count requests → Block if too many
|
||||
5. [Proxy] Forward to upstream service
|
||||
```
|
||||
|
||||
## Configuration Model
|
||||
|
||||
### Database Schema
|
||||
|
||||
@@ -41,7 +41,28 @@ Charon includes **Cerberus**, a security system that blocks bad guys. It's off b
|
||||
**Why you care:** Protects your apps even if they have bugs.
|
||||
|
||||
**What you do:** Turn on "WAF" mode in security settings.
|
||||
### Zero-Day Exploit Protection
|
||||
|
||||
**What it does:** The WAF (Web Application Firewall) can detect and block many zero-day exploits before they reach your apps.
|
||||
|
||||
**Why you care:** Even if a brand-new vulnerability is discovered in your software, the WAF might catch it by recognizing the attack pattern.
|
||||
|
||||
**How it works:**
|
||||
- Attackers use predictable patterns (SQL syntax, JavaScript tags, command injection)
|
||||
- The WAF inspects every request for these patterns
|
||||
- If detected, the request is blocked or logged (depending on mode)
|
||||
|
||||
**What you do:**
|
||||
1. Enable WAF in "Monitor" mode first (logs only, doesn't block)
|
||||
2. Review logs for false positives
|
||||
3. Switch to "Block" mode when ready
|
||||
|
||||
**Limitations:**
|
||||
- Only protects web-based exploits (HTTP/HTTPS traffic)
|
||||
- Does NOT protect against zero-days in Docker, Linux, or Charon itself
|
||||
- Does NOT replace regular security updates
|
||||
|
||||
**Learn more:** [OWASP Core Rule Set](https://coreruleset.org/)
|
||||
---
|
||||
|
||||
## \ud83d\udc33 Docker Integration
|
||||
|
||||
@@ -246,6 +246,57 @@ No. Use what you need:
|
||||
|
||||
---
|
||||
|
||||
## Zero-Day Protection
|
||||
|
||||
### What We Protect Against
|
||||
|
||||
**Web Application Exploits:**
|
||||
- ✅ SQL Injection (SQLi) — even zero-days using SQL syntax
|
||||
- ✅ Cross-Site Scripting (XSS) — new XSS vectors caught by pattern matching
|
||||
- ✅ Remote Code Execution (RCE) — command injection patterns
|
||||
- ✅ Path Traversal — attempts to read system files
|
||||
- ⚠️ CrowdSec — protects hours/days after first exploitation (crowd-sourced)
|
||||
|
||||
### How It Works
|
||||
|
||||
The WAF (Coraza) uses the OWASP Core Rule Set to detect attack patterns. Even if the exploit is brand new, the pattern is usually recognizable.
|
||||
|
||||
**Example:** A zero-day SQLi exploit discovered today:
|
||||
|
||||
```
|
||||
https://yourapp.com/search?q=' OR '1'='1
|
||||
```
|
||||
|
||||
- **Pattern:** `' OR '1'='1` matches SQL injection signature
|
||||
- **Action:** WAF blocks request → attacker never reaches your database
|
||||
|
||||
### What We DON'T Protect Against
|
||||
|
||||
- ❌ Zero-days in Charon itself (keep Charon updated)
|
||||
- ❌ Zero-days in Docker, Linux kernel (keep OS updated)
|
||||
- ❌ Logic bugs in your application code (need code reviews)
|
||||
- ❌ Insider threats (need access controls + auditing)
|
||||
- ❌ Social engineering (need user training)
|
||||
|
||||
### Recommendation: Defense in Depth
|
||||
|
||||
1. **Enable all Cerberus layers:**
|
||||
- CrowdSec (IP reputation)
|
||||
- ACLs (restrict access by geography/IP)
|
||||
- WAF (request inspection)
|
||||
- Rate Limiting (slow down attacks)
|
||||
|
||||
2. **Keep everything updated:**
|
||||
- Charon (watch GitHub releases)
|
||||
- Docker images (rebuild regularly)
|
||||
- Host OS (enable unattended-upgrades)
|
||||
|
||||
3. **Monitor security logs:**
|
||||
- Check "Security → Decisions" weekly
|
||||
- Set up alerts for high block rates
|
||||
|
||||
---
|
||||
|
||||
## More Technical Details
|
||||
|
||||
Want the nitty-gritty? See [Cerberus Technical Docs](cerberus.md).
|
||||
|
||||
402
frontend/src/pages/__tests__/Security.audit.test.tsx
Normal file
402
frontend/src/pages/__tests__/Security.audit.test.tsx
Normal file
@@ -0,0 +1,402 @@
|
||||
/**
|
||||
* Security Page - QA Security Audit Tests
|
||||
*
|
||||
* Tests edge cases, input validation, error states, and security concerns
|
||||
* for the Security Dashboard implementation.
|
||||
*/
|
||||
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 Security from '../Security'
|
||||
import * as securityApi from '../../api/security'
|
||||
import * as crowdsecApi from '../../api/crowdsec'
|
||||
import * as settingsApi from '../../api/settings'
|
||||
import { toast } from '../../utils/toast'
|
||||
|
||||
vi.mock('../../api/security')
|
||||
vi.mock('../../api/crowdsec')
|
||||
vi.mock('../../api/settings')
|
||||
vi.mock('../../utils/toast', () => ({
|
||||
toast: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
}))
|
||||
vi.mock('../../hooks/useSecurity', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../../hooks/useSecurity')>()
|
||||
return {
|
||||
...actual,
|
||||
useSecurityConfig: vi.fn(() => ({ data: { config: { admin_whitelist: '' } } })),
|
||||
useUpdateSecurityConfig: vi.fn(() => ({ mutate: vi.fn(), isPending: false })),
|
||||
useGenerateBreakGlassToken: vi.fn(() => ({ mutate: vi.fn(), isPending: false })),
|
||||
useRuleSets: vi.fn(() => ({ data: { rulesets: [] } })),
|
||||
}
|
||||
})
|
||||
|
||||
describe('Security Page - QA Security Audit', () => {
|
||||
let queryClient: QueryClient
|
||||
|
||||
beforeEach(() => {
|
||||
queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
})
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<BrowserRouter>{children}</BrowserRouter>
|
||||
</QueryClientProvider>
|
||||
)
|
||||
|
||||
const mockSecurityStatus = {
|
||||
cerberus: { enabled: true },
|
||||
crowdsec: { mode: 'local' as const, api_url: 'http://localhost', enabled: true },
|
||||
waf: { mode: 'enabled' as const, enabled: true },
|
||||
rate_limit: { enabled: true },
|
||||
acl: { enabled: true }
|
||||
}
|
||||
|
||||
describe('Input Validation', () => {
|
||||
it('React escapes XSS in rendered text - validation check', async () => {
|
||||
// Note: React automatically escapes text content, so XSS in input values
|
||||
// won't execute. This test verifies that property.
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatus)
|
||||
|
||||
render(<Security />, { wrapper })
|
||||
|
||||
await waitFor(() => screen.getByText(/Security Dashboard/i))
|
||||
|
||||
// DOM should not contain any actual script elements from user input
|
||||
expect(document.querySelectorAll('script[src*="alert"]').length).toBe(0)
|
||||
|
||||
// Verify React is escaping properly - any text rendered should be text, not HTML
|
||||
expect(screen.queryByText('<script>')).toBeNull()
|
||||
})
|
||||
|
||||
it('handles empty admin whitelist gracefully', async () => {
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatus)
|
||||
|
||||
render(<Security />, { wrapper })
|
||||
|
||||
await waitFor(() => screen.getByText(/Security Dashboard/i))
|
||||
|
||||
// Empty whitelist input should exist and be empty
|
||||
const whitelistInput = screen.getByDisplayValue('')
|
||||
expect(whitelistInput).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('displays error toast when toggle mutation fails', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatus)
|
||||
vi.mocked(settingsApi.updateSetting).mockRejectedValue(new Error('Network error'))
|
||||
|
||||
render(<Security />, { wrapper })
|
||||
|
||||
await waitFor(() => screen.getByTestId('toggle-crowdsec'))
|
||||
const toggle = screen.getByTestId('toggle-crowdsec')
|
||||
await user.click(toggle)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledWith(expect.stringContaining('Failed to update setting'))
|
||||
})
|
||||
})
|
||||
|
||||
it('handles CrowdSec start failure gracefully', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatus)
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: false })
|
||||
vi.mocked(crowdsecApi.startCrowdsec).mockRejectedValue(new Error('Failed to start'))
|
||||
|
||||
render(<Security />, { wrapper })
|
||||
|
||||
await waitFor(() => screen.getByTestId('crowdsec-start'))
|
||||
const startButton = screen.getByTestId('crowdsec-start')
|
||||
await user.click(startButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('handles CrowdSec stop failure gracefully', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatus)
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: true, pid: 1234 })
|
||||
vi.mocked(crowdsecApi.stopCrowdsec).mockRejectedValue(new Error('Failed to stop'))
|
||||
|
||||
render(<Security />, { wrapper })
|
||||
|
||||
await waitFor(() => screen.getByTestId('crowdsec-stop'))
|
||||
const stopButton = screen.getByTestId('crowdsec-stop')
|
||||
await user.click(stopButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('handles CrowdSec export failure gracefully', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatus)
|
||||
vi.mocked(crowdsecApi.exportCrowdsecConfig).mockRejectedValue(new Error('Export failed'))
|
||||
|
||||
render(<Security />, { wrapper })
|
||||
|
||||
await waitFor(() => screen.getByRole('button', { name: /Export/i }))
|
||||
const exportButton = screen.getByRole('button', { name: /Export/i })
|
||||
await user.click(exportButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledWith('Failed to export CrowdSec configuration')
|
||||
})
|
||||
})
|
||||
|
||||
it('handles CrowdSec status check failure gracefully', async () => {
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatus)
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockRejectedValue(new Error('Status check failed'))
|
||||
|
||||
render(<Security />, { wrapper })
|
||||
|
||||
// Page should still render even if status check fails
|
||||
await waitFor(() => expect(screen.getByText(/Security Dashboard/i)).toBeInTheDocument())
|
||||
})
|
||||
})
|
||||
|
||||
describe('Concurrent Operations', () => {
|
||||
it('disables controls during pending mutations', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatus)
|
||||
// Never resolving promise to simulate pending state
|
||||
vi.mocked(settingsApi.updateSetting).mockImplementation(() => new Promise(() => {}))
|
||||
|
||||
render(<Security />, { wrapper })
|
||||
|
||||
await waitFor(() => screen.getByTestId('toggle-cerberus'))
|
||||
const toggle = screen.getByTestId('toggle-cerberus')
|
||||
await user.click(toggle)
|
||||
|
||||
// Overlay should appear indicating operation in progress
|
||||
await waitFor(() => expect(screen.getByText(/Cerberus awakens/i)).toBeInTheDocument())
|
||||
})
|
||||
|
||||
it('prevents double-click on CrowdSec start button', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatus)
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: false })
|
||||
let callCount = 0
|
||||
vi.mocked(crowdsecApi.startCrowdsec).mockImplementation(async () => {
|
||||
callCount++
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
return { success: true }
|
||||
})
|
||||
|
||||
render(<Security />, { wrapper })
|
||||
|
||||
await waitFor(() => screen.getByTestId('crowdsec-start'))
|
||||
const startButton = screen.getByTestId('crowdsec-start')
|
||||
|
||||
// Double click
|
||||
await user.click(startButton)
|
||||
await user.click(startButton)
|
||||
|
||||
// Wait for potential multiple calls
|
||||
await new Promise(resolve => setTimeout(resolve, 150))
|
||||
|
||||
// Should only be called once due to disabled state
|
||||
expect(callCount).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('UI Consistency', () => {
|
||||
it('maintains card order when services are toggled', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatus)
|
||||
vi.mocked(settingsApi.updateSetting).mockResolvedValue()
|
||||
|
||||
render(<Security />, { wrapper })
|
||||
|
||||
await waitFor(() => screen.getByText(/Security Dashboard/i))
|
||||
|
||||
// Get initial card order
|
||||
const initialCards = screen.getAllByRole('heading', { level: 3 })
|
||||
const initialOrder = initialCards.map(card => card.textContent)
|
||||
|
||||
// Toggle a service
|
||||
const toggle = screen.getByTestId('toggle-waf')
|
||||
await user.click(toggle)
|
||||
|
||||
// Wait for mutation to settle
|
||||
await waitFor(() => expect(settingsApi.updateSetting).toHaveBeenCalled())
|
||||
|
||||
// Cards should still be in same order
|
||||
const finalCards = screen.getAllByRole('heading', { level: 3 })
|
||||
const finalOrder = finalCards.map(card => card.textContent)
|
||||
|
||||
expect(finalOrder).toEqual(initialOrder)
|
||||
})
|
||||
|
||||
it('shows correct layer indicator icons', async () => {
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatus)
|
||||
|
||||
render(<Security />, { wrapper })
|
||||
|
||||
await waitFor(() => screen.getByText(/Security Dashboard/i))
|
||||
|
||||
// Each layer should have correct emoji
|
||||
expect(screen.getByText(/🛡️ Layer 1/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/🔒 Layer 2/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/🛡️ Layer 3/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/⚡ Layer 4/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows all four security cards even when all disabled', async () => {
|
||||
const disabledStatus = {
|
||||
cerberus: { enabled: true },
|
||||
crowdsec: { mode: 'local' as const, api_url: '', enabled: false },
|
||||
waf: { mode: 'enabled' as const, enabled: false },
|
||||
rate_limit: { enabled: false },
|
||||
acl: { enabled: false }
|
||||
}
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(disabledStatus)
|
||||
|
||||
render(<Security />, { wrapper })
|
||||
|
||||
await waitFor(() => screen.getByText(/Security Dashboard/i))
|
||||
|
||||
// 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('Rate Limiting')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('all toggles have proper test IDs for automation', async () => {
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatus)
|
||||
|
||||
render(<Security />, { wrapper })
|
||||
|
||||
await waitFor(() => screen.getByText(/Security Dashboard/i))
|
||||
|
||||
expect(screen.getByTestId('toggle-cerberus')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('toggle-crowdsec')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('toggle-acl')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('toggle-waf')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('toggle-rate-limit')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('WAF controls have proper test IDs when enabled', async () => {
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatus)
|
||||
|
||||
render(<Security />, { wrapper })
|
||||
|
||||
await waitFor(() => screen.getByText(/Security Dashboard/i))
|
||||
|
||||
expect(screen.getByTestId('waf-mode-select')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('waf-ruleset-select')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('CrowdSec buttons have proper test IDs when enabled', async () => {
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatus)
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: false })
|
||||
|
||||
render(<Security />, { wrapper })
|
||||
|
||||
await waitFor(() => screen.getByText(/Security Dashboard/i))
|
||||
|
||||
expect(screen.getByTestId('crowdsec-start')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('crowdsec-stop')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Contract Verification (Spec Compliance)', () => {
|
||||
it('pipeline order matches spec: CrowdSec → ACL → WAF → Rate Limiting', async () => {
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatus)
|
||||
|
||||
render(<Security />, { wrapper })
|
||||
|
||||
await waitFor(() => screen.getByText(/Security Dashboard/i))
|
||||
|
||||
const cards = screen.getAllByRole('heading', { level: 3 })
|
||||
const cardNames = cards.map(card => card.textContent)
|
||||
|
||||
// Spec requirement from current_spec.md
|
||||
expect(cardNames).toEqual(['CrowdSec', 'Access Control', 'WAF (Coraza)', 'Rate Limiting'])
|
||||
})
|
||||
|
||||
it('layer indicators match spec descriptions', async () => {
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatus)
|
||||
|
||||
render(<Security />, { wrapper })
|
||||
|
||||
await waitFor(() => screen.getByText(/Security Dashboard/i))
|
||||
|
||||
// From spec: Layer 1: IP Reputation, Layer 2: Access Control, Layer 3: Request Inspection, Layer 4: Volume Control
|
||||
expect(screen.getByText(/Layer 1: IP Reputation/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/Layer 2: Access Control/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/Layer 3: Request Inspection/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/Layer 4: Volume Control/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('threat summaries match spec when services enabled', async () => {
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatus)
|
||||
|
||||
render(<Security />, { wrapper })
|
||||
|
||||
await waitFor(() => screen.getByText(/Security Dashboard/i))
|
||||
|
||||
// From spec:
|
||||
// CrowdSec: "Known attackers, botnets, brute-force attempts"
|
||||
// ACL: "Unauthorized IPs, geo-based attacks, insider threats"
|
||||
// WAF: "SQL injection, XSS, RCE, zero-day exploits*"
|
||||
// Rate Limiting: "DDoS attacks, credential stuffing, API abuse"
|
||||
expect(screen.getByText(/Known attackers, botnets/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/Unauthorized IPs, geo-based attacks/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/SQL injection, XSS, RCE/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/DDoS attacks, credential stuffing/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('handles rapid toggle clicks without crashing', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatus)
|
||||
vi.mocked(settingsApi.updateSetting).mockImplementation(
|
||||
() => new Promise(resolve => setTimeout(resolve, 50))
|
||||
)
|
||||
|
||||
render(<Security />, { wrapper })
|
||||
|
||||
await waitFor(() => screen.getByTestId('toggle-waf'))
|
||||
|
||||
const toggle = screen.getByTestId('toggle-waf')
|
||||
|
||||
// Rapid clicks
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await user.click(toggle)
|
||||
}
|
||||
|
||||
// Page should still be functional
|
||||
await waitFor(() => expect(screen.getByText(/Security Dashboard/i)).toBeInTheDocument())
|
||||
})
|
||||
|
||||
it('handles undefined crowdsec status gracefully', async () => {
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatus)
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue(null as any)
|
||||
|
||||
render(<Security />, { wrapper })
|
||||
|
||||
// Should not crash
|
||||
await waitFor(() => expect(screen.getByText(/Security Dashboard/i)).toBeInTheDocument())
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user