@raolin2025/claude-code-node 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,279 @@
1
+ /**
2
+ * BashTool 命令安全检查
3
+ * 对应原版: src/tools/BashTool/bashSecurity.ts (2592行简化版)
4
+ *
5
+ * 防护:
6
+ * 1. 危险命令模式检测(rm -rf /、dd、mkfs 等)
7
+ * 2. cd + git 组合攻击(裸仓库 fsmonitor 绕过)
8
+ * 3. 管道注入(跨段 cd+git)
9
+ * 4. 反引号/命令替换注入
10
+ * 5. 网络数据外泄(curl/wget 到可疑地址)
11
+ * 6. 敏感文件访问(/etc/shadow、SSH 密钥)
12
+ */
13
+
14
+ /**
15
+ * 危险命令模式列表
16
+ * 每个模式: { pattern, reason, severity }
17
+ * severity: 'critical' = 直接拒绝, 'high' = 需要确认, 'medium' = 提示
18
+ */
19
+ const DANGEROUS_PATTERNS = [
20
+ // === 破坏性操作 ===
21
+ {
22
+ pattern: /\brm\s+(-[a-zA-Z]*f[a-zA-Z]*\s+)?(-[a-zA-Z]*r[a-zA-Z]*\s+)?\/\s*$/,
23
+ reason: '递归删除根目录',
24
+ severity: 'critical',
25
+ },
26
+ {
27
+ pattern: /\brm\s+.*-[a-zA-Z]*r[a-zA-Z]*f[a-zA-Z]*.*\s\/[a-zA-Z]*/,
28
+ reason: '递归强制删除系统目录',
29
+ severity: 'critical',
30
+ },
31
+ {
32
+ pattern: /\bdd\s+if=.*of=\/dev\//,
33
+ reason: 'dd 写入设备文件',
34
+ severity: 'critical',
35
+ },
36
+ {
37
+ pattern: /\bmkfs\b/,
38
+ reason: '格式化文件系统',
39
+ severity: 'critical',
40
+ },
41
+ {
42
+ pattern: /\bformat\s+[A-Z]:/i,
43
+ reason: 'Windows 格式化磁盘',
44
+ severity: 'critical',
45
+ },
46
+ {
47
+ pattern: /:\(\)\{\s*:\|\:&\s*\}\s*;/,
48
+ reason: 'Fork bomb(fork 炸弹)',
49
+ severity: 'critical',
50
+ },
51
+ {
52
+ pattern: />\s*\/dev\/sda/,
53
+ reason: '直接写入块设备',
54
+ severity: 'critical',
55
+ },
56
+ {
57
+ pattern: /\bchmod\s+([0-7]{3,4}|[ugo]*[+-][rwx].*)\s+\/(etc|boot|usr)\b/,
58
+ reason: '修改系统目录权限',
59
+ severity: 'critical',
60
+ },
61
+
62
+ // === 敏感文件访问 ===
63
+ {
64
+ pattern: /\/etc\/(shadow|passwd|sudoers|ssh\/sshd_config)\b/,
65
+ reason: '访问敏感系统文件',
66
+ severity: 'high',
67
+ },
68
+ {
69
+ pattern: /~\/\.ssh\/(id_[a-z]+|authorized_keys|config)\b/,
70
+ reason: '访问 SSH 私钥/配置',
71
+ severity: 'high',
72
+ },
73
+ {
74
+ pattern: /\bcat\b.*\/etc\/shadow/,
75
+ reason: '读取 shadow 密码文件',
76
+ severity: 'critical',
77
+ },
78
+
79
+ // === 网络数据外泄 ===
80
+ {
81
+ pattern: /\bcurl\b.*\|\s*(bash|sh|zsh|fish)\b/,
82
+ reason: '从网络下载并执行脚本(curl | bash)',
83
+ severity: 'critical',
84
+ },
85
+ {
86
+ pattern: /\bwget\b.*\|\s*(bash|sh|zsh|fish)\b/,
87
+ reason: '从网络下载并执行脚本(wget | sh)',
88
+ severity: 'critical',
89
+ },
90
+ {
91
+ pattern: /\b(iex|Invoke-Expression)\b.*\b(Invoke-WebRequest|iwr|New-Object.*WebClient)\b/i,
92
+ reason: 'PowerShell 下载并执行',
93
+ severity: 'critical',
94
+ },
95
+ {
96
+ pattern: /\bpython[23]?\s+-c\s+.*import\s+(urllib|requests|http\.client|socket)/,
97
+ reason: 'Python 内联网络请求',
98
+ severity: 'high',
99
+ },
100
+
101
+ // === 提权/权限逃逸 ===
102
+ {
103
+ pattern: /\bsudo\s+su\b/,
104
+ reason: '切换到 root 用户',
105
+ severity: 'high',
106
+ },
107
+ {
108
+ pattern: /\bsudo\s+chmod\s+[0-7]{3,4}\s+\/(etc|usr|boot)\b/,
109
+ reason: 'sudo 修改系统目录权限',
110
+ severity: 'critical',
111
+ },
112
+ {
113
+ pattern: /\bpkexec\b/,
114
+ reason: 'PolicyKit 提权',
115
+ severity: 'high',
116
+ },
117
+
118
+ // === 容器/云逃逸 ===
119
+ {
120
+ pattern: /\bnsenter\b.*--target\s+1\b/,
121
+ reason: '容器 namespace 逃逸到 PID 1',
122
+ severity: 'critical',
123
+ },
124
+ {
125
+ pattern: /\/proc\/sys\/kernel\/core_pattern/,
126
+ reason: '修改 core_pattern(容器逃逸技术)',
127
+ severity: 'critical',
128
+ },
129
+ {
130
+ pattern: /\bdocker\s+(run|exec).*--privileged/,
131
+ reason: '启动特权容器',
132
+ severity: 'high',
133
+ },
134
+ ]
135
+
136
+ /**
137
+ * 分段分析 — 检查跨管道段的 cd+git 组合
138
+ */
139
+ function checkCrossSegmentCdGit(segments) {
140
+ let hasCd = false
141
+ let hasGit = false
142
+
143
+ for (const segment of segments) {
144
+ const trimmed = segment.trim()
145
+ // cd 检测
146
+ if (/^\bcd\s+/.test(trimmed) || /\&\&\s*cd\s+/.test(trimmed) || /\|\s*cd\s+/.test(trimmed)) {
147
+ hasCd = true
148
+ }
149
+ // git 检测
150
+ if (/\bgit\s+/.test(trimmed)) {
151
+ hasGit = true
152
+ }
153
+ }
154
+
155
+ if (hasCd && hasGit) {
156
+ return {
157
+ blocked: true,
158
+ reason: 'cd + git 组合命令:可能利用裸仓库 fsmonitor 绕过安全检查',
159
+ }
160
+ }
161
+
162
+ return { blocked: false }
163
+ }
164
+
165
+ /**
166
+ * 多 cd 命令检测 — 一个命令中多次 cd 容易混淆
167
+ */
168
+ function checkMultipleCd(segments) {
169
+ let cdCount = 0
170
+ for (const segment of segments) {
171
+ const subCommands = segment.split(/\s*&&\s*|\s*;\s*/)
172
+ for (const sub of subCommands) {
173
+ if (/^\bcd\s+/.test(sub.trim())) cdCount++
174
+ }
175
+ }
176
+ if (cdCount > 1) {
177
+ return {
178
+ blocked: true,
179
+ reason: `一条命令中包含 ${cdCount} 次 cd,需要确认以避免混淆`,
180
+ }
181
+ }
182
+ return { blocked: false }
183
+ }
184
+
185
+ /**
186
+ * 分割复合命令为管道段
187
+ */
188
+ function splitPipeSegments(command) {
189
+ // 简单分割 — 不处理引号内的 |
190
+ return command.split(/\s*\|\s*/).filter(s => s.trim())
191
+ }
192
+
193
+ /**
194
+ * 主安全检查入口
195
+ * @param {string} command — 要执行的 bash 命令
196
+ * @returns {{allowed: boolean, severity: string, reasons: string[]}}
197
+ */
198
+ export function checkBashSafety(command) {
199
+ const reasons = []
200
+ let maxSeverity = 'none'
201
+
202
+ // 1. 危险模式匹配
203
+ for (const { pattern, reason, severity } of DANGEROUS_PATTERNS) {
204
+ if (pattern.test(command)) {
205
+ reasons.push(`[${severity.toUpperCase()}] ${reason}`)
206
+ if (severity === 'critical' || (severity === 'high' && maxSeverity !== 'critical')) {
207
+ maxSeverity = severity
208
+ }
209
+ }
210
+ }
211
+
212
+ // 2. 管道段分析
213
+ const segments = splitPipeSegments(command)
214
+
215
+ if (segments.length > 1) {
216
+ // 跨段 cd+git 检查
217
+ const cdGit = checkCrossSegmentCdGit(segments)
218
+ if (cdGit.blocked) {
219
+ reasons.push(`[HIGH] ${cdGit.reason}`)
220
+ if (maxSeverity !== 'critical') maxSeverity = 'high'
221
+ }
222
+ }
223
+
224
+ // 3. 多 cd 检测
225
+ const multiCd = checkMultipleCd(segments)
226
+ if (multiCd.blocked) {
227
+ reasons.push(`[MEDIUM] ${multiCd.reason}`)
228
+ if (maxSeverity === 'none') maxSeverity = 'medium'
229
+ }
230
+
231
+ // 4. 网络外泄检查 — curl/wget 到私有 IP
232
+ const netMatch = command.match(/\b(curl|wget)\s+.*?(https?:\/\/[^\s&|;]+)/g)
233
+ if (netMatch) {
234
+ for (const match of netMatch) {
235
+ const urlMatch = match.match(/(https?:\/\/[^\s&|;]+)/)
236
+ if (urlMatch) {
237
+ try {
238
+ const hostname = new URL(urlMatch[1]).hostname
239
+ // 简单的内网域名检查
240
+ if (/^(10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|169\.254\.|100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\.)/.test(hostname)) {
241
+ reasons.push(`[CRITICAL] curl/wget 到内网地址 ${hostname},疑似数据外泄`)
242
+ maxSeverity = 'critical'
243
+ }
244
+ } catch { /* 无效 URL 忽略 */ }
245
+ }
246
+ }
247
+ }
248
+
249
+ // 5. 重定向到敏感位置
250
+ if (/>>?\s*\/(etc|boot|usr)\//.test(command)) {
251
+ reasons.push('[CRITICAL] 输出重定向到系统目录')
252
+ maxSeverity = 'critical'
253
+ }
254
+
255
+ return {
256
+ allowed: maxSeverity !== 'critical',
257
+ severity: maxSeverity,
258
+ reasons,
259
+ }
260
+ }
261
+
262
+ /**
263
+ * 格式化安全检查结果
264
+ */
265
+ export function formatSafetyReport(result) {
266
+ if (result.allowed && result.reasons.length === 0) {
267
+ return '✅ 命令安全检查通过'
268
+ }
269
+
270
+ const lines = result.allowed
271
+ ? ['⚠️ 命令安全检查发现注意事项:']
272
+ : ['🚫 命令被安全策略阻止:']
273
+
274
+ for (const reason of result.reasons) {
275
+ lines.push(` ${reason}`)
276
+ }
277
+
278
+ return lines.join('\n')
279
+ }
@@ -0,0 +1,310 @@
1
+ /**
2
+ * 权限系统增强版 — 规则持久化 + 审计日志
3
+ * 对应原版: src/hooks/toolPermission/ + src/utils/permissions/
4
+ */
5
+ import { readFile, writeFile, mkdir } from 'fs/promises'
6
+ import { resolve, join } from 'path'
7
+ import { checkBashSafety } from './bash-guard.js'
8
+ import { checkPathSafety, checkWritePathSafety } from './path-guard.js'
9
+ import { checkUrlSafety } from './ssrf-guard.js'
10
+
11
+ const PERMISSIONS_FILE = '.claude-code/permissions.json'
12
+ const AUDIT_LOG_FILE = '.claude-code/audit.log'
13
+
14
+ /**
15
+ * 权限决策类型
16
+ */
17
+ export const PermissionDecision = {
18
+ ALLOW: 'allow',
19
+ DENY: 'deny',
20
+ ASK: 'ask',
21
+ }
22
+
23
+ /**
24
+ * 工具权限规则
25
+ */
26
+ export class PermissionRule {
27
+ constructor({ tool, pattern, decision, reason, expiresAt = null }) {
28
+ this.tool = tool // 工具名或 '*'(所有工具)
29
+ this.pattern = pattern // 匹配模式(glob 或 regex 字符串)
30
+ this.decision = decision // allow / deny / ask
31
+ this.reason = reason // 规则原因
32
+ this.createdAt = Date.now()
33
+ this.expiresAt = expiresAt // 过期时间(会话级规则)
34
+ }
35
+
36
+ /** 检查规则是否已过期 */
37
+ get isExpired() {
38
+ return this.expiresAt && Date.now() > this.expiresAt
39
+ }
40
+
41
+ /** 检查输入是否匹配此规则 */
42
+ matches(input) {
43
+ if (this.isExpired) return false
44
+
45
+ // 简单 glob 匹配
46
+ const pattern = this.pattern
47
+ if (pattern === '*') return true
48
+
49
+ // 路径模式
50
+ if (typeof input === 'string' && input.includes('/')) {
51
+ return this._globMatch(pattern, input)
52
+ }
53
+
54
+ // 命令模式
55
+ if (typeof input === 'string') {
56
+ return input.startsWith(pattern) || this._globMatch(pattern, input)
57
+ }
58
+
59
+ return false
60
+ }
61
+
62
+ _globMatch(pattern, str) {
63
+ const regex = pattern
64
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
65
+ .replace(/\*/g, '.*')
66
+ .replace(/\?/g, '.')
67
+ return new RegExp(`^${regex}$`).test(str)
68
+ }
69
+ }
70
+
71
+ /**
72
+ * 增强版权限检查器
73
+ */
74
+ export class EnhancedPermissionChecker {
75
+ constructor(mode = 'ask', options = {}) {
76
+ this.mode = mode
77
+ this.rules = [] // PermissionRule 列表
78
+ this.auditLog = [] // 审计日志
79
+ this.cwd = options.cwd || process.cwd()
80
+ this.projectDir = options.projectDir || process.cwd()
81
+ this._maxAuditEntries = 1000
82
+ }
83
+
84
+ /** 添加规则 */
85
+ addRule(rule) {
86
+ this.rules.push(new PermissionRule(rule))
87
+ return this
88
+ }
89
+
90
+ /** 移除过期规则 */
91
+ cleanupRules() {
92
+ this.rules = this.rules.filter(r => !r.isExpired)
93
+ }
94
+
95
+ /** 添加会话级允许规则(当前会话有效) */
96
+ allowForSession(tool, pattern = '*') {
97
+ return this.addRule({
98
+ tool,
99
+ pattern,
100
+ decision: PermissionDecision.ALLOW,
101
+ reason: '用户会话级授权',
102
+ expiresAt: null, // 会话级不过期,但可以手动清除
103
+ })
104
+ }
105
+
106
+ /** 添加永久拒绝规则 */
107
+ deny(tool, pattern = '*', reason = '') {
108
+ return this.addRule({
109
+ tool,
110
+ pattern,
111
+ decision: PermissionDecision.DENY,
112
+ reason,
113
+ })
114
+ }
115
+
116
+ /**
117
+ * 综合权限检查
118
+ * @param {string} toolName — 工具名
119
+ * @param {object} input — 工具输入参数
120
+ * @returns {Promise<{allowed: boolean, reason?: string, securityCheck?: object}>}
121
+ */
122
+ async check(toolName, input = {}) {
123
+ // 1. 模式级检查
124
+ if (this.mode === 'deny') {
125
+ this._log(toolName, input, false, '全局拒绝模式')
126
+ return { allowed: false, reason: '全局拒绝模式' }
127
+ }
128
+ if (this.mode === 'always-allow') {
129
+ const securityResult = await this._securityCheck(toolName, input)
130
+ if (!securityResult.safe) {
131
+ this._log(toolName, input, false, securityResult.reason)
132
+ return { allowed: false, reason: securityResult.reason, securityCheck: securityResult }
133
+ }
134
+ this._log(toolName, input, true, '全局允许模式')
135
+ return { allowed: true }
136
+ }
137
+
138
+ // 2. 规则匹配
139
+ this.cleanupRules()
140
+ for (const rule of this.rules) {
141
+ if (rule.tool === toolName || rule.tool === '*') {
142
+ if (rule.matches(this._extractPattern(toolName, input))) {
143
+ if (rule.decision === PermissionDecision.DENY) {
144
+ this._log(toolName, input, false, `规则拒绝: ${rule.reason}`)
145
+ return { allowed: false, reason: rule.reason }
146
+ }
147
+ if (rule.decision === PermissionDecision.ALLOW) {
148
+ // 即使规则允许,也要过安全检查
149
+ const securityResult = await this._securityCheck(toolName, input)
150
+ if (!securityResult.safe) {
151
+ this._log(toolName, input, false, securityResult.reason)
152
+ return { allowed: false, reason: securityResult.reason, securityCheck: securityResult }
153
+ }
154
+ this._log(toolName, input, true, `规则允许: ${rule.reason}`)
155
+ return { allowed: true }
156
+ }
157
+ }
158
+ }
159
+ }
160
+
161
+ // 3. 安全检查(在 ask 之前)
162
+ const securityResult = await this._securityCheck(toolName, input)
163
+ if (!securityResult.safe) {
164
+ this._log(toolName, input, false, securityResult.reason)
165
+ return { allowed: false, reason: securityResult.reason, securityCheck: securityResult }
166
+ }
167
+
168
+ // 4. ask 模式 — 需要用户确认
169
+ this._log(toolName, input, true, 'ask 模式 — 等待用户确认')
170
+ return {
171
+ allowed: true, // 简化版直接允许(完整版应弹出确认对话框)
172
+ securityCheck: securityResult,
173
+ }
174
+ }
175
+
176
+ /**
177
+ * 内部安全检查 — 工具特定的安全逻辑
178
+ */
179
+ async _securityCheck(toolName, input) {
180
+ switch (toolName) {
181
+ case 'Bash': {
182
+ const command = input.command || ''
183
+ if (!command) return { safe: true }
184
+ const result = checkBashSafety(command)
185
+ return {
186
+ safe: result.allowed,
187
+ reason: result.reasons.length > 0 ? result.reasons.join('; ') : undefined,
188
+ detail: result,
189
+ }
190
+ }
191
+
192
+ case 'Read': {
193
+ const filePath = input.file_path || ''
194
+ if (!filePath) return { safe: true }
195
+ const result = checkPathSafety(filePath, { cwd: this.cwd })
196
+ return {
197
+ safe: result.safe,
198
+ reason: result.reasons.length > 0 ? result.reasons.join('; ') : undefined,
199
+ detail: result,
200
+ }
201
+ }
202
+
203
+ case 'Edit':
204
+ case 'Write': {
205
+ const filePath = input.file_path || ''
206
+ if (!filePath) return { safe: true }
207
+ const checker = toolName === 'Write' ? checkWritePathSafety : checkPathSafety
208
+ const result = checker(filePath, { cwd: this.cwd })
209
+ return {
210
+ safe: result.safe,
211
+ reason: result.reasons.length > 0 ? result.reasons.join('; ') : undefined,
212
+ detail: result,
213
+ }
214
+ }
215
+
216
+ case 'WebFetch': {
217
+ const url = input.url || ''
218
+ if (!url) return { safe: true }
219
+ const result = await checkUrlSafety(url)
220
+ return {
221
+ safe: result.allowed,
222
+ reason: result.reason,
223
+ }
224
+ }
225
+
226
+ default:
227
+ return { safe: true }
228
+ }
229
+ }
230
+
231
+ /** 提取规则匹配用的模式字符串 */
232
+ _extractPattern(toolName, input) {
233
+ switch (toolName) {
234
+ case 'Bash': return input.command || ''
235
+ case 'Read':
236
+ case 'Edit':
237
+ case 'Write': return input.file_path || ''
238
+ case 'Glob':
239
+ case 'Grep': return input.path || input.pattern || ''
240
+ case 'WebFetch':
241
+ case 'WebSearch': return input.url || input.query || ''
242
+ default: return JSON.stringify(input)
243
+ }
244
+ }
245
+
246
+ /** 审计日志记录 */
247
+ _log(toolName, input, allowed, reason) {
248
+ this.auditLog.push({
249
+ timestamp: new Date().toISOString(),
250
+ tool: toolName,
251
+ inputSnippet: JSON.stringify(input).slice(0, 200),
252
+ allowed,
253
+ reason,
254
+ })
255
+
256
+ // 限制审计日志大小
257
+ if (this.auditLog.length > this._maxAuditEntries) {
258
+ this.auditLog = this.auditLog.slice(-this._maxAuditEntries)
259
+ }
260
+ }
261
+
262
+ /** 保存权限规则到文件 */
263
+ async saveRules() {
264
+ const dir = join(this.projectDir, '.claude-code')
265
+ await mkdir(dir, { recursive: true })
266
+ const data = this.rules
267
+ .filter(r => !r.isExpired && !r.expiresAt) // 只保存永久规则
268
+ .map(r => ({
269
+ tool: r.tool,
270
+ pattern: r.pattern,
271
+ decision: r.decision,
272
+ reason: r.reason,
273
+ }))
274
+ await writeFile(join(dir, 'permissions.json'), JSON.stringify(data, null, 2), 'utf-8')
275
+ }
276
+
277
+ /** 加载权限规则 */
278
+ async loadRules() {
279
+ try {
280
+ const raw = await readFile(join(this.projectDir, PERMISSIONS_FILE), 'utf-8')
281
+ const data = JSON.parse(raw)
282
+ for (const rule of data) {
283
+ this.addRule(rule)
284
+ }
285
+ } catch {
286
+ // 文件不存在 — 使用默认规则
287
+ }
288
+ }
289
+
290
+ /** 保存审计日志 */
291
+ async saveAuditLog() {
292
+ const dir = join(this.projectDir, '.claude-code')
293
+ await mkdir(dir, { recursive: true })
294
+ const lines = this.auditLog.map(e =>
295
+ `${e.timestamp} | ${e.tool} | ${e.allowed ? 'ALLOW' : 'DENY'} | ${e.reason} | ${e.inputSnippet}`
296
+ ).join('\n')
297
+ await writeFile(join(dir, 'audit.log'), lines, 'utf-8')
298
+ }
299
+
300
+ /** 获取审计摘要 */
301
+ getAuditSummary() {
302
+ const summary = { total: this.auditLog.length, allowed: 0, denied: 0, byTool: {} }
303
+ for (const entry of this.auditLog) {
304
+ if (entry.allowed) summary.allowed++
305
+ else summary.denied++
306
+ summary.byTool[entry.tool] = (summary.byTool[entry.tool] || 0) + 1
307
+ }
308
+ return summary
309
+ }
310
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * 安全模块统一导出
3
+ */
4
+ export { isBlockedAddress, checkHostSafety, checkUrlSafety } from './ssrf-guard.js'
5
+ export { checkBashSafety, formatSafetyReport } from './bash-guard.js'
6
+ export { sanitizePath, checkPathTraversal, checkForbiddenPath, checkPathSafety, checkWritePathSafety } from './path-guard.js'
7
+ export { EnhancedPermissionChecker, PermissionRule, PermissionDecision } from './enhanced-permission.js'