@moonquake2004/dsh-security 0.1.1 → 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-69%2F69%20passing-brightgreen)](#)
5
+ [![Tests](https://img.shields.io/badge/tests-80%2F80%20passing-brightgreen)](#)
6
6
 
7
- **DSH 生态统一安全检查框架** — 覆盖插件全生命周期(发现→安装→运行→更新→退役),17 个内置检查 + 4 个外部工具集成。
7
+ **DSH 生态统一安全检查框架** — 覆盖插件全生命周期(发现→安装→运行→更新→退役),19 个内置检查 + 4 个外部工具集成。
8
8
 
9
9
  > Community security tool. Not an official DeepSeek project.
10
10
 
@@ -74,6 +74,8 @@ dsh-security 的目标:**建立 DSH 生态的统一安全检查层**,让任
74
74
  │ SP4: 恶意 entry 注入检测 │
75
75
  │ SP5: 插件权限声明验证 │
76
76
  │ SP6: 已知漏洞匹配 (OSV/CVE/GHSA) │
77
+ │ SP7: client 产物语法预检 (#2752 白屏源 boot 前拦截) │
78
+ │ SP8: dist-tag 异常检测 (#2763 broken latest) │
77
79
  │ SS1: 凭据泄露检测 (会话日志) │
78
80
  │ SS2: PII 数据暴露检测 │
79
81
  │ SS3: 插件输出敏感数据检测 │
@@ -249,7 +251,7 @@ node --test test/*.mjs
249
251
  dsh-security/
250
252
  ├── src/
251
253
  │ ├── protocol/ # Layer 0: Check Protocol
252
- │ ├── checks/ # 17 个内置检查
254
+ │ ├── checks/ # 19 个内置检查
253
255
  │ ├── integrations/ # 4 个外部工具集成
254
256
  │ ├── session-reader.mjs # 会话日志读取(明文 + zstd)
255
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.1",
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",
@@ -5,6 +5,8 @@ export { sp3Check } from './sp3-sandbox-consistency.mjs';
5
5
  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
+ export { sp7Check } from './sp7-client-syntax.mjs';
9
+ export { sp8Check } from './sp8-dist-tag-health.mjs';
8
10
 
9
11
  // Layer 2: Runtime Checks
10
12
  export { sr1Check } from './sr1-sandbox-violation.mjs';
@@ -0,0 +1,150 @@
1
+ /**
2
+ * SP7: Client Bundle Syntax — 插件 client 产物语法预检
3
+ *
4
+ * 出处:deepseek-ai/deepseek-harness discussion #2752 补充案例——插件 client.js
5
+ * 注释未闭合(语法级错误)→ 浏览器端 ReferenceError → Web UI 整页白屏,
6
+ * 服务端 HTTP 200 且日志零感知。P13 只提取 ctx.provide() 服务名,不做语法解析,
7
+ * 这类错误此前完全逃逸离线检查。
8
+ *
9
+ * 做法:对每个已装 DSH 插件包(package.json 含 dsh 字段)的 client 入口产物
10
+ * (<pkg>/client/*.js|mjs、<pkg>/lib/client.js、<pkg>/client.js)执行
11
+ * `node --check`(Node ≥22 自动探测 ESM/CJS,无需区分模块格式)。
12
+ * 解析失败 = boot 前即可断定的必白屏项 → HIGH。
13
+ *
14
+ * Severity: HIGH
15
+ * Phase: POST_INSTALL
16
+ */
17
+
18
+ import { readFileSync, readdirSync, existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
19
+ import { join, relative } from 'node:path';
20
+ import { tmpdir } from 'node:os';
21
+ import { execFileSync } from 'node:child_process';
22
+ import { Severity } from '../protocol/severity.mjs';
23
+ import { CheckPhase } from '../protocol/phase.mjs';
24
+ import { pass, fail } from '../protocol/check.mjs';
25
+
26
+ const CLIENT_EXTENSIONS = ['.js', '.mjs', '.cjs'];
27
+ const MAX_FILES_PER_PKG = 10;
28
+ const MAX_PKGS = 60; // 上限只约束"识别为 DSH 插件"的包(普通依赖不占额)
29
+ const MAX_TOTAL_FILES = 200; // 全局文件数保险,防病态安装
30
+
31
+ /** 收集一个插件包内的 client 候选产物 */
32
+ function collectClientFiles(pkgDir) {
33
+ const files = [];
34
+ const pushIfClient = (p) => {
35
+ if (files.length >= MAX_FILES_PER_PKG) return;
36
+ if (existsSync(p) && CLIENT_EXTENSIONS.some(ext => p.endsWith(ext))) files.push(p);
37
+ };
38
+ // 形态 A:<pkg>/client/ 目录(真实布局:dsh-doctor、dshmarket 等)
39
+ const clientDir = join(pkgDir, 'client');
40
+ if (existsSync(clientDir)) {
41
+ try {
42
+ for (const e of readdirSync(clientDir, { withFileTypes: true })) {
43
+ if (e.isFile()) pushIfClient(join(clientDir, e.name));
44
+ }
45
+ } catch { /* unreadable dir */ }
46
+ }
47
+ // 形态 B:lib/client.js(真实布局:dsh-better-sidebar、dsh-persist、@xmanrui/dsh-im 等)
48
+ pushIfClient(join(pkgDir, 'lib', 'client.js'));
49
+ // 形态 C:根级 client.js
50
+ pushIfClient(join(pkgDir, 'client.js'));
51
+ return files;
52
+ }
53
+
54
+ /**
55
+ * 对单个文件做语法校验。
56
+ * @returns {{ok: true} | {ok: false, message: string}}
57
+ */
58
+ function syntaxCheck(file) {
59
+ try {
60
+ execFileSync(process.execPath, ['--check', file], { timeout: 10000, stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8' });
61
+ return { ok: true };
62
+ } catch (e) {
63
+ const errText = String(e.stderr || e.message || '');
64
+ // 提取最有信息量的一行:SyntaxError/ReferenceError/... 及其位置
65
+ const diagLine = errText.split('\n').find(l => /^(SyntaxError|ReferenceError|TypeError|RangeError|Error)\b/.test(l.trim()));
66
+ const posLine = errText.split('\n').find(l => l.startsWith(file));
67
+ const message = [posLine ? relativeProcess(posLine) : null, diagLine ? diagLine.trim().slice(0, 160) : null]
68
+ .filter(Boolean).join(' — ') || `node --check 失败(exit ${e.status ?? '?'}, ${errText.slice(0, 80)})`;
69
+ return { ok: false, message };
70
+ }
71
+ }
72
+
73
+ function relativeProcess(posLine) {
74
+ return posLine.replace(/^.*\/node_modules\//, '').slice(0, 120);
75
+ }
76
+
77
+ /** 纯文本版语法校验(供测试与无子进程环境复用):写入临时 .mjs 不需要——直接用文件路径 */
78
+ export async function run(profileDir) {
79
+ const id = 'SP7';
80
+ const nmDir = join(profileDir, 'node_modules');
81
+ if (!existsSync(nmDir)) return pass(id, Severity.HIGH, '无 node_modules,跳过 client 产物语法预检');
82
+
83
+ // 枚举已装 DSH 插件包(含 scoped;以 package.json 的 dsh 字段为门控)
84
+ const pkgs = [];
85
+ let entries = [];
86
+ try { entries = readdirSync(nmDir, { withFileTypes: true }); } catch { entries = []; }
87
+ for (const entry of entries) {
88
+ if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === '.bin') continue;
89
+ if (entry.name.startsWith('@')) {
90
+ let subs = [];
91
+ try { subs = readdirSync(join(nmDir, entry.name), { withFileTypes: true }); } catch { continue; }
92
+ for (const sub of subs) {
93
+ if (sub.isDirectory()) pkgs.push(join(nmDir, entry.name, sub.name));
94
+ }
95
+ } else {
96
+ pkgs.push(join(nmDir, entry.name));
97
+ }
98
+ }
99
+
100
+ const findings = [];
101
+ let scannedFiles = 0;
102
+ let scannedPkgs = 0;
103
+
104
+ for (const pkgDir of pkgs) {
105
+ const pkgJsonPath = join(pkgDir, 'package.json');
106
+ if (!existsSync(pkgJsonPath)) continue;
107
+ try {
108
+ const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf8'));
109
+ if (!pkg.dsh) continue; // 只查 DSH 插件包,避免误扫 hono/undici 等普通依赖
110
+ } catch { continue; }
111
+ if (scannedPkgs >= MAX_PKGS) break;
112
+ scannedPkgs++;
113
+
114
+ for (const file of collectClientFiles(pkgDir)) {
115
+ if (scannedFiles >= MAX_TOTAL_FILES) break;
116
+ scannedFiles++;
117
+ const result = syntaxCheck(file);
118
+ if (!result.ok) {
119
+ findings.push({
120
+ file: relative(profileDir, file),
121
+ message: result.message,
122
+ });
123
+ }
124
+ }
125
+ }
126
+
127
+ if (scannedPkgs === 0) return pass(id, Severity.HIGH, '未发现 DSH 插件包,跳过 client 产物语法预检');
128
+ if (scannedFiles === 0) return pass(id, Severity.HIGH, `扫描 ${scannedPkgs} 个插件包,未发现 client 产物,跳过语法预检`);
129
+
130
+ if (findings.length === 0) {
131
+ return pass(id, Severity.HIGH, `语法预检通过:${scannedPkgs} 个插件包 / ${scannedFiles} 个 client 产物均可正常解析`);
132
+ }
133
+
134
+ const details = findings.slice(0, 10).map(f => `${f.file}\n ${f.message}`).join('\n');
135
+ return fail(id, Severity.HIGH,
136
+ `检测到 ${findings.length} 个 client 产物语法错误(boot 后将整页白屏):\n${details}`,
137
+ '修复对应插件的 client 代码语法错误,或暂时移除该插件;此类错误在浏览器端表现为 Failed to load plugins 白屏且服务端日志无感知',
138
+ ['#2752']
139
+ );
140
+ }
141
+
142
+ export const sp7Check = {
143
+ id: 'SP7',
144
+ name: 'client-syntax',
145
+ severity: Severity.HIGH,
146
+ phase: CheckPhase.POST_INSTALL,
147
+ description: '插件 client 产物语法预检(node --check,boot 前拦截白屏源)',
148
+ src: 'builtin',
149
+ runner: (profileDir) => run(profileDir),
150
+ };
@@ -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 生态提供全生命周期安全检查(17 个检查项,4 层架构)。
4
+ * 为 DSH 生态提供全生命周期安全检查(19 个检查项,4 层架构)。
5
5
  *
6
6
  * @example
7
7
  * import { createDefaultRegistry } from '@moonquake2004/dsh-security';
@@ -22,6 +22,8 @@ export { sp3Check } from './checks/sp3-sandbox-consistency.mjs';
22
22
  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
+ export { sp7Check } from './checks/sp7-client-syntax.mjs';
26
+ export { sp8Check } from './checks/sp8-dist-tag-health.mjs';
25
27
 
26
28
  // Layer 2: Runtime Checks
27
29
  export { sr1Check } from './checks/sr1-sandbox-violation.mjs';
package/src/registry.mjs CHANGED
@@ -147,6 +147,8 @@ 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/sp7-client-syntax.mjs'),
151
+ import('./checks/sp8-dist-tag-health.mjs'),
150
152
  ]);
151
153
  for (const mod of modules) {
152
154
  for (const val of Object.values(mod)) {