Files
Charon/tests/security-enforcement/emergency-reset.spec.ts
akanealw eec8c28fb3
Some checks are pending
Go Benchmark / Performance Regression Check (push) Waiting to run
Cerberus Integration / Cerberus Security Stack Integration (push) Waiting to run
Upload Coverage to Codecov / Backend Codecov Upload (push) Waiting to run
Upload Coverage to Codecov / Frontend Codecov Upload (push) Waiting to run
CodeQL - Analyze / CodeQL analysis (go) (push) Waiting to run
CodeQL - Analyze / CodeQL analysis (javascript-typescript) (push) Waiting to run
CrowdSec Integration / CrowdSec Bouncer Integration (push) Waiting to run
Docker Build, Publish & Test / build-and-push (push) Waiting to run
Docker Build, Publish & Test / Security Scan PR Image (push) Blocked by required conditions
Quality Checks / Auth Route Protection Contract (push) Waiting to run
Quality Checks / Codecov Trigger/Comment Parity Guard (push) Waiting to run
Quality Checks / Backend (Go) (push) Waiting to run
Quality Checks / Frontend (React) (push) Waiting to run
Rate Limit integration / Rate Limiting Integration (push) Waiting to run
Security Scan (PR) / Trivy Binary Scan (push) Waiting to run
Supply Chain Verification (PR) / Verify Supply Chain (push) Waiting to run
WAF integration / Coraza WAF Integration (push) Waiting to run
changed perms
2026-04-22 18:19:14 +00:00

253 lines
6.7 KiB
TypeScript
Executable File

