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