bookworm-smart-assistant/scripts/archive/apply-r2-hardening-patches.js

312 lines
13 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
/**
* Bookworm v6.0 Round-2 加固补丁脚本
*
* 修复项:
* XC10 — route-interceptor.js writeRouteState 原子写入 (temp+rename)
* XC12 — constitution-precheck.js fail-close + 扩展名扩展
* XC13 — sensitive-content-deny.json 恢复关键规则
* XC8 — sensitive-redirect.json 增加 debug/ 目录写保护
*
* 用法:
* node scripts/apply-r2-hardening-patches.js # 预览 (dry-run)
* node scripts/apply-r2-hardening-patches.js --execute # 执行
*
* 创建: 2026-03-17
*/
'use strict';
const fs = require('fs');
const path = require('path');
const os = require('os');
// ─── 根目录检测 ───────────────────────────────────────────
function detectClaudeRoot() {
const selfDir = path.dirname(__filename);
if (selfDir.includes('.claude')) return selfDir.replace(/[/\\]scripts$/, '');
try { return require('./paths.config.js').PATHS.root; } catch {
return (process.env.USERPROFILE || process.env.HOME || '').replace(/\\/g, '/') + '/.claude';
}
}
const ROOT = detectClaudeRoot();
const HOOKS_DIR = path.join(ROOT, 'hooks');
const RULES_DIR = path.join(ROOT, 'hooks', 'rules');
const IS_DRY_RUN = !process.argv.includes('--execute');
const MODE = IS_DRY_RUN ? '[DRY-RUN]' : '[EXECUTE]';
// ─── 工具函数 ─────────────────────────────────────────────
/**
* 原子写入文件 (temp + rename)
* 在 dry-run 模式下只打印不实际写入
*/
function atomicWrite(filePath, content, label) {
if (IS_DRY_RUN) {
console.log(`${MODE} 将写入: ${filePath} (${label})`);
return true;
}
const tmpPath = filePath + '.tmp.' + process.pid;
try {
fs.writeFileSync(tmpPath, content, 'utf8');
fs.renameSync(tmpPath, filePath);
console.log(`[DONE] 已写入: ${filePath} (${label})`);
return true;
} catch (e) {
try { fs.unlinkSync(tmpPath); } catch {}
console.error(`[ERROR] 写入失败: ${filePath}${e.message}`);
return false;
}
}
/**
* 读取文件,失败时返回 null
*/
function safeRead(filePath) {
try { return fs.readFileSync(filePath, 'utf8'); } catch { return null; }
}
// ─── 补丁记录 ─────────────────────────────────────────────
const results = [];
// ═══════════════════════════════════════════════════════════
// XC10: route-interceptor.js — writeRouteState 原子写入
// ═══════════════════════════════════════════════════════════
function patchXC10() {
const filePath = path.join(HOOKS_DIR, 'route-interceptor.js');
const raw = safeRead(filePath);
if (!raw) {
console.error(`[SKIP] XC10: 无法读取 ${filePath}`);
results.push({ id: 'XC10', status: 'SKIP', reason: '文件不存在' });
return;
}
// 检查是否已应用
if (raw.includes('// [XC10] 原子写入')) {
console.log(`[SKIP] XC10: 已应用,跳过`);
results.push({ id: 'XC10', status: 'ALREADY_APPLIED' });
return;
}
// 定位并替换 writeRouteState 中的直接写入部分
const OLD = ` try {
if (!fs.existsSync(DEBUG_DIR)) fs.mkdirSync(DEBUG_DIR, { recursive: true });
fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2) + '\\n');
} catch {}`;
const NEW = ` try {
if (!fs.existsSync(DEBUG_DIR)) fs.mkdirSync(DEBUG_DIR, { recursive: true });
// [XC10] 原子写入: temp + rename防止 Windows 上 timeout 中断导致半写 JSON
const _tmpState = STATE_FILE + '.tmp.' + process.pid;
fs.writeFileSync(_tmpState, JSON.stringify(state, null, 2) + '\\n');
fs.renameSync(_tmpState, STATE_FILE);
} catch {}`;
if (!raw.includes(OLD.trim().slice(0, 60))) {
// 尝试宽松匹配
console.warn(`[WARN] XC10: 精确匹配失败,尝试宽松替换`);
}
// 使用字符串 indexOf+slice 替换,避免 CRLF/LF 差异导致 regex 失配
// 注意: writeFileSync 行中的 '\n' 是源码中的字面量 \n不是换行符
const eol = raw.includes('\r\n') ? '\r\n' : '\n';
// 源码中 writeFileSync 行含字面量 \n反斜杠+n非换行符需用 '\\\\n' 表示
const targetBlock = [
' try {',
' if (!fs.existsSync(DEBUG_DIR)) fs.mkdirSync(DEBUG_DIR, { recursive: true });',
" fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2) + '\\\\n');",
' } catch {}',
].join(eol);
const newBlock = [
' try {',
' if (!fs.existsSync(DEBUG_DIR)) fs.mkdirSync(DEBUG_DIR, { recursive: true });',
' // [XC10] 原子写入: temp + rename防止 Windows 上 timeout 中断导致半写 JSON',
' const _tmpState = STATE_FILE + \'.tmp.\' + process.pid;',
' fs.writeFileSync(_tmpState, JSON.stringify(state, null, 2) + \'\\n\');',
' fs.renameSync(_tmpState, STATE_FILE);',
' } catch {}',
].join(eol);
let patched = raw;
const idx = raw.indexOf(targetBlock);
if (idx === -1) {
console.error(`[ERROR] XC10: 未找到目标代码块EOL=${JSON.stringify(eol)}),跳过`);
results.push({ id: 'XC10', status: 'FAIL', reason: '目标代码块未找到' });
return;
}
patched = raw.slice(0, idx) + newBlock + raw.slice(idx + targetBlock.length);
if (patched === raw) {
console.error(`[ERROR] XC10: 未找到目标代码块,跳过`);
results.push({ id: 'XC10', status: 'FAIL', reason: '目标代码块未找到' });
return;
}
const ok = atomicWrite(filePath, patched, 'XC10 原子写入');
results.push({ id: 'XC10', status: ok ? (IS_DRY_RUN ? 'DRY_RUN' : 'APPLIED') : 'FAIL' });
}
// ═══════════════════════════════════════════════════════════
// XC12: constitution-precheck.js — fail-close + 扩展名扩展
// ═══════════════════════════════════════════════════════════
function patchXC12() {
const filePath = path.join(HOOKS_DIR, 'constitution-precheck.js');
const raw = safeRead(filePath);
if (!raw) {
console.error(`[SKIP] XC12: 无法读取 ${filePath}`);
results.push({ id: 'XC12', status: 'SKIP', reason: '文件不存在' });
return;
}
if (raw.includes('// [XC12]')) {
console.log(`[SKIP] XC12: 已应用,跳过`);
results.push({ id: 'XC12', status: 'ALREADY_APPLIED' });
return;
}
let patched = raw;
// 修复1: fail-open → fail-close (stdin 超大时 exit(0) 改为 ask + exit(2))
patched = patched.replace(
/process\.stdin\.on\('data',\s*c\s*=>\s*\{\s*raw\s*\+=\s*c;\s*if\s*\(raw\.length\s*>\s*MAX_STDIN_SIZE\)\s*process\.exit\(0\);\s*\}\);/,
`process.stdin.on('data', c => {
raw += c;
if (raw.length > MAX_STDIN_SIZE) {
// [XC12] fail-close: stdin 超大视为不可信输入,阻断而非放行
process.stderr.write(JSON.stringify({
hookSpecificOutput: { permissionDecision: 'ask' },
systemMessage: '[constitution-precheck] stdin 超出大小限制,已阻断',
}));
process.exit(2);
}
});`
);
// 修复2: 扩展 CODE_EXTENSIONS补充 .mts .cts .html .svg .xml .yaml .yml .json .toml
patched = patched.replace(
/const CODE_EXTENSIONS = \/\\\.(?:js\|ts\|jsx\|tsx\|mjs\|cjs\|py\|go\|rs\|java\|rb\|php\|sh\|bash\|zsh\|ps1)\$\/i;/,
`const CODE_EXTENSIONS = /\\.(?:js|ts|jsx|tsx|mjs|cjs|mts|cts|py|go|rs|java|rb|php|sh|bash|zsh|ps1|html|svg|xml|yaml|yml|json|toml)$/i; // [XC12] 扩展覆盖更多代码/配置文件类型`
);
if (patched === raw) {
console.error(`[ERROR] XC12: 未找到目标模式,跳过`);
results.push({ id: 'XC12', status: 'FAIL', reason: '目标模式未找到' });
return;
}
const ok = atomicWrite(filePath, patched, 'XC12 fail-close + 扩展名扩展');
results.push({ id: 'XC12', status: ok ? (IS_DRY_RUN ? 'DRY_RUN' : 'APPLIED') : 'FAIL' });
}
// ═══════════════════════════════════════════════════════════
// XC13: sensitive-content-deny.json — 恢复关键规则
// ═══════════════════════════════════════════════════════════
function patchXC13() {
const filePath = path.join(RULES_DIR, 'sensitive-content-deny.json');
const raw = safeRead(filePath);
if (!raw) {
console.error(`[SKIP] XC13: 无法读取 ${filePath}`);
results.push({ id: 'XC13', status: 'SKIP', reason: '文件不存在' });
return;
}
let parsed;
try { parsed = JSON.parse(raw); } catch (e) {
console.error(`[ERROR] XC13: JSON 解析失败 — ${e.message}`);
results.push({ id: 'XC13', status: 'FAIL', reason: 'JSON 解析失败' });
return;
}
// 检查是否已有该规则
const alreadyHas = (parsed.patterns || []).some(
p => p.pattern && p.pattern.includes('skipDangerousModePermissionPrompt')
);
if (alreadyHas) {
console.log(`[SKIP] XC13: skipDangerousModePermissionPrompt 规则已存在`);
results.push({ id: 'XC13', status: 'ALREADY_APPLIED' });
return;
}
// 追加关键安全规则
parsed._version = 'v6.0-r2';
parsed.patterns = parsed.patterns || [];
parsed.patterns.push({
pattern: "skipDangerousModePermissionPrompt[\"']?\\s*[=:]\\s*true",
reason: '禁止将 skipDangerousModePermissionPrompt 设为 true',
flags: 'i',
});
const ok = atomicWrite(filePath, JSON.stringify(parsed, null, 2) + '\n', 'XC13 恢复关键规则');
results.push({ id: 'XC13', status: ok ? (IS_DRY_RUN ? 'DRY_RUN' : 'APPLIED') : 'FAIL' });
}
// ═══════════════════════════════════════════════════════════
// XC8: sensitive-redirect.json — debug/ 关键状态文件写保护
// ═══════════════════════════════════════════════════════════
function patchXC8() {
const filePath = path.join(RULES_DIR, 'sensitive-redirect.json');
const raw = safeRead(filePath);
if (!raw) {
console.error(`[SKIP] XC8: 无法读取 ${filePath}`);
results.push({ id: 'XC8', status: 'SKIP', reason: '文件不存在' });
return;
}
let parsed;
try { parsed = JSON.parse(raw); } catch (e) {
console.error(`[ERROR] XC8: JSON 解析失败 — ${e.message}`);
results.push({ id: 'XC8', status: 'FAIL', reason: 'JSON 解析失败' });
return;
}
// 检查是否已有 debug 保护规则
const alreadyHas = (parsed.patterns || []).some(
p => p.regex && p.regex.includes('debug') && p.regex.includes('route-state')
);
if (alreadyHas) {
console.log(`[SKIP] XC8: debug/ 写保护规则已存在`);
results.push({ id: 'XC8', status: 'ALREADY_APPLIED' });
return;
}
// 追加 debug/ 目录关键状态文件写保护ask不 block以免影响正常 hook 写入日志)
parsed._version = 'v3.8-r2';
parsed.patterns = parsed.patterns || [];
parsed.patterns.push({
regex: '[\\\\/]\\.claude[\\\\/]debug[\\\\/](?:route-state|route-feedback|adaptive-disambiguator|session-memory)',
flags: 'i',
reason: '路由状态文件写保护 (ask): 防止通过 Bash 重定向篡改路由状态',
action: 'ask',
});
const ok = atomicWrite(filePath, JSON.stringify(parsed, null, 2) + '\n', 'XC8 debug/ 写保护');
results.push({ id: 'XC8', status: ok ? (IS_DRY_RUN ? 'DRY_RUN' : 'APPLIED') : 'FAIL' });
}
// ═══════════════════════════════════════════════════════════
// 执行所有补丁
// ═══════════════════════════════════════════════════════════
console.log(`\n${'═'.repeat(55)}`);
console.log(` Bookworm v6.0 Round-2 加固补丁 ${MODE}`);
console.log(` ${new Date().toISOString().slice(0, 19)}`);
console.log(`${'═'.repeat(55)}\n`);
patchXC10();
patchXC12();
patchXC13();
patchXC8();
// ─── 摘要 ─────────────────────────────────────────────────
console.log(`\n${'─'.repeat(55)}`);
console.log('补丁摘要:');
for (const r of results) {
const icon = r.status === 'APPLIED' ? 'DONE' : r.status === 'DRY_RUN' ? 'PREVIEW' : r.status === 'ALREADY_APPLIED' ? 'SKIP' : 'FAIL';
console.log(` [${icon.padEnd(13)}] ${r.id}${r.reason ? ' — ' + r.reason : ''}`);
}
if (IS_DRY_RUN) {
console.log(`\n提示: 以上为预览。运行 \`node scripts/apply-r2-hardening-patches.js --execute\` 执行实际修改。`);
}
console.log(`${'═'.repeat(55)}\n`);