bookworm-smart-assistant/hooks/nda-read-guard.standalone.js

138 lines
4.0 KiB
JavaScript
Raw Permalink Normal View History

#!/usr/bin/env node
/**
* PreToolUse Hook: NDA 读取防护 (standalone 零外部 require)
* 匹配器: Read|Glob|Grep
* 退出码: 0=放行, 2=阻断(deny)
*
* 此文件内联了所有依赖terser 混淆后无 require 链性能损失
* 源码维护版: nda-read-guard.js ( checkInput 导出供测试)
* 构建时由 build-portable.js 用此文件替换 nda-read-guard.js
*/
'use strict';
const fs = require('fs');
const path = require('path');
// --- 内联: root.js ---
const CLAUDE_ROOT = (() => {
if (process.env.CLAUDE_HOME) return process.env.CLAUDE_HOME;
const selfDir = path.dirname(__filename);
if (selfDir.includes('.claude')) return selfDir.replace(/[\/\\]hooks$/, '');
return path.join(process.env.USERPROFILE || process.env.HOME || '', '.claude');
})();
// --- 内联: logSecurityEvent (精简版, fail-open) ---
function logSecurityEvent(decision, reason, detail) {
try {
const dd = path.join(CLAUDE_ROOT, 'debug');
if (!fs.existsSync(dd)) fs.mkdirSync(dd, { recursive: true });
const lf = path.join(dd, 'security-' + new Date().toISOString().slice(0, 10) + '.jsonl');
fs.appendFileSync(lf, JSON.stringify({
ts: new Date().toISOString(),
decision,
hook: 'nda-read-guard',
reason,
detail: (detail || '').slice(0, 200).replace(/[A-Za-z0-9+/]{32,}/g, '[REDACTED]'),
}) + '\n');
} catch {}
}
// --- 路径规范化 ---
function norm(fp) {
if (!fp) return '';
fp = fp.trim();
let r = path.resolve(fp);
try { r = fs.realpathSync(r); } catch {}
return r.replace(/\\/g, '/').replace(/\.+$/, '').replace(/::?\$DATA$/i, '');
}
// --- 白名单 ---
const WL = [
/[\/]\.claude[\/]projects([\/]|$)/i,
/[\/]\.claude[\/]memory([\/]|$)/i,
];
// --- 黑名单 ---
const BL = [
/[\/]\.claude[\/]CLAUDE\.md$/i,
/[\/]\.claude[\/]settings\.json$/i,
/[\/]\.claude[\/]settings\.local\.json$/i,
/[\/]\.claude[\/]stats-compiled\.json$/i,
/[\/]\.claude[\/]MEMORY\.md$/i,
/[\/]\.claude[\/]skills[\/]/i,
/[\/]\.claude[\/]agents[\/]/i,
/[\/]\.claude[\/]hooks[\/]/i,
/[\/]\.claude[\/]scripts[\/]/i,
/[\/]\.claude[\/]rules[\/]/i,
/[\/]\.claude[\/]constitution[\/]/i,
/[\/]\.claude[\/]docs[\/]/i,
/[\/]\.claude[\/]debug[\/]/i,
/[\/]\.claude[\/]templates[\/]/i,
];
// --- Glob 模式黑名单 ---
const GL = [
/\.claude/i, /skills[\/\\]/i, /agents[\/\\]/i, /hooks[\/\\]/i,
/CLAUDE\.md/i, /settings\.json/i, /SKILL\.md/i,
];
const DENY_MSG = '[系统] 该路径属于系统内部区域,无法访问。请直接描述您需要完成的任务,我来帮您。';
function chkPath(np) {
if (!np) return null;
for (const w of WL) { if (w.test(np)) return null; }
for (const b of BL) { if (b.test(np)) return 1; }
if (/[\/]\.claude[\/]/.test(np)) return 1;
return null;
}
function chkGlob(p) {
if (!p) return null;
for (const g of GL) { if (g.test(p)) return 1; }
return null;
}
// --- 内联: readStdin ---
function main() {
let raw = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', c => {
raw += c;
if (raw.length > 512 * 1024) { process.exit(0); }
});
process.stdin.on('end', () => {
try {
const input = JSON.parse(raw);
const tn = (input.tool_name || '').toLowerCase();
const ti = input.tool_input || {};
let hit = null;
let target = '';
if (tn === 'read') {
target = ti.file_path || ti.filePath || '';
hit = chkPath(norm(target));
} else if (tn === 'glob') {
target = ti.path || '';
hit = chkPath(norm(target)) || chkGlob(ti.pattern || '');
if (!target && hit) target = ti.pattern;
} else if (tn === 'grep') {
target = ti.path || '';
hit = chkPath(norm(target));
}
if (hit) {
logSecurityEvent('nda-deny', 'blocked', target);
process.stderr.write(JSON.stringify({
hookSpecificOutput: { permissionDecision: 'deny' },
systemMessage: DENY_MSG,
}));
process.exit(2);
return;
}
} catch {}
process.exit(0);
});
}
main();