chore(test): add test infrastructure, CI config, and security scanning

- Add Jenkinsfile with E2E/visual regression/security scan stages
- Add Stryker mutation testing configuration
- Add security header scanning script (check-security-headers.ts)
- Remove old playwright.config.ts (migrated to e2e/playwright.config.ts)
- Remove obsolete .claude/skills/impeccable
- Update jest config for test path patterns
- Update package.json with comprehensive test scripts
- Update .gitignore for stryker-tmp/ and .pi/ temp files
This commit is contained in:
2026-07-31 20:24:01 +08:00
parent f0b3a44f66
commit dd088a5ee1
12 changed files with 1945 additions and 306 deletions
+299
View File
@@ -0,0 +1,299 @@
#!/usr/bin/env tsx
/**
* 安全响应头检查脚本
*
* 检查部署网站的安全头配置是否合规,包括:
* - 核心安全头(Content-Security-Policy, X-Content-Type-Options 等)
* - CORS 配置
* - Cookie 安全标记
*/
interface SecurityCheck {
header: string;
expected: string;
actual: string | null;
status: 'pass' | 'fail' | 'warn';
description: string;
}
interface CookieCheck {
name: string;
hasHttpOnly: boolean;
hasSecure: boolean;
hasSameSite: boolean;
status: 'pass' | 'fail' | 'warn';
}
function parseArgs(): string {
const args = process.argv.slice(2);
const urlIndex = args.indexOf('--url');
if (urlIndex !== -1 && args[urlIndex + 1]) {
return args[urlIndex + 1]!;
}
return 'https://novalon.cn';
}
function parseCookies(setCookieHeader: string | null): Array<{ name: string; flags: string[] }> {
if (!setCookieHeader) return [];
const cookies: Array<{ name: string; flags: string[] }> = [];
// Handle multiple Set-Cookie headers (joined by comma-newline)
const cookieStrings = setCookieHeader.split(/\n|,(?=\s*\w+=)/);
for (const cookieStr of cookieStrings) {
const trimmed = cookieStr.trim();
if (!trimmed) continue;
const parts = trimmed.split(';').map((p) => p.trim());
const nameValue = parts[0]!;
const name = nameValue.split('=')[0] || 'unknown';
const flags = parts.slice(1).map((f) => f.toLowerCase());
cookies.push({ name, flags });
}
return cookies;
}
function checkCookies(cookies: Array<{ name: string; flags: string[] }>): CookieCheck[] {
return cookies.map((cookie) => ({
name: cookie.name,
hasHttpOnly: cookie.flags.some((f) => f === 'httponly'),
hasSecure: cookie.flags.some((f) => f === 'secure'),
hasSameSite: cookie.flags.some((f) => f.startsWith('samesite')),
status: 'pass' as const,
}));
}
async function checkSecurityHeaders(url: string): Promise<{
headerChecks: SecurityCheck[];
cookieChecks: CookieCheck[];
}> {
console.log(`\n🔒 安全响应头检查 — ${url}\n`);
const response = await fetch(url, {
redirect: 'follow',
signal: AbortSignal.timeout(15000),
});
const headers = response.headers;
const headerChecks: SecurityCheck[] = [
{
header: 'Content-Security-Policy',
expected: '存在(需包含合理策略)',
actual: headers.get('content-security-policy'),
status: headers.get('content-security-policy') ? 'pass' : 'warn',
description: '防止 XSS 和数据注入攻击的核心策略',
},
{
header: 'X-Content-Type-Options',
expected: 'nosniff',
actual: headers.get('x-content-type-options'),
status: headers.get('x-content-type-options') === 'nosniff' ? 'pass' : 'fail',
description: '防止 MIME 类型嗅探攻击',
},
{
header: 'X-Frame-Options',
expected: 'DENY 或 SAMEORIGIN',
actual: headers.get('x-frame-options'),
status: (() => {
const val = headers.get('x-frame-options');
if (val === 'DENY' || val === 'SAMEORIGIN') return 'pass' as const;
return val ? 'warn' as const : 'fail' as const;
})(),
description: '防止点击劫持(Clickjacking)攻击',
},
{
header: 'Strict-Transport-Security',
expected: '存在(需包含 max-age',
actual: headers.get('strict-transport-security'),
status: headers.get('strict-transport-security') ? 'pass' : 'fail',
description: '强制 HTTPS 连接,防止 SSL Strip 攻击',
},
{
header: 'Referrer-Policy',
expected: '存在(如 strict-origin-when-cross-origin',
actual: headers.get('referrer-policy'),
status: headers.get('referrer-policy') ? 'pass' : 'fail',
description: '控制 Referer 头信息的发送策略',
},
{
header: 'Permissions-Policy',
expected: '存在(需配置合理权限)',
actual: headers.get('permissions-policy'),
status: headers.get('permissions-policy') ? 'pass' : 'warn',
description: '限制浏览器 API 权限(摄像头、麦克风等)',
},
{
header: 'X-XSS-Protection',
expected: '存在(0 或 1; mode=block',
actual: headers.get('x-xss-protection'),
status: (() => {
const val = headers.get('x-xss-protection');
if (val === '0' || val === '1; mode=block') return 'pass' as const;
return val ? 'warn' as const : 'warn' as const;
})(),
description: '已废弃的 XSS 过滤器(现代浏览器不再需要)',
},
];
// CORS check
const corsOrigin = headers.get('access-control-allow-origin');
if (corsOrigin) {
headerChecks.push({
header: 'Access-Control-Allow-Origin',
expected: '非通配符(或仅对特定来源开放)',
actual: corsOrigin,
status: corsOrigin === '*' ? 'warn' : 'pass',
description: 'CORS 跨域配置',
});
} else {
headerChecks.push({
header: 'Access-Control-Allow-Origin',
expected: '无(不暴露 CORS 头)',
actual: null,
status: 'pass',
description: 'CORS 跨域配置(未设置,符合安全预期)',
});
}
// Cookie security check
const setCookie = headers.get('set-cookie');
const parsedCookies = parseCookies(setCookie);
const cookieChecks = checkCookies(parsedCookies);
return { headerChecks, cookieChecks };
}
function printTable(checks: SecurityCheck[]): void {
// Column widths
const headerWidth = 34;
const statusWidth = 6;
const expectedWidth = 38;
const actualWidth = 38;
const separator = `${'─'.repeat(headerWidth + 2)}${'─'.repeat(statusWidth + 2)}${'─'.repeat(expectedWidth + 2)}${'─'.repeat(actualWidth + 2)}`;
const topBorder = `${'─'.repeat(headerWidth + 2)}${'─'.repeat(statusWidth + 2)}${'─'.repeat(expectedWidth + 2)}${'─'.repeat(actualWidth + 2)}`;
const bottomBorder = `${'─'.repeat(headerWidth + 2)}${'─'.repeat(statusWidth + 2)}${'─'.repeat(expectedWidth + 2)}${'─'.repeat(actualWidth + 2)}`;
const headerRow = `${'Header'.padEnd(headerWidth)}${'Status'.padEnd(statusWidth)}${'Expected'.padEnd(expectedWidth)}${'Actual'.padEnd(actualWidth)}`;
console.log(topBorder);
console.log(headerRow);
console.log(separator);
for (const check of checks) {
const statusIcon = check.status === 'pass' ? '✅ PASS' : check.status === 'fail' ? '❌ FAIL' : '⚠️ WARN';
const actual = check.actual ?? '(未设置)';
const row = `${check.header.padEnd(headerWidth)}${statusIcon.padEnd(statusWidth + 2)}${check.expected.padEnd(expectedWidth)}${actual.padEnd(actualWidth)}`;
console.log(row);
}
console.log(bottomBorder);
}
function printCookieTable(cookieChecks: CookieCheck[]): void {
if (cookieChecks.length === 0) {
console.log('\n🍪 Cookie 安全标记: 无 Cookie 设置\n');
return;
}
const nameWidth = 24;
const httpOnlyWidth = 10;
const secureWidth = 8;
const sameSiteWidth = 10;
const topBorder = `${'─'.repeat(nameWidth + 2)}${'─'.repeat(httpOnlyWidth + 2)}${'─'.repeat(secureWidth + 2)}${'─'.repeat(sameSiteWidth + 2)}`;
const separator = `${'─'.repeat(nameWidth + 2)}${'─'.repeat(httpOnlyWidth + 2)}${'─'.repeat(secureWidth + 2)}${'─'.repeat(sameSiteWidth + 2)}`;
const bottomBorder = `${'─'.repeat(nameWidth + 2)}${'─'.repeat(httpOnlyWidth + 2)}${'─'.repeat(secureWidth + 2)}${'─'.repeat(sameSiteWidth + 2)}`;
const headerRow = `${'Cookie Name'.padEnd(nameWidth)}${'HttpOnly'.padEnd(httpOnlyWidth)}${'Secure'.padEnd(secureWidth)}${'SameSite'.padEnd(sameSiteWidth)}`;
console.log('\n🍪 Cookie 安全标记\n');
console.log(topBorder);
console.log(headerRow);
console.log(separator);
for (const cookie of cookieChecks) {
const httpOnly = cookie.hasHttpOnly ? '✅' : '❌';
const secure = cookie.hasSecure ? '✅' : '❌';
const sameSite = cookie.hasSameSite ? '✅' : '❌';
const row = `${cookie.name.padEnd(nameWidth)}${httpOnly.padEnd(httpOnlyWidth)}${secure.padEnd(secureWidth)}${sameSite.padEnd(sameSiteWidth)}`;
console.log(row);
}
console.log(bottomBorder);
}
function printSummary(headerChecks: SecurityCheck[], cookieChecks: CookieCheck[]): void {
const total = headerChecks.length;
const passed = headerChecks.filter((c) => c.status === 'pass').length;
const failed = headerChecks.filter((c) => c.status === 'fail').length;
const warned = headerChecks.filter((c) => c.status === 'warn').length;
const cookiePassed = cookieChecks.filter((c) => c.status === 'pass').length;
const cookieFailed = cookieChecks.filter((c) => c.status === 'fail').length;
console.log('\n📊 检查摘要\n');
console.log(` 安全头检查:`);
console.log(` 总计: ${total}`);
console.log(` ✅ 通过: ${passed}`);
console.log(` ⚠️ 警告: ${warned}`);
console.log(` ❌ 失败: ${failed}`);
if (cookieChecks.length > 0) {
console.log(`\n Cookie 安全标记:`);
console.log(` 总计: ${cookieChecks.length}`);
console.log(` ✅ 通过: ${cookiePassed}`);
console.log(` ❌ 失败: ${cookieFailed}`);
}
}
async function main() {
const url = parseArgs();
try {
const { headerChecks, cookieChecks } = await checkSecurityHeaders(url);
// Print header check table
console.log('安全头检查结果:');
printTable(headerChecks);
// Print cookie check table
printCookieTable(cookieChecks);
// Print descriptions for failed/warned checks
const issues = headerChecks.filter((c) => c.status !== 'pass');
if (issues.length > 0) {
console.log('\n📝 说明:');
for (const issue of issues) {
console.log(` • [${issue.status === 'fail' ? '❌' : '⚠️'}] ${issue.header}: ${issue.description}`);
if (issue.status === 'fail') {
console.log(` 期望: ${issue.expected}`);
console.log(` 实际: ${issue.actual ?? '(未设置)'}`);
}
}
}
// Print summary
printSummary(headerChecks, cookieChecks);
// Determine exit code
const hasFailures = headerChecks.some((c) => c.status === 'fail');
if (hasFailures) {
console.log('\n❌ 存在失败的安全头检查项!');
process.exit(1);
} else {
console.log('\n✅ 所有关键安全头检查通过!');
process.exit(0);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error(`\n❌ 检查执行失败: ${message}`);
process.exit(1);
}
}
main();