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
109 lines
3.6 KiB
TypeScript
Executable File
109 lines
3.6 KiB
TypeScript
Executable File
/**
|
|
* 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 '../fixtures/test';
|
|
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://127.0.0.1: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)');
|
|
}
|
|
});
|
|
});
|