@moonquake2004/dsh-security 0.1.3 → 0.1.5
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 +5 -3
- package/package.json +1 -1
- package/src/checks/index.mjs +2 -0
- package/src/checks/sp9-dual-instance-guard.mjs +105 -0
- package/src/checks/ss1-credential-leak.mjs +1 -1
- package/src/checks/ss2-pii-exposure.mjs +1 -1
- package/src/checks/ss4-session-integrity.mjs +117 -0
- package/src/index.mjs +3 -1
- package/src/registry.mjs +2 -0
package/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
[](LICENSE)
|
|
4
4
|
[](https://www.npmjs.com/package/@moonquake2004/dsh-security)
|
|
5
|
-
[](#)
|
|
6
6
|
|
|
7
|
-
**DSH 生态统一安全检查框架** — 覆盖插件全生命周期(发现→安装→运行→更新→退役),
|
|
7
|
+
**DSH 生态统一安全检查框架** — 覆盖插件全生命周期(发现→安装→运行→更新→退役),21 个内置检查 + 4 个外部工具集成。
|
|
8
8
|
|
|
9
9
|
> Community security tool. Not an official DeepSeek project.
|
|
10
10
|
|
|
@@ -76,9 +76,11 @@ dsh-security 的目标:**建立 DSH 生态的统一安全检查层**,让任
|
|
|
76
76
|
│ SP6: 已知漏洞匹配 (OSV/CVE/GHSA) │
|
|
77
77
|
│ SP7: client 产物语法预检 (#2752 白屏源 boot 前拦截) │
|
|
78
78
|
│ SP8: dist-tag 异常检测 (#2763 broken latest) │
|
|
79
|
+
│ SP9: CLI 核心包泄漏检测 (#4640 symbol 分裂) │
|
|
79
80
|
│ SS1: 凭据泄露检测 (会话日志) │
|
|
80
81
|
│ SS2: PII 数据暴露检测 │
|
|
81
82
|
│ SS3: 插件输出敏感数据检测 │
|
|
83
|
+
│ SS4: 会话日志完整性校验 (截断JSON/孤儿tool-call) │
|
|
82
84
|
├──────────────────────────────────────────────────────────┤
|
|
83
85
|
│ Layer 2: Runtime Checks(运行时,基于会话日志) │
|
|
84
86
|
│ SR1: 沙箱逃逸检测 (#1769 mount remount 等) │
|
|
@@ -251,7 +253,7 @@ node --test test/*.mjs
|
|
|
251
253
|
dsh-security/
|
|
252
254
|
├── src/
|
|
253
255
|
│ ├── protocol/ # Layer 0: Check Protocol
|
|
254
|
-
│ ├── checks/ #
|
|
256
|
+
│ ├── checks/ # 21 个内置检查
|
|
255
257
|
│ ├── integrations/ # 4 个外部工具集成
|
|
256
258
|
│ ├── session-reader.mjs # 会话日志读取(明文 + zstd)
|
|
257
259
|
│ ├── registry.mjs # Check Registry(支持 setConfig 注入配置)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moonquake2004/dsh-security",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Unified security check framework for DeepSeek Harness ecosystem — covers the full plugin lifecycle (discovery → install → runtime → update → retirement)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.mjs",
|
package/src/checks/index.mjs
CHANGED
|
@@ -7,6 +7,7 @@ export { sp5Check } from './sp5-permission-model.mjs';
|
|
|
7
7
|
export { sp6Check } from './sp6-vuln-match.mjs';
|
|
8
8
|
export { sp7Check } from './sp7-client-syntax.mjs';
|
|
9
9
|
export { sp8Check } from './sp8-dist-tag-health.mjs';
|
|
10
|
+
export { sp9Check } from './sp9-dual-instance-guard.mjs';
|
|
10
11
|
|
|
11
12
|
// Layer 2: Runtime Checks
|
|
12
13
|
export { sr1Check } from './sr1-sandbox-violation.mjs';
|
|
@@ -24,3 +25,4 @@ export { sl4Check } from './sl4-release-compat.mjs';
|
|
|
24
25
|
export { ss1Check } from './ss1-credential-leak.mjs';
|
|
25
26
|
export { ss2Check } from './ss2-pii-exposure.mjs';
|
|
26
27
|
export { ss3Check } from './ss3-sensitive-output.mjs';
|
|
28
|
+
export { ss4Check } from './ss4-session-integrity.mjs';
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SP9: Dual-Instance Guard — CLI 核心包泄漏检测
|
|
3
|
+
*
|
|
4
|
+
* 出处:#4640 + zoahdev/dsh-ecosystem 家族 4——profile 的 node_modules
|
|
5
|
+
* 中若出现 @deepseek-ai/dsh-* 包(尤其是 dsh-tools),会导致同一进程
|
|
6
|
+
* 两份副本 → TOOL_RUNTIME_SCHEDULER 唯一 symbol 分裂 →
|
|
7
|
+
* undefined.prepare → 所有工具调用失败 → 会话不可恢复。
|
|
8
|
+
*
|
|
9
|
+
* 核心规则:profile 只承载 surfaces 和 plugins;core packages 由 CLI
|
|
10
|
+
* 自身的依赖树提供。profile 中出现任何 @deepseek-ai/dsh-* 包即为异常。
|
|
11
|
+
*
|
|
12
|
+
* 做法:扫描 profile/node_modules/@deepseek-ai/ 下的目录,
|
|
13
|
+
* 检查是否存在 dsh-* 包(排除纯客户端包如 dsh-client-*,
|
|
14
|
+
* 因为它们可能被插件合法引用;只报 dsh-tools/dsh-agent-loop
|
|
15
|
+
* 等核心运行时包)。
|
|
16
|
+
*
|
|
17
|
+
* Severity: CRITICAL(直接导致工具调用全灭)
|
|
18
|
+
* Phase: POST_INSTALL
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
22
|
+
import { join } from 'node:path';
|
|
23
|
+
import { Severity } from '../protocol/severity.mjs';
|
|
24
|
+
import { CheckPhase } from '../protocol/phase.mjs';
|
|
25
|
+
import { pass, fail, skip } from '../protocol/check.mjs';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* CLI 核心运行时包——出现在 profile 中即为异常。
|
|
29
|
+
* 客户端包(dsh-client-*)被插件合法引用,不在此列。
|
|
30
|
+
*/
|
|
31
|
+
const CORE_RUNTIME_PKGS = new Set([
|
|
32
|
+
'dsh-tools',
|
|
33
|
+
'dsh-agent-loop',
|
|
34
|
+
'dsh-sandbox-local',
|
|
35
|
+
'dsh-subprocess-local',
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
export async function run(profileDir) {
|
|
39
|
+
const id = 'SP9';
|
|
40
|
+
const nmDir = join(profileDir, 'node_modules');
|
|
41
|
+
if (!existsSync(nmDir)) return pass(id, Severity.CRITICAL, '无 node_modules,跳过核心包泄漏检测');
|
|
42
|
+
|
|
43
|
+
const dsDir = join(nmDir, '@deepseek-ai');
|
|
44
|
+
if (!existsSync(dsDir)) return pass(id, Severity.CRITICAL, 'profile 中无 @deepseek-ai 包,无泄漏风险');
|
|
45
|
+
|
|
46
|
+
// 枚举 @deepseek-ai/* 下的包
|
|
47
|
+
let entries;
|
|
48
|
+
try { entries = readdirSync(dsDir, { withFileTypes: true }); } catch { entries = []; }
|
|
49
|
+
|
|
50
|
+
const leaked = [];
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
if (!entry.isDirectory()) continue;
|
|
53
|
+
const pkgName = `@deepseek-ai/${entry.name}`;
|
|
54
|
+
const pjPath = join(dsDir, entry.name, 'package.json');
|
|
55
|
+
if (!existsSync(pjPath)) continue;
|
|
56
|
+
|
|
57
|
+
let version = '?';
|
|
58
|
+
try {
|
|
59
|
+
const pkg = JSON.parse(readFileSync(pjPath, 'utf8'));
|
|
60
|
+
version = pkg.version || '?';
|
|
61
|
+
} catch { /* read error */ }
|
|
62
|
+
|
|
63
|
+
if (entry.name.startsWith('dsh-')) {
|
|
64
|
+
leaked.push({ name: pkgName, version, isCore: CORE_RUNTIME_PKGS.has(entry.name) });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (leaked.length === 0) {
|
|
69
|
+
return pass(id, Severity.CRITICAL, 'profile 中无 @deepseek-ai/dsh-* 泄漏,核心包仅由 CLI 提供');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const coreLeaks = leaked.filter(l => l.isCore);
|
|
73
|
+
const otherLeaks = leaked.filter(l => !l.isCore);
|
|
74
|
+
|
|
75
|
+
// 核心运行时包泄漏 = CRITICAL(直接导致 Symbol 分裂 + 工具调用全灭)
|
|
76
|
+
if (coreLeaks.length > 0) {
|
|
77
|
+
const details = coreLeaks.map(l => ` ${l.name}@${l.version}`).join('\n');
|
|
78
|
+
const otherNote = otherLeaks.length > 0
|
|
79
|
+
? `\n另有 ${otherLeaks.length} 个非核心 @deepseek-ai/dsh-* 包在 profile 中:${otherLeaks.map(l => l.name).join(', ')}`
|
|
80
|
+
: '';
|
|
81
|
+
return fail(id, Severity.CRITICAL,
|
|
82
|
+
`检测到 ${coreLeaks.length} 个 CLI 核心运行时包泄漏到 profile(#4640:双实例 symbol 分裂 → 工具调用全灭):\n${details}${otherNote}\n修复: dsh plugin --profile <name> remove <泄漏包名>(或检查安装来源是否误将 CLI 依赖写入 profile)`,
|
|
83
|
+
'核心包(dsh-tools/dsh-agent-loop 等)不应出现在 profile 的 node_modules 中;它们由 CLI 自身依赖树提供',
|
|
84
|
+
['#4640']
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 只有非核心包泄漏 = HIGH(潜在风险但不一定立即崩溃)
|
|
89
|
+
const details = otherLeaks.map(l => ` ${l.name}@${l.version}`).join('\n');
|
|
90
|
+
return fail(id, Severity.HIGH,
|
|
91
|
+
`检测到 ${otherLeaks.length} 个 @deepseek-ai/dsh-* 包泄漏到 profile(非核心包,暂未触发 symbol 分裂,但增加未来冲突风险):\n${details}`,
|
|
92
|
+
'清理 profile 中不需要的 @deepseek-ai/dsh-* 包;若为客户端包且被插件依赖,确认无版本冲突',
|
|
93
|
+
['#4640']
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export const sp9Check = {
|
|
98
|
+
id: 'SP9',
|
|
99
|
+
name: 'dual-instance-guard',
|
|
100
|
+
severity: Severity.CRITICAL,
|
|
101
|
+
phase: CheckPhase.POST_INSTALL,
|
|
102
|
+
description: 'CLI 核心包泄漏检测——profile 中不应出现 @deepseek-ai/dsh-tools 等核心包(#4640 symbol 分裂)',
|
|
103
|
+
src: 'builtin',
|
|
104
|
+
runner: (profileDir) => run(profileDir),
|
|
105
|
+
};
|
|
@@ -124,7 +124,7 @@ export async function run(sessionFile) {
|
|
|
124
124
|
.map(f => `行 ${f.line} — ${f.type}(${f.snippet})`)
|
|
125
125
|
.join('\n');
|
|
126
126
|
|
|
127
|
-
const fix = '
|
|
127
|
+
const fix = '轮换泄露的密钥(不可逆),然后清理会话日志:可用 zoahdev/dsh-redact(GitHub: github.com/zoahdev/dsh-redact)或手动删除含密钥的行';
|
|
128
128
|
|
|
129
129
|
return fail(id, Severity.CRITICAL, `检测到 ${findings.length} 个凭据泄露(${summary}):\n${details}`, fix, ['#962']);
|
|
130
130
|
}
|
|
@@ -74,7 +74,7 @@ export async function run(sessionFile) {
|
|
|
74
74
|
for (const f of findings) { if (!byType[f.type]) byType[f.type] = 0; byType[f.type]++; }
|
|
75
75
|
const summary = Object.entries(byType).map(([t, c]) => `${t}: ${c}`).join(', ');
|
|
76
76
|
const details = findings.slice(0, 10).map(f => `行${f.line} — ${f.type}: ${f.snippet}`).join('\n');
|
|
77
|
-
return fail(id, Severity.MEDIUM, `检测到 ${findings.length} 个 PII 暴露(${summary}):\n${details}`, '
|
|
77
|
+
return fail(id, Severity.MEDIUM, `检测到 ${findings.length} 个 PII 暴露(${summary}):\n${details}`, '分享会话日志前用 zoahdev/dsh-redact(github.com/zoahdev/dsh-redact)脱敏,或手动删除含 PII 的行');
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
export const ss2Check = { id: 'SS2', name: 'pii-exposure', severity: Severity.MEDIUM, phase: CheckPhase.POST_INSTALL, description: 'PII 数据暴露检测', src: 'builtin', runner: (f) => run(f) };
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SS4: Session Integrity — 会话日志完整性校验
|
|
3
|
+
*
|
|
4
|
+
* 出处:zoahdev/dsh-ecosystem 家族 3(坏工件隔离)+ 家族 11(out-of-tree
|
|
5
|
+
* 会话事件信封缺口)——截断的 tool-call 参数、torn session tail、空文件
|
|
6
|
+
* 会导致会话永久砖化或 resume 失败。
|
|
7
|
+
*
|
|
8
|
+
* 检查项:
|
|
9
|
+
* 1. JSON 语法:逐行 parse,无效行 = 截断/损坏
|
|
10
|
+
* 2. tool/call 配对:每个 tool/call 应有对应 tool/result(孤儿调用)
|
|
11
|
+
* 3. 空/极小文件:0 行或 <10 行 = 可能未正常写入
|
|
12
|
+
*
|
|
13
|
+
* Severity: HIGH
|
|
14
|
+
* Phase: SESSION
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { existsSync, statSync } from 'node:fs';
|
|
18
|
+
import { Severity } from '../protocol/severity.mjs';
|
|
19
|
+
import { CheckPhase } from '../protocol/phase.mjs';
|
|
20
|
+
import { pass, fail, skip } from '../protocol/check.mjs';
|
|
21
|
+
import { scanSessionLines } from '../session-reader.mjs';
|
|
22
|
+
|
|
23
|
+
const MIN_LINES = 10;
|
|
24
|
+
|
|
25
|
+
export async function run(sessionFile) {
|
|
26
|
+
const id = 'SS4';
|
|
27
|
+
|
|
28
|
+
if (!sessionFile || !existsSync(sessionFile)) {
|
|
29
|
+
return skip(id, Severity.HIGH, '无会话日志文件,跳过完整性校验');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 检查文件大小
|
|
33
|
+
let fileSize = 0;
|
|
34
|
+
try { fileSize = statSync(sessionFile).size; } catch { /* ignore */ }
|
|
35
|
+
if (fileSize === 0) {
|
|
36
|
+
return fail(id, Severity.HIGH, '会话日志文件为空(0 字节)——可能未正常写入', '检查 dsh 进程是否正常退出;若为预期空会话可忽略', ['#675']);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let totalLines = 0;
|
|
40
|
+
let invalidJson = 0;
|
|
41
|
+
let firstInvalidLine = null;
|
|
42
|
+
const orphanCalls = new Map(); // callId → line number
|
|
43
|
+
let resultCount = 0;
|
|
44
|
+
|
|
45
|
+
await scanSessionLines(sessionFile, (line, lineNum) => {
|
|
46
|
+
totalLines++;
|
|
47
|
+
|
|
48
|
+
// 1. JSON 语法检查
|
|
49
|
+
let parsed;
|
|
50
|
+
try {
|
|
51
|
+
parsed = JSON.parse(line);
|
|
52
|
+
} catch {
|
|
53
|
+
invalidJson++;
|
|
54
|
+
if (!firstInvalidLine) firstInvalidLine = lineNum;
|
|
55
|
+
return; // 无法 parse 的行跳过后续检查
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 2. tool/call 与 tool/result 配对检查
|
|
59
|
+
const type = parsed.type;
|
|
60
|
+
if (type === 'tool/call') {
|
|
61
|
+
const callId = parsed.data?.callId;
|
|
62
|
+
if (callId) orphanCalls.set(callId, lineNum);
|
|
63
|
+
} else if (type === 'tool/result') {
|
|
64
|
+
const callId = parsed.data?.callId;
|
|
65
|
+
if (callId) {
|
|
66
|
+
orphanCalls.delete(callId); // 配对成功
|
|
67
|
+
resultCount++;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// 3. 极小文件检查
|
|
73
|
+
if (totalLines < MIN_LINES && totalLines > 0) {
|
|
74
|
+
return fail(id, Severity.HIGH,
|
|
75
|
+
`会话日志仅 ${totalLines} 行(阈值 ${MIN_LINES})——可能未完整记录`,
|
|
76
|
+
'检查会话是否正常结束;若为极短会话可忽略',
|
|
77
|
+
['#675']
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const issues = [];
|
|
82
|
+
const refs = [];
|
|
83
|
+
|
|
84
|
+
// 汇总孤儿 tool/call
|
|
85
|
+
if (orphanCalls.size > 0) {
|
|
86
|
+
const samples = [...orphanCalls.entries()].slice(0, 5).map(([id, ln]) => ` 行${ln}: callId=${id.slice(0, 16)}…`);
|
|
87
|
+
issues.push(`${orphanCalls.size} 个孤儿 tool/call(有调用无结果):\n${samples.join('\n')}`);
|
|
88
|
+
refs.push('#3234');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 汇总无效 JSON
|
|
92
|
+
if (invalidJson > 0) {
|
|
93
|
+
issues.push(`${invalidJson} 行无效 JSON(首个在行 ${firstInvalidLine})——可能截断或损坏`);
|
|
94
|
+
refs.push('#675', '#1047');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (issues.length === 0) {
|
|
98
|
+
return pass(id, Severity.HIGH,
|
|
99
|
+
`会话日志完整性通过:${totalLines} 行,JSON 全有效,${resultCount} 个 tool/result 均已配对`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return fail(id, Severity.HIGH,
|
|
103
|
+
`检测到会话日志完整性问题(${totalLines} 行):\n${issues.join('\n\n')}`,
|
|
104
|
+
'截断的 tool/call 会导致会话砖化(#3234);无效 JSON 行可能是损坏的事件(#675/#1047);考虑用 dsh-shelf 归档后重新开始会话',
|
|
105
|
+
refs
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export const ss4Check = {
|
|
110
|
+
id: 'SS4',
|
|
111
|
+
name: 'session-integrity',
|
|
112
|
+
severity: Severity.HIGH,
|
|
113
|
+
phase: CheckPhase.POST_INSTALL,
|
|
114
|
+
description: '会话日志完整性校验——检测截断 JSON、孤儿 tool/call、空/极小文件(家族 3/11)',
|
|
115
|
+
src: 'builtin',
|
|
116
|
+
runner: (sessionFile) => run(sessionFile),
|
|
117
|
+
};
|
package/src/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* DSH Security Framework — 统一安全检查框架
|
|
3
3
|
*
|
|
4
|
-
* 为 DSH 生态提供全生命周期安全检查(
|
|
4
|
+
* 为 DSH 生态提供全生命周期安全检查(21 个检查项,4 层架构)。
|
|
5
5
|
*
|
|
6
6
|
* @example
|
|
7
7
|
* import { createDefaultRegistry } from '@moonquake2004/dsh-security';
|
|
@@ -24,6 +24,7 @@ export { sp5Check } from './checks/sp5-permission-model.mjs';
|
|
|
24
24
|
export { sp6Check } from './checks/sp6-vuln-match.mjs';
|
|
25
25
|
export { sp7Check } from './checks/sp7-client-syntax.mjs';
|
|
26
26
|
export { sp8Check } from './checks/sp8-dist-tag-health.mjs';
|
|
27
|
+
export { sp9Check } from './checks/sp9-dual-instance-guard.mjs';
|
|
27
28
|
|
|
28
29
|
// Layer 2: Runtime Checks
|
|
29
30
|
export { sr1Check } from './checks/sr1-sandbox-violation.mjs';
|
|
@@ -41,6 +42,7 @@ export { sl4Check } from './checks/sl4-release-compat.mjs';
|
|
|
41
42
|
export { ss1Check } from './checks/ss1-credential-leak.mjs';
|
|
42
43
|
export { ss2Check } from './checks/ss2-pii-exposure.mjs';
|
|
43
44
|
export { ss3Check } from './checks/ss3-sensitive-output.mjs';
|
|
45
|
+
export { ss4Check } from './checks/ss4-session-integrity.mjs';
|
|
44
46
|
|
|
45
47
|
// Registry
|
|
46
48
|
export { SecurityCheckRegistry, createDefaultRegistry } from './registry.mjs';
|
package/src/registry.mjs
CHANGED
|
@@ -147,8 +147,10 @@ export async function createDefaultRegistry(loadExternal = true, options = {}) {
|
|
|
147
147
|
import('./checks/ss1-credential-leak.mjs'),
|
|
148
148
|
import('./checks/ss2-pii-exposure.mjs'),
|
|
149
149
|
import('./checks/ss3-sensitive-output.mjs'),
|
|
150
|
+
import('./checks/ss4-session-integrity.mjs'),
|
|
150
151
|
import('./checks/sp7-client-syntax.mjs'),
|
|
151
152
|
import('./checks/sp8-dist-tag-health.mjs'),
|
|
153
|
+
import('./checks/sp9-dual-instance-guard.mjs'),
|
|
152
154
|
]);
|
|
153
155
|
for (const mod of modules) {
|
|
154
156
|
for (const val of Object.values(mod)) {
|