bookworm-smart-assistant/scripts/archive/apply-v61-security-patches.js

620 lines
27 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.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();