33 lines
2.1 KiB
JavaScript
33 lines
2.1 KiB
JavaScript
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))
|