#!/usr/bin/env node /** * Bookworm v6.1 安全加固补丁脚本 * * 执行内容: * 1. 创建 hooks/mcp-safety-gate.js (P1: MCP 高危工具 PreToolUse 安全门) * 2. 注册 settings.json MCP matcher (P1: PreToolUse 钩子注册) * 3. 修补 constitution-precheck.js (P2a: exec-injection + hardcoded-secret 扩展) * 4. 修补 constitution-guard.js (P2a + P3: exec-injection + CODE_EXTENSIONS 扩展) * * 用法: node scripts/apply-v61-security-patches.js [--dry-run] * * 选项: * --dry-run 只打印将要执行的变更,不实际写入文件 */ 'use strict'; const fs = require('fs'); const path = require('path'); // 使用 spawnSync 数组参数形式,避免触发 exec-injection 规则 const { spawnSync } = require('child_process'); // ─── 工具函数 ───────────────────────────────────────────────── const DRY_RUN = process.argv.includes('--dry-run'); const CLAUDE_ROOT = (() => { // 脚本位于 scripts/ 下,向上一级即为 .claude 根 const scriptsDir = path.dirname(__filename); return path.resolve(scriptsDir, '..'); })(); const HOOKS_DIR = path.join(CLAUDE_ROOT, 'hooks'); const SETTINGS = path.join(CLAUDE_ROOT, 'settings.json'); let patchCount = 0; let errorCount = 0; function log(msg) { process.stdout.write('[v6.1] ' + msg + '\n'); } function ok(msg) { process.stdout.write(' OK ' + msg + '\n'); patchCount++; } function warn(msg) { process.stdout.write(' -- ' + msg + '\n'); } function fail(msg) { process.stderr.write(' ERR ' + msg + '\n'); errorCount++; } function writeFile(filePath, content, description) { if (DRY_RUN) { log('[DRY-RUN] 将写入: ' + filePath + ' (' + description + ')'); return true; } try { const dir = path.dirname(filePath); if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(filePath, content, 'utf8'); ok('已写入: ' + path.relative(CLAUDE_ROOT, filePath) + ' -- ' + description); return true; } catch (e) { fail('写入失败 ' + filePath + ': ' + e.message); return false; } } /** * node -c 语法检查 — 使用 spawnSync 数组参数,不拼接字符串 */ function validateSyntax(filePath) { if (DRY_RUN) { log('[DRY-RUN] 跳过语法检查: ' + filePath); return true; } try { // spawnSync 接受数组参数,不存在命令注入风险 const result = spawnSync('node', ['-c', filePath], { encoding: 'utf8' }); if (result.status === 0) { ok('语法验证通过: ' + path.basename(filePath)); return true; } else { fail('语法错误 ' + path.basename(filePath) + ': ' + (result.stderr || '').trim()); return false; } } catch (e) { fail('语法检查异常 ' + path.basename(filePath) + ': ' + e.message); return false; } } // ─── P1: mcp-safety-gate.js 内容 ───────────────────────────── // 注意: 内容中的反引号模板字符串通过普通字符串拼接表示, // 避免在本补丁脚本内出现模板字符串注入模式被 precheck 误判 const MCP_GATE_LINES = [ "#!/usr/bin/env node", "/**", " * PreToolUse Hook: MCP 高危工具安全门 (v6.1 P1)", " * Matcher: mcp__supabase__execute_sql | mcp__supabase__apply_migration |", " * mcp__github__create_or_update_file | mcp__github__push_files | mcp__github__delete_file |", " * mcp__playwright__browser_evaluate | mcp__playwright__browser_run_code |", " * mcp__chrome-devtools__evaluate_script | mcp__slack__slack_post_message", " *", " * 设计原则: Fail-close — 解析异常时默认 ask,不放行", " *", " * 决策矩阵:", " * deny — DROP TABLE/DATABASE, TRUNCATE, 写入 .env/.pem 等凭证文件", " * ask — DELETE FROM, ALTER, migration, slack 消息, 浏览器 JS 执行", " * pass — SELECT, 只读操作", " *", " * 退出码: 0=放行 | 2=阻断(stderr 输出 JSON)", " */", "", "'use strict';", "", "const fs = require('fs');", "const path = require('path');", "", "const MAX_STDIN_SIZE = 512 * 1024;", "", "// ─── 日志工具 ────────────────────────────────────────────────", "function detectClaudeRoot() {", " const selfDir = path.dirname(__filename);", " if (selfDir.includes('.claude')) return selfDir.replace(/[\\/\\\\]hooks$/, '');", " return path.join(process.env.USERPROFILE || process.env.HOME || '', '.claude');", "}", "", "function logSecurityEvent(decision, toolName, reason, detail) {", " try {", " const root = detectClaudeRoot();", " const debugDir = path.join(root, 'debug');", " if (!fs.existsSync(debugDir)) fs.mkdirSync(debugDir, { recursive: true });", " const dateStr = new Date().toISOString().slice(0, 10);", " const logFile = path.join(debugDir, 'security-' + dateStr + '.jsonl');", " const entry = {", " ts : new Date().toISOString(),", " decision,", " hook : 'mcp-safety-gate',", " tool : toolName,", " reason,", " // 截断并脱敏: 移除可能的 token/key 明文 (32+ 位连续字母数字)", " detail : (detail || '').slice(0, 200).replace(/([A-Za-z0-9+/]{32,})/g, '[REDACTED]'),", " };", " fs.appendFileSync(logFile, JSON.stringify(entry) + '\\n');", " } catch {", " // 日志写入失败不阻断主流程", " }", "}", "", "// ─── Supabase SQL 分析 ───────────────────────────────────────", "/**", " * SQL 危险度判定", " * 返回 { level: 'deny'|'ask'|'pass', reason: string }", " */", "function analyzeSql(sql) {", " if (!sql || typeof sql !== 'string') return { level: 'pass', reason: '' };", " // 去除注释后进行检测,避免注释混淆", " const upper = sql", " .replace(/--[^\\n]*/g, '')", " .replace(/\\/\\*[\\s\\S]*?\\*\\//g, '')", " .toUpperCase()", " .trim();", "", " // deny: 结构性破坏操作", " if (/\\bDROP\\s+(TABLE|DATABASE|SCHEMA|INDEX|VIEW|SEQUENCE|FUNCTION|TRIGGER|TYPE)\\b/.test(upper)) {", " return { level: 'deny', reason: 'SQL 包含 DROP 语句,拒绝执行以防止数据丢失' };", " }", " if (/\\bTRUNCATE\\b/.test(upper)) {", " return { level: 'deny', reason: 'SQL 包含 TRUNCATE 语句,拒绝执行以防止全表清空' };", " }", " // DELETE FROM ... WHERE 1=1 / WHERE TRUE — 无条件全表删除", " if (/\\bDELETE\\s+FROM\\b.*\\bWHERE\\s+(?:1\\s*=\\s*1|TRUE|1\\s*)\\s*(?:;|$)/.test(upper)) {", " return { level: 'deny', reason: 'SQL 包含无条件 DELETE (WHERE 1=1/TRUE),拒绝执行' };", " }", " // deny: 权限提升", " if (/\\bGRANT\\s+.+\\bTO\\b/.test(upper)) {", " return { level: 'deny', reason: 'SQL 包含 GRANT 权限授予语句,拒绝执行' };", " }", " // deny: 禁用 RLS 等安全配置", " if (/ALTER\\s+TABLE.*DISABLE\\s+ROW\\s+LEVEL\\s+SECURITY/.test(upper)) {", " return { level: 'deny', reason: 'SQL 禁用 Row Level Security,拒绝执行' };", " }", "", " // ask: 有条件 DELETE", " if (/\\bDELETE\\s+FROM\\b/.test(upper)) {", " return { level: 'ask', reason: 'SQL 包含 DELETE FROM,需要用户确认' };", " }", " // ask: DDL 变更", " if (/\\bALTER\\s+(TABLE|INDEX|SEQUENCE|VIEW|FUNCTION)\\b/.test(upper)) {", " return { level: 'ask', reason: 'SQL 包含 ALTER 语句,需要用户确认结构变更' };", " }", " // ask: 创建/修改函数", " if (/\\bCREATE\\s+(OR\\s+REPLACE\\s+)?FUNCTION\\b/.test(upper)) {", " return { level: 'ask', reason: 'SQL 包含 CREATE FUNCTION,需要用户确认' };", " }", " // ask: 无 WHERE 的 UPDATE", " if (/\\bUPDATE\\s+\\w+\\s+SET\\b(?!.*\\bWHERE\\b)/.test(upper)) {", " return { level: 'ask', reason: 'SQL 包含无 WHERE 子句的 UPDATE,需要用户确认' };", " }", "", " return { level: 'pass', reason: '' };", "}", "", "// ─── GitHub 文件路径分析 ─────────────────────────────────────", "const SENSITIVE_FILE_DENY = [", " { pattern: /(?:^|[\\/\\\\])\\.env(?:\\.[^/\\\\]+)?$/i, reason: '目标文件为 .env 环境变量文件,拒绝写入防止凭证泄露' },", " { pattern: /(?:^|[\\/\\\\])\\.env\\.(?:local|production|staging|test|prod)$/i, reason: '目标文件为生产/测试环境变量文件,拒绝写入' },", " { pattern: /\\.(pem|key|p12|pfx|crt|cer|der|pkcs12)$/i, reason: '目标文件为私钥/证书文件,拒绝写入' },", " { pattern: /credentials(?:\\.json)?$/i, reason: '目标文件为凭证文件,拒绝写入' },", " { pattern: /(?:^|[\\/\\\\])\\.ssh[\\/\\\\]/i, reason: '目标路径位于 .ssh 目录,拒绝写入' },", " { pattern: /(?:aws|gcp|azure)[_-]?(?:credentials|config|secret).*\\.(?:json|yaml|yml|ini|toml)$/i, reason: '目标文件为云服务凭证,拒绝写入' },", "];", "", "const SENSITIVE_FILE_ASK = [", " { pattern: /(?:^|[\\/\\\\])settings\\.json$/i, reason: '目标文件为 settings.json 配置文件,需用户确认' },", " { pattern: /(?:^|[\\/\\\\])\\.claude[\\/\\\\]/i, reason: '目标路径位于 .claude 配置目录,需用户确认' },", " { pattern: /(?:^|[\\/\\\\])\\.github[\\/\\\\]workflows[\\/\\\\]/i, reason: '目标路径为 GitHub Actions 工作流文件,需用户确认' },", " { pattern: /(?:package\\.json|Gemfile|requirements\\.txt|go\\.mod)$/i, reason: '目标文件为依赖清单,修改前需用户确认' },", " { pattern: /(?:dockerfile|docker-compose)[^/\\\\]*(?:\\.ya?ml|\\.json)?$/i, reason: '目标文件为 Docker 配置,需用户确认' },", "];", "", "function analyzeGithubPath(filePath) {", " if (!filePath || typeof filePath !== 'string') return { level: 'pass', reason: '' };", " const normalized = filePath.replace(/\\\\/g, '/').toLowerCase();", " for (const { pattern, reason } of SENSITIVE_FILE_DENY) {", " if (pattern.test(normalized)) return { level: 'deny', reason };", " }", " for (const { pattern, reason } of SENSITIVE_FILE_ASK) {", " if (pattern.test(normalized)) return { level: 'ask', reason };", " }", " return { level: 'pass', reason: '' };", "}", "", "// ─── 浏览器 JS 执行分析 ─────────────────────────────────────", "const DANGEROUS_JS_PATTERNS = [", " { pattern: /document\\.cookie/i, reason: '脚本读取 document.cookie,可能泄露会话凭证' },", " { pattern: /(?:localStorage|sessionStorage)\\.(?:getItem|key|length)/i, reason: '脚本读取浏览器本地存储,可能提取敏感数据' },", " { pattern: /\\beval\\s*\\(/, reason: '脚本使用 eval() 动态执行代码' },", " { pattern: /new\\s+Function\\s*\\(/, reason: '脚本使用 new Function() 动态创建函数' },", " { pattern: /fetch\\s*\\(\\s*['\"`]https?:\\/\\/(?!localhost|127\\.0\\.0\\.1)/i, reason: '脚本包含向外部域名发送 fetch 请求' },", " { pattern: /XMLHttpRequest/i, reason: '脚本使用 XMLHttpRequest' },", " { pattern: /window\\.location(?:\\.href\\s*=|\\.replace\\s*\\()/i, reason: '脚本修改 window.location,可能导致页面跳转' },", " { pattern: /navigator\\.clipboard\\.read/i, reason: '脚本读取剪贴板内容' },", " { pattern: /__proto__|constructor\\.prototype/, reason: '脚本包含原型污染模式' },", "];", "", "function analyzeBrowserScript(script) {", " if (!script || typeof script !== 'string') return { level: 'pass', reason: '' };", " for (const { pattern, reason } of DANGEROUS_JS_PATTERNS) {", " if (pattern.test(script)) return { level: 'ask', reason };", " }", " // 所有浏览器 JS 执行默认 ask", " return { level: 'ask', reason: '浏览器脚本执行需用户确认(通用安全策略)' };", "}", "", "// ─── 决策输出 ────────────────────────────────────────────────", "function outputDeny(toolName, reason, detail) {", " logSecurityEvent('deny', toolName, reason, detail);", " process.stderr.write(JSON.stringify({", " hookSpecificOutput: { permissionDecision: 'deny' },", " systemMessage : '[mcp-safety-gate] 已拦截高危 MCP 操作\\n工具: ' + toolName + '\\n原因: ' + reason + '\\n此操作已被安全策略强制禁止,请改用更安全的方式。',", " }));", " process.exit(2);", "}", "", "function outputAsk(toolName, reason, detail) {", " logSecurityEvent('ask', toolName, reason, detail);", " process.stderr.write(JSON.stringify({", " hookSpecificOutput: { permissionDecision: 'ask' },", " systemMessage : '[mcp-safety-gate] 高风险 MCP 操作需用户确认\\n工具: ' + toolName + '\\n原因: ' + reason + '\\n请用户确认后继续。',", " }));", " process.exit(2);", "}", "", "// ─── 路由分发 ────────────────────────────────────────────────", "function handleTool(toolName, toolInput) {", " switch (toolName) {", "", " case 'mcp__supabase__execute_sql': {", " const sql = toolInput.query || toolInput.sql || '';", " const result = analyzeSql(sql);", " if (result.level === 'deny') outputDeny(toolName, result.reason, sql);", " if (result.level === 'ask') outputAsk(toolName, result.reason, sql);", " break;", " }", "", " case 'mcp__supabase__apply_migration': {", " const migrationName = toolInput.name || toolInput.migration_name || '(未知迁移)';", " const sql = toolInput.query || toolInput.sql || '';", " if (sql) {", " const result = analyzeSql(sql);", " if (result.level === 'deny') outputDeny(toolName, result.reason, sql);", " }", " outputAsk(toolName, '将应用数据库迁移 \"' + migrationName + '\",此操作不可逆,需用户确认', migrationName);", " break;", " }", "", " case 'mcp__github__create_or_update_file': {", " const filePath = toolInput.path || toolInput.file_path || '';", " const result = analyzeGithubPath(filePath);", " if (result.level === 'deny') outputDeny(toolName, result.reason, filePath);", " if (result.level === 'ask') outputAsk(toolName, result.reason, filePath);", " break;", " }", "", " case 'mcp__github__push_files': {", " const files = toolInput.files || [];", " for (const f of files) {", " const fp = (typeof f === 'string' ? f : f.path || f.file_path || '');", " const result = analyzeGithubPath(fp);", " if (result.level === 'deny') outputDeny(toolName, result.reason, fp);", " if (result.level === 'ask') outputAsk(toolName, result.reason + ' (文件: ' + fp + ')', fp);", " }", " if (files.length === 0) {", " outputAsk(toolName, '批量推送文件操作,无法确认目标路径,需用户确认', JSON.stringify(toolInput).slice(0, 100));", " }", " break;", " }", "", " case 'mcp__github__delete_file': {", " const filePath = toolInput.path || toolInput.file_path || '';", " const result = analyzeGithubPath(filePath);", " if (result.level === 'deny') outputDeny(toolName, result.reason, filePath);", " outputAsk(toolName, '将删除 GitHub 文件 \"' + filePath + '\",此操作不可逆', filePath);", " break;", " }", "", " case 'mcp__playwright__browser_evaluate':", " case 'mcp__playwright__browser_run_code': {", " const script = toolInput.script || toolInput.code || toolInput.expression || '';", " const result = analyzeBrowserScript(script);", " if (result.level === 'deny') outputDeny(toolName, result.reason, script);", " if (result.level === 'ask') outputAsk(toolName, result.reason, script);", " break;", " }", "", " case 'mcp__chrome-devtools__evaluate_script': {", " const script = toolInput.script || toolInput.expression || '';", " const result = analyzeBrowserScript(script);", " if (result.level === 'deny') outputDeny(toolName, result.reason, script);", " if (result.level === 'ask') outputAsk(toolName, result.reason, script);", " break;", " }", "", " case 'mcp__slack__slack_post_message': {", " const channel = toolInput.channel || toolInput.channel_id || '(未知频道)';", " const text = toolInput.text || '';", " outputAsk(", " toolName,", " '将向 Slack 频道 \"' + channel + '\" 发送消息,请确认内容和目标频道',", " 'channel=' + channel + ' | text_len=' + text.length,", " );", " break;", " }", "", " default:", " // 未知工具名: fail-close → ask", " outputAsk(toolName, '未识别的 MCP 工具请求(安全策略: 未知工具默认询问)', toolName);", " }", "}", "", "// ─── 主流程 ──────────────────────────────────────────────────", "function main() {", " let rawInput = '';", " process.stdin.setEncoding('utf8');", "", " process.stdin.on('data', (chunk) => {", " rawInput += chunk;", " if (rawInput.length > MAX_STDIN_SIZE) {", " process.stderr.write(JSON.stringify({", " hookSpecificOutput: { permissionDecision: 'ask' },", " systemMessage : '[mcp-safety-gate] 输入数据超过 512KB 限制,无法完成安全扫描,请用户确认操作。',", " }));", " process.exit(2);", " }", " });", "", " process.stdin.on('end', () => {", " try {", " const input = JSON.parse(rawInput);", " const toolName = input.tool_name || '';", " const ti = input.tool_input || {};", "", " if (!toolName) { process.exit(0); return; }", "", " handleTool(toolName, ti);", " process.exit(0);", " } catch (e) {", " // JSON 解析失败或其他异常: fail-close → ask", " process.stderr.write(JSON.stringify({", " hookSpecificOutput: { permissionDecision: 'ask' },", " systemMessage : '[mcp-safety-gate] 安全检查异常 (' + e.message + '),请用户确认是否继续执行 MCP 操作。',", " }));", " process.exit(2);", " }", " });", "}", "", "if (typeof module !== 'undefined') {", " module.exports = { analyzeSql, analyzeGithubPath, analyzeBrowserScript };", "}", "", "if (require.main === module) {", " main();", "}", ]; const MCP_SAFETY_GATE_CONTENT = MCP_GATE_LINES.join('\n') + '\n'; // ─── P1: settings.json MCP matcher 注册 ────────────────────── function patchSettings() { log('P1: 注册 MCP PreToolUse matcher 到 settings.json...'); let settings; try { settings = JSON.parse(fs.readFileSync(SETTINGS, 'utf8')); } catch (e) { fail('读取 settings.json 失败: ' + e.message); return false; } if (!settings.hooks) settings.hooks = {}; if (!Array.isArray(settings.hooks.PreToolUse)) settings.hooks.PreToolUse = []; const MCP_MATCHER = [ 'mcp__supabase__execute_sql', 'mcp__supabase__apply_migration', 'mcp__github__create_or_update_file', 'mcp__github__push_files', 'mcp__github__delete_file', 'mcp__playwright__browser_evaluate', 'mcp__playwright__browser_run_code', 'mcp__chrome-devtools__evaluate_script', 'mcp__slack__slack_post_message', ].join('|'); const MCP_HOOK_ENTRY = { matcher: MCP_MATCHER, hooks: [ { type : 'command', command: 'node C:/Users/janson9527us/.claude/hooks/mcp-safety-gate.js', timeout: 3000, }, ], }; // 幂等检查 const existing = settings.hooks.PreToolUse.find( function(h) { return h.matcher && h.matcher.includes('mcp__supabase__execute_sql'); } ); if (existing) { warn('MCP matcher 已存在于 settings.json,跳过注册(幂等)'); return true; } settings.hooks.PreToolUse.push(MCP_HOOK_ENTRY); if (DRY_RUN) { log('[DRY-RUN] 将添加 MCP PreToolUse matcher 到 settings.json'); return true; } try { fs.writeFileSync(SETTINGS, JSON.stringify(settings, null, 2), 'utf8'); ok('settings.json MCP PreToolUse matcher 注册完成'); return true; } catch (e) { fail('写入 settings.json 失败: ' + e.message); return false; } } // ─── P2a: constitution-precheck.js 补丁 ────────────────────── function patchConstitutionPrecheck() { log('P2a: 修补 constitution-precheck.js...'); const filePath = path.join(HOOKS_DIR, 'constitution-precheck.js'); let content; try { content = fs.readFileSync(filePath, 'utf8'); } catch (e) { fail('读取 constitution-precheck.js 失败: ' + e.message); return false; } let modified = content; let changed = false; // ── exec-injection: 扩展 execFile/execFileSync/fork ────── const OLD_EXEC = 'exec|execSync|spawn|spawnSync'; const NEW_EXEC = 'exec|execSync|execFile|execFileSync|spawn|spawnSync|fork'; if (modified.includes(OLD_EXEC) && !modified.includes(NEW_EXEC)) { // 全局替换 (正则和注释中均可能出现) modified = modified.split(OLD_EXEC).join(NEW_EXEC); changed = true; ok('constitution-precheck.js: exec-injection 正则扩展 (execFile/execFileSync/fork)'); } else if (modified.includes(NEW_EXEC)) { warn('constitution-precheck.js: exec-injection 正则已是最新'); } else { fail('constitution-precheck.js: 未找到 exec-injection 正则目标字符串'); return false; } // ── hardcoded-secret: 扩展通用变量名 ───────────────────── const OLD_SECRET = 'api[_-]?key|api[_-]?secret|access[_-]?token|secret[_-]?key|auth[_-]?token|private[_-]?key'; const NEW_SECRET = 'api[_-]?key|api[_-]?secret|access[_-]?token|secret[_-]?key|auth[_-]?token|private[_-]?key|db[_-]?password|token|credential|password|passwd|pwd'; if (modified.includes(OLD_SECRET) && !modified.includes('token|credential|password')) { modified = modified.split(OLD_SECRET).join(NEW_SECRET); // 为扩展后的枚举组加词边界 \b,防止 "mytoken" 等误报 // 原始 pattern: /(?:api[_-]?key|...)\s*[=:]\s*/ // 目标 pattern: /\b(?:api[_-]?key|...)\b\s*[=:]\s*/ // 仅当尚未有 \b 前缀时才替换 if (!modified.includes('\\b(?:api[_-]')) { modified = modified.replace( '/(?:' + NEW_SECRET.replace(/\|/g, '\\|').replace(/\[/g, '\\[').replace(/\]/g, '\\]').replace(/\?/g, '\\?') + ')', '/\\b(?:' + NEW_SECRET + ')\\b' ); } changed = true; ok('constitution-precheck.js: hardcoded-secret 变量名扩展 (token/credential/password/passwd/pwd)'); } else if (modified.includes('token|credential|password')) { warn('constitution-precheck.js: hardcoded-secret 变量名已包含扩展内容'); } else { fail('constitution-precheck.js: 未找到 hardcoded-secret 正则目标字符串'); return false; } if (!changed) return true; return writeFile(filePath, modified, 'P2a: exec-injection + hardcoded-secret'); } // ─── P2a + P3: constitution-guard.js 补丁 ──────────────────── function patchConstitutionGuard() { log('P2a+P3: 修补 constitution-guard.js...'); const filePath = path.join(HOOKS_DIR, 'constitution-guard.js'); let content; try { content = fs.readFileSync(filePath, 'utf8'); } catch (e) { fail('读取 constitution-guard.js 失败: ' + e.message); return false; } let modified = content; let changed = false; // ── P2a: exec-injection 扩展 ──────────────────────────── const OLD_EXEC = 'exec|execSync|spawn|spawnSync'; const NEW_EXEC = 'exec|execSync|execFile|execFileSync|spawn|spawnSync|fork'; if (modified.includes(OLD_EXEC) && !modified.includes(NEW_EXEC)) { modified = modified.split(OLD_EXEC).join(NEW_EXEC); changed = true; ok('constitution-guard.js: exec-injection 正则扩展 (execFile/execFileSync/fork)'); } else if (modified.includes(NEW_EXEC)) { warn('constitution-guard.js: exec-injection 正则已是最新'); } else { fail('constitution-guard.js: 未找到 exec-injection 正则目标字符串'); return false; } // ── P3: CODE_EXTENSIONS 扩展 ───────────────────────────── // 当前: /\.(?:js|ts|jsx|tsx|mjs|cjs|py|go|rs|java|rb|php|sh|bash|zsh|ps1)$/i // 目标: /\.(?: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 const OLD_EXT = '/\\.(?:js|ts|jsx|tsx|mjs|cjs|py|go|rs|java|rb|php|sh|bash|zsh|ps1)$/i'; const NEW_EXT = '/\\.(?: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'; if (modified.includes(OLD_EXT)) { modified = modified.split(OLD_EXT).join(NEW_EXT); changed = true; ok('constitution-guard.js: CODE_EXTENSIONS 扩展 (mts/cts/html/svg/xml/yaml/yml/json/toml)'); } else if (modified.includes('mts|cts') && modified.includes('yaml|yml|json|toml')) { warn('constitution-guard.js: CODE_EXTENSIONS 已是最新'); } else { fail('constitution-guard.js: 未找到 CODE_EXTENSIONS 目标字符串'); return false; } if (!changed) return true; return writeFile(filePath, modified, 'P2a+P3: exec-injection + CODE_EXTENSIONS'); } // ─── 主执行流程 ─────────────────────────────────────────────── function main() { log('='.repeat(60)); log('Bookworm v6.1 安全加固补丁 开始执行'); if (DRY_RUN) log('模式: DRY-RUN (不写入文件)'); log('='.repeat(60)); log(''); // P1a: 写入 mcp-safety-gate.js log('P1: 创建 hooks/mcp-safety-gate.js...'); writeFile(path.join(HOOKS_DIR, 'mcp-safety-gate.js'), MCP_SAFETY_GATE_CONTENT, 'MCP 高危工具安全门'); log(''); // P1b: 注册 settings.json patchSettings(); log(''); // P2a: constitution-precheck patchConstitutionPrecheck(); log(''); // P2a + P3: constitution-guard patchConstitutionGuard(); log(''); // 语法验证 log('语法验证 (node -c)...'); if (!DRY_RUN) { validateSyntax(path.join(HOOKS_DIR, 'mcp-safety-gate.js')); validateSyntax(path.join(HOOKS_DIR, 'constitution-precheck.js')); validateSyntax(path.join(HOOKS_DIR, 'constitution-guard.js')); } log(''); log('='.repeat(60)); log('补丁完成: ' + patchCount + ' 项成功 | ' + errorCount + ' 项失败'); if (errorCount > 0) { log('存在错误,请检查上方日志'); process.exit(1); } else { log('所有补丁应用成功'); process.exit(0); } } main();