@moonquake2004/dsh-security 0.1.2 → 0.1.3

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 CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
4
4
  [![npm](https://img.shields.io/npm/v/@moonquake2004/dsh-security)](https://www.npmjs.com/package/@moonquake2004/dsh-security)
5
- [![Tests](https://img.shields.io/badge/tests-75%2F75%20passing-brightgreen)](#)
5
+ [![Tests](https://img.shields.io/badge/tests-80%2F80%20passing-brightgreen)](#)
6
6
 
7
- **DSH 生态统一安全检查框架** — 覆盖插件全生命周期(发现→安装→运行→更新→退役),18 个内置检查 + 4 个外部工具集成。
7
+ **DSH 生态统一安全检查框架** — 覆盖插件全生命周期(发现→安装→运行→更新→退役),19 个内置检查 + 4 个外部工具集成。
8
8
 
9
9
  > Community security tool. Not an official DeepSeek project.
10
10
 
@@ -75,6 +75,7 @@ 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) │
78
79
  │ SS1: 凭据泄露检测 (会话日志) │
79
80
  │ SS2: PII 数据暴露检测 │
80
81
  │ SS3: 插件输出敏感数据检测 │
@@ -250,7 +251,7 @@ node --test test/*.mjs
250
251
  dsh-security/
251
252
  ├── src/
252
253
  │ ├── protocol/ # Layer 0: Check Protocol
253
- │ ├── checks/ # 18 个内置检查
254
+ │ ├── checks/ # 19 个内置检查
254
255
  │ ├── integrations/ # 4 个外部工具集成
255
256
  │ ├── session-reader.mjs # 会话日志读取(明文 + zstd)
256
257
  │ ├── 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.2",
3
+ "version": "0.1.3",
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",
@@ -6,6 +6,7 @@ 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';
9
10
 
10
11
  // Layer 2: Runtime Checks
11
12
  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
+ };
package/src/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * DSH Security Framework — 统一安全检查框架
3
3
  *
4
- * 为 DSH 生态提供全生命周期安全检查(18 个检查项,4 层架构)。
4
+ * 为 DSH 生态提供全生命周期安全检查(19 个检查项,4 层架构)。
5
5
  *
6
6
  * @example
7
7
  * import { createDefaultRegistry } from '@moonquake2004/dsh-security';
@@ -23,6 +23,7 @@ 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';
26
27
 
27
28
  // Layer 2: Runtime Checks
28
29
  export { sr1Check } from './checks/sr1-sandbox-violation.mjs';
package/src/registry.mjs CHANGED
@@ -148,6 +148,7 @@ 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'),
151
152
  ]);
152
153
  for (const mod of modules) {
153
154
  for (const val of Object.values(mod)) {