#!/usr/bin/env node /** * PreToolUse Hook: 宿法 ERROR 级规则预检 (v6.0 F2-4, v6.1 P1-13) * Matcher: Write|Edit * * constitution-guard.js 在 PostToolUse 阻不住写入前的错误。 * 本钉子在 PreToolUse 阶段对最高风险 ERROR 规则进行前置拦截。 * * 规则: * exec-injection — child_process 参数含变量拼接 * hardcoded-secret — 硬编码 API Key / Token * hidden-network-egress — 新增到非本地地址的网络请求 (从 constitution-guard 提升) * prototype-pollution — __proto__ / constructor.prototype 赋值 (从 constitution-guard 提升) * * 退出码: 0=放行 | 2=阻断 Fail-close: 异常时 exit(2) 阻断写入 (P1-11) */ 'use strict'; const fs = require('fs'); const path = require('path'); const readStdin = require('./lib/read-stdin.js'); try { const { isEnabled } = require('../scripts/feature-flags.js'); if (!isEnabled('constitution-precheck')) process.exit(0); } catch {} const CODE_EXTENSIONS = /.(?: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; // [XC12] 扩展覆盖更多代码/配置文件类型 const PRECHECK_RULES = [ { id: 'exec-injection', label: '命令注入: child_process 参数含变量拼接', pattern: /(?:exec|execSync|execFile|execFileSync|spawn|spawnSync|fork)\s*\(\s*(?:`[^`]*\$\{|['"][^'"]*['"]\s*\+)/, }, { id: 'hardcoded-secret', label: '硬编码密鑰: 包含高熵密鑰字符串', pattern: /(?:api[_-]?key|api[_-]?secret|access[_-]?token|secret[_-]?key|auth[_-]?token|private[_-]?key|db[_-]?password|token|credential|password|passwd|pwd)\s*[=:]\s*['"`][A-Za-z0-9+\/=_\-]{20,}['"`]/i, }, // #11: 从 constitution-guard warning 提升到 precheck 硬拦截 { id: 'timed-network-call', label: '定时外联: setInterval/setTimeout 中包含网络请求', pattern: /(?:setInterval|setTimeout)\s*\(\s*(?:async\s*)?\(\s*\)\s*=>\s*\{[^}]*(?:fetch|https?\.request|https?\.get)/, }, { id: 'base64-obfuscation', label: 'Base64 混淆: decode 后执行', pattern: /(?:Buffer\.from|atob)\s*\([^)]+,\s*['"]base64['"]\s*\).*(?:eval|Function|require)/, }, // P1-13: 从 constitution-guard.js (PostToolUse) 提升到 PreToolUse 实现硬拦截 { id: 'hidden-network-egress', label: '疑似隐蔽外联: 新增到非本地地址的网络请求', pattern: /(?:https?\.request|https?\.get|fetch)\s*\(\s*['"`]https?:\/\/(?!localhost|127\.|0\.0\.0\.0|10\.|192\.168\.|172\.(?:1[6-9]|2\d|3[01])\.|169\.254\.|::1|fe80:|fc[0-9a-f]{2}:|fd[0-9a-f]{2}:|2001:0?:|2002:|64:ff9b:)/, // PATCH-SSRF-IPv6-RFC1918-V1: IPv6+RFC1918 }, { id: 'prototype-pollution', label: '原型污染风险: __proto__ 或 constructor.prototype 赋值', pattern: /(?:__proto__|constructor\s*\.\s*prototype)\s*(?:=|\[)/, }, // P1-CC-W1: 宪法 11.1 禁止 eval/new Function/vm.runIn* (之前仅覆盖 exec) { id: 'eval-execution', label: '禁止 eval/new Function/vm.runIn*: 宪法第十一章硬拦截', pattern: /\b(?:eval|new\s+Function|vm\s*\.\s*runIn(?:NewContext|ThisContext|Context))\s*\(/, }, ]; function extractContent(ti) { return ti ? (ti.content || '') + (ti.new_string || '') : ''; } function detectViolation(content) { const lines = (content || '').split('\n'); // Phase 1: 单行检测 for (const rule of PRECHECK_RULES) { for (let i = 0; i < lines.length; i++) { if (rule.pattern && rule.pattern.test(lines[i])) { return { id: rule.id, label: rule.label, line: i + 1 }; } } } // Phase 2: L3 多行检测 — 合并相邻行匹配高危规则 const ML_IDS = new Set(['exec-injection', 'eval-execution', 'hardcoded-secret']); const mlRules = PRECHECK_RULES.filter(r => ML_IDS.has(r.id)); if (mlRules.length > 0 && lines.length > 1 && content.length < 200 * 1024) { for (let i = 0; i < lines.length - 1; i++) { const m2 = lines[i].trimEnd() + " " + lines[i + 1].trimStart(); for (const rule of mlRules) { if (rule.pattern && rule.pattern.test(m2)) { return { id: rule.id, label: rule.label + ' (跨行检测)', line: i + 1 }; } } if (i < lines.length - 2) { const m3 = m2.trimEnd() + " " + lines[i + 2].trimStart(); for (const rule of mlRules) { if (rule.pattern && rule.pattern.test(m3)) { return { id: rule.id, label: rule.label + ' (跨行检测)', line: i + 1 }; } } } } } return null; } function logEvent(ruleId, filePath, lineNo) { try { const root = path.dirname(__filename).replace(/[\/\\]hooks$/, ''); const dir = path.join(root, 'debug'); if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); const f = path.join(dir, `security-${new Date().toISOString().slice(0,10)}.jsonl`); fs.appendFileSync(f, JSON.stringify({ ts: new Date().toISOString(), decision: 'deny', hook: 'constitution-precheck', rule: ruleId, line: lineNo }) + '\n'); } catch {} } function main() { readStdin({ maxSize: 512 * 1024 }).then(input => { const fp = input.tool_input?.file_path || ''; if (!fp || !CODE_EXTENSIONS.test(fp)) { process.exit(0); return; } const content = extractContent(input.tool_input); if (!content) { process.exit(0); return; } const v = detectViolation(content); if (v) { logEvent(v.id, fp, v.line); process.stderr.write(JSON.stringify({ hookSpecificOutput: { permissionDecision: 'deny' }, systemMessage: [ '[constitution-precheck] ERROR 级违规,已阻断写入', `文件: ${path.basename(fp)}, 行: ${v.line}`, `规则: [${v.id}] ${v.label}`, '请修复后重试。参考: constitution/AI-CONSTITUTION.md 第十一章', ].join('\n'), })); process.exit(2); return; } process.exit(0); }).catch(() => process.exit(2)); // P1-11: fail-close } if (typeof module !== 'undefined') { module.exports = { PRECHECK_RULES, detectViolation, extractContent, CODE_EXTENSIONS }; } if (require.main === module) main();