@ian2018cs/agenthub 0.1.53 → 0.1.55
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.
- package/dist/assets/{index-_a9nlevD.js → index-8gJOK0K5.js} +20 -20
- package/dist/index.html +1 -1
- package/package.json +2 -2
- package/server/claude-sdk.js +41 -0
- package/server/projects.js +2 -1
- package/server/services/tool-guard/index.js +75 -0
- package/server/services/tool-guard/llm-reviewer.js +128 -0
- package/server/services/tool-guard/rules.js +635 -0
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool Guard — 安全规则引擎
|
|
3
|
+
*
|
|
4
|
+
* 可扩展的规则数组,每条规则定义:
|
|
5
|
+
* name: 规则名称
|
|
6
|
+
* tools: 适用的工具列表('*' = 所有工具)
|
|
7
|
+
* check: (toolName, input, context) => { result: 'allow'|'deny'|'uncertain', reason? }
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import { readFileSync, statSync } from 'fs';
|
|
12
|
+
|
|
13
|
+
// ─── 脚本内容审查模式(供 script-execution-guard 和 write-content-guard 复用)───
|
|
14
|
+
|
|
15
|
+
/** 危险命令模式(合并自规则 4 & 5 的核心模式) */
|
|
16
|
+
const DANGEROUS_SCRIPT_PATTERNS = [
|
|
17
|
+
// 提权
|
|
18
|
+
{ pattern: /\bsudo\b/, reason: '使用 sudo' },
|
|
19
|
+
{ pattern: /\bsu\s+\w/, reason: '切换用户' },
|
|
20
|
+
{ pattern: /\bdoas\b/, reason: '使用 doas' },
|
|
21
|
+
// 关机/重启
|
|
22
|
+
{ pattern: /\b(shutdown|reboot|halt|poweroff)\b/, reason: '关机/重启系统' },
|
|
23
|
+
{ pattern: /\binit\s+[06]\b/, reason: '关机/重启系统' },
|
|
24
|
+
// 杀进程
|
|
25
|
+
{ pattern: /\bkill\s+(-\d+\s+)?1\b/, reason: '杀 init 进程' },
|
|
26
|
+
{ pattern: /\bkillall\b/, reason: '使用 killall' },
|
|
27
|
+
{ pattern: /\bpkill\b/, reason: '使用 pkill' },
|
|
28
|
+
// 破坏性删除
|
|
29
|
+
{ pattern: /\brm\s+[^|]*-[^\s]*r[^\s]*\s+\/(\s|$)/, reason: '递归删除根目录' },
|
|
30
|
+
{ pattern: /\brm\s+[^|]*-[^\s]*r[^\s]*\s+\/(etc|usr|var|bin|sbin|lib|boot|sys|proc|dev)\b/, reason: '删除系统目录' },
|
|
31
|
+
// 磁盘操作
|
|
32
|
+
{ pattern: /\bdd\s+.*\bof=\/dev\//, reason: '直接写入磁盘设备' },
|
|
33
|
+
{ pattern: /\bmkfs\b/, reason: '格式化磁盘' },
|
|
34
|
+
{ pattern: /\bfdisk\b/, reason: '操作磁盘分区' },
|
|
35
|
+
// 网络/防火墙
|
|
36
|
+
{ pattern: /\b(iptables|nftables|ufw)\b/, reason: '修改防火墙规则' },
|
|
37
|
+
// 定时任务
|
|
38
|
+
{ pattern: /\bcrontab\s+(-e|-r)\b/, reason: '编辑/删除 crontab' },
|
|
39
|
+
// 用户/组管理
|
|
40
|
+
{ pattern: /\b(useradd|userdel|usermod|groupadd|groupdel)\b/, reason: '管理系统用户/组' },
|
|
41
|
+
{ pattern: /\bpasswd\b/, reason: '修改密码' },
|
|
42
|
+
{ pattern: /\bvisudo\b/, reason: '编辑 sudoers' },
|
|
43
|
+
// 内核模块
|
|
44
|
+
{ pattern: /\b(insmod|rmmod|modprobe)\b/, reason: '操作内核模块' },
|
|
45
|
+
// 服务管理
|
|
46
|
+
{ pattern: /\b(systemctl|service)\s+(start|stop|restart|enable|disable|reload)\b/, reason: '管理系统服务' },
|
|
47
|
+
{ pattern: /\blaunchctl\s+(load|unload|start|stop|bootstrap|bootout)\b/, reason: '管理 macOS 服务' },
|
|
48
|
+
// 容器/云平台
|
|
49
|
+
{ pattern: /\bdocker\s+(rm|rmi|stop|kill|restart|network|volume|system\s+prune)\b/, reason: '管理 Docker' },
|
|
50
|
+
{ pattern: /\bkubectl\b/, reason: '操作 Kubernetes' },
|
|
51
|
+
// Web 服务器
|
|
52
|
+
{ pattern: /\bnginx\s+(-s\s+)?(reload|stop|start|restart|quit)\b/, reason: '管理 nginx' },
|
|
53
|
+
{ pattern: /\bapachectl\b/, reason: '管理 Apache' },
|
|
54
|
+
// 系统配置写入
|
|
55
|
+
{ pattern: />\s*\/etc\//, reason: '写入 /etc/' },
|
|
56
|
+
{ pattern: /\btee\s+(-a\s+)?\/etc\//, reason: '写入 /etc/' },
|
|
57
|
+
{ pattern: /\bsed\s+-i[^\s]*\s+\/etc\//, reason: '修改 /etc/ 文件' },
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 检查文本内容中的系统端口查询操作
|
|
62
|
+
*/
|
|
63
|
+
function checkSystemPortsInContent(content) {
|
|
64
|
+
for (const envVar of ['PORT', 'VITE_PORT']) {
|
|
65
|
+
const port = process.env[envVar];
|
|
66
|
+
if (port && /^\d+$/.test(port)) {
|
|
67
|
+
if (new RegExp(`\\blsof\\b[^\\n]*:${port}\\b`).test(content) ||
|
|
68
|
+
new RegExp(`\\bfuser\\b[^\\n]*\\b${port}\\b`).test(content)) {
|
|
69
|
+
return { denied: true, reason: `包含对系统端口 ${port} 的查询操作` };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return { denied: false };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 检查文本内容中的绝对路径是否越界(复用白名单逻辑)
|
|
78
|
+
*/
|
|
79
|
+
function checkPathsInContent(content, context) {
|
|
80
|
+
const pathPattern = /(?:^|\s|=|"|')(\/[^\s"'`;|&><){}$]+)/gm;
|
|
81
|
+
let match;
|
|
82
|
+
while ((match = pathPattern.exec(content)) !== null) {
|
|
83
|
+
const resolvedPath = path.resolve(match[1]);
|
|
84
|
+
// 安全路径跳过
|
|
85
|
+
if (SAFE_SPECIAL_PATHS.includes(resolvedPath)) continue;
|
|
86
|
+
if (SAFE_COMMAND_PATHS.some(prefix => resolvedPath.startsWith(prefix))) continue;
|
|
87
|
+
if (isPathAllowed(resolvedPath, context)) continue;
|
|
88
|
+
if (isInTempDir(resolvedPath, context)) continue;
|
|
89
|
+
return { denied: true, reason: `访问项目目录之外的路径: ${resolvedPath}` };
|
|
90
|
+
}
|
|
91
|
+
return { denied: false };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ─── 工具函数 ──────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 检查路径是否在允许的前缀列表内
|
|
98
|
+
*/
|
|
99
|
+
function isPathAllowed(resolvedPath, context) {
|
|
100
|
+
// 在用户的允许目录内
|
|
101
|
+
for (const prefix of context.allowedPrefixes) {
|
|
102
|
+
if (resolvedPath.startsWith(prefix + '/') || resolvedPath === prefix) {
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// 在 cwd 内(cwd 应当已在 user-projects/{uuid}/ 下)
|
|
107
|
+
if (context.cwd && (resolvedPath.startsWith(context.cwd + '/') || resolvedPath === context.cwd)) {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 检查路径是否属于其他用户
|
|
115
|
+
*/
|
|
116
|
+
function isCrossUserPath(resolvedPath, context) {
|
|
117
|
+
const crossUserPattern = /\/(user-data|user-projects)\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\//i;
|
|
118
|
+
const match = resolvedPath.match(crossUserPattern);
|
|
119
|
+
if (match) {
|
|
120
|
+
const pathUuid = match[2].toLowerCase();
|
|
121
|
+
if (pathUuid !== context.userUuid.toLowerCase()) {
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 检查路径是否在临时目录中
|
|
130
|
+
*/
|
|
131
|
+
function isInTempDir(resolvedPath, context) {
|
|
132
|
+
return context.tempDirs.some(tmpDir => resolvedPath.startsWith(tmpDir + '/') || resolvedPath === tmpDir);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 检查临时目录路径是否包含用户 UUID 子目录
|
|
137
|
+
*/
|
|
138
|
+
function hasTempUserSubdir(resolvedPath, context) {
|
|
139
|
+
return resolvedPath.includes('/' + context.userUuid + '/') || resolvedPath.includes('/' + context.userUuid);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** 系统敏感路径前缀 */
|
|
143
|
+
const SYSTEM_PATHS = ['/etc/', '/usr/', '/var/', '/sys/', '/proc/', '/dev/', '/boot/', '/sbin/', '/lib/', '/bin/'];
|
|
144
|
+
|
|
145
|
+
/** 安全命令路径前缀(用于 Bash 路径提取时排除) */
|
|
146
|
+
const SAFE_COMMAND_PATHS = ['/usr/bin/', '/usr/local/bin/', '/bin/', '/sbin/', '/usr/sbin/', '/opt/homebrew/bin/'];
|
|
147
|
+
|
|
148
|
+
/** 安全的特殊路径 */
|
|
149
|
+
const SAFE_SPECIAL_PATHS = ['/dev/null', '/dev/stdin', '/dev/stdout', '/dev/stderr', '/dev/zero', '/dev/urandom', '/dev/random'];
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* 检查单个文件路径的安全性
|
|
153
|
+
* @returns {{ result: 'allow'|'deny'|'uncertain', reason?: string }}
|
|
154
|
+
*/
|
|
155
|
+
function checkFilePath(filePath, context, toolName) {
|
|
156
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
157
|
+
return { result: 'allow' };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const resolvedPath = path.isAbsolute(filePath)
|
|
161
|
+
? path.resolve(filePath)
|
|
162
|
+
: context.cwd
|
|
163
|
+
? path.resolve(context.cwd, filePath)
|
|
164
|
+
: null;
|
|
165
|
+
|
|
166
|
+
if (!resolvedPath) {
|
|
167
|
+
return { result: 'uncertain', reason: `无法解析路径: ${filePath}` };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 在允许的目录内 → ALLOW
|
|
171
|
+
if (isPathAllowed(resolvedPath, context)) {
|
|
172
|
+
return { result: 'allow' };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 在临时目录内 → 检查用户子目录
|
|
176
|
+
if (isInTempDir(resolvedPath, context)) {
|
|
177
|
+
if (hasTempUserSubdir(resolvedPath, context)) {
|
|
178
|
+
return { result: 'allow' };
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
result: 'deny',
|
|
182
|
+
reason: `请在临时目录中使用以用户 ID 命名的子文件夹。正确用法: /tmp/${context.userUuid}/your-file`
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 访问其他用户的目录 → DENY(提供更具体的错误信息)
|
|
187
|
+
if (isCrossUserPath(resolvedPath, context)) {
|
|
188
|
+
return { result: 'deny', reason: '不允许访问其他用户的文件' };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 不在白名单中(非用户目录、非临时目录)→ DENY
|
|
192
|
+
return { result: 'deny', reason: `不允许访问项目目录之外的路径: ${resolvedPath}` };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ─── 规则定义 ──────────────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
const rules = [
|
|
198
|
+
// ── 规则 1: 文件路径隔离 ──
|
|
199
|
+
{
|
|
200
|
+
name: 'file-path-isolation',
|
|
201
|
+
tools: ['Read', 'Write', 'Edit'],
|
|
202
|
+
check: (_toolName, input, context) => {
|
|
203
|
+
return checkFilePath(input?.file_path, context, _toolName);
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
// ── 规则 2: 搜索路径隔离 ──
|
|
208
|
+
{
|
|
209
|
+
name: 'search-path-isolation',
|
|
210
|
+
tools: ['Glob', 'Grep'],
|
|
211
|
+
check: (_toolName, input, context) => {
|
|
212
|
+
// Glob/Grep 的 path 参数为搜索根目录,可选
|
|
213
|
+
if (!input?.path) {
|
|
214
|
+
// 未指定 path 时,SDK 默认使用 cwd,视为安全
|
|
215
|
+
return { result: 'allow' };
|
|
216
|
+
}
|
|
217
|
+
return checkFilePath(input.path, context, _toolName);
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
// ── 规则 9: 写入内容安全审查 ──
|
|
222
|
+
{
|
|
223
|
+
name: 'write-content-guard',
|
|
224
|
+
tools: ['Write', 'Edit'],
|
|
225
|
+
check: (toolName, input, context) => {
|
|
226
|
+
const content = toolName === 'Write' ? input?.content : input?.new_string;
|
|
227
|
+
if (!content || typeof content !== 'string') return { result: 'allow' };
|
|
228
|
+
|
|
229
|
+
// 仅审查脚本文件(扩展名)或含 shebang 的文件
|
|
230
|
+
const filePath = input?.file_path || '';
|
|
231
|
+
const isShellScript = /\.(sh|bash|zsh|fish|ksh|csh)$/i.test(filePath);
|
|
232
|
+
const hasShebang = /^#!\s*\//.test(content.trimStart());
|
|
233
|
+
|
|
234
|
+
if (!isShellScript && !hasShebang) return { result: 'allow' };
|
|
235
|
+
|
|
236
|
+
// 检查危险命令模式
|
|
237
|
+
for (const { pattern, reason } of DANGEROUS_SCRIPT_PATTERNS) {
|
|
238
|
+
if (pattern.test(content)) {
|
|
239
|
+
return { result: 'deny', reason: `不允许写入包含危险操作的脚本: ${reason}` };
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// 检查系统端口查询
|
|
244
|
+
const portCheck = checkSystemPortsInContent(content);
|
|
245
|
+
if (portCheck.denied) {
|
|
246
|
+
return { result: 'deny', reason: `不允许写入${portCheck.reason}的脚本` };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// 检查路径越界
|
|
250
|
+
const pathCheck = checkPathsInContent(content, context);
|
|
251
|
+
if (pathCheck.denied) {
|
|
252
|
+
return { result: 'deny', reason: `不允许写入${pathCheck.reason}的脚本` };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return { result: 'allow' };
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
|
|
259
|
+
// ── 规则 3: 临时目录隔离(Bash 命令中的临时路径) ──
|
|
260
|
+
{
|
|
261
|
+
name: 'temp-dir-isolation',
|
|
262
|
+
tools: ['Bash'],
|
|
263
|
+
check: (_toolName, input, context) => {
|
|
264
|
+
const command = input?.command;
|
|
265
|
+
if (!command || typeof command !== 'string') return { result: 'allow' };
|
|
266
|
+
|
|
267
|
+
// 提取命令中涉及的临时目录路径
|
|
268
|
+
const tempPathPattern = /(\/tmp\/[^\s"'`;|&><)]+|\/var\/tmp\/[^\s"'`;|&><)]+|\/private\/tmp\/[^\s"'`;|&><)]+)/g;
|
|
269
|
+
const tempPaths = command.match(tempPathPattern);
|
|
270
|
+
|
|
271
|
+
if (!tempPaths || tempPaths.length === 0) {
|
|
272
|
+
return { result: 'allow' };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
for (const tmpPath of tempPaths) {
|
|
276
|
+
if (!hasTempUserSubdir(tmpPath, context)) {
|
|
277
|
+
return {
|
|
278
|
+
result: 'deny',
|
|
279
|
+
reason: `请在临时目录中使用以用户 ID 命名的子文件夹。正确用法: /tmp/${context.userUuid}/your-dir/`
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return { result: 'allow' };
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
|
|
288
|
+
// ── 规则 4: 危险命令拦截 ──
|
|
289
|
+
{
|
|
290
|
+
name: 'dangerous-commands',
|
|
291
|
+
tools: ['Bash'],
|
|
292
|
+
check: (_toolName, input, _context) => {
|
|
293
|
+
const command = input?.command;
|
|
294
|
+
if (!command || typeof command !== 'string') return { result: 'allow' };
|
|
295
|
+
|
|
296
|
+
const DANGEROUS_PATTERNS = [
|
|
297
|
+
// 提权
|
|
298
|
+
{ pattern: /\bsudo\b/, reason: '不允许使用 sudo' },
|
|
299
|
+
{ pattern: /\bsu\s+\w/, reason: '不允许切换用户' },
|
|
300
|
+
{ pattern: /\bdoas\b/, reason: '不允许使用 doas' },
|
|
301
|
+
|
|
302
|
+
// 关机/重启
|
|
303
|
+
{ pattern: /\b(shutdown|reboot|halt|poweroff)\b/, reason: '不允许关机/重启系统' },
|
|
304
|
+
{ pattern: /\binit\s+[06]\b/, reason: '不允许关机/重启系统' },
|
|
305
|
+
|
|
306
|
+
// 杀进程(危险)
|
|
307
|
+
{ pattern: /\bkill\s+(-\d+\s+)?1\b/, reason: '不允许杀 init 进程' },
|
|
308
|
+
{ pattern: /\bkillall\b/, reason: '不允许使用 killall(可能影响系统服务,请使用 kill <PID> 终止特定进程)' },
|
|
309
|
+
{ pattern: /\bpkill\b/, reason: '不允许使用 pkill(可能影响系统服务,请使用 kill <PID> 终止特定进程)' },
|
|
310
|
+
|
|
311
|
+
// 破坏性删除
|
|
312
|
+
{ pattern: /\brm\s+[^|]*-[^\s]*r[^\s]*\s+\/(\s|$)/, reason: '不允许递归删除根目录' },
|
|
313
|
+
{ pattern: /\brm\s+[^|]*-[^\s]*r[^\s]*\s+\/(etc|usr|var|bin|sbin|lib|boot|sys|proc|dev)\b/,
|
|
314
|
+
reason: '不允许删除系统目录' },
|
|
315
|
+
|
|
316
|
+
// 磁盘操作
|
|
317
|
+
{ pattern: /\bdd\s+.*\bof=\/dev\//, reason: '不允许直接写入磁盘设备' },
|
|
318
|
+
{ pattern: /\bmkfs\b/, reason: '不允许格式化磁盘' },
|
|
319
|
+
{ pattern: /\bfdisk\b/, reason: '不允许操作磁盘分区' },
|
|
320
|
+
|
|
321
|
+
// 网络/防火墙
|
|
322
|
+
{ pattern: /\b(iptables|nftables|ufw)\b/, reason: '不允许修改防火墙规则' },
|
|
323
|
+
|
|
324
|
+
// 定时任务
|
|
325
|
+
{ pattern: /\bcrontab\s+(-e|-r)\b/, reason: '不允许编辑/删除 crontab' },
|
|
326
|
+
|
|
327
|
+
// 用户/组管理
|
|
328
|
+
{ pattern: /\b(useradd|userdel|usermod|groupadd|groupdel)\b/, reason: '不允许管理系统用户/组' },
|
|
329
|
+
{ pattern: /\bpasswd\b/, reason: '不允许修改密码' },
|
|
330
|
+
{ pattern: /\bvisudo\b/, reason: '不允许编辑 sudoers' },
|
|
331
|
+
|
|
332
|
+
// 内核模块
|
|
333
|
+
{ pattern: /\b(insmod|rmmod|modprobe)\b/, reason: '不允许操作内核模块' },
|
|
334
|
+
];
|
|
335
|
+
|
|
336
|
+
for (const { pattern, reason } of DANGEROUS_PATTERNS) {
|
|
337
|
+
if (pattern.test(command)) {
|
|
338
|
+
return { result: 'deny', reason };
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return { result: 'allow' };
|
|
343
|
+
}
|
|
344
|
+
},
|
|
345
|
+
|
|
346
|
+
// ── 规则 5: 系统环境保护 ──
|
|
347
|
+
{
|
|
348
|
+
name: 'no-system-modification',
|
|
349
|
+
tools: ['Bash'],
|
|
350
|
+
check: (_toolName, input, _context) => {
|
|
351
|
+
const command = input?.command;
|
|
352
|
+
if (!command || typeof command !== 'string') return { result: 'allow' };
|
|
353
|
+
|
|
354
|
+
const SYSTEM_MOD_PATTERNS = [
|
|
355
|
+
// 服务管理
|
|
356
|
+
{ pattern: /\b(systemctl|service)\s+(start|stop|restart|enable|disable|reload)\b/,
|
|
357
|
+
reason: '不允许管理系统服务' },
|
|
358
|
+
{ pattern: /\blaunchctl\s+(load|unload|start|stop|bootstrap|bootout)\b/,
|
|
359
|
+
reason: '不允许管理 macOS 服务' },
|
|
360
|
+
|
|
361
|
+
// 容器/云平台
|
|
362
|
+
{ pattern: /\bdocker\s+(rm|rmi|stop|kill|restart|network|volume|system\s+prune)\b/,
|
|
363
|
+
reason: '不允许管理 Docker 容器和资源' },
|
|
364
|
+
{ pattern: /\bkubectl\b/, reason: '不允许操作 Kubernetes' },
|
|
365
|
+
{ pattern: /\bgcloud\b/, reason: '不允许操作 Google Cloud' },
|
|
366
|
+
{ pattern: /\baws\s+(?!configure\s+list\b)/, reason: '不允许操作 AWS' },
|
|
367
|
+
{ pattern: /\bterraform\s+(apply|destroy|import)\b/, reason: '不允许执行 Terraform 变更' },
|
|
368
|
+
|
|
369
|
+
// Web 服务器
|
|
370
|
+
{ pattern: /\bnginx\s+(-s\s+)?(reload|stop|start|restart|quit)\b/,
|
|
371
|
+
reason: '不允许管理 nginx' },
|
|
372
|
+
{ pattern: /\bapachectl\b/, reason: '不允许管理 Apache' },
|
|
373
|
+
{ pattern: /\bcaddy\s+(stop|start|reload|reverse-proxy)\b/, reason: '不允许管理 Caddy' },
|
|
374
|
+
|
|
375
|
+
// 系统配置写入
|
|
376
|
+
{ pattern: />\s*\/etc\//, reason: '不允许写入 /etc/' },
|
|
377
|
+
{ pattern: /\btee\s+(-a\s+)?\/etc\//, reason: '不允许写入 /etc/' },
|
|
378
|
+
{ pattern: /\bsed\s+-i[^\s]*\s+\/etc\//, reason: '不允许修改 /etc/ 下的文件' },
|
|
379
|
+
{ pattern: /\bcp\s+.*\s+\/etc\//, reason: '不允许复制文件到 /etc/' },
|
|
380
|
+
{ pattern: /\bmv\s+.*\s+\/etc\//, reason: '不允许移动文件到 /etc/' },
|
|
381
|
+
|
|
382
|
+
// SSH
|
|
383
|
+
{ pattern: /\/etc\/ssh\/sshd_config/, reason: '不允许修改 SSH 配置' },
|
|
384
|
+
|
|
385
|
+
// 权限变更(系统目录)
|
|
386
|
+
{ pattern: /\bchmod\b.*\/(etc|usr|var|bin|sbin|lib|boot)\b/, reason: '不允许修改系统目录权限' },
|
|
387
|
+
{ pattern: /\bchown\b.*\/(etc|usr|var|bin|sbin|lib|boot)\b/, reason: '不允许修改系统目录所有者' },
|
|
388
|
+
];
|
|
389
|
+
|
|
390
|
+
for (const { pattern, reason } of SYSTEM_MOD_PATTERNS) {
|
|
391
|
+
if (pattern.test(command)) {
|
|
392
|
+
return { result: 'deny', reason };
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return { result: 'allow' };
|
|
397
|
+
}
|
|
398
|
+
},
|
|
399
|
+
|
|
400
|
+
// ── 规则 6: 全局安装拦截 ──
|
|
401
|
+
{
|
|
402
|
+
name: 'no-global-install',
|
|
403
|
+
tools: ['Bash'],
|
|
404
|
+
check: (_toolName, input, _context) => {
|
|
405
|
+
const command = input?.command;
|
|
406
|
+
if (!command || typeof command !== 'string') return { result: 'allow' };
|
|
407
|
+
|
|
408
|
+
const GLOBAL_INSTALL_PATTERNS = [
|
|
409
|
+
{ pattern: /\bnpm\s+(install|i|add)\s+[^|]*(-g|--global)\b/,
|
|
410
|
+
reason: '不允许全局安装 npm 包,请使用本地安装' },
|
|
411
|
+
{ pattern: /\bnpm\s+(-g|--global)\s+(install|i|add)\b/,
|
|
412
|
+
reason: '不允许全局安装 npm 包,请使用本地安装' },
|
|
413
|
+
{ pattern: /\byarn\s+global\s+add\b/,
|
|
414
|
+
reason: '不允许全局安装 yarn 包' },
|
|
415
|
+
{ pattern: /\bgem\s+install\b/,
|
|
416
|
+
reason: '不允许全局安装 Ruby gem' },
|
|
417
|
+
{ pattern: /\bcargo\s+install\b/,
|
|
418
|
+
reason: '不允许全局安装 Rust 包' },
|
|
419
|
+
];
|
|
420
|
+
|
|
421
|
+
for (const { pattern, reason } of GLOBAL_INSTALL_PATTERNS) {
|
|
422
|
+
if (pattern.test(command)) {
|
|
423
|
+
return { result: 'deny', reason };
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// pip install 特殊处理:venv 内使用放行,否则 UNCERTAIN
|
|
428
|
+
if (/\bpip3?\s+install\b/.test(command)) {
|
|
429
|
+
// 如果命令中包含 venv 相关路径,视为虚拟环境内安装
|
|
430
|
+
if (/venv\/bin\/pip|\.venv\/bin\/pip|virtualenv/.test(command)) {
|
|
431
|
+
return { result: 'allow' };
|
|
432
|
+
}
|
|
433
|
+
// 如果有 --user 标志,允许
|
|
434
|
+
if (/--user\b/.test(command)) {
|
|
435
|
+
return { result: 'allow' };
|
|
436
|
+
}
|
|
437
|
+
// 其他情况交给 LLM 判断
|
|
438
|
+
return { result: 'uncertain', reason: 'pip install 可能在虚拟环境外执行,需要 LLM 审查' };
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return { result: 'allow' };
|
|
442
|
+
}
|
|
443
|
+
},
|
|
444
|
+
|
|
445
|
+
// ── 规则 8: 系统端口保护 ──
|
|
446
|
+
{
|
|
447
|
+
name: 'system-port-protection',
|
|
448
|
+
tools: ['Bash'],
|
|
449
|
+
check: (_toolName, input, _context) => {
|
|
450
|
+
const command = input?.command;
|
|
451
|
+
if (!command || typeof command !== 'string') return { result: 'allow' };
|
|
452
|
+
|
|
453
|
+
// 从环境变量收集系统端口
|
|
454
|
+
const systemPorts = [];
|
|
455
|
+
for (const envVar of ['PORT', 'VITE_PORT']) {
|
|
456
|
+
const port = process.env[envVar];
|
|
457
|
+
if (port && /^\d+$/.test(port)) systemPorts.push(port);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (systemPorts.length === 0) return { result: 'allow' };
|
|
461
|
+
|
|
462
|
+
for (const port of systemPorts) {
|
|
463
|
+
// lsof -ti :PORT, lsof -i :PORT
|
|
464
|
+
if (new RegExp(`\\blsof\\b[^|]*:${port}\\b`).test(command)) {
|
|
465
|
+
return { result: 'deny', reason: `不允许查询系统端口 ${port} 的进程信息(该端口为平台服务端口)` };
|
|
466
|
+
}
|
|
467
|
+
// fuser PORT/tcp, fuser :PORT
|
|
468
|
+
if (new RegExp(`\\bfuser\\b[^|]*\\b${port}\\b`).test(command)) {
|
|
469
|
+
return { result: 'deny', reason: `不允许查询系统端口 ${port} 的进程信息(该端口为平台服务端口)` };
|
|
470
|
+
}
|
|
471
|
+
// ss -tlnp | grep PORT, netstat ... PORT
|
|
472
|
+
if (new RegExp(`\\b(ss|netstat)\\b[^|]*\\b${port}\\b`).test(command)) {
|
|
473
|
+
return { result: 'deny', reason: `不允许查询系统端口 ${port} 的连接信息(该端口为平台服务端口)` };
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return { result: 'allow' };
|
|
478
|
+
}
|
|
479
|
+
},
|
|
480
|
+
|
|
481
|
+
// ── 规则 10: 脚本执行内容审查 ──
|
|
482
|
+
{
|
|
483
|
+
name: 'script-execution-guard',
|
|
484
|
+
tools: ['Bash'],
|
|
485
|
+
check: (_toolName, input, context) => {
|
|
486
|
+
const command = input?.command;
|
|
487
|
+
if (!command || typeof command !== 'string') return { result: 'allow' };
|
|
488
|
+
|
|
489
|
+
// 检测脚本文件执行:bash/sh/python/node/ruby/perl 后跟文件路径
|
|
490
|
+
const interpreterMatch = command.match(
|
|
491
|
+
/\b(?:bash|sh|zsh|dash|source)\s+(?:-\S+\s+)*([^\s;|&><"'`-][^\s;|&><"'`]*)/
|
|
492
|
+
);
|
|
493
|
+
const langMatch = command.match(
|
|
494
|
+
/\b(?:python3?|node|ruby|perl|php)\s+(?:-\S+\s+)*([^\s;|&><"'`-][^\s;|&><"'`]*)/
|
|
495
|
+
);
|
|
496
|
+
const directMatch = command.match(/\.\/([^\s;|&><"'`]+)/);
|
|
497
|
+
|
|
498
|
+
const scriptFile = interpreterMatch?.[1] || langMatch?.[1] || directMatch?.[1];
|
|
499
|
+
if (!scriptFile) return { result: 'allow' };
|
|
500
|
+
|
|
501
|
+
// 解析脚本路径
|
|
502
|
+
const resolvedScript = path.isAbsolute(scriptFile)
|
|
503
|
+
? path.resolve(scriptFile)
|
|
504
|
+
: context.cwd
|
|
505
|
+
? path.resolve(context.cwd, scriptFile)
|
|
506
|
+
: null;
|
|
507
|
+
|
|
508
|
+
if (!resolvedScript) return { result: 'allow' };
|
|
509
|
+
|
|
510
|
+
try {
|
|
511
|
+
// 限制文件大小,防止读取巨大文件阻塞
|
|
512
|
+
const stat = statSync(resolvedScript);
|
|
513
|
+
if (stat.size > 1024 * 1024) {
|
|
514
|
+
return { result: 'uncertain', reason: '脚本文件过大,需要 LLM 审查其安全性' };
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const content = readFileSync(resolvedScript, 'utf8');
|
|
518
|
+
|
|
519
|
+
// 检查危险命令模式
|
|
520
|
+
for (const { pattern, reason } of DANGEROUS_SCRIPT_PATTERNS) {
|
|
521
|
+
if (pattern.test(content)) {
|
|
522
|
+
return { result: 'deny', reason: `脚本文件包含危险操作: ${reason}` };
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// 检查系统端口查询
|
|
527
|
+
const portCheck = checkSystemPortsInContent(content);
|
|
528
|
+
if (portCheck.denied) {
|
|
529
|
+
return { result: 'deny', reason: `脚本文件${portCheck.reason}` };
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// 检查路径越界
|
|
533
|
+
const pathCheck = checkPathsInContent(content, context);
|
|
534
|
+
if (pathCheck.denied) {
|
|
535
|
+
return { result: 'deny', reason: `脚本文件${pathCheck.reason}` };
|
|
536
|
+
}
|
|
537
|
+
} catch {
|
|
538
|
+
// 文件不存在或无法读取 — 交由其他规则处理
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return { result: 'allow' };
|
|
542
|
+
}
|
|
543
|
+
},
|
|
544
|
+
|
|
545
|
+
// ── 规则 7: Bash 路径隔离 ──
|
|
546
|
+
{
|
|
547
|
+
name: 'bash-path-isolation',
|
|
548
|
+
tools: ['Bash'],
|
|
549
|
+
check: (_toolName, input, context) => {
|
|
550
|
+
const command = input?.command;
|
|
551
|
+
if (!command || typeof command !== 'string') return { result: 'allow' };
|
|
552
|
+
|
|
553
|
+
// 检测 ~ 和 $HOME 引用 — 指向服务器主目录,不在用户允许范围内
|
|
554
|
+
if (/(?:^|\s|=|"|')~\//.test(command) || /(?:^|\s|=|"|')~(?:\s|$|;|&&|\|)/.test(command)) {
|
|
555
|
+
return { result: 'deny', reason: '不允许访问服务器主目录(~),请仅在项目目录内操作' };
|
|
556
|
+
}
|
|
557
|
+
if (/\$HOME\b/.test(command)) {
|
|
558
|
+
return { result: 'deny', reason: '不允许访问服务器主目录($HOME),请仅在项目目录内操作' };
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// 提取命令中的绝对路径(以 / 开头)
|
|
562
|
+
const pathPattern = /(?:^|\s|=|"|')(\/[^\s"'`;|&><){}$]+)/g;
|
|
563
|
+
let match;
|
|
564
|
+
const extractedPaths = [];
|
|
565
|
+
while ((match = pathPattern.exec(command)) !== null) {
|
|
566
|
+
extractedPaths.push(match[1]);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (extractedPaths.length === 0) {
|
|
570
|
+
return { result: 'allow' };
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
for (const rawPath of extractedPaths) {
|
|
574
|
+
const resolvedPath = path.resolve(rawPath);
|
|
575
|
+
|
|
576
|
+
// 白名单:只有以下路径允许通过,其他一律拒绝
|
|
577
|
+
// 1. 安全的特殊路径(/dev/null 等)
|
|
578
|
+
if (SAFE_SPECIAL_PATHS.includes(resolvedPath)) continue;
|
|
579
|
+
|
|
580
|
+
// 2. 命令路径(/usr/bin/ 等)— 执行命令本身,不是访问文件
|
|
581
|
+
if (SAFE_COMMAND_PATHS.some(prefix => resolvedPath.startsWith(prefix))) continue;
|
|
582
|
+
|
|
583
|
+
// 3. 在用户允许的目录内(user-data/{uuid}/, user-projects/{uuid}/, cwd)
|
|
584
|
+
if (isPathAllowed(resolvedPath, context)) continue;
|
|
585
|
+
|
|
586
|
+
// 4. 在临时目录中且有用户子目录 → 已由 temp-dir-isolation 规则处理,跳过
|
|
587
|
+
if (isInTempDir(resolvedPath, context)) continue;
|
|
588
|
+
|
|
589
|
+
// 不在白名单中 → DENY
|
|
590
|
+
return { result: 'deny', reason: `不允许访问项目目录之外的路径: ${resolvedPath}` };
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// 检查命令复杂度:包含变量替换、子 shell 等 → UNCERTAIN
|
|
594
|
+
if (/\$\(|\$\{|`/.test(command)) {
|
|
595
|
+
return { result: 'uncertain', reason: '命令包含变量替换或子 shell,需要 LLM 审查路径安全性' };
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return { result: 'allow' };
|
|
599
|
+
}
|
|
600
|
+
},
|
|
601
|
+
];
|
|
602
|
+
|
|
603
|
+
// ─── 规则引擎 ──────────────────────────────────────────────
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* 执行所有适用规则
|
|
607
|
+
* @returns {{ denied: boolean, uncertain: boolean, denyReasons: string[], uncertainReasons: string[] }}
|
|
608
|
+
*/
|
|
609
|
+
export function evaluateRules(toolName, input, context) {
|
|
610
|
+
const denyReasons = [];
|
|
611
|
+
const uncertainReasons = [];
|
|
612
|
+
|
|
613
|
+
for (const rule of rules) {
|
|
614
|
+
// 检查规则是否适用于当前工具
|
|
615
|
+
if (!rule.tools.includes('*') && !rule.tools.includes(toolName)) {
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const result = rule.check(toolName, input, context);
|
|
620
|
+
|
|
621
|
+
if (result.result === 'deny') {
|
|
622
|
+
denyReasons.push(result.reason || rule.name);
|
|
623
|
+
} else if (result.result === 'uncertain') {
|
|
624
|
+
uncertainReasons.push(result.reason || rule.name);
|
|
625
|
+
}
|
|
626
|
+
// 'allow' → 继续检查下一条规则
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
return {
|
|
630
|
+
denied: denyReasons.length > 0,
|
|
631
|
+
uncertain: uncertainReasons.length > 0,
|
|
632
|
+
denyReasons,
|
|
633
|
+
uncertainReasons,
|
|
634
|
+
};
|
|
635
|
+
}
|