138 lines
4.0 KiB
JavaScript
138 lines
4.0 KiB
JavaScript
#!/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();
|