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

312 lines
13 KiB
JavaScript
Raw Normal View History

#!/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`);