2026-07-23 00:49:02 -04:00
|
|
|
import { createHash } from 'node:crypto';
|
|
|
|
|
import { readFileSync, writeFileSync, readdirSync, statSync } from 'node:fs';
|
|
|
|
|
import { join, dirname } from 'node:path';
|
|
|
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
|
|
|
|
|
|
const BUILD_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', 'build');
|
|
|
|
|
const scriptRe = /<script(?![^>]*\bsrc=)[^>]*>([\s\S]*?)<\/script>/gi;
|
|
|
|
|
|
|
|
|
|
function* walk(dir) {
|
|
|
|
|
for (const entry of readdirSync(dir)) {
|
|
|
|
|
const path = join(dir, entry);
|
|
|
|
|
if (statSync(path).isDirectory()) yield* walk(path);
|
|
|
|
|
else if (entry.endsWith('.html')) yield path;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-23 01:19:22 -04:00
|
|
|
// La politique complète (hors script-src) est lue depuis build/_headers,
|
|
|
|
|
// unique source de vérité partagée avec .htaccess
|
|
|
|
|
const headers = readFileSync(join(BUILD_DIR, '_headers'), 'utf8');
|
|
|
|
|
const cspLine = headers
|
|
|
|
|
.split('\n')
|
|
|
|
|
.map((line) => line.trim())
|
|
|
|
|
.find((line) => line.startsWith('Content-Security-Policy:'));
|
|
|
|
|
if (!cspLine) throw new Error('[csp] Content-Security-Policy introuvable dans build/_headers');
|
|
|
|
|
const sharedPolicy = cspLine
|
|
|
|
|
.slice('Content-Security-Policy:'.length)
|
|
|
|
|
.trim()
|
|
|
|
|
// frame-ancestors est ignoré (et logué) dans une balise meta : il reste porté par l'en-tête HTTP
|
|
|
|
|
.replace(/frame-ancestors 'none';\s*/, '');
|
|
|
|
|
|
2026-07-23 00:49:02 -04:00
|
|
|
let pages = 0;
|
|
|
|
|
|
|
|
|
|
for (const file of walk(BUILD_DIR)) {
|
|
|
|
|
const html = readFileSync(file, 'utf8');
|
|
|
|
|
const hashes = new Set();
|
|
|
|
|
|
|
|
|
|
for (const match of html.matchAll(scriptRe)) {
|
|
|
|
|
const content = match[1];
|
|
|
|
|
if (!content.trim()) continue;
|
|
|
|
|
hashes.add(`'sha256-${createHash('sha256').update(content).digest('base64')}'`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (hashes.size === 0) continue;
|
|
|
|
|
|
2026-07-23 01:19:22 -04:00
|
|
|
const csp = `default-src 'self'; script-src 'self' ${[...hashes].join(' ')}; ${sharedPolicy}`;
|
2026-07-23 00:49:02 -04:00
|
|
|
const meta = `<meta http-equiv="Content-Security-Policy" content="${csp}" />`;
|
|
|
|
|
|
|
|
|
|
const anchor = html.match(/<meta charset="[^"]+"\s*\/?>/);
|
|
|
|
|
if (!anchor) {
|
|
|
|
|
console.warn(`[csp] pas d'ancre charset dans ${file}, page ignorée`);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
writeFileSync(file, html.replace(anchor[0], `${anchor[0]}\n\t\t${meta}`));
|
|
|
|
|
pages++;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-23 01:19:22 -04:00
|
|
|
console.log(`[csp] meta CSP complète injectée dans ${pages} page(s)`);
|