620 lines
27 KiB
JavaScript
620 lines
27 KiB
JavaScript
#!/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();
|