import { readFile, readdir } from 'node:fs/promises' import { resolve } from 'node:path' const root = resolve(new URL('..', import.meta.url).pathname) const baseline = JSON.parse(await readFile(resolve(root, 'config/security-baseline.json'), 'utf8')) const headers = await readFile(resolve(root, baseline.headersPath), 'utf8') const fail = (message) => { throw new Error(`Security baseline: ${message}`) } if (baseline.schemaVersion !== 1 || baseline.browserScope !== 'chrome-only') fail('schema or browser scope is invalid.') for (const [name, value] of Object.entries(baseline.requiredHeaders)) if (!headers.includes(`${name}: ${value}`)) fail(`missing ${name}.`) const cspLine = headers.split('\n').find((line) => line.includes('Content-Security-Policy:')) if (!cspLine) fail('Content-Security-Policy is missing.') for (const [directive, values] of Object.entries(baseline.contentSecurityPolicy)) { const expected = `${directive} ${values.join(' ')}` if (!cspLine.includes(expected)) fail(`CSP directive is incomplete: ${expected}`) } if (cspLine.includes("'unsafe-eval'")) fail('CSP must not allow unsafe-eval.') const viteConfig = await readFile(resolve(root, 'vite.config.ts'), 'utf8') for (const [name, value] of Object.entries(baseline.requiredHeaders)) if (!viteConfig.includes(`'${name}': '${value}'`) && !viteConfig.includes(`"${name}": "${value}"`)) fail(`Vite preview headers do not declare ${name}.`) const sourceFiles = [] const collect = async (directory) => { for (const entry of await readdir(directory, { withFileTypes: true })) { const path = resolve(directory, entry.name) if (entry.isDirectory()) await collect(path) else if (/\.(ts|tsx|mjs)$/.test(entry.name)) sourceFiles.push(path) } } await collect(resolve(root, 'src')) for (const path of sourceFiles) { const source = await readFile(path, 'utf8') for (const pattern of baseline.forbiddenSourcePatterns) if (source.includes(pattern)) fail(`forbidden dynamic execution pattern '${pattern}' in ${path.replace(`${root}/`, '')}.`) } console.log(JSON.stringify({ status: 'security-baseline-pass', browserScope: baseline.browserScope, sourceFiles: sourceFiles.length, csp: cspLine.trim() }, null, 2))