- VERSION file as authoritative version source - export.mjs reads VERSION with package.json fallback - bw-ota.ps1 DryRun mode for safe testing - auto-setup.ps1 bumped to v3.2.0 (Phase 8 OTA)
138 lines
6.0 KiB
JavaScript
138 lines
6.0 KiB
JavaScript
#!/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();
|