177 lines
5.4 KiB
JavaScript
177 lines
5.4 KiB
JavaScript
|
|
#!/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();
|
|||
|
|
}
|