@moonquake2004/dsh-security 0.1.2 → 0.1.4
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/sp8-dist-tag-health.mjs +138 -0
- package/src/checks/sp9-dual-instance-guard.mjs +105 -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 生态统一安全检查框架** — 覆盖插件全生命周期(发现→安装→运行→更新→退役),20 个内置检查 + 4 个外部工具集成。
|
|
8
8
|
|
|
9
9
|
> Community security tool. Not an official DeepSeek project.
|
|
10
10
|
|
|
@@ -75,6 +75,8 @@ dsh-security 的目标:**建立 DSH 生态的统一安全检查层**,让任
|
|
|
75
75
|
│ SP5: 插件权限声明验证 │
|
|
76
76
|
│ SP6: 已知漏洞匹配 (OSV/CVE/GHSA) │
|
|
77
77
|
│ SP7: client 产物语法预检 (#2752 白屏源 boot 前拦截) │
|
|
78
|
+
│ SP8: dist-tag 异常检测 (#2763 broken latest) │
|
|
79
|
+
│ SP9: CLI 核心包泄漏检测 (#4640 symbol 分裂) │
|
|
78
80
|
│ SS1: 凭据泄露检测 (会话日志) │
|
|
79
81
|
│ SS2: PII 数据暴露检测 │
|
|
80
82
|
│ SS3: 插件输出敏感数据检测 │
|
|
@@ -250,7 +252,7 @@ node --test test/*.mjs
|
|
|
250
252
|
dsh-security/
|
|
251
253
|
├── src/
|
|
252
254
|
│ ├── protocol/ # Layer 0: Check Protocol
|
|
253
|
-
│ ├── checks/ #
|
|
255
|
+
│ ├── checks/ # 20 个内置检查
|
|
254
256
|
│ ├── integrations/ # 4 个外部工具集成
|
|
255
257
|
│ ├── session-reader.mjs # 会话日志读取(明文 + zstd)
|
|
256
258
|
│ ├── 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.4",
|
|
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
|
@@ -6,6 +6,8 @@ export { sp4Check } from './sp4-entry-poison.mjs';
|
|
|
6
6
|
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
|
+
export { sp8Check } from './sp8-dist-tag-health.mjs';
|
|
10
|
+
export { sp9Check } from './sp9-dual-instance-guard.mjs';
|
|
9
11
|
|
|
10
12
|
// Layer 2: Runtime Checks
|
|
11
13
|
export { sr1Check } from './sr1-sandbox-violation.mjs';
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SP8: Dist-tag Health — npm dist-tag 异常检测
|
|
3
|
+
*
|
|
4
|
+
* 出处:zoahdev/dsh-ecosystem supply-chain-health 报告 + #2763——
|
|
5
|
+
* @deepseek-ai/* 子包的 dist-tags.latest 卡在 0.0.1-rc.1(broken),
|
|
6
|
+
* 导致声明 peer dep ^0.1.0-rc.6 的插件无法正确解析到可用版本。
|
|
7
|
+
* 注册表 325 个可安装插件中 79 个受影响。
|
|
8
|
+
*
|
|
9
|
+
* 做法:对每个已装 DSH 插件包(dsh 字段门控)的 peerDependencies
|
|
10
|
+
* 中引用的 @deepseek-ai/dsh-* 包,查询 npm registry dist-tags.latest:
|
|
11
|
+
* 若 latest 为明显的 broken 版本(匹配 0.0.1-rc.*),则标记该插件受影响。
|
|
12
|
+
*
|
|
13
|
+
* Severity: HIGH
|
|
14
|
+
* Phase: POST_INSTALL
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
import { Severity } from '../protocol/severity.mjs';
|
|
20
|
+
import { CheckPhase } from '../protocol/phase.mjs';
|
|
21
|
+
import { pass, fail, skip } from '../protocol/check.mjs';
|
|
22
|
+
|
|
23
|
+
const MAX_PKGS = 60;
|
|
24
|
+
const BROKEN_LATEST_RE = /^0\.0\.1-rc\./;
|
|
25
|
+
|
|
26
|
+
async function fetchDistTags(pkgName) {
|
|
27
|
+
try {
|
|
28
|
+
const url = `https://registry.npmjs.org/${encodeURIComponent(pkgName)}`;
|
|
29
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(10000) });
|
|
30
|
+
if (!res.ok) return null;
|
|
31
|
+
const data = await res.json();
|
|
32
|
+
return data['dist-tags'] || null;
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function run(profileDir) {
|
|
39
|
+
const id = 'SP8';
|
|
40
|
+
const nmDir = join(profileDir, 'node_modules');
|
|
41
|
+
if (!existsSync(nmDir)) return pass(id, Severity.HIGH, '无 node_modules,跳过 dist-tag 健康检查');
|
|
42
|
+
|
|
43
|
+
// 枚举 DSH 插件包
|
|
44
|
+
let dirEntries;
|
|
45
|
+
try { dirEntries = readdirSync(nmDir, { withFileTypes: true }); } catch { dirEntries = []; }
|
|
46
|
+
|
|
47
|
+
const plugins = [];
|
|
48
|
+
for (const entry of dirEntries) {
|
|
49
|
+
if (plugins.length >= MAX_PKGS) break;
|
|
50
|
+
if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === '.bin') continue;
|
|
51
|
+
let pkgDirs = [];
|
|
52
|
+
if (entry.name.startsWith('@')) {
|
|
53
|
+
try {
|
|
54
|
+
pkgDirs = readdirSync(join(nmDir, entry.name), { withFileTypes: true })
|
|
55
|
+
.filter(s => s.isDirectory())
|
|
56
|
+
.map(s => join(nmDir, entry.name, s.name));
|
|
57
|
+
} catch { /* skip unreadable scope */ }
|
|
58
|
+
} else {
|
|
59
|
+
pkgDirs = [join(nmDir, entry.name)];
|
|
60
|
+
}
|
|
61
|
+
for (const pd of pkgDirs) {
|
|
62
|
+
const pjPath = join(pd, 'package.json');
|
|
63
|
+
if (!existsSync(pjPath)) continue;
|
|
64
|
+
try {
|
|
65
|
+
const pkg = JSON.parse(readFileSync(pjPath, 'utf8'));
|
|
66
|
+
if (!pkg.dsh) continue;
|
|
67
|
+
const peers = pkg.peerDependencies || {};
|
|
68
|
+
const dshPeers = Object.entries(peers)
|
|
69
|
+
.filter(([k]) => k.startsWith('@deepseek-ai/dsh-'))
|
|
70
|
+
.map(([k, v]) => ({ name: k, range: v }));
|
|
71
|
+
if (dshPeers.length > 0) {
|
|
72
|
+
plugins.push({ name: pkg.name || entry.name, dshPeers });
|
|
73
|
+
}
|
|
74
|
+
} catch { continue; }
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (plugins.length === 0)
|
|
79
|
+
return pass(id, Severity.HIGH, '未发现带 @deepseek-ai/dsh-* peer 依赖的 DSH 插件,跳过 dist-tag 检查');
|
|
80
|
+
|
|
81
|
+
// 去重查询
|
|
82
|
+
const uniquePkgs = [...new Set(plugins.flatMap(p => p.dshPeers.map(dp => dp.name)))];
|
|
83
|
+
const cache = new Map();
|
|
84
|
+
let queriesFailed = 0;
|
|
85
|
+
|
|
86
|
+
for (const pkgName of uniquePkgs) {
|
|
87
|
+
const dt = await fetchDistTags(pkgName);
|
|
88
|
+
if (dt) cache.set(pkgName, dt);
|
|
89
|
+
else queriesFailed++;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (queriesFailed === uniquePkgs.length && uniquePkgs.length > 0) {
|
|
93
|
+
return skip(id, Severity.HIGH,
|
|
94
|
+
`无法查询 npm registry(${uniquePkgs.length} 个包均查询失败),跳过 dist-tag 健康检查`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 逐插件检查
|
|
98
|
+
const affected = [];
|
|
99
|
+
for (const plugin of plugins) {
|
|
100
|
+
for (const dp of plugin.dshPeers) {
|
|
101
|
+
const dt = cache.get(dp.name);
|
|
102
|
+
if (!dt) continue;
|
|
103
|
+
if (BROKEN_LATEST_RE.test(dt.latest)) {
|
|
104
|
+
affected.push({
|
|
105
|
+
plugin: plugin.name,
|
|
106
|
+
peerPkg: dp.name,
|
|
107
|
+
range: dp.range,
|
|
108
|
+
brokenLatest: dt.latest,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (affected.length === 0) {
|
|
115
|
+
return pass(id, Severity.HIGH,
|
|
116
|
+
`dist-tag 健康检查通过:${plugins.length} 个插件 / ${cache.size} 个 @deepseek-ai/dsh-* 包的 latest 均正常`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const details = affected.slice(0, 15).map(a =>
|
|
120
|
+
` ${a.plugin} — peer ${a.peerPkg} ${a.range} → latest=${a.brokenLatest}(broken)`
|
|
121
|
+
).join('\n');
|
|
122
|
+
|
|
123
|
+
return fail(id, Severity.HIGH,
|
|
124
|
+
`检测到 ${affected.length} 个插件受 dist-tag 异常影响(#2763:@deepseek-ai/* 子包 latest 卡在 broken 版本):\n${details}`,
|
|
125
|
+
'对受影响插件显式 pin 版本(dsh plugin add <pkg>@<working-version>),或等待官方修复 dist-tags',
|
|
126
|
+
['#2763']
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export const sp8Check = {
|
|
131
|
+
id: 'SP8',
|
|
132
|
+
name: 'dist-tag-health',
|
|
133
|
+
severity: Severity.HIGH,
|
|
134
|
+
phase: CheckPhase.POST_INSTALL,
|
|
135
|
+
description: 'npm dist-tag 异常检测——检查 @deepseek-ai/dsh-* 包的 latest 是否为 broken 版本(#2763)',
|
|
136
|
+
src: 'builtin',
|
|
137
|
+
runner: (profileDir) => run(profileDir),
|
|
138
|
+
};
|
|
@@ -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
|
+
};
|
package/src/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* DSH Security Framework — 统一安全检查框架
|
|
3
3
|
*
|
|
4
|
-
* 为 DSH 生态提供全生命周期安全检查(
|
|
4
|
+
* 为 DSH 生态提供全生命周期安全检查(20 个检查项,4 层架构)。
|
|
5
5
|
*
|
|
6
6
|
* @example
|
|
7
7
|
* import { createDefaultRegistry } from '@moonquake2004/dsh-security';
|
|
@@ -23,6 +23,8 @@ export { sp4Check } from './checks/sp4-entry-poison.mjs';
|
|
|
23
23
|
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
|
+
export { sp8Check } from './checks/sp8-dist-tag-health.mjs';
|
|
27
|
+
export { sp9Check } from './checks/sp9-dual-instance-guard.mjs';
|
|
26
28
|
|
|
27
29
|
// Layer 2: Runtime Checks
|
|
28
30
|
export { sr1Check } from './checks/sr1-sandbox-violation.mjs';
|
package/src/registry.mjs
CHANGED
|
@@ -148,6 +148,8 @@ export async function createDefaultRegistry(loadExternal = true, options = {}) {
|
|
|
148
148
|
import('./checks/ss2-pii-exposure.mjs'),
|
|
149
149
|
import('./checks/ss3-sensitive-output.mjs'),
|
|
150
150
|
import('./checks/sp7-client-syntax.mjs'),
|
|
151
|
+
import('./checks/sp8-dist-tag-health.mjs'),
|
|
152
|
+
import('./checks/sp9-dual-instance-guard.mjs'),
|
|
151
153
|
]);
|
|
152
154
|
for (const mod of modules) {
|
|
153
155
|
for (const val of Object.values(mod)) {
|