@moonquake2004/dsh-security 0.1.0 → 0.1.1
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/README.md +229 -48
- package/package.json +1 -1
- package/src/checks/sl1-supply-chain.mjs +44 -15
- package/src/checks/sl3-reputation-score.mjs +22 -9
- package/src/checks/sl4-release-compat.mjs +16 -9
- package/src/checks/sp2-secret-scan.mjs +3 -1
- package/src/checks/sp4-entry-poison.mjs +4 -1
- package/src/checks/sp5-permission-model.mjs +24 -6
- package/src/checks/sp6-vuln-match.mjs +41 -12
- package/src/checks/sr1-sandbox-violation.mjs +35 -41
- package/src/checks/sr2-privilege-escalation.mjs +22 -15
- package/src/checks/sr3-data-exfiltration.mjs +27 -50
- package/src/checks/sr4-isolation-verify.mjs +18 -13
- package/src/checks/ss1-credential-leak.mjs +30 -48
- package/src/checks/ss2-pii-exposure.mjs +22 -12
- package/src/checks/ss3-sensitive-output.mjs +17 -14
- package/src/config.mjs +21 -16
- package/src/index.mjs +2 -1
- package/src/integrations/ecosystem.mjs +60 -23
- package/src/integrations/plugin-reducer.mjs +17 -14
- package/src/integrations/poison-guard.mjs +9 -15
- package/src/integrations/sandbox-audit.mjs +10 -11
- package/src/protocol/check.mjs +9 -0
- package/src/registry.mjs +68 -16
- package/src/session-reader.mjs +63 -0
|
@@ -37,21 +37,39 @@ export async function run(profileDir) {
|
|
|
37
37
|
const issues = [];
|
|
38
38
|
for (const entry of readdirSync(nmDir, { withFileTypes: true })) {
|
|
39
39
|
if (entry.name.startsWith('.') || entry.name === '.bin') continue;
|
|
40
|
-
|
|
41
|
-
if (
|
|
40
|
+
// scoped 包(@scope/pkg):复审修复——此前把 @scope 当包名拼路径,scoped 插件全部漏扫
|
|
41
|
+
if (entry.isDirectory() && entry.name.startsWith('@')) {
|
|
42
|
+
const scopeDir = join(nmDir, entry.name);
|
|
43
|
+
let pkgs = [];
|
|
44
|
+
try { pkgs = readdirSync(scopeDir, { withFileTypes: true }); } catch { continue; }
|
|
45
|
+
for (const pkg of pkgs) {
|
|
46
|
+
if (!pkg.isDirectory()) continue;
|
|
47
|
+
issues.push(...inspectPackage(join(scopeDir, pkg.name), join(entry.name, pkg.name)));
|
|
48
|
+
}
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (!entry.isDirectory()) continue;
|
|
52
|
+
issues.push(...inspectPackage(join(nmDir, entry.name), entry.name));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function inspectPackage(pkgDir, displayName) {
|
|
56
|
+
const found = [];
|
|
57
|
+
const pkgPath = join(pkgDir, 'package.json');
|
|
58
|
+
if (!existsSync(pkgPath)) return found;
|
|
42
59
|
try {
|
|
43
60
|
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
44
|
-
if (!pkg.dsh?.bundle)
|
|
45
|
-
const patchPath = join(
|
|
61
|
+
if (!pkg.dsh?.bundle) return found;
|
|
62
|
+
const patchPath = join(pkgDir, 'cordis.patch.yml');
|
|
46
63
|
if (existsSync(patchPath)) {
|
|
47
64
|
const content = readFileSync(patchPath, 'utf8');
|
|
48
|
-
|
|
65
|
+
found.push(...scanForUndeclaredCapabilities(content, pkg.name || displayName));
|
|
49
66
|
}
|
|
50
67
|
} catch { /* skip */ }
|
|
68
|
+
return found;
|
|
51
69
|
}
|
|
52
70
|
|
|
53
71
|
if (issues.length === 0) return pass(id, Severity.MEDIUM, '插件权限声明一致');
|
|
54
|
-
const details = issues.slice(0, 10).map(i => `[${i.severity}] ${i.detail}`).join('\n');
|
|
72
|
+
const details = issues.slice(0, 10).map(i => `[${i.severity}] ${i.type}: ${i.detail}`).join('\n');
|
|
55
73
|
return fail(id, Severity.MEDIUM, `检测到 ${issues.length} 个权限声明问题:\n${details}`, '为插件显式声明所需的权限范围');
|
|
56
74
|
}
|
|
57
75
|
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 通过 OSV API 查询已知漏洞(CVE/GHSA):
|
|
5
5
|
* - 查询 npm 包的已知漏洞
|
|
6
|
-
* -
|
|
7
|
-
* -
|
|
6
|
+
* - 按 OSV 严重级别映射整体结果严重度,detail 标注 critical/high 数量
|
|
7
|
+
* - 输出受影响的包与漏洞编号(references)
|
|
8
8
|
*
|
|
9
9
|
* Severity: HIGH
|
|
10
10
|
* Phase: POST_INSTALL
|
|
@@ -14,8 +14,12 @@ import { readFileSync, existsSync } from 'node:fs';
|
|
|
14
14
|
import { join } from 'node:path';
|
|
15
15
|
import { Severity, maxSeverity } from '../protocol/severity.mjs';
|
|
16
16
|
import { CheckPhase } from '../protocol/phase.mjs';
|
|
17
|
-
import { pass, fail } from '../protocol/check.mjs';
|
|
17
|
+
import { pass, fail, skip } from '../protocol/check.mjs';
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* 查询 OSV API。
|
|
21
|
+
* @returns {Promise<Array|null>} 成功返回漏洞数组(可能为空);网络失败返回 null
|
|
22
|
+
*/
|
|
19
23
|
async function queryOSV(packageName, version) {
|
|
20
24
|
try {
|
|
21
25
|
const response = await fetch('https://api.osv.dev/v1/query', {
|
|
@@ -24,15 +28,22 @@ async function queryOSV(packageName, version) {
|
|
|
24
28
|
body: JSON.stringify({ package: { name: packageName, ecosystem: 'npm' }, version }),
|
|
25
29
|
signal: AbortSignal.timeout(10000),
|
|
26
30
|
});
|
|
27
|
-
if (!response.ok) return
|
|
31
|
+
if (!response.ok) return null;
|
|
28
32
|
const data = await response.json();
|
|
29
33
|
return (data.vulns || []).map(v => ({
|
|
30
34
|
id: v.id,
|
|
31
35
|
summary: v.summary || v.details?.slice(0, 100) || 'No summary',
|
|
32
36
|
severity: v.database_specific?.severity || v.severity?.[0]?.score || 'unknown',
|
|
33
|
-
fixed: v.fix_versions?.[0] || 'unknown',
|
|
34
37
|
}));
|
|
35
|
-
} catch { return
|
|
38
|
+
} catch { return null; }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function osvSeverityToLevel(sev) {
|
|
42
|
+
const s = String(sev).toUpperCase();
|
|
43
|
+
if (s.includes('CRITICAL')) return Severity.CRITICAL;
|
|
44
|
+
if (s.includes('HIGH')) return Severity.HIGH;
|
|
45
|
+
if (s.includes('MODERATE') || s.includes('MEDIUM')) return Severity.MEDIUM;
|
|
46
|
+
return Severity.LOW;
|
|
36
47
|
}
|
|
37
48
|
|
|
38
49
|
export async function run(profileDir) {
|
|
@@ -48,19 +59,37 @@ export async function run(profileDir) {
|
|
|
48
59
|
if (dshPackages.length === 0) return pass(id, Severity.HIGH, '无 dsh 相关依赖,跳过漏洞匹配');
|
|
49
60
|
|
|
50
61
|
const allVulns = [];
|
|
62
|
+
const refs = new Set();
|
|
63
|
+
let okQueries = 0;
|
|
64
|
+
let failedQueries = 0;
|
|
51
65
|
for (const [name, version] of dshPackages.slice(0, 10)) {
|
|
66
|
+
// 只去掉首个范围前缀字符;workspace:/catalog:/file: 等协议版本无法映射,原样传给 OSV(查询无结果)
|
|
52
67
|
const cleanVersion = version.replace(/^[~^>=<]/, '');
|
|
53
68
|
const vulns = await queryOSV(name, cleanVersion);
|
|
69
|
+
if (vulns === null) { failedQueries++; continue; }
|
|
70
|
+
okQueries++;
|
|
54
71
|
allVulns.push(...vulns.map(v => ({ package: name, ...v })));
|
|
72
|
+
for (const v of vulns) if (v.id) refs.add(v.id);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 全部查询失败 → skip(离线时不应谎报"未发现已知漏洞")
|
|
76
|
+
if (okQueries === 0 && failedQueries > 0) {
|
|
77
|
+
return skip(id, Severity.HIGH, `OSV API 不可达(${failedQueries} 个包全部查询失败),跳过已知漏洞匹配`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (allVulns.length === 0) {
|
|
81
|
+
const note = failedQueries > 0 ? `(${failedQueries} 个包查询失败未覆盖)` : '';
|
|
82
|
+
return pass(id, Severity.HIGH, `成功查询 ${okQueries}/${dshPackages.length} 个包,未发现已知漏洞${note}`);
|
|
55
83
|
}
|
|
56
84
|
|
|
57
|
-
|
|
85
|
+
const details = allVulns.slice(0, 10).map(v => `${v.package} — ${v.id}: ${v.summary}(severity: ${v.severity})`).join('\n');
|
|
86
|
+
const overallSeverity = maxSeverity(allVulns.map(v => osvSeverityToLevel(v.severity)));
|
|
87
|
+
const criticalHigh = allVulns.filter(v => ['critical', 'high'].includes(osvSeverityToLevel(v.severity))).length;
|
|
58
88
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
fixable > 0 ? '升级受影响的包到修复版本' : '关注上游修复进展'
|
|
89
|
+
return fail(id, overallSeverity,
|
|
90
|
+
`检测到 ${allVulns.length} 个已知漏洞(critical/high ${criticalHigh} 个):\n${details}`,
|
|
91
|
+
'按 OSV 建议升级受影响的包到修复版本',
|
|
92
|
+
[...refs].slice(0, 5)
|
|
64
93
|
);
|
|
65
94
|
}
|
|
66
95
|
|
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SR1: Sandbox Violation — 沙箱逃逸检测
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* -
|
|
6
|
-
* - 访问系统级资源(/etc, /proc, 环境变量)
|
|
4
|
+
* 分析会话日志中的工具调用(tool/call),检测潜在的沙箱逃逸行为:
|
|
5
|
+
* - 访问系统级资源(/etc, /proc)
|
|
7
6
|
* - 已知逃逸模式匹配(#1769 mount remount 等)
|
|
7
|
+
* - 管道执行远程脚本
|
|
8
|
+
*
|
|
9
|
+
* 支持明文与 zstd 压缩会话日志(session-reader)。
|
|
8
10
|
*
|
|
9
11
|
* Severity: CRITICAL
|
|
10
12
|
* Phase: RUNTIME
|
|
11
13
|
*/
|
|
12
14
|
|
|
13
|
-
import {
|
|
14
|
-
import { createInterface } from 'node:readline';
|
|
15
|
+
import { existsSync } from 'node:fs';
|
|
15
16
|
import { Severity } from '../protocol/severity.mjs';
|
|
16
17
|
import { CheckPhase } from '../protocol/phase.mjs';
|
|
17
|
-
import { pass, fail } from '../protocol/check.mjs';
|
|
18
|
+
import { pass, fail, skip } from '../protocol/check.mjs';
|
|
19
|
+
import { scanSessionLines } from '../session-reader.mjs';
|
|
18
20
|
|
|
19
21
|
/** 已知逃逸模式(#1769 及同类) */
|
|
20
22
|
const ESCAPE_PATTERNS = [
|
|
@@ -28,7 +30,8 @@ const ESCAPE_PATTERNS = [
|
|
|
28
30
|
const SYSTEM_RESOURCE_PATTERNS = [
|
|
29
31
|
{ name: '/etc access', regex: /\/etc\/(passwd|shadow|sudoers|hosts)/gi, severity: 'high' },
|
|
30
32
|
{ name: '/proc access', regex: /\/proc\/(self|1|environ|cmdline)/gi, severity: 'high' },
|
|
31
|
-
|
|
33
|
+
// 复审修复:旧正则 /env\b|set\b.*\|/ 几乎命中一切文本;改为命令位置的 env/printenv 转储
|
|
34
|
+
{ name: 'env dump', regex: /(?:^|["'|;&]\s*)(?:printenv|env)\s*(?:[|;"']|$)/gm, severity: 'medium' },
|
|
32
35
|
{ name: 'sudo usage', regex: /sudo\s+/gi, severity: 'high' },
|
|
33
36
|
{ name: 'curl/wget to unknown', regex: /curl\s+.*\|\s*(bash|sh|node)|wget\s+.*\|\s*(bash|sh|node)/gi, severity: 'critical' },
|
|
34
37
|
];
|
|
@@ -38,31 +41,30 @@ const WORKSPACE_ESCAPE_PATTERNS = [
|
|
|
38
41
|
{ name: 'write to /tmp', regex: /writeFileSync|writeFile|fs\.write|echo\s+.*>\s*\/tmp/gi, severity: 'low' },
|
|
39
42
|
{ name: 'write to home', regex: /writeFileSync|writeFile.*\/Users\/|\/home\//gi, severity: 'low' },
|
|
40
43
|
{ name: 'pipe to shell', regex: /\|\s*(bash|sh|zsh)\b/gi, severity: 'high' },
|
|
41
|
-
|
|
44
|
+
// 复审修复:去掉过宽的 exec\b(命中 "execute" 等普通词),保留具体进程 API
|
|
45
|
+
{ name: 'exec subprocess', regex: /child_process|execSync|spawnSync/gi, severity: 'medium' },
|
|
42
46
|
];
|
|
43
47
|
|
|
44
48
|
/**
|
|
45
|
-
* 从一行 JSONL
|
|
49
|
+
* 从一行 JSONL 中提取工具调用信息(只看 tool/call:result 是数据不是行为)
|
|
46
50
|
*/
|
|
47
51
|
function extractToolCalls(line) {
|
|
48
52
|
try {
|
|
49
53
|
const event = JSON.parse(line);
|
|
50
|
-
if (event.type !== 'tool/call'
|
|
54
|
+
if (event.type !== 'tool/call') return [];
|
|
51
55
|
|
|
52
|
-
const calls = [];
|
|
53
56
|
const data = event.data || {};
|
|
54
57
|
const name = data.name || data.tool || '';
|
|
55
58
|
const args = data.args || data.input || {};
|
|
56
59
|
const text = typeof args === 'string' ? args : JSON.stringify(args);
|
|
57
60
|
|
|
58
|
-
|
|
61
|
+
return [{
|
|
59
62
|
type: event.type,
|
|
60
63
|
name,
|
|
61
64
|
text,
|
|
62
65
|
seq: event.seq,
|
|
63
66
|
turn: event.turn,
|
|
64
|
-
}
|
|
65
|
-
return calls;
|
|
67
|
+
}];
|
|
66
68
|
} catch {
|
|
67
69
|
return [];
|
|
68
70
|
}
|
|
@@ -74,7 +76,8 @@ function extractToolCalls(line) {
|
|
|
74
76
|
function scanPatterns(text, patterns) {
|
|
75
77
|
const findings = [];
|
|
76
78
|
for (const pattern of patterns) {
|
|
77
|
-
const
|
|
79
|
+
const flags = [...new Set((pattern.regex.flags + 'g').split(''))].join('');
|
|
80
|
+
const matches = text.matchAll(new RegExp(pattern.regex.source, flags));
|
|
78
81
|
for (const match of matches) {
|
|
79
82
|
findings.push({
|
|
80
83
|
type: pattern.name,
|
|
@@ -89,7 +92,7 @@ function scanPatterns(text, patterns) {
|
|
|
89
92
|
|
|
90
93
|
/**
|
|
91
94
|
* SR1 检查:分析会话日志中的沙箱逃逸行为
|
|
92
|
-
* @param {string} sessionFile -
|
|
95
|
+
* @param {string} sessionFile - 会话日志文件路径(.jsonl 或 .jsonl.zstd)
|
|
93
96
|
* @returns {Promise<import('../protocol/check.mjs').SecurityCheckResult>}
|
|
94
97
|
*/
|
|
95
98
|
export async function run(sessionFile) {
|
|
@@ -99,35 +102,26 @@ export async function run(sessionFile) {
|
|
|
99
102
|
return pass(id, Severity.CRITICAL, '无会话日志,跳过运行时安全检查');
|
|
100
103
|
}
|
|
101
104
|
|
|
102
|
-
if (sessionFile.endsWith('.zstd')) {
|
|
103
|
-
return pass(id, Severity.CRITICAL, 'zstd 压缩的会话文件需先解压再检查');
|
|
104
|
-
}
|
|
105
|
-
|
|
106
105
|
const findings = [];
|
|
107
106
|
let lineCount = 0;
|
|
108
107
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
});
|
|
108
|
+
try {
|
|
109
|
+
lineCount = await scanSessionLines(sessionFile, (line, lineNo) => {
|
|
110
|
+
if (!line.trim()) return;
|
|
111
|
+
const calls = extractToolCalls(line);
|
|
112
|
+
for (const call of calls) {
|
|
113
|
+
const allPatterns = [...ESCAPE_PATTERNS, ...SYSTEM_RESOURCE_PATTERNS, ...WORKSPACE_ESCAPE_PATTERNS];
|
|
114
|
+
const textFindings = scanPatterns(call.text, allPatterns);
|
|
115
|
+
for (const f of textFindings) {
|
|
116
|
+
findings.push({ ...f, line: lineNo, tool: call.name, seq: call.seq, turn: call.turn });
|
|
117
|
+
}
|
|
129
118
|
}
|
|
119
|
+
});
|
|
120
|
+
} catch (e) {
|
|
121
|
+
if (e.code === 'ZSTD_UNAVAILABLE') {
|
|
122
|
+
return skip(id, Severity.CRITICAL, 'zstd 命令不可用,无法解压压缩会话日志,跳过沙箱逃逸检测');
|
|
130
123
|
}
|
|
124
|
+
throw e;
|
|
131
125
|
}
|
|
132
126
|
|
|
133
127
|
if (findings.length === 0) {
|
|
@@ -171,7 +165,7 @@ export const sr1Check = {
|
|
|
171
165
|
name: 'sandbox-violation',
|
|
172
166
|
severity: Severity.CRITICAL,
|
|
173
167
|
phase: CheckPhase.RUNTIME,
|
|
174
|
-
description: '
|
|
168
|
+
description: '沙箱逃逸行为检测(会话日志分析,支持 zstd)',
|
|
175
169
|
src: 'builtin',
|
|
176
170
|
runner: (sessionFile) => run(sessionFile),
|
|
177
171
|
};
|
|
@@ -1,23 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SR2: Privilege Escalation — 权限提升检测
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 分析会话日志中的工具调用(tool/call)中的权限提升行为:
|
|
5
5
|
* - sudo/doas 使用
|
|
6
6
|
* - 文件权限变更(chmod/chown)
|
|
7
|
-
* -
|
|
7
|
+
* - setuid/setgid、Linux capability 操作
|
|
8
|
+
*
|
|
9
|
+
* 支持明文与 zstd 压缩会话日志(session-reader)。
|
|
8
10
|
*
|
|
9
11
|
* Severity: HIGH
|
|
10
12
|
* Phase: RUNTIME
|
|
11
13
|
*/
|
|
12
14
|
|
|
13
|
-
import { existsSync
|
|
14
|
-
import { createInterface } from 'node:readline';
|
|
15
|
+
import { existsSync } from 'node:fs';
|
|
15
16
|
import { Severity } from '../protocol/severity.mjs';
|
|
16
17
|
import { CheckPhase } from '../protocol/phase.mjs';
|
|
17
|
-
import { pass, fail } from '../protocol/check.mjs';
|
|
18
|
+
import { pass, fail, skip } from '../protocol/check.mjs';
|
|
19
|
+
import { scanSessionLines } from '../session-reader.mjs';
|
|
18
20
|
|
|
19
21
|
const PRIVILEGE_PATTERNS = [
|
|
20
22
|
{ name: 'sudo usage', regex: /sudo\s+/gi, severity: 'high' },
|
|
23
|
+
{ name: 'doas usage', regex: /\bdoas\s+/gi, severity: 'high' },
|
|
21
24
|
{ name: 'chmod', regex: /chmod\s+[^/]*\s+[0-7]*7[0-7][0-7]/gi, severity: 'medium' },
|
|
22
25
|
{ name: 'chown', regex: /chown\s+/gi, severity: 'medium' },
|
|
23
26
|
{ name: 'setuid', regex: /setuid|setgid/gi, severity: 'high' },
|
|
@@ -36,22 +39,26 @@ function extractToolCalls(line) {
|
|
|
36
39
|
export async function run(sessionFile) {
|
|
37
40
|
const id = 'SR2';
|
|
38
41
|
if (!sessionFile || !existsSync(sessionFile)) return pass(id, Severity.HIGH, '无会话文件,跳过权限提升检测');
|
|
39
|
-
if (sessionFile.endsWith('.zstd')) return pass(id, Severity.HIGH, 'zstd 文件需先解压');
|
|
40
42
|
|
|
41
43
|
const findings = [];
|
|
42
44
|
let lineCount = 0;
|
|
43
|
-
const rl = createInterface({ input: createReadStream(sessionFile, { encoding: 'utf8' }), crlfDelay: Infinity });
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
lineCount
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
46
|
+
try {
|
|
47
|
+
lineCount = await scanSessionLines(sessionFile, (line, lineNo) => {
|
|
48
|
+
if (!line.trim()) return;
|
|
49
|
+
for (const call of extractToolCalls(line)) {
|
|
50
|
+
for (const pattern of PRIVILEGE_PATTERNS) {
|
|
51
|
+
for (const match of call.text.matchAll(new RegExp(pattern.regex.source, 'gi'))) {
|
|
52
|
+
findings.push({ type: pattern.name, severity: pattern.severity, line: lineNo, tool: call.name, snippet: match[0].slice(0, 40) });
|
|
53
|
+
}
|
|
52
54
|
}
|
|
53
55
|
}
|
|
56
|
+
});
|
|
57
|
+
} catch (e) {
|
|
58
|
+
if (e.code === 'ZSTD_UNAVAILABLE') {
|
|
59
|
+
return skip(id, Severity.HIGH, 'zstd 命令不可用,无法解压压缩会话日志,跳过权限提升检测');
|
|
54
60
|
}
|
|
61
|
+
throw e;
|
|
55
62
|
}
|
|
56
63
|
|
|
57
64
|
if (findings.length === 0) return pass(id, Severity.HIGH, `扫描 ${lineCount} 行,未检测到权限提升行为`);
|
|
@@ -64,4 +71,4 @@ export async function run(sessionFile) {
|
|
|
64
71
|
);
|
|
65
72
|
}
|
|
66
73
|
|
|
67
|
-
export const sr2Check = { id: 'SR2', name: 'privilege-escalation', severity: Severity.HIGH, phase: CheckPhase.RUNTIME, description: '
|
|
74
|
+
export const sr2Check = { id: 'SR2', name: 'privilege-escalation', severity: Severity.HIGH, phase: CheckPhase.RUNTIME, description: '权限提升行为检测(支持 zstd)', src: 'builtin', runner: (f) => run(f) };
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SR3: Data Exfiltration — 数据外泄检测
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
7
|
-
*
|
|
4
|
+
* 分析会话日志中的工具调用与结果,检测潜在的数据外泄行为:
|
|
5
|
+
* - 凭据转发(API key / token / Bearer 出现在命令或返回内容中)
|
|
6
|
+
* - 网络外发模式(curl POST、fetch 等)
|
|
7
|
+
*
|
|
8
|
+
* 支持明文与 zstd 压缩会话日志(session-reader)。
|
|
8
9
|
*
|
|
9
10
|
* Severity: HIGH
|
|
10
11
|
* Phase: RUNTIME
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
|
-
import { existsSync
|
|
14
|
-
import { createInterface } from 'node:readline';
|
|
14
|
+
import { existsSync } from 'node:fs';
|
|
15
15
|
import { Severity } from '../protocol/severity.mjs';
|
|
16
16
|
import { CheckPhase } from '../protocol/phase.mjs';
|
|
17
|
-
import { pass, fail } from '../protocol/check.mjs';
|
|
17
|
+
import { pass, fail, skip } from '../protocol/check.mjs';
|
|
18
|
+
import { scanSessionLines } from '../session-reader.mjs';
|
|
18
19
|
|
|
19
20
|
/** 凭据转发模式 */
|
|
20
21
|
const CREDENTIAL_FORWARD_PATTERNS = [
|
|
@@ -31,36 +32,25 @@ const NETWORK_EXFIL_PATTERNS = [
|
|
|
31
32
|
{ name: 'HTTP request with data', regex: /http\.request|https\.request|axios\.post/gi, severity: 'low' },
|
|
32
33
|
];
|
|
33
34
|
|
|
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
35
|
/**
|
|
41
|
-
* 从一行 JSONL
|
|
36
|
+
* 从一行 JSONL 中提取文本信息(call 与 result 都扫:凭据出现在返回内容中同样是泄露)
|
|
42
37
|
*/
|
|
43
38
|
function extractToolCalls(line) {
|
|
44
39
|
try {
|
|
45
40
|
const event = JSON.parse(line);
|
|
46
41
|
if (event.type !== 'tool/call' && event.type !== 'tool/result') return [];
|
|
47
42
|
|
|
48
|
-
const calls = [];
|
|
49
43
|
const data = event.data || {};
|
|
50
44
|
const name = data.name || data.tool || '';
|
|
51
|
-
const args = data.args || data.input || {};
|
|
45
|
+
const args = data.args || data.input || data.output || data.result || data.text || {};
|
|
52
46
|
const text = typeof args === 'string' ? args : JSON.stringify(args);
|
|
53
47
|
|
|
54
|
-
|
|
55
|
-
return calls;
|
|
48
|
+
return [{ type: event.type, name, text, seq: event.seq, turn: event.turn }];
|
|
56
49
|
} catch {
|
|
57
50
|
return [];
|
|
58
51
|
}
|
|
59
52
|
}
|
|
60
53
|
|
|
61
|
-
/**
|
|
62
|
-
* 扫描文本中的危险模式
|
|
63
|
-
*/
|
|
64
54
|
function scanPatterns(text, patterns) {
|
|
65
55
|
const findings = [];
|
|
66
56
|
for (const pattern of patterns) {
|
|
@@ -76,11 +66,6 @@ function scanPatterns(text, patterns) {
|
|
|
76
66
|
return findings;
|
|
77
67
|
}
|
|
78
68
|
|
|
79
|
-
/**
|
|
80
|
-
* SR3 检查:分析会话日志中的数据外泄行为
|
|
81
|
-
* @param {string} sessionFile - 会话日志文件路径
|
|
82
|
-
* @returns {Promise<import('../protocol/check.mjs').SecurityCheckResult>}
|
|
83
|
-
*/
|
|
84
69
|
export async function run(sessionFile) {
|
|
85
70
|
const id = 'SR3';
|
|
86
71
|
|
|
@@ -88,34 +73,26 @@ export async function run(sessionFile) {
|
|
|
88
73
|
return pass(id, Severity.HIGH, '无会话日志,跳过数据外泄检查');
|
|
89
74
|
}
|
|
90
75
|
|
|
91
|
-
if (sessionFile.endsWith('.zstd')) {
|
|
92
|
-
return pass(id, Severity.HIGH, 'zstd 压缩的会话文件需先解压再检查');
|
|
93
|
-
}
|
|
94
|
-
|
|
95
76
|
const findings = [];
|
|
96
77
|
let lineCount = 0;
|
|
97
78
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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' });
|
|
79
|
+
try {
|
|
80
|
+
lineCount = await scanSessionLines(sessionFile, (line, lineNo) => {
|
|
81
|
+
if (!line.trim()) return;
|
|
82
|
+
for (const call of extractToolCalls(line)) {
|
|
83
|
+
for (const f of scanPatterns(call.text, CREDENTIAL_FORWARD_PATTERNS)) {
|
|
84
|
+
findings.push({ ...f, line: lineNo, tool: call.name, category: 'credential-forward' });
|
|
85
|
+
}
|
|
86
|
+
for (const f of scanPatterns(call.text, NETWORK_EXFIL_PATTERNS)) {
|
|
87
|
+
findings.push({ ...f, line: lineNo, tool: call.name, category: 'network-exfil' });
|
|
88
|
+
}
|
|
117
89
|
}
|
|
90
|
+
});
|
|
91
|
+
} catch (e) {
|
|
92
|
+
if (e.code === 'ZSTD_UNAVAILABLE') {
|
|
93
|
+
return skip(id, Severity.HIGH, 'zstd 命令不可用,无法解压压缩会话日志,跳过数据外泄检查');
|
|
118
94
|
}
|
|
95
|
+
throw e;
|
|
119
96
|
}
|
|
120
97
|
|
|
121
98
|
if (findings.length === 0) {
|
|
@@ -157,7 +134,7 @@ export const sr3Check = {
|
|
|
157
134
|
name: 'data-exfiltration',
|
|
158
135
|
severity: Severity.HIGH,
|
|
159
136
|
phase: CheckPhase.RUNTIME,
|
|
160
|
-
description: '
|
|
137
|
+
description: '数据外泄行为检测(会话日志分析,支持 zstd)',
|
|
161
138
|
src: 'builtin',
|
|
162
139
|
runner: (sessionFile) => run(sessionFile),
|
|
163
140
|
};
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
* Phase: RUNTIME
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { existsSync
|
|
13
|
-
import { createInterface } from 'node:readline';
|
|
12
|
+
import { existsSync } from 'node:fs';
|
|
14
13
|
import { Severity } from '../protocol/severity.mjs';
|
|
15
14
|
import { CheckPhase } from '../protocol/phase.mjs';
|
|
16
|
-
import { pass, fail } from '../protocol/check.mjs';
|
|
15
|
+
import { pass, fail, skip } from '../protocol/check.mjs';
|
|
16
|
+
import { scanSessionLines } from '../session-reader.mjs';
|
|
17
17
|
|
|
18
18
|
function extractToolCalls(line) {
|
|
19
19
|
try {
|
|
@@ -27,19 +27,24 @@ function extractToolCalls(line) {
|
|
|
27
27
|
export async function run(sessionFile) {
|
|
28
28
|
const id = 'SR4';
|
|
29
29
|
if (!sessionFile || !existsSync(sessionFile)) return pass(id, Severity.MEDIUM, '无会话文件,跳过隔离验证');
|
|
30
|
-
if (sessionFile.endsWith('.zstd')) return pass(id, Severity.MEDIUM, 'zstd 文件需先解压');
|
|
31
30
|
|
|
32
31
|
const toolCalls = [];
|
|
33
32
|
let lineCount = 0;
|
|
34
|
-
const rl = createInterface({ input: createReadStream(sessionFile, { encoding: 'utf8' }), crlfDelay: Infinity });
|
|
35
33
|
|
|
36
|
-
|
|
37
|
-
lineCount
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
try {
|
|
35
|
+
lineCount = await scanSessionLines(sessionFile, (line) => {
|
|
36
|
+
if (!line.trim()) return;
|
|
37
|
+
toolCalls.push(...extractToolCalls(line));
|
|
38
|
+
});
|
|
39
|
+
} catch (e) {
|
|
40
|
+
if (e.code === 'ZSTD_UNAVAILABLE') {
|
|
41
|
+
return skip(id, Severity.MEDIUM, 'zstd 命令不可用,无法解压压缩会话日志,跳过隔离验证');
|
|
42
|
+
}
|
|
43
|
+
throw e;
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
// 简单的隔离检查:检测同一 turn 内不同工具的数据传递
|
|
47
|
+
// 复审修复:正常工作流一个 turn 用 4-5 个工具很常见,阈值 3→6 降低误报
|
|
43
48
|
const findings = [];
|
|
44
49
|
const byTurn = {};
|
|
45
50
|
for (const call of toolCalls) {
|
|
@@ -49,13 +54,13 @@ export async function run(sessionFile) {
|
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
for (const [turn, calls] of Object.entries(byTurn)) {
|
|
52
|
-
const tools = new Set(calls.map(c => c.name));
|
|
53
|
-
if (tools.size >
|
|
54
|
-
findings.push({ type: 'multi-tool-turn', severity: 'low', detail: `turn ${turn} 使用了 ${tools.size} 个不同工具:${
|
|
57
|
+
const tools = [...new Set(calls.map(c => c.name).filter(Boolean))];
|
|
58
|
+
if (tools.size > 6) {
|
|
59
|
+
findings.push({ type: 'multi-tool-turn', severity: 'low', detail: `turn ${turn} 使用了 ${tools.size} 个不同工具:${tools.slice(0, 8).join(', ')}` });
|
|
55
60
|
}
|
|
56
61
|
}
|
|
57
62
|
|
|
58
|
-
if (findings.length === 0) return pass(id, Severity.MEDIUM, `分析 ${toolCalls.length}
|
|
63
|
+
if (findings.length === 0) return pass(id, Severity.MEDIUM, `分析 ${toolCalls.length} 个工具调用(${lineCount} 行),未检测到隔离问题`);
|
|
59
64
|
|
|
60
65
|
const details = findings.slice(0, 10).map(f => `[${f.severity}] ${f.detail}`).join('\n');
|
|
61
66
|
return fail(id, Severity.MEDIUM, `检测到 ${findings.length} 个潜在隔离问题:\n${details}`, '检查工具间的数据流是否在预期范围内');
|