@moonquake2004/dsh-security 0.1.1 → 0.1.2
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 +4 -3
- package/package.json +1 -1
- package/src/checks/index.mjs +1 -0
- package/src/checks/sp7-client-syntax.mjs +150 -0
- package/src/index.mjs +2 -1
- package/src/registry.mjs +1 -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 生态统一安全检查框架** — 覆盖插件全生命周期(发现→安装→运行→更新→退役),18 个内置检查 + 4 个外部工具集成。
|
|
8
8
|
|
|
9
9
|
> Community security tool. Not an official DeepSeek project.
|
|
10
10
|
|
|
@@ -74,6 +74,7 @@ dsh-security 的目标:**建立 DSH 生态的统一安全检查层**,让任
|
|
|
74
74
|
│ SP4: 恶意 entry 注入检测 │
|
|
75
75
|
│ SP5: 插件权限声明验证 │
|
|
76
76
|
│ SP6: 已知漏洞匹配 (OSV/CVE/GHSA) │
|
|
77
|
+
│ SP7: client 产物语法预检 (#2752 白屏源 boot 前拦截) │
|
|
77
78
|
│ SS1: 凭据泄露检测 (会话日志) │
|
|
78
79
|
│ SS2: PII 数据暴露检测 │
|
|
79
80
|
│ SS3: 插件输出敏感数据检测 │
|
|
@@ -249,7 +250,7 @@ node --test test/*.mjs
|
|
|
249
250
|
dsh-security/
|
|
250
251
|
├── src/
|
|
251
252
|
│ ├── protocol/ # Layer 0: Check Protocol
|
|
252
|
-
│ ├── checks/ #
|
|
253
|
+
│ ├── checks/ # 18 个内置检查
|
|
253
254
|
│ ├── integrations/ # 4 个外部工具集成
|
|
254
255
|
│ ├── session-reader.mjs # 会话日志读取(明文 + zstd)
|
|
255
256
|
│ ├── 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.2",
|
|
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
|
@@ -5,6 +5,7 @@ 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';
|
|
8
9
|
|
|
9
10
|
// Layer 2: Runtime Checks
|
|
10
11
|
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
|
+
};
|
package/src/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* DSH Security Framework — 统一安全检查框架
|
|
3
3
|
*
|
|
4
|
-
* 为 DSH 生态提供全生命周期安全检查(
|
|
4
|
+
* 为 DSH 生态提供全生命周期安全检查(18 个检查项,4 层架构)。
|
|
5
5
|
*
|
|
6
6
|
* @example
|
|
7
7
|
* import { createDefaultRegistry } from '@moonquake2004/dsh-security';
|
|
@@ -22,6 +22,7 @@ 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';
|
|
25
26
|
|
|
26
27
|
// Layer 2: Runtime Checks
|
|
27
28
|
export { sr1Check } from './checks/sr1-sandbox-violation.mjs';
|
package/src/registry.mjs
CHANGED
|
@@ -147,6 +147,7 @@ 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'),
|
|
150
151
|
]);
|
|
151
152
|
for (const mod of modules) {
|
|
152
153
|
for (const val of Object.values(mod)) {
|