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

177 lines
5.4 KiB
JavaScript
Raw Normal View History

#!/usr/bin/env node
/**
* PreToolUse Hook: NDA 读取防护
* 匹配器: Read|Glob|Grep
* 阻止 AI 读取 Bookworm 系统配置文件防止通过工具调用泄露架构信息
*
* 退出码: 0=放行, 2=阻断(deny)
* 激活条件: 仅在 Portable 发行版的 settings.json 中注册
*/
'use strict';
const fs = require('fs');
const path = require('path');
const readStdin = require('./lib/read-stdin.js');
const CLAUDE_ROOT = require('./lib/root.js');
// 安全日志 (fail-open)
let logSecurityEvent = () => {};
try {
logSecurityEvent = require('./lib/security-log.js').logSecurityEvent;
} catch {}
// --- 路径规范化 (对齐 block-sensitive-reads 的逻辑) ---
function normalizePath(fp) {
if (!fp) return '';
fp = fp.trim();
let resolved = path.resolve(fp);
try { resolved = fs.realpathSync(resolved); } catch {}
return resolved.replace(/\\/g, '/')
.replace(/\.+$/, '')
.replace(/::?\$DATA$/i, '');
}
// --- 白名单: 用户自己的数据AI 需要访问 ---
const WHITELIST = [
/[\/]\.claude[\/]projects([\/]|$)/i, // 项目级 CLAUDE.md (用户项目配置)
/[\/]\.claude[\/]memory([\/]|$)/i, // 用户记忆文件
];
// --- 黑名单: 系统核心文件/目录 ---
const BLACKLIST_PATHS = [
// 精确文件
{ pattern: /[\/]\.claude[\/]CLAUDE\.md$/i, reason: '系统配置' },
{ pattern: /[\/]\.claude[\/]settings\.json$/i, reason: '系统配置' },
{ pattern: /[\/]\.claude[\/]settings\.local\.json$/i, reason: '系统配置' },
{ pattern: /[\/]\.claude[\/]stats-compiled\.json$/i, reason: '系统数据' },
{ pattern: /[\/]\.claude[\/]MEMORY\.md$/i, reason: '系统索引' },
// 目录级
{ pattern: /[\/]\.claude[\/]skills[\/]/i, reason: '系统模块' },
{ pattern: /[\/]\.claude[\/]agents[\/]/i, reason: '系统模块' },
{ pattern: /[\/]\.claude[\/]hooks[\/]/i, reason: '系统模块' },
{ pattern: /[\/]\.claude[\/]scripts[\/]/i, reason: '系统脚本' },
{ pattern: /[\/]\.claude[\/]rules[\/]/i, reason: '系统规则' },
{ pattern: /[\/]\.claude[\/]constitution[\/]/i, reason: '系统规则' },
{ pattern: /[\/]\.claude[\/]docs[\/]/i, reason: '系统文档' },
{ pattern: /[\/]\.claude[\/]debug[\/]/i, reason: '系统日志' },
{ pattern: /[\/]\.claude[\/]templates[\/]/i, reason: '系统模板' },
];
// --- Glob 模式黑名单 (匹配 Glob 工具的 pattern 参数) ---
const GLOB_BLACKLIST = [
/\.claude/i,
/skills[\/\\]/i,
/agents[\/\\]/i,
/hooks[\/\\]/i,
/CLAUDE\.md/i,
/settings\.json/i,
/SKILL\.md/i,
];
// 拒绝消息 (不暴露防护机制)
const DENY_MSG = '[系统] 该路径属于系统内部区域,无法访问。请直接描述您需要完成的任务,我来帮您。';
/**
* 检查路径是否命中黑名单
* @returns {string|null} 命中原因null 表示放行
*/
function checkPath(normalizedPath) {
if (!normalizedPath) return null;
// 白名单优先
for (const wp of WHITELIST) {
if (wp.test(normalizedPath)) return null;
}
// 黑名单
for (const { pattern, reason } of BLACKLIST_PATHS) {
if (pattern.test(normalizedPath)) return reason;
}
// 兜底: 任何 .claude/ 下未白名单的路径
if (/[\/]\.claude[\/]/.test(normalizedPath)) {
// 排除 .claude/projects/ 和 .claude/memory/ (已在白名单)
return '系统目录';
}
return null;
}
/**
* 检查 Glob pattern 参数
*/
function checkGlobPattern(pattern) {
if (!pattern) return null;
for (const bp of GLOB_BLACKLIST) {
if (bp.test(pattern)) return '系统模式搜索';
}
return null;
}
/**
* 可导出的统一检查入口 ( dispatcher 或测试调用)
*/
function checkInput(input) {
const toolName = (input.tool_name || '').toLowerCase();
const ti = input.tool_input || {};
// --- Read ---
if (toolName === 'read') {
const fp = normalizePath(ti.file_path || ti.filePath || '');
const reason = checkPath(fp);
if (reason) return { decision: 'deny', reason, path: ti.file_path };
}
// --- Glob ---
if (toolName === 'glob') {
// 检查搜索目录
const searchPath = normalizePath(ti.path || '');
const pathReason = checkPath(searchPath);
if (pathReason) return { decision: 'deny', reason: pathReason, path: ti.path };
// 检查 glob pattern
const patternReason = checkGlobPattern(ti.pattern || '');
if (patternReason) return { decision: 'deny', reason: patternReason, path: ti.pattern };
}
// --- Grep ---
if (toolName === 'grep') {
const searchPath = normalizePath(ti.path || '');
const reason = checkPath(searchPath);
if (reason) return { decision: 'deny', reason, path: ti.path };
}
return null; // 放行
}
// --- 主流程 ---
function main() {
readStdin({ maxSize: 512 * 1024 }).then(input => {
const result = checkInput(input);
if (result) {
logSecurityEvent('nda-deny', 'nda-read-guard', result.reason, result.path || '');
process.stderr.write(JSON.stringify({
hookSpecificOutput: { permissionDecision: 'deny' },
systemMessage: DENY_MSG,
}));
process.exit(2);
return;
}
process.exit(0);
}).catch(() => {
// fail-open: 解析异常时放行 (不阻断用户正常工作)
process.exit(0);
});
}
if (typeof module !== 'undefined') {
module.exports = { checkInput, checkPath, checkGlobPattern, normalizePath };
}
if (require.main === module) {
main();
}