@moonquake2004/dsh-security 0.1.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,67 @@
1
+ /**
2
+ * SP6: Vulnerability Match — 已知漏洞匹配
3
+ *
4
+ * 通过 OSV API 查询已知漏洞(CVE/GHSA):
5
+ * - 查询 npm 包的已知漏洞
6
+ * - 过滤 critical/high 级别
7
+ * - 输出受影响的包和修复版本
8
+ *
9
+ * Severity: HIGH
10
+ * Phase: POST_INSTALL
11
+ */
12
+
13
+ import { readFileSync, existsSync } from 'node:fs';
14
+ import { join } from 'node:path';
15
+ import { Severity, maxSeverity } from '../protocol/severity.mjs';
16
+ import { CheckPhase } from '../protocol/phase.mjs';
17
+ import { pass, fail } from '../protocol/check.mjs';
18
+
19
+ async function queryOSV(packageName, version) {
20
+ try {
21
+ const response = await fetch('https://api.osv.dev/v1/query', {
22
+ method: 'POST',
23
+ headers: { 'Content-Type': 'application/json' },
24
+ body: JSON.stringify({ package: { name: packageName, ecosystem: 'npm' }, version }),
25
+ signal: AbortSignal.timeout(10000),
26
+ });
27
+ if (!response.ok) return [];
28
+ const data = await response.json();
29
+ return (data.vulns || []).map(v => ({
30
+ id: v.id,
31
+ summary: v.summary || v.details?.slice(0, 100) || 'No summary',
32
+ severity: v.database_specific?.severity || v.severity?.[0]?.score || 'unknown',
33
+ fixed: v.fix_versions?.[0] || 'unknown',
34
+ }));
35
+ } catch { return []; }
36
+ }
37
+
38
+ export async function run(profileDir) {
39
+ const id = 'SP6';
40
+ const packageJsonPath = join(profileDir, 'package.json');
41
+ if (!existsSync(packageJsonPath)) return pass(id, Severity.HIGH, '无 package.json,跳过漏洞匹配');
42
+
43
+ let manifest;
44
+ try { manifest = JSON.parse(readFileSync(packageJsonPath, 'utf8')); } catch { return pass(id, Severity.HIGH, 'package.json 解析失败'); }
45
+
46
+ const deps = manifest.dependencies || {};
47
+ const dshPackages = Object.entries(deps).filter(([n]) => n.includes('dsh') || n.includes('deepseek'));
48
+ if (dshPackages.length === 0) return pass(id, Severity.HIGH, '无 dsh 相关依赖,跳过漏洞匹配');
49
+
50
+ const allVulns = [];
51
+ for (const [name, version] of dshPackages.slice(0, 10)) {
52
+ const cleanVersion = version.replace(/^[~^>=<]/, '');
53
+ const vulns = await queryOSV(name, cleanVersion);
54
+ allVulns.push(...vulns.map(v => ({ package: name, ...v })));
55
+ }
56
+
57
+ if (allVulns.length === 0) return pass(id, Severity.HIGH, `查询 ${dshPackages.length} 个包,未发现已知漏洞`);
58
+
59
+ const details = allVulns.slice(0, 10).map(v => `${v.package} — ${v.id}: ${v.summary}(fixed: ${v.fixed})`).join('\n');
60
+ const fixable = allVulns.filter(v => v.fixed !== 'unknown').length;
61
+ return fail(id, Severity.HIGH,
62
+ `检测到 ${allVulns.length} 个已知漏洞(${fixable} 个可修复):\n${details}`,
63
+ fixable > 0 ? '升级受影响的包到修复版本' : '关注上游修复进展'
64
+ );
65
+ }
66
+
67
+ export const sp6Check = { id: 'SP6', name: 'vuln-match', severity: Severity.HIGH, phase: CheckPhase.POST_INSTALL, description: '已知漏洞匹配(OSV/CVE/GHSA)', src: 'builtin', runner: (d) => run(d) };
@@ -0,0 +1,177 @@
1
+ /**
2
+ * SR1: Sandbox Violation — 沙箱逃逸检测
3
+ *
4
+ * 分析会话日志中的工具调用,检测潜在的沙箱逃逸行为:
5
+ * - 写操作超出 workspace 范围
6
+ * - 访问系统级资源(/etc, /proc, 环境变量)
7
+ * - 已知逃逸模式匹配(#1769 mount remount 等)
8
+ *
9
+ * Severity: CRITICAL
10
+ * Phase: RUNTIME
11
+ */
12
+
13
+ import { readFileSync, existsSync, createReadStream } from 'node:fs';
14
+ import { createInterface } from 'node:readline';
15
+ import { Severity } from '../protocol/severity.mjs';
16
+ import { CheckPhase } from '../protocol/phase.mjs';
17
+ import { pass, fail } from '../protocol/check.mjs';
18
+
19
+ /** 已知逃逸模式(#1769 及同类) */
20
+ const ESCAPE_PATTERNS = [
21
+ { name: 'mount remount', regex: /mount\s+.*-o\s+remount.*rw/gi, severity: 'critical', ref: '#1769' },
22
+ { name: 'chroot escape', regex: /chroot\s+\//gi, severity: 'critical', ref: null },
23
+ { name: 'nsenter', regex: /nsenter\s+/gi, severity: 'critical', ref: null },
24
+ { name: 'unshare', regex: /unshare\s+/gi, severity: 'high', ref: null },
25
+ ];
26
+
27
+ /** 系统资源访问模式 */
28
+ const SYSTEM_RESOURCE_PATTERNS = [
29
+ { name: '/etc access', regex: /\/etc\/(passwd|shadow|sudoers|hosts)/gi, severity: 'high' },
30
+ { name: '/proc access', regex: /\/proc\/(self|1|environ|cmdline)/gi, severity: 'high' },
31
+ { name: 'env dump', regex: /env\b|printenv|set\b.*\|/gi, severity: 'medium' },
32
+ { name: 'sudo usage', regex: /sudo\s+/gi, severity: 'high' },
33
+ { name: 'curl/wget to unknown', regex: /curl\s+.*\|\s*(bash|sh|node)|wget\s+.*\|\s*(bash|sh|node)/gi, severity: 'critical' },
34
+ ];
35
+
36
+ /** 工作区逃逸路径模式 */
37
+ const WORKSPACE_ESCAPE_PATTERNS = [
38
+ { name: 'write to /tmp', regex: /writeFileSync|writeFile|fs\.write|echo\s+.*>\s*\/tmp/gi, severity: 'low' },
39
+ { name: 'write to home', regex: /writeFileSync|writeFile.*\/Users\/|\/home\//gi, severity: 'low' },
40
+ { name: 'pipe to shell', regex: /\|\s*(bash|sh|zsh)\b/gi, severity: 'high' },
41
+ { name: 'exec subprocess', regex: /child_process|execSync|spawnSync|exec\b/gi, severity: 'medium' },
42
+ ];
43
+
44
+ /**
45
+ * 从一行 JSONL 中提取工具调用信息
46
+ */
47
+ function extractToolCalls(line) {
48
+ try {
49
+ const event = JSON.parse(line);
50
+ if (event.type !== 'tool/call' && event.type !== 'tool/result') return [];
51
+
52
+ const calls = [];
53
+ const data = event.data || {};
54
+ const name = data.name || data.tool || '';
55
+ const args = data.args || data.input || {};
56
+ const text = typeof args === 'string' ? args : JSON.stringify(args);
57
+
58
+ calls.push({
59
+ type: event.type,
60
+ name,
61
+ text,
62
+ seq: event.seq,
63
+ turn: event.turn,
64
+ });
65
+ return calls;
66
+ } catch {
67
+ return [];
68
+ }
69
+ }
70
+
71
+ /**
72
+ * 扫描文本中的危险模式
73
+ */
74
+ function scanPatterns(text, patterns) {
75
+ const findings = [];
76
+ for (const pattern of patterns) {
77
+ const matches = text.matchAll(new RegExp(pattern.regex.source, 'gi'));
78
+ for (const match of matches) {
79
+ findings.push({
80
+ type: pattern.name,
81
+ severity: pattern.severity,
82
+ ref: pattern.ref || null,
83
+ snippet: match[0].slice(0, 60),
84
+ });
85
+ }
86
+ }
87
+ return findings;
88
+ }
89
+
90
+ /**
91
+ * SR1 检查:分析会话日志中的沙箱逃逸行为
92
+ * @param {string} sessionFile - 会话日志文件路径
93
+ * @returns {Promise<import('../protocol/check.mjs').SecurityCheckResult>}
94
+ */
95
+ export async function run(sessionFile) {
96
+ const id = 'SR1';
97
+
98
+ if (!sessionFile || !existsSync(sessionFile)) {
99
+ return pass(id, Severity.CRITICAL, '无会话日志,跳过运行时安全检查');
100
+ }
101
+
102
+ if (sessionFile.endsWith('.zstd')) {
103
+ return pass(id, Severity.CRITICAL, 'zstd 压缩的会话文件需先解压再检查');
104
+ }
105
+
106
+ const findings = [];
107
+ let lineCount = 0;
108
+
109
+ const fileStream = createReadStream(sessionFile, { encoding: 'utf8' });
110
+ const rl = createInterface({ input: fileStream, crlfDelay: Infinity });
111
+
112
+ for await (const line of rl) {
113
+ lineCount++;
114
+ if (!line.trim()) continue;
115
+
116
+ const calls = extractToolCalls(line);
117
+ for (const call of calls) {
118
+ // 扫描工具参数中的危险模式
119
+ const allPatterns = [...ESCAPE_PATTERNS, ...SYSTEM_RESOURCE_PATTERNS, ...WORKSPACE_ESCAPE_PATTERNS];
120
+ const textFindings = scanPatterns(call.text, allPatterns);
121
+ for (const f of textFindings) {
122
+ findings.push({
123
+ ...f,
124
+ line: lineCount,
125
+ tool: call.name,
126
+ seq: call.seq,
127
+ turn: call.turn,
128
+ });
129
+ }
130
+ }
131
+ }
132
+
133
+ if (findings.length === 0) {
134
+ return pass(id, Severity.CRITICAL, `扫描 ${lineCount} 行会话日志,未检测到沙箱逃逸行为`);
135
+ }
136
+
137
+ // 按严重度分组
138
+ const bySeverity = {};
139
+ for (const f of findings) {
140
+ if (!bySeverity[f.severity]) bySeverity[f.severity] = [];
141
+ bySeverity[f.severity].push(f);
142
+ }
143
+
144
+ const summary = Object.entries(bySeverity)
145
+ .map(([sev, items]) => `${sev}: ${items.length}`)
146
+ .join(', ');
147
+
148
+ const details = findings
149
+ .slice(0, 10)
150
+ .map(f => `[${f.severity}] 行${f.line} ${f.tool} — ${f.type}(${f.snippet})${f.ref ? ' [' + f.ref + ']' : ''}`)
151
+ .join('\n');
152
+
153
+ const criticalCount = findings.filter(f => f.severity === 'critical').length;
154
+ const overallSeverity = criticalCount > 0 ? Severity.CRITICAL : Severity.HIGH;
155
+
156
+ const fix = criticalCount > 0
157
+ ? '检测到 CRITICAL 级别沙箱逃逸行为,立即检查相关工具和插件'
158
+ : '检查相关工具调用是否在预期的沙箱策略内';
159
+
160
+ const refs = [...new Set(findings.filter(f => f.ref).map(f => f.ref))];
161
+
162
+ return fail(id, overallSeverity,
163
+ `检测到 ${findings.length} 个潜在沙箱逃逸行为(${summary}):\n${details}`,
164
+ fix,
165
+ refs
166
+ );
167
+ }
168
+
169
+ export const sr1Check = {
170
+ id: 'SR1',
171
+ name: 'sandbox-violation',
172
+ severity: Severity.CRITICAL,
173
+ phase: CheckPhase.RUNTIME,
174
+ description: '沙箱逃逸行为检测(会话日志分析)',
175
+ src: 'builtin',
176
+ runner: (sessionFile) => run(sessionFile),
177
+ };
@@ -0,0 +1,67 @@
1
+ /**
2
+ * SR2: Privilege Escalation — 权限提升检测
3
+ *
4
+ * 分析会话日志中的权限提升行为:
5
+ * - sudo/doas 使用
6
+ * - 文件权限变更(chmod/chown)
7
+ * - 特权端口绑定
8
+ *
9
+ * Severity: HIGH
10
+ * Phase: RUNTIME
11
+ */
12
+
13
+ import { existsSync, createReadStream } from 'node:fs';
14
+ import { createInterface } from 'node:readline';
15
+ import { Severity } from '../protocol/severity.mjs';
16
+ import { CheckPhase } from '../protocol/phase.mjs';
17
+ import { pass, fail } from '../protocol/check.mjs';
18
+
19
+ const PRIVILEGE_PATTERNS = [
20
+ { name: 'sudo usage', regex: /sudo\s+/gi, severity: 'high' },
21
+ { name: 'chmod', regex: /chmod\s+[^/]*\s+[0-7]*7[0-7][0-7]/gi, severity: 'medium' },
22
+ { name: 'chown', regex: /chown\s+/gi, severity: 'medium' },
23
+ { name: 'setuid', regex: /setuid|setgid/gi, severity: 'high' },
24
+ { name: 'capability', regex: /cap_|CAP_/gi, severity: 'high' },
25
+ ];
26
+
27
+ function extractToolCalls(line) {
28
+ try {
29
+ const event = JSON.parse(line);
30
+ if (event.type !== 'tool/call') return [];
31
+ const data = event.data || {};
32
+ return [{ name: data.name || data.tool || '', text: JSON.stringify(data.args || data.input || {}), seq: event.seq }];
33
+ } catch { return []; }
34
+ }
35
+
36
+ export async function run(sessionFile) {
37
+ const id = 'SR2';
38
+ if (!sessionFile || !existsSync(sessionFile)) return pass(id, Severity.HIGH, '无会话文件,跳过权限提升检测');
39
+ if (sessionFile.endsWith('.zstd')) return pass(id, Severity.HIGH, 'zstd 文件需先解压');
40
+
41
+ const findings = [];
42
+ let lineCount = 0;
43
+ const rl = createInterface({ input: createReadStream(sessionFile, { encoding: 'utf8' }), crlfDelay: Infinity });
44
+
45
+ for await (const line of rl) {
46
+ lineCount++;
47
+ if (!line.trim()) continue;
48
+ for (const call of extractToolCalls(line)) {
49
+ for (const pattern of PRIVILEGE_PATTERNS) {
50
+ for (const match of call.text.matchAll(new RegExp(pattern.regex.source, 'gi'))) {
51
+ findings.push({ type: pattern.name, severity: pattern.severity, line: lineCount, tool: call.name, snippet: match[0].slice(0, 40) });
52
+ }
53
+ }
54
+ }
55
+ }
56
+
57
+ if (findings.length === 0) return pass(id, Severity.HIGH, `扫描 ${lineCount} 行,未检测到权限提升行为`);
58
+
59
+ const details = findings.slice(0, 10).map(f => `[${f.severity}] 行${f.line} ${f.tool} — ${f.type}: ${f.snippet}`).join('\n');
60
+ const highCount = findings.filter(f => f.severity === 'high').length;
61
+ return fail(id, highCount > 0 ? Severity.HIGH : Severity.MEDIUM,
62
+ `检测到 ${findings.length} 个权限提升行为:\n${details}`,
63
+ '检查相关工具调用是否在预期的权限范围内'
64
+ );
65
+ }
66
+
67
+ export const sr2Check = { id: 'SR2', name: 'privilege-escalation', severity: Severity.HIGH, phase: CheckPhase.RUNTIME, description: '权限提升行为检测', src: 'builtin', runner: (f) => run(f) };
@@ -0,0 +1,163 @@
1
+ /**
2
+ * SR3: Data Exfiltration — 数据外泄检测
3
+ *
4
+ * 分析会话日志中的工具调用,检测潜在的数据外泄行为:
5
+ * - 大量文件读取后接网络请求
6
+ * - 凭据转发(将密钥传递给外部工具)
7
+ * - 异常的数据流模式
8
+ *
9
+ * Severity: HIGH
10
+ * Phase: RUNTIME
11
+ */
12
+
13
+ import { existsSync, createReadStream } from 'node:fs';
14
+ import { createInterface } from 'node:readline';
15
+ import { Severity } from '../protocol/severity.mjs';
16
+ import { CheckPhase } from '../protocol/phase.mjs';
17
+ import { pass, fail } from '../protocol/check.mjs';
18
+
19
+ /** 凭据转发模式 */
20
+ const CREDENTIAL_FORWARD_PATTERNS = [
21
+ { name: 'API key in command', regex: /sk-[a-zA-Z0-9]{20,}|ghp_[a-zA-Z0-9]{36}|AKIA[0-9A-Z]{16}/g, severity: 'high' },
22
+ { name: 'Token in URL', regex: /https?:\/\/[^ ]*token=[^ &]+/gi, severity: 'high' },
23
+ { name: 'Bearer in command', regex: /Bearer\s+[a-zA-Z0-9._-]{20,}/gi, severity: 'high' },
24
+ ];
25
+
26
+ /** 网络外发模式 */
27
+ const NETWORK_EXFIL_PATTERNS = [
28
+ { name: 'curl POST', regex: /curl\s+.*-X\s*POST|curl\s+.*--data/gi, severity: 'medium' },
29
+ { name: 'wget POST', regex: /wget\s+.*--post-data/gi, severity: 'medium' },
30
+ { name: 'fetch API', regex: /fetch\s*\(|\.post\s*\(/gi, severity: 'low' },
31
+ { name: 'HTTP request with data', regex: /http\.request|https\.request|axios\.post/gi, severity: 'low' },
32
+ ];
33
+
34
+ /** 大量数据读取模式 */
35
+ const BULK_READ_PATTERNS = [
36
+ { name: 'read entire file', regex: /readFileSync|readFile\b|cat\s+[^|]+$/gm, severity: 'low' },
37
+ { name: 'directory listing', regex: /readdirSync|readdir\b|ls\s+-[la]*/gi, severity: 'low' },
38
+ ];
39
+
40
+ /**
41
+ * 从一行 JSONL 中提取工具调用信息
42
+ */
43
+ function extractToolCalls(line) {
44
+ try {
45
+ const event = JSON.parse(line);
46
+ if (event.type !== 'tool/call' && event.type !== 'tool/result') return [];
47
+
48
+ const calls = [];
49
+ const data = event.data || {};
50
+ const name = data.name || data.tool || '';
51
+ const args = data.args || data.input || {};
52
+ const text = typeof args === 'string' ? args : JSON.stringify(args);
53
+
54
+ calls.push({ type: event.type, name, text, seq: event.seq, turn: event.turn });
55
+ return calls;
56
+ } catch {
57
+ return [];
58
+ }
59
+ }
60
+
61
+ /**
62
+ * 扫描文本中的危险模式
63
+ */
64
+ function scanPatterns(text, patterns) {
65
+ const findings = [];
66
+ for (const pattern of patterns) {
67
+ const matches = text.matchAll(new RegExp(pattern.regex.source, 'gi'));
68
+ for (const match of matches) {
69
+ findings.push({
70
+ type: pattern.name,
71
+ severity: pattern.severity,
72
+ snippet: match[0].slice(0, 60),
73
+ });
74
+ }
75
+ }
76
+ return findings;
77
+ }
78
+
79
+ /**
80
+ * SR3 检查:分析会话日志中的数据外泄行为
81
+ * @param {string} sessionFile - 会话日志文件路径
82
+ * @returns {Promise<import('../protocol/check.mjs').SecurityCheckResult>}
83
+ */
84
+ export async function run(sessionFile) {
85
+ const id = 'SR3';
86
+
87
+ if (!sessionFile || !existsSync(sessionFile)) {
88
+ return pass(id, Severity.HIGH, '无会话日志,跳过数据外泄检查');
89
+ }
90
+
91
+ if (sessionFile.endsWith('.zstd')) {
92
+ return pass(id, Severity.HIGH, 'zstd 压缩的会话文件需先解压再检查');
93
+ }
94
+
95
+ const findings = [];
96
+ let lineCount = 0;
97
+
98
+ const fileStream = createReadStream(sessionFile, { encoding: 'utf8' });
99
+ const rl = createInterface({ input: fileStream, crlfDelay: Infinity });
100
+
101
+ for await (const line of rl) {
102
+ lineCount++;
103
+ if (!line.trim()) continue;
104
+
105
+ const calls = extractToolCalls(line);
106
+ for (const call of calls) {
107
+ // 扫描凭据转发
108
+ const credFindings = scanPatterns(call.text, CREDENTIAL_FORWARD_PATTERNS);
109
+ for (const f of credFindings) {
110
+ findings.push({ ...f, line: lineCount, tool: call.name, category: 'credential-forward' });
111
+ }
112
+
113
+ // 扫描网络外发
114
+ const netFindings = scanPatterns(call.text, NETWORK_EXFIL_PATTERNS);
115
+ for (const f of netFindings) {
116
+ findings.push({ ...f, line: lineCount, tool: call.name, category: 'network-exfil' });
117
+ }
118
+ }
119
+ }
120
+
121
+ if (findings.length === 0) {
122
+ return pass(id, Severity.HIGH, `扫描 ${lineCount} 行会话日志,未检测到数据外泄行为`);
123
+ }
124
+
125
+ // 按类别分组
126
+ const byCategory = {};
127
+ for (const f of findings) {
128
+ if (!byCategory[f.category]) byCategory[f.category] = [];
129
+ byCategory[f.category].push(f);
130
+ }
131
+
132
+ const summary = Object.entries(byCategory)
133
+ .map(([cat, items]) => `${cat}: ${items.length}`)
134
+ .join(', ');
135
+
136
+ const details = findings
137
+ .slice(0, 10)
138
+ .map(f => `[${f.severity}] 行${f.line} ${f.tool} — ${f.type}(${f.snippet})`)
139
+ .join('\n');
140
+
141
+ const highCount = findings.filter(f => f.severity === 'high').length;
142
+ const overallSeverity = highCount > 0 ? Severity.HIGH : Severity.MEDIUM;
143
+
144
+ const fix = highCount > 0
145
+ ? '检测到凭据转发或敏感数据外发,检查相关工具是否在传递密钥给外部服务'
146
+ : '检查网络请求是否在预期的数据流范围内';
147
+
148
+ return fail(id, overallSeverity,
149
+ `检测到 ${findings.length} 个潜在数据外泄行为(${summary}):\n${details}`,
150
+ fix,
151
+ ['#962']
152
+ );
153
+ }
154
+
155
+ export const sr3Check = {
156
+ id: 'SR3',
157
+ name: 'data-exfiltration',
158
+ severity: Severity.HIGH,
159
+ phase: CheckPhase.RUNTIME,
160
+ description: '数据外泄行为检测(会话日志分析)',
161
+ src: 'builtin',
162
+ runner: (sessionFile) => run(sessionFile),
163
+ };
@@ -0,0 +1,64 @@
1
+ /**
2
+ * SR4: Isolation Verify — 插件隔离验证
3
+ *
4
+ * 分析会话日志中的跨插件数据泄漏:
5
+ * - 检测工具调用中的交叉引用
6
+ * - 检测异常的数据流模式
7
+ *
8
+ * Severity: MEDIUM
9
+ * Phase: RUNTIME
10
+ */
11
+
12
+ import { existsSync, createReadStream } from 'node:fs';
13
+ import { createInterface } from 'node:readline';
14
+ import { Severity } from '../protocol/severity.mjs';
15
+ import { CheckPhase } from '../protocol/phase.mjs';
16
+ import { pass, fail } from '../protocol/check.mjs';
17
+
18
+ function extractToolCalls(line) {
19
+ try {
20
+ const event = JSON.parse(line);
21
+ if (event.type !== 'tool/call') return [];
22
+ const data = event.data || {};
23
+ return [{ name: data.name || data.tool || '', args: data.args || data.input || {}, seq: event.seq, turn: event.turn }];
24
+ } catch { return []; }
25
+ }
26
+
27
+ export async function run(sessionFile) {
28
+ const id = 'SR4';
29
+ if (!sessionFile || !existsSync(sessionFile)) return pass(id, Severity.MEDIUM, '无会话文件,跳过隔离验证');
30
+ if (sessionFile.endsWith('.zstd')) return pass(id, Severity.MEDIUM, 'zstd 文件需先解压');
31
+
32
+ const toolCalls = [];
33
+ let lineCount = 0;
34
+ const rl = createInterface({ input: createReadStream(sessionFile, { encoding: 'utf8' }), crlfDelay: Infinity });
35
+
36
+ for await (const line of rl) {
37
+ lineCount++;
38
+ if (!line.trim()) continue;
39
+ toolCalls.push(...extractToolCalls(line));
40
+ }
41
+
42
+ // 简单的隔离检查:检测同一 turn 内不同工具的数据传递
43
+ const findings = [];
44
+ const byTurn = {};
45
+ for (const call of toolCalls) {
46
+ const turn = call.turn || 0;
47
+ if (!byTurn[turn]) byTurn[turn] = [];
48
+ byTurn[turn].push(call);
49
+ }
50
+
51
+ for (const [turn, calls] of Object.entries(byTurn)) {
52
+ const tools = new Set(calls.map(c => c.name));
53
+ if (tools.size > 3) {
54
+ findings.push({ type: 'multi-tool-turn', severity: 'low', detail: `turn ${turn} 使用了 ${tools.size} 个不同工具:${[...tools].join(', ')}` });
55
+ }
56
+ }
57
+
58
+ if (findings.length === 0) return pass(id, Severity.MEDIUM, `分析 ${toolCalls.length} 个工具调用,未检测到隔离问题`);
59
+
60
+ const details = findings.slice(0, 10).map(f => `[${f.severity}] ${f.detail}`).join('\n');
61
+ return fail(id, Severity.MEDIUM, `检测到 ${findings.length} 个潜在隔离问题:\n${details}`, '检查工具间的数据流是否在预期范围内');
62
+ }
63
+
64
+ export const sr4Check = { id: 'SR4', name: 'isolation-verify', severity: Severity.MEDIUM, phase: CheckPhase.RUNTIME, description: '插件隔离验证', src: 'builtin', runner: (f) => run(f) };