203 lines
6.9 KiB
JavaScript
203 lines
6.9 KiB
JavaScript
|
|
#!/usr/bin/env node
|
|||
|
|
/**
|
|||
|
|
* PostToolUse Hook: 宪法反腐败守卫
|
|||
|
|
* Matcher: Edit|Write
|
|||
|
|
*
|
|||
|
|
* 补充 post-edit-quality-check.js 未覆盖的反腐败模式 (宪法第十一章)
|
|||
|
|
* 专注检测: 隐蔽外联、环境探测、原型污染、文件篡改、定时外联
|
|||
|
|
*
|
|||
|
|
* stdin: { tool_name, tool_input: { file_path, content/new_string }, tool_result }
|
|||
|
|
* 退出码: 0 (始终放行, PostToolUse 不阻断, 通过 systemMessage 告警)
|
|||
|
|
*
|
|||
|
|
* Fail-open: 任何异常 → exit(0)
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
const path = require('path');
|
|||
|
|
|
|||
|
|
const readStdin = require('./lib/read-stdin.js');
|
|||
|
|
|
|||
|
|
// ─── Feature Flag 检查 ───────────────────────────────
|
|||
|
|
try {
|
|||
|
|
const { isEnabled } = require('../scripts/feature-flags.js');
|
|||
|
|
if (!isEnabled('constitution-guard')) {
|
|||
|
|
process.exit(0);
|
|||
|
|
}
|
|||
|
|
} catch {
|
|||
|
|
// feature-flags 不存在时默认启用 (新 hook 首次运行)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── 宪法反腐败规则 (补充 post-edit-quality-check 未覆盖的) ────
|
|||
|
|
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;
|
|||
|
|
|
|||
|
|
const CONSTITUTION_RULES = [
|
|||
|
|
// 隐蔽外联 (宪法 11.1)
|
|||
|
|
{
|
|||
|
|
id: 'hidden-network-egress',
|
|||
|
|
label: '疑似隐蔽外联: 新增未知域名的网络请求',
|
|||
|
|
severity: 'error',
|
|||
|
|
pattern: /(?:https?\.request|https?\.get|fetch)\s*\(\s*['"`]https?:\/\/(?!localhost|127\.0\.0\.1|0\.0\.0\.0)/,
|
|||
|
|
},
|
|||
|
|
// 原型污染 (宪法 11.1)
|
|||
|
|
{
|
|||
|
|
id: 'prototype-pollution',
|
|||
|
|
label: '原型污染风险: __proto__ 或 constructor.prototype 赋值',
|
|||
|
|
severity: 'error',
|
|||
|
|
pattern: /(?:__proto__|constructor\s*\.\s*prototype)\s*(?:=|\[)/,
|
|||
|
|
},
|
|||
|
|
// 环境探测+外发 (宪法 11.1)
|
|||
|
|
{
|
|||
|
|
id: 'env-probe-exfil',
|
|||
|
|
label: '环境探测: 读取 os.hostname/os.userInfo 可能用于信息收集',
|
|||
|
|
severity: 'warning',
|
|||
|
|
pattern: /os\s*\.\s*(?:hostname|userInfo|networkInterfaces|cpus|platform|arch|release)\s*\(/,
|
|||
|
|
},
|
|||
|
|
// 文件篡改 — package.json scripts (宪法 11.1)
|
|||
|
|
{
|
|||
|
|
id: 'pkg-scripts-tamper',
|
|||
|
|
label: '疑似篡改 package.json scripts 字段',
|
|||
|
|
severity: 'error',
|
|||
|
|
// 检测对 package.json 中 scripts 字段的写入
|
|||
|
|
test: (content, filePath) => {
|
|||
|
|
// [OPT-2] Desktop 临时文件跳过
|
|||
|
|
const _fp = filePath || '';
|
|||
|
|
if (_fp.includes('Desktop') || _fp.includes('desktop')) { return false; }
|
|||
|
|
|
|||
|
|
if (!filePath) return false;
|
|||
|
|
const basename = path.basename(filePath).toLowerCase();
|
|||
|
|
if (basename !== 'package.json') return false;
|
|||
|
|
return /"scripts"\s*:/.test(content);
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
// 定时外联 (宪法 11.1)
|
|||
|
|
{
|
|||
|
|
id: 'timed-network-call',
|
|||
|
|
label: '疑似定时外联: setInterval/setTimeout 中包含网络请求',
|
|||
|
|
severity: 'warning',
|
|||
|
|
pattern: /(?:setInterval|setTimeout)\s*\(\s*(?:async\s*)?\(\s*\)\s*=>\s*\{[^}]*(?:fetch|https?\.request|https?\.get)/,
|
|||
|
|
},
|
|||
|
|
// child_process 含变量拼接 (增强 post-edit-quality-check 的 eval 检测)
|
|||
|
|
{
|
|||
|
|
id: 'exec-injection',
|
|||
|
|
label: '命令注入风险: child_process 参数含变量拼接',
|
|||
|
|
severity: 'error',
|
|||
|
|
pattern: /(?:exec|execSync|execFile|execFileSync|spawn|spawnSync|fork)\s*\(\s*(?:`[^`]*\$\{|['"][^'"]*['"]\s*\+)/,
|
|||
|
|
},
|
|||
|
|
// Base64 混淆执行 (宪法 11.1)
|
|||
|
|
{
|
|||
|
|
id: 'base64-obfuscation',
|
|||
|
|
label: '疑似 Base64 混淆: decode 后可能执行',
|
|||
|
|
severity: 'warning',
|
|||
|
|
pattern: /(?:Buffer\.from|atob)\s*\([^)]+,\s*['"]base64['"]\s*\).*(?:eval|Function|require)/,
|
|||
|
|
},
|
|||
|
|
// .gitignore 篡改
|
|||
|
|
{
|
|||
|
|
id: 'gitignore-tamper',
|
|||
|
|
label: '疑似篡改 .gitignore (可能隐藏恶意文件)',
|
|||
|
|
severity: 'warning',
|
|||
|
|
test: (content, filePath) => {
|
|||
|
|
if (!filePath) return false;
|
|||
|
|
return path.basename(filePath).toLowerCase() === '.gitignore';
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 扫描内容,返回匹配的宪法违规
|
|||
|
|
*/
|
|||
|
|
function detectConstitutionViolations(content, filePath) {
|
|||
|
|
if (!content || typeof content !== 'string') return [];
|
|||
|
|
const findings = [];
|
|||
|
|
const lines = content.split('\n');
|
|||
|
|
|
|||
|
|
for (const rule of CONSTITUTION_RULES) {
|
|||
|
|
// 自定义 test 函数
|
|||
|
|
if (rule.test) {
|
|||
|
|
if (rule.test(content, filePath)) {
|
|||
|
|
findings.push({ id: rule.id, label: rule.label, severity: rule.severity, line: 0 });
|
|||
|
|
}
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
// 正则逐行匹配
|
|||
|
|
if (rule.pattern) {
|
|||
|
|
for (let i = 0; i < lines.length; i++) {
|
|||
|
|
if (rule.pattern.test(lines[i])) {
|
|||
|
|
findings.push({ id: rule.id, label: rule.label, severity: rule.severity, line: i + 1 });
|
|||
|
|
break; // 每条规则只报一次
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return findings;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 从 tool_input 中提取内容
|
|||
|
|
*/
|
|||
|
|
function extractContent(input) {
|
|||
|
|
if (input.tool_input?.content) return input.tool_input.content;
|
|||
|
|
if (input.tool_input?.new_string) return input.tool_input.new_string;
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── 主流程 ──────────────────────────────────────────
|
|||
|
|
function main() {
|
|||
|
|
readStdin({ maxSize: 256 * 1024 }).then(input => {
|
|||
|
|
const filePath = input.tool_input?.file_path;
|
|||
|
|
|
|||
|
|
if (!filePath) {
|
|||
|
|
process.exit(0);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 代码文件 + package.json + .gitignore
|
|||
|
|
const isCodeFile = CODE_EXTENSIONS.test(filePath);
|
|||
|
|
const isSpecialFile = /(?:package\.json|\.gitignore|\.env)$/i.test(filePath);
|
|||
|
|
|
|||
|
|
if (!isCodeFile && !isSpecialFile) {
|
|||
|
|
process.exit(0);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const content = extractContent(input);
|
|||
|
|
if (!content) {
|
|||
|
|
process.exit(0);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const findings = detectConstitutionViolations(content, filePath);
|
|||
|
|
|
|||
|
|
if (findings.length > 0) {
|
|||
|
|
const errors = findings.filter(f => f.severity === 'error');
|
|||
|
|
const warnings = findings.filter(f => f.severity === 'warning');
|
|||
|
|
|
|||
|
|
const parts = [];
|
|||
|
|
if (errors.length > 0) {
|
|||
|
|
parts.push(errors.map(f => ` [ERROR][${f.id}] ${f.line ? 'L' + f.line + ': ' : ''}${f.label}`).join('\n'));
|
|||
|
|
}
|
|||
|
|
if (warnings.length > 0) {
|
|||
|
|
parts.push(warnings.map(f => ` [WARN][${f.id}] ${f.line ? 'L' + f.line + ': ' : ''}${f.label}`).join('\n'));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const severity = errors.length > 0 ? 'ERROR' : 'WARN';
|
|||
|
|
|
|||
|
|
const result = {
|
|||
|
|
continue: true,
|
|||
|
|
systemMessage: `[constitution-guard] (${path.basename(filePath)}) ${severity}:\n${parts.join('\n')}\n\n${errors.length > 0 ? '请检查以上 error 级别问题是否为误报,若非误报必须修复。参考: constitution/AI-CONSTITUTION.md 第十一章' : '(仅提醒,不阻断)'}`,
|
|||
|
|
};
|
|||
|
|
process.stdout.write(JSON.stringify(result));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
process.exit(0);
|
|||
|
|
}).catch(() => process.exit(0));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 模块导出 (供测试)
|
|||
|
|
if (typeof module !== 'undefined') {
|
|||
|
|
module.exports = { CONSTITUTION_RULES, detectConstitutionViolations, extractContent };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (require.main === module) {
|
|||
|
|
main();
|
|||
|
|
}
|