chore: clean .gitignore cache

This commit is contained in:
GitHub Actions
2026-01-26 19:21:33 +00:00
parent 1b1b3a70b1
commit e5f0fec5db
1483 changed files with 0 additions and 472793 deletions

View File

@@ -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}`);
}
});
});

View File

@@ -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'
);
});
});

View File

@@ -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');
}
});
});

View File

@@ -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);
});
});

View File

@@ -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');
});
});

View File

@@ -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'
);
});
});

View File

@@ -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)');
}
});
});

View File

@@ -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');
});
});