@moonquake2004/dsh-security 0.1.5 → 0.1.6
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/sp10-poison-pattern.mjs +198 -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 生态统一安全检查框架** — 覆盖插件全生命周期(发现→安装→运行→更新→退役),22 个内置检查 + 4 个外部工具集成。
|
|
8
8
|
|
|
9
9
|
> Community security tool. Not an official DeepSeek project.
|
|
10
10
|
|
|
@@ -77,6 +77,7 @@ dsh-security 的目标:**建立 DSH 生态的统一安全检查层**,让任
|
|
|
77
77
|
│ SP7: client 产物语法预检 (#2752 白屏源 boot 前拦截) │
|
|
78
78
|
│ SP8: dist-tag 异常检测 (#2763 broken latest) │
|
|
79
79
|
│ SP9: CLI 核心包泄漏检测 (#4640 symbol 分裂) │
|
|
80
|
+
│ SP10: 混淆投毒模式检测 (Buffer.from/atob/eval) │
|
|
80
81
|
│ SS1: 凭据泄露检测 (会话日志) │
|
|
81
82
|
│ SS2: PII 数据暴露检测 │
|
|
82
83
|
│ SS3: 插件输出敏感数据检测 │
|
|
@@ -253,7 +254,7 @@ node --test test/*.mjs
|
|
|
253
254
|
dsh-security/
|
|
254
255
|
├── src/
|
|
255
256
|
│ ├── protocol/ # Layer 0: Check Protocol
|
|
256
|
-
│ ├── checks/ #
|
|
257
|
+
│ ├── checks/ # 22 个内置检查
|
|
257
258
|
│ ├── integrations/ # 4 个外部工具集成
|
|
258
259
|
│ ├── session-reader.mjs # 会话日志读取(明文 + zstd)
|
|
259
260
|
│ ├── 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.6",
|
|
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
|
@@ -8,6 +8,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
10
|
export { sp9Check } from './sp9-dual-instance-guard.mjs';
|
|
11
|
+
export { sp10Check } from './sp10-poison-pattern.mjs';
|
|
11
12
|
|
|
12
13
|
// Layer 2: Runtime Checks
|
|
13
14
|
export { sr1Check } from './sr1-sandbox-violation.mjs';
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SP10: Poison Pattern — 混淆投毒模式检测
|
|
3
|
+
*
|
|
4
|
+
* 出处:dsh-poison-guard(zoahdev)三层检测中的正则层——AST 层需要
|
|
5
|
+
* JS-X-Ray 依赖(零依赖约束下不可用),但高价值的混淆模式可以
|
|
6
|
+
* 用增强正则捕获:
|
|
7
|
+
*
|
|
8
|
+
* 检测模式:
|
|
9
|
+
* - Buffer.from("hex") / Buffer.from("base64") → 去混淆后的 require/import
|
|
10
|
+
* - atob("...") → 隐藏的外发 URL
|
|
11
|
+
* - String.fromCharCode(...) → 去混淆后的命令(curl/wget 等)
|
|
12
|
+
* - eval(...) / new Function(...) → 动态代码执行
|
|
13
|
+
* - Function("return this")() → 全局对象泄漏
|
|
14
|
+
*
|
|
15
|
+
* Severity: HIGH
|
|
16
|
+
* Phase: POST_INSTALL
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs';
|
|
20
|
+
import { join, extname } from 'node:path';
|
|
21
|
+
import { Severity } from '../protocol/severity.mjs';
|
|
22
|
+
import { CheckPhase } from '../protocol/phase.mjs';
|
|
23
|
+
import { pass, fail, skip } from '../protocol/check.mjs';
|
|
24
|
+
|
|
25
|
+
const SCANNABLE_EXT = new Set(['.js', '.mjs', '.cjs', '.ts', '.jsx', '.tsx']);
|
|
26
|
+
const MAX_FILE_SIZE = 512 * 1024; // 512KB
|
|
27
|
+
const MAX_FILES = 200;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 混淆投毒检测模式
|
|
31
|
+
* 每个 pattern:{ name, regex, severity, description }
|
|
32
|
+
*/
|
|
33
|
+
const POISON_PATTERNS = [
|
|
34
|
+
{
|
|
35
|
+
name: 'deobfuscated-import',
|
|
36
|
+
regex: /Buffer\.from\s*\(\s*['"][0-9a-fA-F]{4,}['"]\s*,\s*['"]hex['"]\s*\)/g,
|
|
37
|
+
description: 'Buffer.from(hex) 去混淆 require/import(常见于投毒包隐藏真实依赖)',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'deobfuscated-base64-import',
|
|
41
|
+
regex: /Buffer\.from\s*\(\s*['"][A-Za-z0-9+/]{8,}={0,2}['"]\s*,\s*['"]base64['"]\s*\)/g,
|
|
42
|
+
description: 'Buffer.from(base64) 去混淆 require/import',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'hidden-url-atob',
|
|
46
|
+
regex: /(?:atob|btoa)\s*\(\s*['"][A-Za-z0-9+/]{16,}={0,2}['"]\s*\)/g,
|
|
47
|
+
description: 'atob/btoa 隐藏 URL(去混淆后可能是外发 C2 地址)',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'deobfuscated-command',
|
|
51
|
+
regex: /String\.fromCharCode\s*\(\s*\d{2,3}(?:\s*,\s*\d{2,3}){4,}\s*\)/g,
|
|
52
|
+
description: 'String.fromCharCode 去混淆命令(常见于投毒包隐藏 curl/wget)',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'eval-execution',
|
|
56
|
+
regex: /(?<!\w)eval\s*\(/g,
|
|
57
|
+
description: 'eval() 动态代码执行(投毒高危信号)',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'function-constructor',
|
|
61
|
+
regex: /new\s+Function\s*\(/g,
|
|
62
|
+
description: 'new Function() 动态代码构造(投毒高危信号)',
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: 'global-sandbox-escape',
|
|
66
|
+
regex: /Function\s*\(\s*['"]return\s+this['"]\s*\)\s*\(\s*\)/g,
|
|
67
|
+
description: 'Function("return this")() 全局对象泄漏(沙箱逃逸手段)',
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 递归收集可扫描文件
|
|
73
|
+
*/
|
|
74
|
+
function collectFiles(dir, files = [], depth = 0) {
|
|
75
|
+
if (depth > 8 || files.length >= MAX_FILES) return files;
|
|
76
|
+
let entries;
|
|
77
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return files; }
|
|
78
|
+
for (const entry of entries) {
|
|
79
|
+
if (files.length >= MAX_FILES) break;
|
|
80
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules' || entry.name === '.git') continue;
|
|
81
|
+
const fullPath = join(dir, entry.name);
|
|
82
|
+
if (entry.isDirectory()) {
|
|
83
|
+
collectFiles(fullPath, files, depth + 1);
|
|
84
|
+
} else if (entry.isFile() && SCANNABLE_EXT.has(extname(entry.name).toLowerCase())) {
|
|
85
|
+
try {
|
|
86
|
+
const stat = statSync(fullPath);
|
|
87
|
+
if (stat.size > 0 && stat.size <= MAX_FILE_SIZE) files.push(fullPath);
|
|
88
|
+
} catch { /* skip */ }
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return files;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function run(profileDir) {
|
|
95
|
+
const id = 'SP10';
|
|
96
|
+
const nmDir = join(profileDir, 'node_modules');
|
|
97
|
+
if (!existsSync(nmDir)) return pass(id, Severity.HIGH, '无 node_modules,跳过投毒模式检测');
|
|
98
|
+
|
|
99
|
+
// 枚举 DSH 插件包
|
|
100
|
+
let dirEntries;
|
|
101
|
+
try { dirEntries = readdirSync(nmDir, { withFileTypes: true }); } catch { dirEntries = []; }
|
|
102
|
+
|
|
103
|
+
const plugins = [];
|
|
104
|
+
for (const entry of dirEntries) {
|
|
105
|
+
if (plugins.length >= 60) break;
|
|
106
|
+
if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === '.bin') continue;
|
|
107
|
+
let pkgDirs = [];
|
|
108
|
+
if (entry.name.startsWith('@')) {
|
|
109
|
+
try {
|
|
110
|
+
pkgDirs = readdirSync(join(nmDir, entry.name), { withFileTypes: true })
|
|
111
|
+
.filter(s => s.isDirectory())
|
|
112
|
+
.map(s => join(nmDir, entry.name, s.name));
|
|
113
|
+
} catch { /* skip */ }
|
|
114
|
+
} else {
|
|
115
|
+
pkgDirs = [join(nmDir, entry.name)];
|
|
116
|
+
}
|
|
117
|
+
for (const pd of pkgDirs) {
|
|
118
|
+
const pjPath = join(pd, 'package.json');
|
|
119
|
+
if (!existsSync(pjPath)) continue;
|
|
120
|
+
try {
|
|
121
|
+
const pkg = JSON.parse(readFileSync(pjPath, 'utf8'));
|
|
122
|
+
if (!pkg.dsh) continue;
|
|
123
|
+
plugins.push({ name: pkg.name || entry.name, dir: pd });
|
|
124
|
+
} catch { continue; }
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (plugins.length === 0) return pass(id, Severity.HIGH, '未发现 DSH 插件包,跳过投毒模式检测');
|
|
129
|
+
|
|
130
|
+
const findings = [];
|
|
131
|
+
let scannedFiles = 0;
|
|
132
|
+
|
|
133
|
+
for (const plugin of plugins) {
|
|
134
|
+
const files = collectFiles(plugin.dir);
|
|
135
|
+
for (const file of files) {
|
|
136
|
+
scannedFiles++;
|
|
137
|
+
let content;
|
|
138
|
+
try { content = readFileSync(file, 'utf8'); } catch { continue; }
|
|
139
|
+
|
|
140
|
+
for (const pattern of POISON_PATTERNS) {
|
|
141
|
+
const matches = content.match(pattern.regex);
|
|
142
|
+
if (matches && matches.length > 0) {
|
|
143
|
+
// 找到匹配行号
|
|
144
|
+
const lines = content.split('\n');
|
|
145
|
+
let matchLine = 1;
|
|
146
|
+
let searchIdx = 0;
|
|
147
|
+
for (let i = 0; i < lines.length; i++) {
|
|
148
|
+
if (content.indexOf(matches[0], searchIdx) < searchIdx + lines[i].length + 1) {
|
|
149
|
+
matchLine = i + 1;
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
searchIdx += lines[i].length + 1;
|
|
153
|
+
}
|
|
154
|
+
findings.push({
|
|
155
|
+
plugin: plugin.name,
|
|
156
|
+
file: file.replace(plugin.dir + '/', ''),
|
|
157
|
+
line: matchLine,
|
|
158
|
+
pattern: pattern.name,
|
|
159
|
+
count: matches.length,
|
|
160
|
+
description: pattern.description,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (findings.length === 0) {
|
|
168
|
+
return pass(id, Severity.HIGH,
|
|
169
|
+
`投毒模式检测通过:${plugins.length} 个插件 / ${scannedFiles} 个源文件,未发现混淆投毒特征`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 按 pattern 分组统计
|
|
173
|
+
const byPattern = {};
|
|
174
|
+
for (const f of findings) {
|
|
175
|
+
byPattern[f.pattern] = (byPattern[f.pattern] || 0) + f.count;
|
|
176
|
+
}
|
|
177
|
+
const patternSummary = Object.entries(byPattern).map(([p, c]) => `${p}(${c})`).join(', ');
|
|
178
|
+
|
|
179
|
+
const details = findings.slice(0, 10).map(f =>
|
|
180
|
+
` ${f.plugin}/${f.file}:${f.line} — ${f.pattern} ×${f.count}: ${f.description}`
|
|
181
|
+
).join('\n');
|
|
182
|
+
|
|
183
|
+
return fail(id, Severity.HIGH,
|
|
184
|
+
`检测到 ${findings.length} 处投毒模式命中(${patternSummary}):\n${details}`,
|
|
185
|
+
'逐一审查命中行:确认是否为合法使用(如 Buffer.from(hex) 在插件中用于解码配置)还是投毒信号;疑似投毒时用 dsh-poison-guard(github.com/zoahdev/dsh-poison-guard)做 AST 级深度扫描',
|
|
186
|
+
['#2312']
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export const sp10Check = {
|
|
191
|
+
id: 'SP10',
|
|
192
|
+
name: 'poison-pattern',
|
|
193
|
+
severity: Severity.HIGH,
|
|
194
|
+
phase: CheckPhase.POST_INSTALL,
|
|
195
|
+
description: '混淆投毒模式检测——Buffer.from hex/atob/String.fromCharCode/eval 等(借鉴 dsh-poison-guard regex 层)',
|
|
196
|
+
src: 'builtin',
|
|
197
|
+
runner: (profileDir) => run(profileDir),
|
|
198
|
+
};
|
package/src/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* DSH Security Framework — 统一安全检查框架
|
|
3
3
|
*
|
|
4
|
-
* 为 DSH 生态提供全生命周期安全检查(
|
|
4
|
+
* 为 DSH 生态提供全生命周期安全检查(22 个检查项,4 层架构)。
|
|
5
5
|
*
|
|
6
6
|
* @example
|
|
7
7
|
* import { createDefaultRegistry } from '@moonquake2004/dsh-security';
|
|
@@ -25,6 +25,7 @@ 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
27
|
export { sp9Check } from './checks/sp9-dual-instance-guard.mjs';
|
|
28
|
+
export { sp10Check } from './checks/sp10-poison-pattern.mjs';
|
|
28
29
|
|
|
29
30
|
// Layer 2: Runtime Checks
|
|
30
31
|
export { sr1Check } from './checks/sr1-sandbox-violation.mjs';
|
package/src/registry.mjs
CHANGED
|
@@ -151,6 +151,7 @@ export async function createDefaultRegistry(loadExternal = true, options = {}) {
|
|
|
151
151
|
import('./checks/sp7-client-syntax.mjs'),
|
|
152
152
|
import('./checks/sp8-dist-tag-health.mjs'),
|
|
153
153
|
import('./checks/sp9-dual-instance-guard.mjs'),
|
|
154
|
+
import('./checks/sp10-poison-pattern.mjs'),
|
|
154
155
|
]);
|
|
155
156
|
for (const mod of modules) {
|
|
156
157
|
for (const val of Object.values(mod)) {
|