chore: clean .gitignore cache
This commit is contained in:
@@ -1,182 +0,0 @@
|
||||
/**
|
||||
* ACL Enforcement Tests
|
||||
*
|
||||
* Tests that verify the Access Control List (ACL) module correctly blocks/allows
|
||||
* requests based on IP whitelist and blacklist rules.
|
||||
*
|
||||
* Pattern: Toggle-On-Test-Toggle-Off
|
||||
* - Enable ACL at start of describe block
|
||||
* - Run enforcement tests
|
||||
* - Disable ACL in afterAll (handled by security-teardown project)
|
||||
*
|
||||
* @see /projects/Charon/docs/plans/current_spec.md - ACL Enforcement Tests
|
||||
*/
|
||||
|
||||
import { test, expect } from '@bgotink/playwright-coverage';
|
||||
import { request } from '@playwright/test';
|
||||
import type { APIRequestContext } from '@playwright/test';
|
||||
import { STORAGE_STATE } from '../constants';
|
||||
import {
|
||||
getSecurityStatus,
|
||||
setSecurityModuleEnabled,
|
||||
captureSecurityState,
|
||||
restoreSecurityState,
|
||||
CapturedSecurityState,
|
||||
} from '../utils/security-helpers';
|
||||
|
||||
test.describe('ACL Enforcement', () => {
|
||||
let requestContext: APIRequestContext;
|
||||
let originalState: CapturedSecurityState;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
requestContext = await request.newContext({
|
||||
baseURL: process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:8080',
|
||||
storageState: STORAGE_STATE,
|
||||
});
|
||||
|
||||
// Capture original state
|
||||
try {
|
||||
originalState = await captureSecurityState(requestContext);
|
||||
} catch (error) {
|
||||
console.error('Failed to capture original security state:', error);
|
||||
}
|
||||
|
||||
// Enable Cerberus (master toggle) first
|
||||
try {
|
||||
await setSecurityModuleEnabled(requestContext, 'cerberus', true);
|
||||
console.log('✓ Cerberus enabled');
|
||||
} catch (error) {
|
||||
console.error('Failed to enable Cerberus:', error);
|
||||
}
|
||||
|
||||
// Enable ACL
|
||||
try {
|
||||
await setSecurityModuleEnabled(requestContext, 'acl', true);
|
||||
console.log('✓ ACL enabled');
|
||||
} catch (error) {
|
||||
console.error('Failed to enable ACL:', error);
|
||||
}
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
// Restore original state
|
||||
if (originalState) {
|
||||
try {
|
||||
await restoreSecurityState(requestContext, originalState);
|
||||
console.log('✓ Security state restored');
|
||||
} catch (error) {
|
||||
console.error('Failed to restore security state:', error);
|
||||
// Emergency disable ACL to prevent deadlock
|
||||
try {
|
||||
await setSecurityModuleEnabled(requestContext, 'acl', false);
|
||||
await setSecurityModuleEnabled(requestContext, 'cerberus', false);
|
||||
} catch {
|
||||
console.error('Emergency ACL disable also failed');
|
||||
}
|
||||
}
|
||||
}
|
||||
await requestContext.dispose();
|
||||
});
|
||||
|
||||
test('should verify ACL is enabled', async () => {
|
||||
const status = await getSecurityStatus(requestContext);
|
||||
expect(status.acl.enabled).toBe(true);
|
||||
expect(status.cerberus.enabled).toBe(true);
|
||||
});
|
||||
|
||||
test('should return security status with ACL mode', async () => {
|
||||
const response = await requestContext.get('/api/v1/security/status');
|
||||
expect(response.ok()).toBe(true);
|
||||
|
||||
const status = await response.json();
|
||||
expect(status.acl).toBeDefined();
|
||||
expect(status.acl.mode).toBeDefined();
|
||||
expect(typeof status.acl.enabled).toBe('boolean');
|
||||
});
|
||||
|
||||
test('should list access lists when ACL enabled', async () => {
|
||||
const response = await requestContext.get('/api/v1/access-lists');
|
||||
expect(response.ok()).toBe(true);
|
||||
|
||||
const data = await response.json();
|
||||
expect(Array.isArray(data)).toBe(true);
|
||||
});
|
||||
|
||||
test('should test IP against access list', async () => {
|
||||
// First, get the list of access lists
|
||||
const listResponse = await requestContext.get('/api/v1/access-lists');
|
||||
expect(listResponse.ok()).toBe(true);
|
||||
|
||||
const lists = await listResponse.json();
|
||||
|
||||
// If there are any access lists, test an IP against the first one
|
||||
if (lists.length > 0) {
|
||||
const testIp = '192.168.1.1';
|
||||
const testResponse = await requestContext.get(
|
||||
`/api/v1/access-lists/${lists[0].id}/test?ip=${testIp}`
|
||||
);
|
||||
expect(testResponse.ok()).toBe(true);
|
||||
|
||||
const result = await testResponse.json();
|
||||
expect(typeof result.allowed).toBe('boolean');
|
||||
} else {
|
||||
// No access lists exist - this is valid, just log it
|
||||
console.log('No access lists exist to test against');
|
||||
}
|
||||
});
|
||||
|
||||
test('should show correct error response format for blocked requests', async () => {
|
||||
// Create a temporary blacklist with test IP, make blocked request, then cleanup
|
||||
// For now, verify the error message format from the blocked response
|
||||
|
||||
// This test verifies the error handling structure exists
|
||||
// The actual blocking test would require:
|
||||
// 1. Create blacklist entry with test IP
|
||||
// 2. Make request from that IP (requires proxy setup)
|
||||
// 3. Verify 403 with "Blocked by access control list" message
|
||||
// 4. Delete blacklist entry
|
||||
|
||||
// Instead, we verify the API structure for ACL CRUD
|
||||
const createResponse = await requestContext.post('/api/v1/access-lists', {
|
||||
data: {
|
||||
name: 'Test Enforcement ACL',
|
||||
satisfy: 'any',
|
||||
pass_auth: false,
|
||||
items: [
|
||||
{
|
||||
type: 'deny',
|
||||
address: '10.255.255.255/32',
|
||||
directive: 'deny',
|
||||
comment: 'Test blocked IP',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
if (createResponse.ok()) {
|
||||
const createdList = await createResponse.json();
|
||||
expect(createdList.id).toBeDefined();
|
||||
|
||||
// Verify the list was created with correct structure
|
||||
expect(createdList.name).toBe('Test Enforcement ACL');
|
||||
|
||||
// Test IP against the list
|
||||
const testResponse = await requestContext.get(
|
||||
`/api/v1/access-lists/${createdList.id}/test?ip=10.255.255.255`
|
||||
);
|
||||
expect(testResponse.ok()).toBe(true);
|
||||
const testResult = await testResponse.json();
|
||||
expect(testResult.allowed).toBe(false);
|
||||
|
||||
// Cleanup: Delete the test ACL
|
||||
const deleteResponse = await requestContext.delete(
|
||||
`/api/v1/access-lists/${createdList.id}`
|
||||
);
|
||||
expect(deleteResponse.ok()).toBe(true);
|
||||
} else {
|
||||
// May fail if ACL already exists or other issue
|
||||
const errorBody = await createResponse.text();
|
||||
console.log(`Note: Could not create test ACL: ${errorBody}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,225 +0,0 @@
|
||||
/**
|
||||
* Combined Security Enforcement Tests
|
||||
*
|
||||
* Tests that verify multiple security modules working together,
|
||||
* settings persistence, and audit logging integration.
|
||||
*
|
||||
* Pattern: Toggle-On-Test-Toggle-Off
|
||||
*
|
||||
* @see /projects/Charon/docs/plans/current_spec.md - Combined Enforcement Tests
|
||||
*/
|
||||
|
||||
import { test, expect } from '@bgotink/playwright-coverage';
|
||||
import { request } from '@playwright/test';
|
||||
import type { APIRequestContext } from '@playwright/test';
|
||||
import { STORAGE_STATE } from '../constants';
|
||||
import {
|
||||
getSecurityStatus,
|
||||
setSecurityModuleEnabled,
|
||||
captureSecurityState,
|
||||
restoreSecurityState,
|
||||
CapturedSecurityState,
|
||||
SecurityStatus,
|
||||
} from '../utils/security-helpers';
|
||||
|
||||
test.describe('Combined Security Enforcement', () => {
|
||||
let requestContext: APIRequestContext;
|
||||
let originalState: CapturedSecurityState;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
requestContext = await request.newContext({
|
||||
baseURL: process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:8080',
|
||||
storageState: STORAGE_STATE,
|
||||
});
|
||||
|
||||
// Capture original state
|
||||
try {
|
||||
originalState = await captureSecurityState(requestContext);
|
||||
} catch (error) {
|
||||
console.error('Failed to capture original security state:', error);
|
||||
}
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
// Restore original state
|
||||
if (originalState) {
|
||||
try {
|
||||
await restoreSecurityState(requestContext, originalState);
|
||||
console.log('✓ Security state restored');
|
||||
} catch (error) {
|
||||
console.error('Failed to restore security state:', error);
|
||||
// Emergency disable all
|
||||
try {
|
||||
await setSecurityModuleEnabled(requestContext, 'acl', false);
|
||||
await setSecurityModuleEnabled(requestContext, 'waf', false);
|
||||
await setSecurityModuleEnabled(requestContext, 'rateLimit', false);
|
||||
await setSecurityModuleEnabled(requestContext, 'crowdsec', false);
|
||||
await setSecurityModuleEnabled(requestContext, 'cerberus', false);
|
||||
} catch {
|
||||
console.error('Emergency security disable also failed');
|
||||
}
|
||||
}
|
||||
}
|
||||
await requestContext.dispose();
|
||||
});
|
||||
|
||||
test('should enable all security modules simultaneously', async () => {
|
||||
// This test verifies that all security modules can be enabled together.
|
||||
// Due to parallel test execution and shared database state, we need to be
|
||||
// resilient to timing issues. We enable modules sequentially and verify
|
||||
// each setting was saved before proceeding.
|
||||
|
||||
// Enable Cerberus first (master toggle) and verify
|
||||
await setSecurityModuleEnabled(requestContext, 'cerberus', true);
|
||||
|
||||
// Wait for Cerberus to be enabled before enabling sub-modules
|
||||
let status = await getSecurityStatus(requestContext);
|
||||
let cerberusRetries = 5;
|
||||
while (!status.cerberus.enabled && cerberusRetries > 0) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
status = await getSecurityStatus(requestContext);
|
||||
cerberusRetries--;
|
||||
}
|
||||
|
||||
// If Cerberus still not enabled after retries, test environment may have
|
||||
// shared state issues (parallel tests resetting security settings).
|
||||
// Skip the dependent assertions rather than fail flakily.
|
||||
if (!status.cerberus.enabled) {
|
||||
console.log('⚠ Cerberus could not be enabled - possible test isolation issue in parallel execution');
|
||||
test.skip();
|
||||
return;
|
||||
}
|
||||
|
||||
// Enable all sub-modules
|
||||
await setSecurityModuleEnabled(requestContext, 'acl', true);
|
||||
await setSecurityModuleEnabled(requestContext, 'waf', true);
|
||||
await setSecurityModuleEnabled(requestContext, 'rateLimit', true);
|
||||
await setSecurityModuleEnabled(requestContext, 'crowdsec', true);
|
||||
|
||||
// Verify all are enabled with retry logic for timing tolerance
|
||||
const allModulesEnabled = (s: SecurityStatus) =>
|
||||
s.cerberus.enabled && s.acl.enabled && s.waf.enabled &&
|
||||
s.rate_limit.enabled && s.crowdsec.enabled;
|
||||
|
||||
status = await getSecurityStatus(requestContext);
|
||||
let retries = 5;
|
||||
while (!allModulesEnabled(status) && retries > 0) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
status = await getSecurityStatus(requestContext);
|
||||
retries--;
|
||||
}
|
||||
|
||||
expect(status.cerberus.enabled).toBe(true);
|
||||
expect(status.acl.enabled).toBe(true);
|
||||
expect(status.waf.enabled).toBe(true);
|
||||
expect(status.rate_limit.enabled).toBe(true);
|
||||
expect(status.crowdsec.enabled).toBe(true);
|
||||
|
||||
console.log('✓ All security modules enabled simultaneously');
|
||||
});
|
||||
|
||||
test('should log security events to audit log', async () => {
|
||||
// Make a settings change to trigger audit log entry
|
||||
await setSecurityModuleEnabled(requestContext, 'cerberus', true);
|
||||
await setSecurityModuleEnabled(requestContext, 'acl', true);
|
||||
|
||||
// Wait a moment for audit log to be written
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
// Fetch audit logs
|
||||
const response = await requestContext.get('/api/v1/security/audit-logs');
|
||||
|
||||
if (response.ok()) {
|
||||
const logs = await response.json();
|
||||
expect(Array.isArray(logs) || logs.items !== undefined).toBe(true);
|
||||
|
||||
// Verify structure (may be empty if audit logging not configured)
|
||||
console.log(`✓ Audit log endpoint accessible, ${Array.isArray(logs) ? logs.length : logs.items?.length || 0} entries`);
|
||||
} else {
|
||||
// Audit logs may require additional configuration
|
||||
console.log(`Audit logs endpoint returned ${response.status()}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('should handle rapid module toggle without race conditions', async () => {
|
||||
// Enable Cerberus first
|
||||
await setSecurityModuleEnabled(requestContext, 'cerberus', true);
|
||||
|
||||
// Rapidly toggle ACL on/off
|
||||
const toggles = 5;
|
||||
for (let i = 0; i < toggles; i++) {
|
||||
await requestContext.post('/api/v1/settings', {
|
||||
data: { key: 'security.acl.enabled', value: i % 2 === 0 ? 'true' : 'false' },
|
||||
});
|
||||
}
|
||||
|
||||
// Final toggle leaves ACL in known state (i=4 sets 'true')
|
||||
// Wait with retry for state to propagate
|
||||
let status = await getSecurityStatus(requestContext);
|
||||
let retries = 5;
|
||||
while (!status.acl.enabled && retries > 0) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
status = await getSecurityStatus(requestContext);
|
||||
retries--;
|
||||
}
|
||||
|
||||
// After 5 toggles (0,1,2,3,4), final state is i=4 which sets 'true'
|
||||
expect(status.acl.enabled).toBe(true);
|
||||
|
||||
console.log('✓ Rapid toggle completed without race conditions');
|
||||
});
|
||||
|
||||
test('should persist settings across API calls', async () => {
|
||||
// Enable a specific configuration
|
||||
await setSecurityModuleEnabled(requestContext, 'cerberus', true);
|
||||
await setSecurityModuleEnabled(requestContext, 'waf', true);
|
||||
await setSecurityModuleEnabled(requestContext, 'acl', false);
|
||||
|
||||
// Create a new request context to simulate fresh session
|
||||
const freshContext = await request.newContext({
|
||||
baseURL: process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:8080',
|
||||
storageState: STORAGE_STATE,
|
||||
});
|
||||
|
||||
try {
|
||||
const status = await getSecurityStatus(freshContext);
|
||||
|
||||
expect(status.cerberus.enabled).toBe(true);
|
||||
expect(status.waf.enabled).toBe(true);
|
||||
expect(status.acl.enabled).toBe(false);
|
||||
|
||||
console.log('✓ Settings persisted across API calls');
|
||||
} finally {
|
||||
await freshContext.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
test('should enforce correct priority when multiple modules enabled', async () => {
|
||||
// Enable all modules
|
||||
await setSecurityModuleEnabled(requestContext, 'cerberus', true);
|
||||
await setSecurityModuleEnabled(requestContext, 'acl', true);
|
||||
await setSecurityModuleEnabled(requestContext, 'waf', true);
|
||||
await setSecurityModuleEnabled(requestContext, 'rateLimit', true);
|
||||
|
||||
// Verify security status shows all enabled
|
||||
const status = await getSecurityStatus(requestContext);
|
||||
|
||||
expect(status.cerberus.enabled).toBe(true);
|
||||
expect(status.acl.enabled).toBe(true);
|
||||
expect(status.waf.enabled).toBe(true);
|
||||
expect(status.rate_limit.enabled).toBe(true);
|
||||
|
||||
// The actual priority enforcement is:
|
||||
// Layer 1: CrowdSec (IP reputation/bans)
|
||||
// Layer 2: ACL (IP whitelist/blacklist)
|
||||
// Layer 3: WAF (attack patterns)
|
||||
// Layer 4: Rate Limiting (threshold enforcement)
|
||||
//
|
||||
// A blocked request at Layer 1 never reaches Layer 2-4
|
||||
// This is enforced at the Caddy/middleware level
|
||||
|
||||
console.log(
|
||||
'✓ Multiple modules enabled - priority enforcement is at middleware level'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,116 +0,0 @@
|
||||
/**
|
||||
* CrowdSec Enforcement Tests
|
||||
*
|
||||
* Tests that verify CrowdSec integration for IP reputation and ban management.
|
||||
*
|
||||
* Pattern: Toggle-On-Test-Toggle-Off
|
||||
*
|
||||
* @see /projects/Charon/docs/plans/current_spec.md - CrowdSec Enforcement Tests
|
||||
*/
|
||||
|
||||
import { test, expect } from '@bgotink/playwright-coverage';
|
||||
import { request } from '@playwright/test';
|
||||
import type { APIRequestContext } from '@playwright/test';
|
||||
import { STORAGE_STATE } from '../constants';
|
||||
import {
|
||||
getSecurityStatus,
|
||||
setSecurityModuleEnabled,
|
||||
captureSecurityState,
|
||||
restoreSecurityState,
|
||||
CapturedSecurityState,
|
||||
} from '../utils/security-helpers';
|
||||
|
||||
test.describe('CrowdSec Enforcement', () => {
|
||||
let requestContext: APIRequestContext;
|
||||
let originalState: CapturedSecurityState;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
requestContext = await request.newContext({
|
||||
baseURL: process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:8080',
|
||||
storageState: STORAGE_STATE,
|
||||
});
|
||||
|
||||
// Capture original state
|
||||
try {
|
||||
originalState = await captureSecurityState(requestContext);
|
||||
} catch (error) {
|
||||
console.error('Failed to capture original security state:', error);
|
||||
}
|
||||
|
||||
// Enable Cerberus (master toggle) first
|
||||
try {
|
||||
await setSecurityModuleEnabled(requestContext, 'cerberus', true);
|
||||
console.log('✓ Cerberus enabled');
|
||||
} catch (error) {
|
||||
console.error('Failed to enable Cerberus:', error);
|
||||
}
|
||||
|
||||
// Enable CrowdSec
|
||||
try {
|
||||
await setSecurityModuleEnabled(requestContext, 'crowdsec', true);
|
||||
console.log('✓ CrowdSec enabled');
|
||||
} catch (error) {
|
||||
console.error('Failed to enable CrowdSec:', error);
|
||||
}
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
// Restore original state
|
||||
if (originalState) {
|
||||
try {
|
||||
await restoreSecurityState(requestContext, originalState);
|
||||
console.log('✓ Security state restored');
|
||||
} catch (error) {
|
||||
console.error('Failed to restore security state:', error);
|
||||
// Emergency disable
|
||||
try {
|
||||
await setSecurityModuleEnabled(requestContext, 'crowdsec', false);
|
||||
await setSecurityModuleEnabled(requestContext, 'cerberus', false);
|
||||
} catch {
|
||||
console.error('Emergency CrowdSec disable also failed');
|
||||
}
|
||||
}
|
||||
}
|
||||
await requestContext.dispose();
|
||||
});
|
||||
|
||||
test('should verify CrowdSec is enabled', async () => {
|
||||
const status = await getSecurityStatus(requestContext);
|
||||
expect(status.crowdsec.enabled).toBe(true);
|
||||
expect(status.cerberus.enabled).toBe(true);
|
||||
});
|
||||
|
||||
test('should list CrowdSec decisions', async () => {
|
||||
const response = await requestContext.get('/api/v1/security/decisions');
|
||||
|
||||
// CrowdSec may not be fully configured in test environment
|
||||
if (response.ok()) {
|
||||
const decisions = await response.json();
|
||||
expect(Array.isArray(decisions) || decisions.decisions !== undefined).toBe(
|
||||
true
|
||||
);
|
||||
} else {
|
||||
// 500/502/503 is acceptable if CrowdSec LAPI is not running
|
||||
const errorText = await response.text();
|
||||
console.log(
|
||||
`CrowdSec LAPI not available (expected in test env): ${response.status()} - ${errorText}`
|
||||
);
|
||||
expect([500, 502, 503]).toContain(response.status());
|
||||
}
|
||||
});
|
||||
|
||||
test('should return CrowdSec status with mode and API URL', async () => {
|
||||
const response = await requestContext.get('/api/v1/security/status');
|
||||
expect(response.ok()).toBe(true);
|
||||
|
||||
const status = await response.json();
|
||||
expect(status.crowdsec).toBeDefined();
|
||||
expect(typeof status.crowdsec.enabled).toBe('boolean');
|
||||
expect(status.crowdsec.mode).toBeDefined();
|
||||
|
||||
// API URL may be present when configured
|
||||
if (status.crowdsec.api_url) {
|
||||
expect(typeof status.crowdsec.api_url).toBe('string');
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,83 +0,0 @@
|
||||
/**
|
||||
* Emergency Security Reset (Break-Glass) E2E Tests
|
||||
*
|
||||
* Tests the emergency reset endpoint that bypasses ACL and disables all security
|
||||
* modules. This is a break-glass mechanism for recovery when locked out.
|
||||
*
|
||||
* @see POST /api/v1/emergency/security-reset
|
||||
*/
|
||||
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Emergency Security Reset (Break-Glass)', () => {
|
||||
const EMERGENCY_TOKEN = process.env.CHARON_EMERGENCY_TOKEN || 'test-emergency-token-for-e2e-32chars';
|
||||
|
||||
test('should reset security when called with valid token', async ({ request }) => {
|
||||
const response = await request.post('/api/v1/emergency/security-reset', {
|
||||
headers: {
|
||||
'X-Emergency-Token': EMERGENCY_TOKEN,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data: { reason: 'E2E test validation' },
|
||||
});
|
||||
|
||||
expect(response.ok()).toBeTruthy();
|
||||
const body = await response.json();
|
||||
expect(body.success).toBe(true);
|
||||
expect(body.disabled_modules).toContain('security.acl.enabled');
|
||||
expect(body.disabled_modules).toContain('feature.cerberus.enabled');
|
||||
});
|
||||
|
||||
test('should reject request with invalid token', async ({ request }) => {
|
||||
const response = await request.post('/api/v1/emergency/security-reset', {
|
||||
headers: {
|
||||
'X-Emergency-Token': 'invalid-token-here',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status()).toBe(401);
|
||||
});
|
||||
|
||||
test('should reject request without token', async ({ request }) => {
|
||||
const response = await request.post('/api/v1/emergency/security-reset');
|
||||
expect(response.status()).toBe(401);
|
||||
});
|
||||
|
||||
test('should allow recovery when ACL blocks everything', async ({ request }) => {
|
||||
// This test verifies the emergency reset works when normal API is blocked
|
||||
// Pre-condition: ACL must be enabled and blocking requests
|
||||
// The emergency endpoint should still work because it bypasses ACL
|
||||
|
||||
// Attempt emergency reset - should succeed even if ACL is blocking
|
||||
const response = await request.post('/api/v1/emergency/security-reset', {
|
||||
headers: {
|
||||
'X-Emergency-Token': EMERGENCY_TOKEN,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data: { reason: 'E2E test - ACL recovery validation' },
|
||||
});
|
||||
|
||||
// Verify reset was successful
|
||||
expect(response.ok()).toBeTruthy();
|
||||
const body = await response.json();
|
||||
expect(body.success).toBe(true);
|
||||
expect(body.disabled_modules).toContain('security.acl.enabled');
|
||||
});
|
||||
|
||||
// Rate limit test runs LAST to avoid blocking subsequent tests
|
||||
test.skip('should rate limit after 5 attempts', async ({ request }) => {
|
||||
// Rate limiting is covered in emergency-token.spec.ts (Test 2), which also
|
||||
// waits for the limiter window to reset to avoid affecting subsequent specs.
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await request.post('/api/v1/emergency/security-reset', {
|
||||
headers: { 'X-Emergency-Token': 'wrong' },
|
||||
});
|
||||
}
|
||||
|
||||
const response = await request.post('/api/v1/emergency/security-reset', {
|
||||
headers: { 'X-Emergency-Token': 'wrong' },
|
||||
});
|
||||
expect(response.status()).toBe(429);
|
||||
});
|
||||
});
|
||||
@@ -1,292 +0,0 @@
|
||||
/**
|
||||
* Emergency Token Break Glass Protocol Tests
|
||||
*
|
||||
* Tests the 3-tier break glass architecture for emergency access recovery.
|
||||
* Validates that the emergency token can bypass all security controls when
|
||||
* an administrator is locked out.
|
||||
*
|
||||
* Reference: docs/plans/break_glass_protocol_redesign.md
|
||||
*/
|
||||
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { TestDataManager } from '../utils/TestDataManager';
|
||||
import { EMERGENCY_TOKEN, enableSecurity, waitForSecurityPropagation } from '../fixtures/security';
|
||||
|
||||
test.describe('Emergency Token Break Glass Protocol', () => {
|
||||
test('Test 1: Emergency token bypasses ACL', async ({ request }) => {
|
||||
const testData = new TestDataManager(request, 'emergency-token-bypass-acl');
|
||||
|
||||
try {
|
||||
// Step 1: Enable Cerberus security suite
|
||||
await request.post('/api/v1/settings', {
|
||||
data: { key: 'feature.cerberus.enabled', value: 'true' },
|
||||
});
|
||||
|
||||
// Step 2: Create restrictive ACL (whitelist only 192.168.1.0/24)
|
||||
const { id: aclId } = await testData.createAccessList({
|
||||
name: 'test-restrictive-acl',
|
||||
type: 'whitelist',
|
||||
ipRules: [{ cidr: '192.168.1.0/24', description: 'Restricted test network' }],
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
// Step 3: Enable ACL globally
|
||||
await request.post('/api/v1/settings', {
|
||||
data: { key: 'security.acl.enabled', value: 'true' },
|
||||
});
|
||||
|
||||
await waitForSecurityPropagation(3000);
|
||||
|
||||
// Step 4: Verify ACL is blocking regular requests
|
||||
const blockedResponse = await request.get('/api/v1/proxy-hosts');
|
||||
expect(blockedResponse.status()).toBe(403);
|
||||
const blockedBody = await blockedResponse.json();
|
||||
expect(blockedBody.error).toContain('Blocked by access control');
|
||||
|
||||
// Step 5: Use emergency token to disable security
|
||||
const emergencyResponse = await request.post('/api/v1/emergency/security-reset', {
|
||||
headers: {
|
||||
'X-Emergency-Token': EMERGENCY_TOKEN,
|
||||
},
|
||||
});
|
||||
|
||||
expect(emergencyResponse.status()).toBe(200);
|
||||
const emergencyBody = await emergencyResponse.json();
|
||||
expect(emergencyBody.success).toBe(true);
|
||||
expect(emergencyBody.disabled_modules).toBeDefined();
|
||||
expect(emergencyBody.disabled_modules).toContain('security.acl.enabled');
|
||||
expect(emergencyBody.disabled_modules).toContain('feature.cerberus.enabled');
|
||||
|
||||
await waitForSecurityPropagation(3000);
|
||||
|
||||
// Step 6: Verify ACL is now disabled - requests should succeed
|
||||
const allowedResponse = await request.get('/api/v1/proxy-hosts');
|
||||
expect(allowedResponse.ok()).toBeTruthy();
|
||||
|
||||
console.log('✅ Test 1 passed: Emergency token successfully bypassed ACL');
|
||||
} finally {
|
||||
await testData.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test('Test 2: Emergency endpoint has NO rate limiting', async ({ request }) => {
|
||||
console.log('🧪 Verifying emergency endpoint has no rate limiting...');
|
||||
console.log(' ℹ️ Emergency endpoints are "break-glass" - they must work immediately without artificial delays');
|
||||
|
||||
const wrongToken = 'wrong-token-for-no-rate-limit-test-32chars';
|
||||
|
||||
// Make 10 rapid attempts with wrong token to verify NO rate limiting applied
|
||||
const responses = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const response = await request.post('/api/v1/emergency/security-reset', {
|
||||
headers: { 'X-Emergency-Token': wrongToken },
|
||||
});
|
||||
responses.push(response);
|
||||
}
|
||||
|
||||
// ALL requests should be unauthorized (401), NONE should be rate limited (429)
|
||||
for (let i = 0; i < responses.length; i++) {
|
||||
expect(responses[i].status()).toBe(401);
|
||||
const body = await responses[i].json();
|
||||
expect(body.error).toBe('unauthorized');
|
||||
}
|
||||
|
||||
console.log(`✅ Test 2 passed: No rate limiting on emergency endpoint (${responses.length} rapid requests all got 401, not 429)`);
|
||||
console.log(' ℹ️ Emergency endpoints protected by: token validation + IP restrictions + audit logging');
|
||||
});
|
||||
|
||||
test('Test 3: Emergency token requires valid token', async ({ request }) => {
|
||||
console.log('🧪 Testing emergency token validation...');
|
||||
|
||||
// Test with wrong token
|
||||
const wrongResponse = await request.post('/api/v1/emergency/security-reset', {
|
||||
headers: { 'X-Emergency-Token': 'invalid-token-that-should-not-work-32chars' },
|
||||
});
|
||||
|
||||
expect(wrongResponse.status()).toBe(401);
|
||||
const wrongBody = await wrongResponse.json();
|
||||
expect(wrongBody.error).toBe('unauthorized');
|
||||
|
||||
// Verify settings were NOT changed by checking status
|
||||
const statusResponse = await request.get('/api/v1/security/status');
|
||||
if (statusResponse.ok()) {
|
||||
const status = await statusResponse.json();
|
||||
// If security was previously enabled, it should still be enabled
|
||||
console.log(' ✓ Security settings were not modified by invalid token');
|
||||
}
|
||||
|
||||
console.log('✅ Test 3 passed: Invalid token properly rejected');
|
||||
});
|
||||
|
||||
test('Test 4: Emergency token audit logging', async ({ request }) => {
|
||||
console.log('🧪 Testing emergency token audit logging...');
|
||||
|
||||
// Use valid emergency token
|
||||
const emergencyResponse = await request.post('/api/v1/emergency/security-reset', {
|
||||
headers: { 'X-Emergency-Token': EMERGENCY_TOKEN },
|
||||
});
|
||||
|
||||
expect(emergencyResponse.ok()).toBeTruthy();
|
||||
|
||||
// Wait for audit log to be written
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// Check audit logs for emergency event
|
||||
const auditResponse = await request.get('/api/v1/audit-logs');
|
||||
expect(auditResponse.ok()).toBeTruthy();
|
||||
|
||||
const auditPayload = await auditResponse.json();
|
||||
const auditLogs = Array.isArray(auditPayload)
|
||||
? auditPayload
|
||||
: Array.isArray(auditPayload?.audit_logs)
|
||||
? auditPayload.audit_logs
|
||||
: [];
|
||||
|
||||
// Look for emergency reset event
|
||||
const emergencyLog = auditLogs.find(
|
||||
(log: any) =>
|
||||
log.action === 'emergency_reset_success' || log.details?.includes('emergency')
|
||||
);
|
||||
|
||||
// Audit logging should capture the event
|
||||
console.log(
|
||||
` ${emergencyLog ? '✓' : '⚠'} Audit log ${emergencyLog ? 'found' : 'not found'} for emergency event`
|
||||
);
|
||||
|
||||
if (emergencyLog) {
|
||||
console.log(` ✓ Audit log action: ${emergencyLog.action}`);
|
||||
console.log(` ✓ Audit log timestamp: ${emergencyLog.timestamp}`);
|
||||
expect(emergencyLog).toBeDefined();
|
||||
}
|
||||
|
||||
console.log('✅ Test 4 passed: Audit logging verified');
|
||||
});
|
||||
|
||||
test('Test 5: Emergency token from unauthorized IP (documentation test)', async ({
|
||||
request,
|
||||
}) => {
|
||||
console.log('🧪 Testing emergency token IP restrictions (documentation)...');
|
||||
|
||||
// Note: This is difficult to test in E2E environment since we can't easily
|
||||
// spoof the source IP. This test documents the expected behavior.
|
||||
|
||||
// In production, the emergency bypass middleware checks:
|
||||
// 1. Client IP is in management CIDR (default: RFC1918 private networks)
|
||||
// 2. Token matches configured emergency token
|
||||
// 3. Token meets minimum length (32 chars)
|
||||
|
||||
// For E2E tests running in Docker, the client IP appears as Docker gateway IP (172.17.0.1)
|
||||
// which IS in the RFC1918 range, so emergency token should work.
|
||||
|
||||
const response = await request.post('/api/v1/emergency/security-reset', {
|
||||
headers: { 'X-Emergency-Token': EMERGENCY_TOKEN },
|
||||
});
|
||||
|
||||
// In E2E environment, this should succeed since Docker IP is in allowed range
|
||||
expect(response.ok()).toBeTruthy();
|
||||
|
||||
console.log('✅ Test 5 passed: IP restriction behavior documented');
|
||||
console.log(
|
||||
' ℹ️ Manual test required: Verify production blocks IPs outside management CIDR'
|
||||
);
|
||||
});
|
||||
|
||||
test('Test 6: Emergency token minimum length validation', async ({ request }) => {
|
||||
console.log('🧪 Testing emergency token minimum length validation...');
|
||||
|
||||
// The backend requires minimum 32 characters for the emergency token
|
||||
// This is enforced at startup, not per-request, so we can't test it directly in E2E
|
||||
|
||||
// Instead, we verify that our E2E token meets the requirement
|
||||
expect(EMERGENCY_TOKEN.length).toBeGreaterThanOrEqual(32);
|
||||
console.log(` ✓ E2E emergency token length: ${EMERGENCY_TOKEN.length} chars (minimum: 32)`);
|
||||
|
||||
// Verify the token works
|
||||
const response = await request.post('/api/v1/emergency/security-reset', {
|
||||
headers: { 'X-Emergency-Token': EMERGENCY_TOKEN },
|
||||
});
|
||||
|
||||
expect(response.ok()).toBeTruthy();
|
||||
|
||||
console.log('✅ Test 6 passed: Minimum length requirement documented and verified');
|
||||
console.log(' ℹ️ Backend unit test required: Verify startup rejects short tokens');
|
||||
});
|
||||
|
||||
test('Test 7: Emergency token header stripped', async ({ request }) => {
|
||||
console.log('🧪 Testing emergency token header security...');
|
||||
|
||||
// Use emergency token
|
||||
const response = await request.post('/api/v1/emergency/security-reset', {
|
||||
headers: { 'X-Emergency-Token': EMERGENCY_TOKEN },
|
||||
});
|
||||
|
||||
expect(response.ok()).toBeTruthy();
|
||||
|
||||
// The emergency bypass middleware should strip the token header before
|
||||
// the request reaches the handler, preventing token exposure in logs
|
||||
|
||||
// Verify token doesn't appear in response headers
|
||||
const responseHeaders = response.headers();
|
||||
expect(responseHeaders['x-emergency-token']).toBeUndefined();
|
||||
|
||||
// Check audit logs to ensure token is NOT logged
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
const auditResponse = await request.get('/api/v1/audit-logs');
|
||||
if (auditResponse.ok()) {
|
||||
const auditPayload = await auditResponse.json();
|
||||
const auditLogs = Array.isArray(auditPayload)
|
||||
? auditPayload
|
||||
: Array.isArray(auditPayload?.audit_logs)
|
||||
? auditPayload.audit_logs
|
||||
: [];
|
||||
const recentLog = auditLogs[0];
|
||||
|
||||
if (!recentLog) {
|
||||
console.log(' ⚠ No audit logs returned; skipping token redaction assertion');
|
||||
console.log('✅ Test 7 passed: Emergency token properly stripped for security');
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify token value doesn't appear in audit log
|
||||
const logString = JSON.stringify(recentLog);
|
||||
expect(logString).not.toContain(EMERGENCY_TOKEN);
|
||||
console.log(' ✓ Token not found in audit log (properly stripped)');
|
||||
}
|
||||
|
||||
console.log('✅ Test 7 passed: Emergency token properly stripped for security');
|
||||
});
|
||||
|
||||
test('Test 8: Emergency reset idempotency', async ({ request }) => {
|
||||
console.log('🧪 Testing emergency reset idempotency...');
|
||||
|
||||
// First reset
|
||||
const firstResponse = await request.post('/api/v1/emergency/security-reset', {
|
||||
headers: { 'X-Emergency-Token': EMERGENCY_TOKEN },
|
||||
});
|
||||
|
||||
expect(firstResponse.ok()).toBeTruthy();
|
||||
const firstBody = await firstResponse.json();
|
||||
expect(firstBody.success).toBe(true);
|
||||
console.log(' ✓ First reset successful');
|
||||
|
||||
// Wait a moment
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// Second reset (should also succeed)
|
||||
const secondResponse = await request.post('/api/v1/emergency/security-reset', {
|
||||
headers: { 'X-Emergency-Token': EMERGENCY_TOKEN },
|
||||
});
|
||||
|
||||
expect(secondResponse.ok()).toBeTruthy();
|
||||
const secondBody = await secondResponse.json();
|
||||
expect(secondBody.success).toBe(true);
|
||||
console.log(' ✓ Second reset successful');
|
||||
|
||||
// Both should return success, no errors
|
||||
expect(firstBody.success).toBe(secondBody.success);
|
||||
console.log(' ✓ No errors on repeated resets');
|
||||
|
||||
console.log('✅ Test 8 passed: Emergency reset is idempotent');
|
||||
});
|
||||
});
|
||||
@@ -1,123 +0,0 @@
|
||||
/**
|
||||
* Rate Limiting Enforcement Tests
|
||||
*
|
||||
* Tests that verify rate limiting configuration and expected behavior.
|
||||
*
|
||||
* NOTE: Actual rate limiting happens at Caddy layer. These tests verify
|
||||
* the rate limiting configuration API and presets.
|
||||
*
|
||||
* Pattern: Toggle-On-Test-Toggle-Off
|
||||
*
|
||||
* @see /projects/Charon/docs/plans/current_spec.md - Rate Limit Enforcement Tests
|
||||
*/
|
||||
|
||||
import { test, expect } from '@bgotink/playwright-coverage';
|
||||
import { request } from '@playwright/test';
|
||||
import type { APIRequestContext } from '@playwright/test';
|
||||
import { STORAGE_STATE } from '../constants';
|
||||
import {
|
||||
getSecurityStatus,
|
||||
setSecurityModuleEnabled,
|
||||
captureSecurityState,
|
||||
restoreSecurityState,
|
||||
CapturedSecurityState,
|
||||
} from '../utils/security-helpers';
|
||||
|
||||
test.describe('Rate Limit Enforcement', () => {
|
||||
let requestContext: APIRequestContext;
|
||||
let originalState: CapturedSecurityState;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
requestContext = await request.newContext({
|
||||
baseURL: process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:8080',
|
||||
storageState: STORAGE_STATE,
|
||||
});
|
||||
|
||||
// Capture original state
|
||||
try {
|
||||
originalState = await captureSecurityState(requestContext);
|
||||
} catch (error) {
|
||||
console.error('Failed to capture original security state:', error);
|
||||
}
|
||||
|
||||
// Enable Cerberus (master toggle) first
|
||||
try {
|
||||
await setSecurityModuleEnabled(requestContext, 'cerberus', true);
|
||||
console.log('✓ Cerberus enabled');
|
||||
} catch (error) {
|
||||
console.error('Failed to enable Cerberus:', error);
|
||||
}
|
||||
|
||||
// Enable Rate Limiting
|
||||
try {
|
||||
await setSecurityModuleEnabled(requestContext, 'rateLimit', true);
|
||||
console.log('✓ Rate Limiting enabled');
|
||||
} catch (error) {
|
||||
console.error('Failed to enable Rate Limiting:', error);
|
||||
}
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
// Restore original state
|
||||
if (originalState) {
|
||||
try {
|
||||
await restoreSecurityState(requestContext, originalState);
|
||||
console.log('✓ Security state restored');
|
||||
} catch (error) {
|
||||
console.error('Failed to restore security state:', error);
|
||||
// Emergency disable
|
||||
try {
|
||||
await setSecurityModuleEnabled(requestContext, 'rateLimit', false);
|
||||
await setSecurityModuleEnabled(requestContext, 'cerberus', false);
|
||||
} catch {
|
||||
console.error('Emergency Rate Limit disable also failed');
|
||||
}
|
||||
}
|
||||
}
|
||||
await requestContext.dispose();
|
||||
});
|
||||
|
||||
test('should verify rate limiting is enabled', async () => {
|
||||
const status = await getSecurityStatus(requestContext);
|
||||
expect(status.rate_limit.enabled).toBe(true);
|
||||
expect(status.cerberus.enabled).toBe(true);
|
||||
});
|
||||
|
||||
test('should return rate limit presets', async () => {
|
||||
const response = await requestContext.get(
|
||||
'/api/v1/security/rate-limit/presets'
|
||||
);
|
||||
expect(response.ok()).toBe(true);
|
||||
|
||||
const data = await response.json();
|
||||
const presets = data.presets;
|
||||
expect(Array.isArray(presets)).toBe(true);
|
||||
|
||||
// Presets should have expected structure
|
||||
if (presets.length > 0) {
|
||||
const preset = presets[0];
|
||||
expect(preset.name || preset.id).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
test('should document threshold behavior when rate exceeded', async () => {
|
||||
// Rate limiting enforcement happens at Caddy layer
|
||||
// When threshold is exceeded, Caddy returns 429 Too Many Requests
|
||||
//
|
||||
// With rate limiting enabled:
|
||||
// - Requests exceeding the configured rate per IP/path return 429
|
||||
// - The response includes Retry-After header
|
||||
//
|
||||
// Direct API requests to backend bypass Caddy rate limiting
|
||||
|
||||
const status = await getSecurityStatus(requestContext);
|
||||
expect(status.rate_limit.enabled).toBe(true);
|
||||
|
||||
// Document: When rate limiting is enabled and request goes through Caddy:
|
||||
// - Requests exceeding threshold return 429 Too Many Requests
|
||||
// - X-RateLimit-Limit, X-RateLimit-Remaining headers are included
|
||||
console.log(
|
||||
'Rate limiting configured - threshold enforcement active at Caddy layer'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,108 +0,0 @@
|
||||
/**
|
||||
* Security Headers Enforcement Tests
|
||||
*
|
||||
* Tests that verify security headers are properly set on responses.
|
||||
*
|
||||
* NOTE: Security headers are applied at Caddy layer. These tests verify
|
||||
* the expected headers through the API proxy.
|
||||
*
|
||||
* @see /projects/Charon/docs/plans/current_spec.md - Security Headers Enforcement Tests
|
||||
*/
|
||||
|
||||
import { test, expect } from '@bgotink/playwright-coverage';
|
||||
import { request } from '@playwright/test';
|
||||
import type { APIRequestContext } from '@playwright/test';
|
||||
import { STORAGE_STATE } from '../constants';
|
||||
|
||||
test.describe('Security Headers Enforcement', () => {
|
||||
let requestContext: APIRequestContext;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
requestContext = await request.newContext({
|
||||
baseURL: process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:8080',
|
||||
storageState: STORAGE_STATE,
|
||||
});
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await requestContext.dispose();
|
||||
});
|
||||
|
||||
test('should return X-Content-Type-Options header', async () => {
|
||||
const response = await requestContext.get('/api/v1/health');
|
||||
expect(response.ok()).toBe(true);
|
||||
|
||||
// X-Content-Type-Options should be 'nosniff'
|
||||
const header = response.headers()['x-content-type-options'];
|
||||
if (header) {
|
||||
expect(header).toBe('nosniff');
|
||||
} else {
|
||||
// If backend doesn't set it, Caddy should when configured
|
||||
console.log(
|
||||
'X-Content-Type-Options not set directly (may be set at Caddy layer)'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('should return X-Frame-Options header', async () => {
|
||||
const response = await requestContext.get('/api/v1/health');
|
||||
expect(response.ok()).toBe(true);
|
||||
|
||||
// X-Frame-Options should be 'DENY' or 'SAMEORIGIN'
|
||||
const header = response.headers()['x-frame-options'];
|
||||
if (header) {
|
||||
expect(['DENY', 'SAMEORIGIN', 'deny', 'sameorigin']).toContain(header);
|
||||
} else {
|
||||
// If backend doesn't set it, Caddy should when configured
|
||||
console.log(
|
||||
'X-Frame-Options not set directly (may be set at Caddy layer)'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('should document HSTS behavior on HTTPS', async () => {
|
||||
// HSTS (Strict-Transport-Security) is only set on HTTPS responses
|
||||
// In test environment, we typically use HTTP
|
||||
//
|
||||
// Expected header on HTTPS:
|
||||
// Strict-Transport-Security: max-age=31536000; includeSubDomains
|
||||
//
|
||||
// This test verifies HSTS is not incorrectly set on HTTP
|
||||
|
||||
const response = await requestContext.get('/api/v1/health');
|
||||
expect(response.ok()).toBe(true);
|
||||
|
||||
const hsts = response.headers()['strict-transport-security'];
|
||||
|
||||
// On HTTP, HSTS should not be present (browsers ignore it anyway)
|
||||
if (process.env.PLAYWRIGHT_BASE_URL?.startsWith('https://')) {
|
||||
expect(hsts).toBeDefined();
|
||||
expect(hsts).toContain('max-age');
|
||||
} else {
|
||||
// HTTP is fine without HSTS in test env
|
||||
console.log('HSTS not present on HTTP (expected behavior)');
|
||||
}
|
||||
});
|
||||
|
||||
test('should verify Content-Security-Policy when configured', async () => {
|
||||
// CSP is optional and configured per-host
|
||||
// This test verifies CSP header handling when present
|
||||
|
||||
const response = await requestContext.get('/');
|
||||
// May be 200 or redirect
|
||||
expect(response.status()).toBeLessThan(500);
|
||||
|
||||
const csp = response.headers()['content-security-policy'];
|
||||
if (csp) {
|
||||
// CSP should contain valid directives
|
||||
expect(
|
||||
csp.includes("default-src") ||
|
||||
csp.includes("script-src") ||
|
||||
csp.includes("style-src")
|
||||
).toBe(true);
|
||||
} else {
|
||||
// CSP is optional - document its behavior when configured
|
||||
console.log('CSP not configured (optional - set per proxy host)');
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,136 +0,0 @@
|
||||
/**
|
||||
* WAF (Coraza) Enforcement Tests
|
||||
*
|
||||
* Tests that verify the Web Application Firewall correctly blocks malicious
|
||||
* requests such as SQL injection and XSS attempts.
|
||||
*
|
||||
* NOTE: Full WAF blocking tests require Caddy proxy with Coraza plugin.
|
||||
* These tests verify the WAF configuration API and expected behavior.
|
||||
*
|
||||
* Pattern: Toggle-On-Test-Toggle-Off
|
||||
*
|
||||
* @see /projects/Charon/docs/plans/current_spec.md - WAF Enforcement Tests
|
||||
*/
|
||||
|
||||
import { test, expect } from '@bgotink/playwright-coverage';
|
||||
import { request } from '@playwright/test';
|
||||
import type { APIRequestContext } from '@playwright/test';
|
||||
import { STORAGE_STATE } from '../constants';
|
||||
import {
|
||||
getSecurityStatus,
|
||||
setSecurityModuleEnabled,
|
||||
captureSecurityState,
|
||||
restoreSecurityState,
|
||||
CapturedSecurityState,
|
||||
} from '../utils/security-helpers';
|
||||
|
||||
test.describe('WAF Enforcement', () => {
|
||||
let requestContext: APIRequestContext;
|
||||
let originalState: CapturedSecurityState;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
requestContext = await request.newContext({
|
||||
baseURL: process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:8080',
|
||||
storageState: STORAGE_STATE,
|
||||
});
|
||||
|
||||
// Capture original state
|
||||
try {
|
||||
originalState = await captureSecurityState(requestContext);
|
||||
} catch (error) {
|
||||
console.error('Failed to capture original security state:', error);
|
||||
}
|
||||
|
||||
// Enable Cerberus (master toggle) first
|
||||
try {
|
||||
await setSecurityModuleEnabled(requestContext, 'cerberus', true);
|
||||
console.log('✓ Cerberus enabled');
|
||||
} catch (error) {
|
||||
console.error('Failed to enable Cerberus:', error);
|
||||
}
|
||||
|
||||
// Enable WAF
|
||||
try {
|
||||
await setSecurityModuleEnabled(requestContext, 'waf', true);
|
||||
console.log('✓ WAF enabled');
|
||||
} catch (error) {
|
||||
console.error('Failed to enable WAF:', error);
|
||||
}
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
// Restore original state
|
||||
if (originalState) {
|
||||
try {
|
||||
await restoreSecurityState(requestContext, originalState);
|
||||
console.log('✓ Security state restored');
|
||||
} catch (error) {
|
||||
console.error('Failed to restore security state:', error);
|
||||
// Emergency disable WAF to prevent interference
|
||||
try {
|
||||
await setSecurityModuleEnabled(requestContext, 'waf', false);
|
||||
await setSecurityModuleEnabled(requestContext, 'cerberus', false);
|
||||
} catch {
|
||||
console.error('Emergency WAF disable also failed');
|
||||
}
|
||||
}
|
||||
}
|
||||
await requestContext.dispose();
|
||||
});
|
||||
|
||||
test('should verify WAF is enabled', async () => {
|
||||
const status = await getSecurityStatus(requestContext);
|
||||
expect(status.waf.enabled).toBe(true);
|
||||
expect(status.cerberus.enabled).toBe(true);
|
||||
});
|
||||
|
||||
test('should return WAF configuration from security status', async () => {
|
||||
const response = await requestContext.get('/api/v1/security/status');
|
||||
expect(response.ok()).toBe(true);
|
||||
|
||||
const status = await response.json();
|
||||
expect(status.waf).toBeDefined();
|
||||
expect(status.waf.mode).toBeDefined();
|
||||
expect(typeof status.waf.enabled).toBe('boolean');
|
||||
});
|
||||
|
||||
test('should detect SQL injection patterns in request validation', async () => {
|
||||
// WAF blocking happens at Caddy/Coraza layer before reaching the API
|
||||
// This test documents the expected behavior when SQL injection is attempted
|
||||
//
|
||||
// With WAF enabled and Caddy configured, requests like:
|
||||
// GET /api/v1/users?id=1' OR 1=1--
|
||||
// Should return 403 or 418 (I'm a teapot - Coraza signature)
|
||||
//
|
||||
// Since we're making direct API requests (not through Caddy proxy),
|
||||
// we verify the WAF is configured and document expected blocking behavior
|
||||
|
||||
const status = await getSecurityStatus(requestContext);
|
||||
expect(status.waf.enabled).toBe(true);
|
||||
|
||||
// Document: When WAF is enabled and request goes through Caddy:
|
||||
// - SQL injection patterns like ' OR 1=1-- should return 403/418
|
||||
// - The response will contain WAF block message
|
||||
console.log(
|
||||
'WAF configured - SQL injection blocking active at Caddy/Coraza layer'
|
||||
);
|
||||
});
|
||||
|
||||
test('should document XSS blocking behavior', async () => {
|
||||
// Similar to SQL injection, XSS blocking happens at Caddy/Coraza layer
|
||||
//
|
||||
// With WAF enabled, requests containing:
|
||||
// <script>alert('xss')</script>
|
||||
// Should be blocked with 403/418
|
||||
//
|
||||
// Direct API requests bypass Caddy, so we verify configuration
|
||||
|
||||
const status = await getSecurityStatus(requestContext);
|
||||
expect(status.waf.enabled).toBe(true);
|
||||
|
||||
// Document: When WAF is enabled and request goes through Caddy:
|
||||
// - XSS patterns like <script> tags should return 403/418
|
||||
// - Common XSS payloads are blocked by Coraza OWASP CoreRuleSet
|
||||
console.log('WAF configured - XSS blocking active at Caddy/Coraza layer');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user