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

344 lines
16 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 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);