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

344 lines
16 KiB
JavaScript
Raw Permalink Normal View History

#!/usr/bin/env node
/**
* Bookworm v6.0 Round-2 Critical Patches hooks/ 补丁脚本
*
* 涵盖:
* XC2 route-compliance-gate.js: PowerShell/wmic 双执行 + driveLetter 校验
* XC3 block-sensitive-files.js: 安全日志 detail 字段未脱敏
* XC11 block-sensitive-files.js: isSettingsSafeWrite 只查键不查值
* XC14 route-interceptor.js: task-notification 污染 route-state
*
* 用法: node scripts/apply-r2-critical-patches.js [--dry-run] [--patch=XC2,XC3,...]
*
* 幂等性: 每个补丁通过 sentinel 字符串检测是否已应用已修复则跳过
* Fail-safe: 任何单个补丁失败不影响其他补丁继续执行
* 原子写入: 先写 .tmp rename防止写入中断导致文件损坏
*/
'use strict';
const fs = require('fs');
const path = require('path');
// ─── 路径检测 ────────────────────────────────────────────
function detectClaudeRoot() {
const selfDir = __dirname;
if (selfDir.includes('.claude')) return selfDir.replace(/[/\\]scripts$/, '');
return (process.env.USERPROFILE || process.env.HOME || '').replace(/\\/g, '/') + '/.claude';
}
const ROOT = detectClaudeRoot();
const HOOKS_DIR = path.join(ROOT, 'hooks');
// ─── CLI 参数 ────────────────────────────────────────────
const args = process.argv.slice(2);
const DRY_RUN = args.includes('--dry-run');
const patchArg = args.find(a => a.startsWith('--patch='));
const PATCH_FILTER = patchArg
? new Set(patchArg.replace('--patch=', '').split(',').map(s => s.trim().toUpperCase()))
: null;
// ─── 结果收集 ────────────────────────────────────────────
const results = [];
/**
* 通用补丁函数基于 sentinel 幂等检测 + 原子写入
* @param {string} id 补丁 ID
* @param {string} file 目标文件绝对路径
* @param {string} sentinel 已应用标记存在则跳过
* @param {Function} transform (content: string) => string返回修改后内容
* @param {string} desc 描述
*/
function applyPatch(id, file, sentinel, transform, desc) {
if (PATCH_FILTER && !PATCH_FILTER.has(id)) {
results.push({ id, status: 'skip', reason: '未在 --patch 过滤列表中', file: path.basename(file) });
return;
}
try {
if (!fs.existsSync(file)) {
results.push({ id, status: 'error', reason: `文件不存在: ${file}`, file: path.basename(file) });
return;
}
const content = fs.readFileSync(file, 'utf8');
// 幂等检测
if (content.includes(sentinel)) {
results.push({ id, status: 'skip', reason: '已应用(幂等跳过)', file: path.basename(file) });
return;
}
const patched = transform(content);
if (patched === content) {
results.push({ id, status: 'error', reason: '目标字符串未找到(版本不匹配?)', file: path.basename(file) });
return;
}
if (DRY_RUN) {
results.push({ id, status: 'dry-run', desc, file: path.basename(file) });
return;
}
// 原子写入
const tmp = file + '.r2patch.tmp';
fs.writeFileSync(tmp, patched, 'utf8');
fs.renameSync(tmp, file);
results.push({ id, status: 'ok', desc, file: path.basename(file) });
} catch (err) {
results.push({ id, status: 'error', reason: err.message, file: path.basename(file) });
}
}
// ═══════════════════════════════════════════════════════════
// XC2: route-compliance-gate.js
// 问题: PowerShell 成功后 wmic 仍无条件执行(缓存 miss 时延迟翻倍)
// 修复: PowerShell 成功立即写缓存并 returnwmic 仅在 PowerShell 失败时执行
// 同时增加 driveLetter 输入校验防止命令注入
// ═══════════════════════════════════════════════════════════
applyPatch(
'XC2',
path.join(HOOKS_DIR, 'route-compliance-gate.js'),
'XC2 修复: driveLetter 输入校验', // sentinel
(content) => {
// 精确匹配现有代码块(从 driveLetter 声明到 wmic 执行结束)
const OLD = [
' // 优先 PowerShell Get-PSDrive (Windows 10/11 通用,无弃用风险)',
' try {',
' const driveLetter = drive.charAt(0);',
' const psOut = execSync(`powershell -NoProfile -Command "(Get-PSDrive ${driveLetter}).Free"`, { encoding: \'utf8\', timeout: 3000 }).trim();',
' const freeSpace = parseInt(psOut, 10);',
' if (!isNaN(freeSpace)) { result = freeSpace < THRESHOLD; }',
' } catch {}',
'',
' // Fallback: wmic (Windows 10 及旧版本)',
' const wmicOut = execSync(`wmic logicaldisk where "DeviceID=\'${drive}\'" get FreeSpace /value`, { encoding: \'utf8\', timeout: 2000 });',
' const match = wmicOut.match(/FreeSpace=(\\d+)/);',
' if (match) { result = parseInt(match[1], 10) < THRESHOLD; }',
].join('\n');
const NEW = [
' // XC2 修复: driveLetter 输入校验,防止命令注入',
' const driveLetter = drive.charAt(0);',
' if (!/^[A-Za-z]$/.test(driveLetter)) {',
' return false; // 非法驱动器字母,默认不阻断',
' }',
'',
' // 优先 PowerShell Get-PSDrive (Windows 10/11 通用,无弃用风险)',
' let psSuccess = false;',
' try {',
' const psOut = execSync(`powershell -NoProfile -Command "(Get-PSDrive ${driveLetter}).Free"`, { encoding: \'utf8\', timeout: 3000 }).trim();',
' const freeSpace = parseInt(psOut, 10);',
' if (!isNaN(freeSpace)) {',
' result = freeSpace < THRESHOLD;',
' psSuccess = true;',
' // XC2 修复: PowerShell 成功后立即写缓存并返回,不再执行 wmic',
' try {',
' require(\'fs\').writeFileSync(DISK_CACHE_FILE, JSON.stringify({ ts: Date.now(), critical: result }));',
' } catch {}',
' return result;',
' }',
' } catch {}',
'',
' // XC2 修复: Fallback wmic 仅在 PowerShell 失败时执行',
' if (!psSuccess) {',
' const wmicOut = execSync(`wmic logicaldisk where "DeviceID=\'${drive}\'" get FreeSpace /value`, { encoding: \'utf8\', timeout: 2000 });',
' const match = wmicOut.match(/FreeSpace=(\\d+)/);',
' if (match) { result = parseInt(match[1], 10) < THRESHOLD; }',
' }',
].join('\n');
return content.includes(OLD) ? content.replace(OLD, NEW) : content;
},
'XC2: PowerShell 成功后跳过 wmic + driveLetter 输入校验',
);
// ═══════════════════════════════════════════════════════════
// XC3-import: block-sensitive-files.js
// 注入 _sanitize 辅助函数到文件顶部 require 区域之后
// ═══════════════════════════════════════════════════════════
applyPatch(
'XC3-import',
path.join(HOOKS_DIR, 'block-sensitive-files.js'),
'// XC3 修复: 引入 sanitize 模块', // sentinel
(content) => {
const OLD = 'const fs = require(\'fs\');\nconst path = require(\'path\');';
const NEW = [
"const fs = require('fs');",
"const path = require('path');",
'',
'// XC3 修复: 引入 sanitize 模块,对安全日志中的敏感内容脱敏(防止 API Key 写入日志)',
'const _sanitize = (() => {',
' try {',
' const _root = (() => {',
' const selfDir = path.dirname(__filename);',
" if (selfDir.includes('.claude')) return selfDir.replace(/[\\/\\\\]hooks$/, '');",
" return (process.env.USERPROFILE || process.env.HOME || '').replace(/\\\\/g, '/') + '/.claude';",
' })();',
" return require(path.join(_root, 'scripts', 'sanitize.js')).sanitize;",
" } catch { return (t) => (t || ''); } // 模块缺失时透传,不阻断主流程",
'})();',
].join('\n');
return content.includes(OLD) ? content.replace(OLD, NEW) : content;
},
'XC3-import: 注入 _sanitize 辅助函数',
);
// ═══════════════════════════════════════════════════════════
// XC3: block-sensitive-files.js
// 对 logSecurityEvent 的 detail 字段调用 _sanitize 脱敏
// ═══════════════════════════════════════════════════════════
applyPatch(
'XC3',
path.join(HOOKS_DIR, 'block-sensitive-files.js'),
'// XC3 修复: 脱敏后写入', // sentinel
(content) => {
const OLD = " detail: (detail || '').slice(0, 200),";
const NEW = " detail: _sanitize((detail || '').slice(0, 200)), // XC3 修复: 脱敏后写入,防止 API Key 泄露到安全日志";
return content.includes(OLD) ? content.replace(OLD, NEW) : content;
},
'XC3: logSecurityEvent detail 字段脱敏',
);
// ═══════════════════════════════════════════════════════════
// XC11: block-sensitive-files.js
// isSettingsSafeWrite 增加值校验
// skipDangerousModePermissionPrompt 只允许设为 false
// ═══════════════════════════════════════════════════════════
applyPatch(
'XC11',
path.join(HOOKS_DIR, 'block-sensitive-files.js'),
'XC11 修复: 校验每个键的值', // sentinel
(content) => {
const OLD = [
' function isSettingsSafeWrite(fp, c) {',
" if (!/[\\/]\\.\\.claude[\\/]settings\\.json$/.test(fp)) return false;",
' if (!c) return false;',
' try {',
' const parsed = JSON.parse(c);',
' const keys = Object.keys(parsed);',
' return keys.length > 0 && keys.every(k => SETTINGS_SAFE_KEYS.has(k));',
' } catch { return false; }',
' }',
].join('\n');
// 原文件实际内容(从 Read 工具确认的)
const OLD2 =
' function isSettingsSafeWrite(fp, c) {\n' +
" if (!/[\\/]\\.claude[\\/]settings\\.json$/.test(fp)) return false;\n" +
' if (!c) return false;\n' +
' try {\n' +
' const parsed = JSON.parse(c);\n' +
' const keys = Object.keys(parsed);\n' +
' return keys.length > 0 && keys.every(k => SETTINGS_SAFE_KEYS.has(k));\n' +
' } catch { return false; }\n' +
' }';
const NEW = [
' // XC11 修复: 除键名白名单外,还限制允许写入的值',
' // 攻击者不能将 skipDangerousModePermissionPrompt 设回 true',
' const SETTINGS_SAFE_VALUES = {',
" 'skipDangerousModePermissionPrompt': [false],",
' };',
' function isSettingsSafeWrite(fp, c) {',
" if (!/[\\/]\\.claude[\\/]settings\\.json$/.test(fp)) return false;",
' if (!c) return false;',
' try {',
' const parsed = JSON.parse(c);',
' const keys = Object.keys(parsed);',
' if (keys.length === 0) return false;',
' if (!keys.every(k => SETTINGS_SAFE_KEYS.has(k))) return false;',
' // XC11 修复: 校验每个键的值是否在允许列表中',
' for (const [k, v] of Object.entries(parsed)) {',
' const allowed = SETTINGS_SAFE_VALUES[k];',
' if (allowed !== undefined && !allowed.includes(v)) return false;',
' }',
' return true;',
' } catch { return false; }',
' }',
].join('\n');
if (content.includes(OLD2)) return content.replace(OLD2, NEW);
if (content.includes(OLD)) return content.replace(OLD, NEW);
return content;
},
'XC11: isSettingsSafeWrite 增加值校验skipDangerousModePermissionPrompt 只允许 false',
);
// ═══════════════════════════════════════════════════════════
// XC14: route-interceptor.js
// task-notification 系统消息在 main() 中提前退出
// 注意: appendRouteLog 已有同类过滤,但 writeRouteState 调用早于它
// ═══════════════════════════════════════════════════════════
applyPatch(
'XC14',
path.join(HOOKS_DIR, 'route-interceptor.js'),
'XC14 修复: task-notification 系统消息提前退出', // sentinel
(content) => {
const OLD =
' if (!prompt || typeof prompt !== \'string\' || prompt.trim().length === 0) {\n' +
' process.exit(0);\n' +
' return;\n' +
' }\n' +
'\n' +
' // v5.3: 会话首次激活横幅 (返回横幅文本或 null)';
const NEW =
' if (!prompt || typeof prompt !== \'string\' || prompt.trim().length === 0) {\n' +
' process.exit(0);\n' +
' return;\n' +
' }\n' +
'\n' +
' // XC14 修复: task-notification 系统消息提前退出,不写 route-state-current.json\n' +
' // appendRouteLog 已有同类过滤,但 writeRouteState 调用早于它state 文件会被污染\n' +
' if (prompt.includes(\'<task-notification>\')) {\n' +
' process.exit(0);\n' +
' return;\n' +
' }\n' +
'\n' +
' // v5.3: 会话首次激活横幅 (返回横幅文本或 null)';
return content.includes(OLD) ? content.replace(OLD, NEW) : content;
},
'XC14: task-notification 提前退出,不写 route-state',
);
// ─── 运行语法验证 ─────────────────────────────────────────
function verifySyntax(file) {
if (DRY_RUN) return;
try {
const { execSync } = require('child_process');
execSync(`node -c "${file}"`, { encoding: 'utf8', timeout: 5000 });
return true;
} catch (err) {
return false;
}
}
// ─── 输出结果 ─────────────────────────────────────────────
console.log('\n=== Bookworm v6.0 Round-2 Patch Results ===\n');
let okCount = 0, skipCount = 0, errorCount = 0;
for (const r of results) {
const icon = r.status === 'ok' ? '✓' : r.status === 'skip' ? '' : r.status === 'dry-run' ? '○' : '✗';
const detail = r.desc || r.reason || '';
console.log(` ${icon} [${r.id}] ${r.file}: ${r.status}${detail ? ' — ' + detail : ''}`);
if (r.status === 'ok' || r.status === 'dry-run') okCount++;
else if (r.status === 'skip') skipCount++;
else errorCount++;
}
// 语法验证(非 dry-run
if (!DRY_RUN) {
const toVerify = [
path.join(HOOKS_DIR, 'route-compliance-gate.js'),
path.join(HOOKS_DIR, 'block-sensitive-files.js'),
path.join(HOOKS_DIR, 'route-interceptor.js'),
];
console.log('\n--- 语法验证 ---');
for (const f of toVerify) {
const ok = verifySyntax(f);
console.log(` ${ok ? '✓' : '✗'} node -c ${path.basename(f)}: ${ok ? 'OK' : 'SYNTAX ERROR'}`);
if (!ok) errorCount++;
}
}
console.log(`\n Applied: ${okCount} Skipped: ${skipCount} Errors: ${errorCount}`);
if (DRY_RUN) console.log(' (dry-run 模式,未实际写入文件)');
console.log('');
process.exit(errorCount > 0 ? 1 : 0);