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

138 lines
4.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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();