import { test, expect, APIRequestContext } from '@playwright/test';
import { EMERGENCY_TOKEN } from '../fixtures/security';
type SettingsMap = Record<string, string>;
const STAGE_A_LIMITS = {
enabled: true,
requests: 120,
window: 60,
burst: 20,
};
const STAGE_B_LIMITS = {
enabled: true,
requests: 3,
window: 10,
burst: 1,
};
const DEFAULT_LIMITS = {
enabled: false,
requests: 100,
window: 60,
burst: 20,
};
function parseSettingValue(value: unknown): string | number | boolean | undefined {
if (value === null || value === undefined) {
return undefined;
}
if (typeof value === 'boolean' || typeof value === 'number') {
return value;
}
if (typeof value === 'string') {
const trimmed = value.trim();
const lowered = trimmed.toLowerCase();
if (lowered === 'true' || lowered === 'false') {
return lowered === 'true';
}
if (/^-?\d+$/.test(trimmed)) {
return Number(trimmed);
}
return trimmed;
}
return String(value);
}
function coerceBoolean(value: unknown, fallback: boolean): boolean {
const parsed = parseSettingValue(value);
return typeof parsed === 'boolean' ? parsed : fallback;
}
function coerceNumber(value: unknown, fallback: number): number {
const parsed = parseSettingValue(value);
return typeof parsed === 'number' ? parsed : fallback;
}
function settingsMatch(settings: SettingsMap, expected: typeof STAGE_A_LIMITS): boolean {
return (
parseSettingValue(settings['security.rate_limit.enabled']) === expected.enabled &&
parseSettingValue(settings['security.rate_limit.requests']) === expected.requests &&
parseSettingValue(settings['security.rate_limit.window']) === expected.window &&
parseSettingValue(settings['security.rate_limit.burst']) === expected.burst
);
}
async function fetchSettings(token: string, request: APIRequestContext): Promise<SettingsMap> {
const response = await request.get('/api/v1/settings', {
headers: {
Authorization: `Bearer ${token}`,
},
});
expect(response.ok()).toBeTruthy();
return response.json();
}
async function patchRateLimit(
token: string,
request: APIRequestContext,
limits: typeof STAGE_A_LIMITS
): Promise<void> {
const maxRetries = 5;
const retryDelayMs = 1000;
for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
const response = await request.patch('/api/v1/config', {
headers: {
Authorization: `Bearer ${token}`,
},
data: {
security: {
rate_limit: limits,
},
},
});
if (response.ok()) {
return;
}
if (response.status() !== 429 || attempt === maxRetries) {
expect(response.ok()).toBeTruthy();
return;
}
await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
}
}
async function waitForSettings(
token: string,
request: APIRequestContext,
expected: typeof STAGE_A_LIMITS
): Promise<void> {
const maxDurationMs = 65000;
const intervalMs = 2000;
const deadline = Date.now() + maxDurationMs;
while (Date.now() < deadline) {
const settings = await fetchSettings(token, request);
if (settingsMatch(settings, expected)) {
return;
}
await new Promise((resolve) => setTimeout(resolve, intervalMs));
}
const lastSettings = await fetchSettings(token, request);
throw new Error(`Rate limit settings did not propagate: ${JSON.stringify(lastSettings)}`);
}
test.describe('Emergency Access & Rate Limiting', () => {
test.describe.configure({ mode: 'serial' });
let token: string;
let originalSettings: SettingsMap = {};
test.beforeAll(async ({ request }) => {
const email = process.env.E2E_TEST_EMAIL || 'e2e-test@example.com';
const password = process.env.E2E_TEST_PASSWORD || 'TestPassword123!';
await test.step('Authenticate admin user', async () => {
const loginResponse = await request.post('/api/v1/auth/login', {
data: {
email,
password,
},
});
expect(loginResponse.ok()).toBeTruthy();
const loginBody = await loginResponse.json();
token = loginBody.token;
});
await test.step('Capture original settings and apply Stage A limits', async () => {
originalSettings = await fetchSettings(token, request);
await patchRateLimit(token, request, STAGE_A_LIMITS);
await waitForSettings(token, request, STAGE_A_LIMITS);
});
await test.step('Advisory security status check (Stage A only)', async () => {
const statusResponse = await request.get('/api/v1/security/status', {
headers: { Authorization: `Bearer ${token}` },
});
if (statusResponse.ok()) {
const status = await statusResponse.json();
if (status?.rate_limit?.enabled !== undefined) {
expect(status.rate_limit.enabled).toBe(true);
}
}
});
});
test.afterAll(async ({ request }) => {
const restore = {
enabled: coerceBoolean(
originalSettings['security.rate_limit.enabled'],
DEFAULT_LIMITS.enabled
),
requests: coerceNumber(
originalSettings['security.rate_limit.requests'],
DEFAULT_LIMITS.requests
),
window: coerceNumber(
originalSettings['security.rate_limit.window'],
DEFAULT_LIMITS.window
),
burst: coerceNumber(
originalSettings['security.rate_limit.burst'],
DEFAULT_LIMITS.burst
),
};
await patchRateLimit(token, request, restore);
});
test('Emergency endpoint bypasses rate limits while others do not', async ({ request }) => {
let stageBBurstUsed = 0;
await test.step('Emergency reset runs before Stage B', async () => {
const emergencyResponse = await request.post('/api/v1/emergency/security-reset', {
headers: {
'X-Emergency-Token': EMERGENCY_TOKEN,
},
});
expect(emergencyResponse.ok()).toBeTruthy();
});
await test.step('Apply Stage B limits and verify once', async () => {
await patchRateLimit(token, request, STAGE_B_LIMITS);
const settings = await fetchSettings(token, request);
expect(settingsMatch(settings, STAGE_B_LIMITS)).toBe(true);
stageBBurstUsed = 1;
});
await test.step('Burst until rate limit hits 429', async () => {
const maxAttempts = 10;
let attempts = stageBBurstUsed;
let rateLimitHit = false;
while (attempts < maxAttempts) {
const response = await request.get('/api/v1/auth/verify', {
headers: {
Authorization: `Bearer ${token}`,
},
});
attempts += 1;
const status = response.status();
if (status === 429) {
rateLimitHit = true;
break;
}
expect(status).toBe(200);
}
expect(rateLimitHit).toBeTruthy();
});
});
});