Files
Charon/tests/security-teardown.setup.ts
GitHub Actions 892b89fc9d feat: break-glass security reset
Implement dual-registry container publishing to both GHCR and Docker Hub
for maximum distribution reach. Add emergency security reset endpoint
("break-glass" mechanism) to recover from ACL lockout situations.

Key changes:

Docker Hub + GHCR dual publishing with Cosign signing and SBOM
Emergency reset endpoint POST /api/v1/emergency/security-reset
Token-based authentication bypasses Cerberus middleware
Rate limited (5/hour) with audit logging
30 new security enforcement E2E tests covering ACL, WAF, CrowdSec,
Rate Limiting, Security Headers, and Combined scenarios
Fixed container startup permission issue (tmpfs directory ownership)
Playwright config updated with testIgnore for browser projects
Security: Token via CHARON_EMERGENCY_TOKEN env var (32+ chars recommended)
Tests: 689 passed, 86% backend coverage, 85% frontend coverage
2026-01-25 20:14:06 +00:00

117 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Security Teardown Setup
*
* This file runs AFTER all security-tests complete.
* It disables all security modules to ensure browser tests run without blocking.
*
* Uses a two-strategy approach:
* 1. Try normal API with authentication
* 2. Fall back to emergency reset endpoint if API is blocked by ACL/security
*
* Uses continue-on-error pattern - individual module disable failures won't
* prevent other modules from being disabled.
*
* @see /projects/Charon/docs/plans/current_spec.md - Security Module Testing Plan
*/
import { test as teardown } from '@bgotink/playwright-coverage';
import { request } from '@playwright/test';
teardown('disable-all-security-modules', async () => {
console.log('\n🔒 Security Teardown: Disabling all security modules...');
const baseURL = process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:8080';
const emergencyToken = process.env.CHARON_EMERGENCY_TOKEN;
const modules = [
{ key: 'security.acl.enabled', value: 'false' },
{ key: 'security.waf.enabled', value: 'false' },
{ key: 'security.crowdsec.enabled', value: 'false' },
{ key: 'security.rate_limit.enabled', value: 'false' },
{ key: 'feature.cerberus.enabled', value: 'false' },
];
// Strategy 1: Try normal API with auth
const requestContext = await request.newContext({
baseURL,
storageState: 'playwright/.auth/user.json',
});
const errors: string[] = [];
let apiBlocked = false;
for (const { key, value } of modules) {
try {
const response = await requestContext.post('/api/v1/settings', {
data: { key, value },
});
if (response.status() === 403) {
apiBlocked = true;
console.warn(` ⚠ API blocked (403) while disabling ${key}`);
break;
}
console.log(` ✓ Disabled via API: ${key}`);
} catch (e) {
const errorMsg = `Failed to disable ${key}: ${e}`;
errors.push(errorMsg);
console.warn(`${errorMsg}`);
apiBlocked = true;
break;
}
}
await requestContext.dispose();
// Strategy 2: If API is blocked, use emergency reset endpoint
if (apiBlocked && emergencyToken) {
console.log(' ⚠ API blocked - using emergency reset endpoint...');
try {
const emergencyContext = await request.newContext({ baseURL });
const response = await emergencyContext.post(
'/api/v1/emergency/security-reset',
{
headers: {
'X-Emergency-Token': emergencyToken,
'Content-Type': 'application/json',
},
data: { reason: 'Playwright teardown - API was blocked' },
}
);
if (response.ok()) {
const body = await response.json();
console.log(
` ✓ Emergency reset successful: ${body.disabled.join(', ')}`
);
// Clear errors since emergency reset succeeded
errors.length = 0;
} else {
console.error(` ✗ Emergency reset failed: ${response.status()}`);
errors.push(`Emergency reset failed with status ${response.status()}`);
}
await emergencyContext.dispose();
} catch (e) {
console.error(' ✗ Emergency reset error:', e);
errors.push(`Emergency reset error: ${e}`);
}
} else if (apiBlocked && !emergencyToken) {
console.error(' ✗ API blocked but CHARON_EMERGENCY_TOKEN not set!');
errors.push('API blocked and no emergency token available');
}
// Stabilization delay - wait for Caddy config reload
console.log(' ⏳ Waiting for Caddy config reload...');
await new Promise((resolve) => setTimeout(resolve, 1000));
if (errors.length > 0) {
console.error(
'\n⚠ Security teardown had errors (continuing anyway):',
errors.join('\n ')
);
// Don't throw - let other tests run even if teardown partially failed
} else {
console.log('✅ Security teardown complete: All modules disabled\n');
}
});