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

177 lines
5.4 KiB
JavaScript
Raw Permalink 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 读取防护
* 匹配器: 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();
}