bookworm-smart-assistant/hooks/constitution-precheck.js
Bookworm Admin b7a8e29d21 release: v6.7.0 - OTA E2E test release
- 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)
2026-04-27 17:59:44 +08:00

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();