@moonquake2004/dsh-security 0.1.0
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 +102 -0
- package/package.json +25 -0
- package/src/checks/index.mjs +24 -0
- package/src/checks/sl1-supply-chain.mjs +157 -0
- package/src/checks/sl2-update-integrity.mjs +59 -0
- package/src/checks/sl3-reputation-score.mjs +73 -0
- package/src/checks/sl4-release-compat.mjs +159 -0
- package/src/checks/sp1-dependency-audit.mjs +157 -0
- package/src/checks/sp2-secret-scan.mjs +131 -0
- package/src/checks/sp3-sandbox-consistency.mjs +220 -0
- package/src/checks/sp4-entry-poison.mjs +74 -0
- package/src/checks/sp5-permission-model.mjs +58 -0
- package/src/checks/sp6-vuln-match.mjs +67 -0
- package/src/checks/sr1-sandbox-violation.mjs +177 -0
- package/src/checks/sr2-privilege-escalation.mjs +67 -0
- package/src/checks/sr3-data-exfiltration.mjs +163 -0
- package/src/checks/sr4-isolation-verify.mjs +64 -0
- package/src/checks/ss1-credential-leak.mjs +158 -0
- package/src/checks/ss2-pii-exposure.mjs +70 -0
- package/src/checks/ss3-sensitive-output.mjs +63 -0
- package/src/config.mjs +43 -0
- package/src/index.mjs +49 -0
- package/src/integrations/ecosystem.mjs +95 -0
- package/src/integrations/index.mjs +37 -0
- package/src/integrations/plugin-reducer.mjs +66 -0
- package/src/integrations/poison-guard.mjs +83 -0
- package/src/integrations/sandbox-audit.mjs +68 -0
- package/src/protocol/check.mjs +49 -0
- package/src/protocol/index.mjs +3 -0
- package/src/protocol/phase.mjs +16 -0
- package/src/protocol/severity.mjs +40 -0
- package/src/registry.mjs +122 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SS1: Credential Leak — 会话日志凭据泄露检测
|
|
3
|
+
*
|
|
4
|
+
* 扫描 session.jsonl 中的敏感凭据(API key/token/私钥)。
|
|
5
|
+
* 检查是否已通过 dsh-redact 脱敏。
|
|
6
|
+
*
|
|
7
|
+
* Severity: CRITICAL
|
|
8
|
+
* Phase: POST_INSTALL
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
12
|
+
import { createReadStream } from 'node:fs';
|
|
13
|
+
import { createInterface } from 'node:readline';
|
|
14
|
+
import { Severity } from '../protocol/severity.mjs';
|
|
15
|
+
import { CheckPhase } from '../protocol/phase.mjs';
|
|
16
|
+
import { pass, fail } from '../protocol/check.mjs';
|
|
17
|
+
|
|
18
|
+
/** 凭据模式 */
|
|
19
|
+
const CREDENTIAL_PATTERNS = [
|
|
20
|
+
{ name: 'OpenAI API Key', regex: /sk-[a-zA-Z0-9]{20,}/g },
|
|
21
|
+
{ name: 'GitHub PAT', regex: /ghp_[a-zA-Z0-9]{36}/g },
|
|
22
|
+
{ name: 'GitHub Fine-grained PAT', regex: /github_pat_[a-zA-Z0-9_]{20,}/g },
|
|
23
|
+
{ name: 'AWS Access Key', regex: /AKIA[0-9A-Z]{16}/g },
|
|
24
|
+
{ name: 'Slack Token', regex: /xox[baprs]-[a-zA-Z0-9-]+/g },
|
|
25
|
+
{ name: 'Google API Key', regex: /AIza[0-9A-Za-z_-]{35}/g },
|
|
26
|
+
{ name: 'PEM Private Key', regex: /-----BEGIN (RSA |EC |DSA )?PRIVATE KEY-----/g },
|
|
27
|
+
{ name: 'Bearer Token', regex: /Bearer\s+[a-zA-Z0-9._-]{20,}/g },
|
|
28
|
+
{ name: 'Basic Auth', regex: /Basic\s+[a-zA-Z0-9+/=]{20,}/g },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 从一行 JSONL 中提取文本内容
|
|
33
|
+
*/
|
|
34
|
+
function extractTextFromLine(line) {
|
|
35
|
+
try {
|
|
36
|
+
const event = JSON.parse(line);
|
|
37
|
+
// 递归提取所有字符串值
|
|
38
|
+
const texts = [];
|
|
39
|
+
function extract(obj) {
|
|
40
|
+
if (typeof obj === 'string') {
|
|
41
|
+
texts.push(obj);
|
|
42
|
+
} else if (Array.isArray(obj)) {
|
|
43
|
+
for (const item of obj) extract(item);
|
|
44
|
+
} else if (obj && typeof obj === 'object') {
|
|
45
|
+
for (const val of Object.values(obj)) extract(val);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
extract(event.data || event);
|
|
49
|
+
return texts.join(' ');
|
|
50
|
+
} catch {
|
|
51
|
+
return '';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 扫描单行中的凭据
|
|
57
|
+
*/
|
|
58
|
+
function scanLine(text) {
|
|
59
|
+
const findings = [];
|
|
60
|
+
for (const pattern of CREDENTIAL_PATTERNS) {
|
|
61
|
+
const matches = text.matchAll(new RegExp(pattern.regex.source, 'g'));
|
|
62
|
+
for (const match of matches) {
|
|
63
|
+
findings.push({
|
|
64
|
+
type: pattern.name,
|
|
65
|
+
snippet: match[0].slice(0, 20) + '...' + match[0].slice(-4),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return findings;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* SS1 检查:扫描会话日志中的凭据泄露
|
|
74
|
+
* @param {string} sessionFile - session.jsonl 或 session.jsonl.zstd 路径
|
|
75
|
+
* @returns {Promise<import('../protocol/check.mjs').SecurityCheckResult>}
|
|
76
|
+
*/
|
|
77
|
+
export async function run(sessionFile) {
|
|
78
|
+
const id = 'SS1';
|
|
79
|
+
|
|
80
|
+
if (!existsSync(sessionFile)) {
|
|
81
|
+
return pass(id, Severity.CRITICAL, '会话文件不存在,跳过检查');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 检查是否是 zstd 压缩的(需要先解压)
|
|
85
|
+
if (sessionFile.endsWith('.zstd')) {
|
|
86
|
+
return pass(id, Severity.CRITICAL, 'zstd 压缩的会话文件需先解压再检查(或使用 dsh-redact 直接处理)');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 检查是否已通过 dsh-redact 脱敏
|
|
90
|
+
try {
|
|
91
|
+
const firstLines = createReadStream(sessionFile, { encoding: 'utf8' });
|
|
92
|
+
const rl = createInterface({ input: firstLines, crlfDelay: Infinity });
|
|
93
|
+
let alreadyRedacted = false;
|
|
94
|
+
let checkCount = 0;
|
|
95
|
+
for await (const line of rl) {
|
|
96
|
+
if (checkCount++ > 5) break;
|
|
97
|
+
if (line.includes('[REDACTED]') || line.includes('[redacted]') || line.includes('***')) {
|
|
98
|
+
alreadyRedacted = true;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
firstLines.destroy();
|
|
103
|
+
if (alreadyRedacted) {
|
|
104
|
+
return pass(id, Severity.CRITICAL, '会话文件似乎已通过 dsh-redact 脱敏');
|
|
105
|
+
}
|
|
106
|
+
} catch { /* 继续正常检查 */ }
|
|
107
|
+
|
|
108
|
+
const findings = [];
|
|
109
|
+
let lineCount = 0;
|
|
110
|
+
|
|
111
|
+
const fileStream = createReadStream(sessionFile, { encoding: 'utf8' });
|
|
112
|
+
const rl = createInterface({ input: fileStream, crlfDelay: Infinity });
|
|
113
|
+
|
|
114
|
+
for await (const line of rl) {
|
|
115
|
+
lineCount++;
|
|
116
|
+
if (!line.trim()) continue;
|
|
117
|
+
const text = extractTextFromLine(line);
|
|
118
|
+
if (!text) continue;
|
|
119
|
+
const lineFindings = scanLine(text);
|
|
120
|
+
for (const f of lineFindings) {
|
|
121
|
+
findings.push({ ...f, line: lineCount });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (findings.length === 0) {
|
|
126
|
+
return pass(id, Severity.CRITICAL, `扫描 ${lineCount} 行会话日志,未检测到凭据泄露`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 按类型聚合
|
|
130
|
+
const byType = {};
|
|
131
|
+
for (const f of findings) {
|
|
132
|
+
if (!byType[f.type]) byType[f.type] = 0;
|
|
133
|
+
byType[f.type]++;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const summary = Object.entries(byType)
|
|
137
|
+
.map(([type, count]) => `${type}: ${count}`)
|
|
138
|
+
.join(', ');
|
|
139
|
+
|
|
140
|
+
const details = findings
|
|
141
|
+
.slice(0, 10)
|
|
142
|
+
.map(f => `行 ${f.line} — ${f.type}(${f.snippet})`)
|
|
143
|
+
.join('\n');
|
|
144
|
+
|
|
145
|
+
const fix = '使用 dsh-redact 脱敏后再分享会话日志:npx dsh-redact <session.jsonl> --out redacted.jsonl';
|
|
146
|
+
|
|
147
|
+
return fail(id, Severity.CRITICAL, `检测到 ${findings.length} 个凭据泄露(${summary}):\n${details}`, fix, ['#962']);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export const ss1Check = {
|
|
151
|
+
id: 'SS1',
|
|
152
|
+
name: 'credential-leak',
|
|
153
|
+
severity: Severity.CRITICAL,
|
|
154
|
+
phase: CheckPhase.POST_INSTALL,
|
|
155
|
+
description: '会话日志凭据泄露检测',
|
|
156
|
+
src: 'builtin',
|
|
157
|
+
runner: (sessionFile) => run(sessionFile),
|
|
158
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SS2: PII Exposure — PII 数据暴露检测
|
|
3
|
+
*
|
|
4
|
+
* 扫描会话日志中的个人身份信息(PII):
|
|
5
|
+
* - 邮箱地址
|
|
6
|
+
* - 电话号码
|
|
7
|
+
* - 身份证号码
|
|
8
|
+
* - IP 地址
|
|
9
|
+
*
|
|
10
|
+
* Severity: MEDIUM
|
|
11
|
+
* Phase: POST_INSTALL
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { existsSync, createReadStream } from 'node:fs';
|
|
15
|
+
import { createInterface } from 'node:readline';
|
|
16
|
+
import { Severity } from '../protocol/severity.mjs';
|
|
17
|
+
import { CheckPhase } from '../protocol/phase.mjs';
|
|
18
|
+
import { pass, fail } from '../protocol/check.mjs';
|
|
19
|
+
|
|
20
|
+
const PII_PATTERNS = [
|
|
21
|
+
{ name: 'email', regex: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, severity: 'medium' },
|
|
22
|
+
{ name: 'phone', regex: /(\+?86)?1[3-9]\d{9}/g, severity: 'medium' },
|
|
23
|
+
{ name: 'ID card', regex: /[1-9]\d{5}(19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]/g, severity: 'high' },
|
|
24
|
+
{ name: 'IPv4', regex: /\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b/g, severity: 'low' },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
function extractTextFromLine(line) {
|
|
28
|
+
try {
|
|
29
|
+
const event = JSON.parse(line);
|
|
30
|
+
const texts = [];
|
|
31
|
+
function extract(obj) {
|
|
32
|
+
if (typeof obj === 'string') texts.push(obj);
|
|
33
|
+
else if (Array.isArray(obj)) obj.forEach(extract);
|
|
34
|
+
else if (obj && typeof obj === 'object') Object.values(obj).forEach(extract);
|
|
35
|
+
}
|
|
36
|
+
extract(event.data || event);
|
|
37
|
+
return texts.join(' ');
|
|
38
|
+
} catch { return ''; }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function run(sessionFile) {
|
|
42
|
+
const id = 'SS2';
|
|
43
|
+
if (!sessionFile || !existsSync(sessionFile)) return pass(id, Severity.MEDIUM, '无会话文件,跳过 PII 检测');
|
|
44
|
+
if (sessionFile.endsWith('.zstd')) return pass(id, Severity.MEDIUM, 'zstd 文件需先解压');
|
|
45
|
+
|
|
46
|
+
const findings = [];
|
|
47
|
+
let lineCount = 0;
|
|
48
|
+
const rl = createInterface({ input: createReadStream(sessionFile, { encoding: 'utf8' }), crlfDelay: Infinity });
|
|
49
|
+
|
|
50
|
+
for await (const line of rl) {
|
|
51
|
+
lineCount++;
|
|
52
|
+
if (!line.trim()) continue;
|
|
53
|
+
const text = extractTextFromLine(line);
|
|
54
|
+
for (const pattern of PII_PATTERNS) {
|
|
55
|
+
for (const match of text.matchAll(new RegExp(pattern.regex.source, 'g'))) {
|
|
56
|
+
findings.push({ type: pattern.name, severity: pattern.severity, line: lineCount, snippet: match[0].slice(0, 30) });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (findings.length === 0) return pass(id, Severity.MEDIUM, `扫描 ${lineCount} 行,未检测到 PII 暴露`);
|
|
62
|
+
|
|
63
|
+
const byType = {};
|
|
64
|
+
for (const f of findings) { if (!byType[f.type]) byType[f.type] = 0; byType[f.type]++; }
|
|
65
|
+
const summary = Object.entries(byType).map(([t, c]) => `${t}: ${c}`).join(', ');
|
|
66
|
+
const details = findings.slice(0, 10).map(f => `行${f.line} — ${f.type}: ${f.snippet}`).join('\n');
|
|
67
|
+
return fail(id, Severity.MEDIUM, `检测到 ${findings.length} 个 PII 暴露(${summary}):\n${details}`, '使用 dsh-redact 脱敏后再分享会话日志');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const ss2Check = { id: 'SS2', name: 'pii-exposure', severity: Severity.MEDIUM, phase: CheckPhase.POST_INSTALL, description: 'PII 数据暴露检测', src: 'builtin', runner: (f) => run(f) };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SS3: Sensitive Output — 插件输出敏感数据检测
|
|
3
|
+
*
|
|
4
|
+
* 扫描会话日志中工具输出的敏感数据:
|
|
5
|
+
* - 大量文件内容泄露
|
|
6
|
+
* - 环境变量输出
|
|
7
|
+
* - 配置文件内容
|
|
8
|
+
*
|
|
9
|
+
* Severity: LOW
|
|
10
|
+
* Phase: POST_INSTALL
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { existsSync, createReadStream } from 'node:fs';
|
|
14
|
+
import { createInterface } from 'node:readline';
|
|
15
|
+
import { Severity } from '../protocol/severity.mjs';
|
|
16
|
+
import { CheckPhase } from '../protocol/phase.mjs';
|
|
17
|
+
import { pass, fail } from '../protocol/check.mjs';
|
|
18
|
+
|
|
19
|
+
const SENSITIVE_OUTPUT_PATTERNS = [
|
|
20
|
+
{ name: 'env dump', regex: /(?:process\.env|ENV|env)\s*[=:]\s*\{[^}]{50,}/gi, severity: 'medium' },
|
|
21
|
+
{ name: 'config dump', regex: /(?:config|settings|credentials)\s*[=:]\s*\{[^}]{100,}/gi, severity: 'medium' },
|
|
22
|
+
{ name: 'large file content', regex: /(?:readFileSync|cat\s+)\S+[\s\S]{500,}/gi, severity: 'low' },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
function extractToolOutputs(line) {
|
|
26
|
+
try {
|
|
27
|
+
const event = JSON.parse(line);
|
|
28
|
+
if (event.type !== 'tool/result') return [];
|
|
29
|
+
const data = event.data || {};
|
|
30
|
+
const output = data.output || data.result || data.text || '';
|
|
31
|
+
return [typeof output === 'string' ? output : JSON.stringify(output)];
|
|
32
|
+
} catch { return []; }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function run(sessionFile) {
|
|
36
|
+
const id = 'SS3';
|
|
37
|
+
if (!sessionFile || !existsSync(sessionFile)) return pass(id, Severity.LOW, '无会话文件,跳过敏感输出检测');
|
|
38
|
+
if (sessionFile.endsWith('.zstd')) return pass(id, Severity.LOW, 'zstd 文件需先解压');
|
|
39
|
+
|
|
40
|
+
const findings = [];
|
|
41
|
+
let lineCount = 0;
|
|
42
|
+
const rl = createInterface({ input: createReadStream(sessionFile, { encoding: 'utf8' }), crlfDelay: Infinity });
|
|
43
|
+
|
|
44
|
+
for await (const line of rl) {
|
|
45
|
+
lineCount++;
|
|
46
|
+
if (!line.trim()) continue;
|
|
47
|
+
const outputs = extractToolOutputs(line);
|
|
48
|
+
for (const output of outputs) {
|
|
49
|
+
for (const pattern of SENSITIVE_OUTPUT_PATTERNS) {
|
|
50
|
+
for (const match of output.matchAll(new RegExp(pattern.regex.source, 'g'))) {
|
|
51
|
+
findings.push({ type: pattern.name, severity: pattern.severity, line: lineCount, snippet: match[0].slice(0, 60) });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (findings.length === 0) return pass(id, Severity.LOW, `扫描 ${lineCount} 行,未检测到敏感输出`);
|
|
58
|
+
|
|
59
|
+
const details = findings.slice(0, 10).map(f => `行${f.line} — ${f.type}: ${f.snippet}`).join('\n');
|
|
60
|
+
return fail(id, Severity.LOW, `检测到 ${findings.length} 个敏感输出模式:\n${details}`, '检查工具输出是否包含不必要的敏感信息');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const ss3Check = { id: 'SS3', name: 'sensitive-output', severity: Severity.LOW, phase: CheckPhase.POST_INSTALL, description: '插件输出敏感数据检测', src: 'builtin', runner: (f) => run(f) };
|
package/src/config.mjs
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSH Security Framework — 配置管理
|
|
3
|
+
*
|
|
4
|
+
* 支持 ~/.dsh/security.json 配置文件:
|
|
5
|
+
* - 启用/禁用特定检查
|
|
6
|
+
* - 覆盖 severity 阈值
|
|
7
|
+
* - 配置外部工具集成
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { homedir } from 'node:os';
|
|
13
|
+
|
|
14
|
+
const DEFAULT_CONFIG = {
|
|
15
|
+
enabled: false,
|
|
16
|
+
checks: {},
|
|
17
|
+
external: {},
|
|
18
|
+
severityThreshold: 'info',
|
|
19
|
+
autoRedact: true,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function loadConfig(dshHome) {
|
|
23
|
+
const configPath = join(dshHome || join(homedir(), '.dsh'), 'security.json');
|
|
24
|
+
if (!existsSync(configPath)) return DEFAULT_CONFIG;
|
|
25
|
+
try {
|
|
26
|
+
const raw = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
27
|
+
return { ...DEFAULT_CONFIG, ...raw };
|
|
28
|
+
} catch {
|
|
29
|
+
return DEFAULT_CONFIG;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function isCheckEnabled(config, checkId) {
|
|
34
|
+
if (!config.enabled) return false;
|
|
35
|
+
const checkConfig = config.checks[checkId];
|
|
36
|
+
if (checkConfig && checkConfig.enabled === false) return false;
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getCheckSeverity(config, checkId, defaultSeverity) {
|
|
41
|
+
const checkConfig = config.checks[checkId];
|
|
42
|
+
return checkConfig?.severity || defaultSeverity;
|
|
43
|
+
}
|
package/src/index.mjs
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSH Security Framework — 统一安全检查框架
|
|
3
|
+
*
|
|
4
|
+
* 为 DSH 生态提供全生命周期安全检查(17 个检查项,4 层架构)。
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* import { createDefaultRegistry } from '@moonquake2004/dsh-security';
|
|
8
|
+
* const registry = await createDefaultRegistry();
|
|
9
|
+
* const { results, exitCode, summary } = await registry.runAll((check) => profileDir);
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Protocol
|
|
13
|
+
export { Severity, severityToExitCode, severityGte, maxSeverity } from './protocol/severity.mjs';
|
|
14
|
+
export { CheckPhase } from './protocol/phase.mjs';
|
|
15
|
+
export { createResult, pass, fail } from './protocol/check.mjs';
|
|
16
|
+
|
|
17
|
+
// Layer 1: Static Checks
|
|
18
|
+
export { sp1Check } from './checks/sp1-dependency-audit.mjs';
|
|
19
|
+
export { sp2Check } from './checks/sp2-secret-scan.mjs';
|
|
20
|
+
export { sp3Check } from './checks/sp3-sandbox-consistency.mjs';
|
|
21
|
+
export { sp4Check } from './checks/sp4-entry-poison.mjs';
|
|
22
|
+
export { sp5Check } from './checks/sp5-permission-model.mjs';
|
|
23
|
+
export { sp6Check } from './checks/sp6-vuln-match.mjs';
|
|
24
|
+
|
|
25
|
+
// Layer 2: Runtime Checks
|
|
26
|
+
export { sr1Check } from './checks/sr1-sandbox-violation.mjs';
|
|
27
|
+
export { sr2Check } from './checks/sr2-privilege-escalation.mjs';
|
|
28
|
+
export { sr3Check } from './checks/sr3-data-exfiltration.mjs';
|
|
29
|
+
export { sr4Check } from './checks/sr4-isolation-verify.mjs';
|
|
30
|
+
|
|
31
|
+
// Layer 3: Lifecycle Checks
|
|
32
|
+
export { sl1Check } from './checks/sl1-supply-chain.mjs';
|
|
33
|
+
export { sl2Check } from './checks/sl2-update-integrity.mjs';
|
|
34
|
+
export { sl3Check } from './checks/sl3-reputation-score.mjs';
|
|
35
|
+
export { sl4Check } from './checks/sl4-release-compat.mjs';
|
|
36
|
+
|
|
37
|
+
// Session Checks
|
|
38
|
+
export { ss1Check } from './checks/ss1-credential-leak.mjs';
|
|
39
|
+
export { ss2Check } from './checks/ss2-pii-exposure.mjs';
|
|
40
|
+
export { ss3Check } from './checks/ss3-sensitive-output.mjs';
|
|
41
|
+
|
|
42
|
+
// Registry
|
|
43
|
+
export { SecurityCheckRegistry, createDefaultRegistry } from './registry.mjs';
|
|
44
|
+
|
|
45
|
+
// Integrations
|
|
46
|
+
export { poisonGuardCheck, sandboxAuditCheck, ecosystemCheck, pluginReducerCheck } from './integrations/index.mjs';
|
|
47
|
+
|
|
48
|
+
// Config
|
|
49
|
+
export { loadConfig, isCheckEnabled, getCheckSeverity } from './config.mjs';
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-ecosystem 集成
|
|
3
|
+
*
|
|
4
|
+
* 从 dsh-ecosystem 获取发布兼容性数据:
|
|
5
|
+
* - 已知 bug 状态
|
|
6
|
+
* - 发布兼容性报告
|
|
7
|
+
* - 生态健康信号
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Severity } from '../protocol/severity.mjs';
|
|
11
|
+
import { CheckPhase } from '../protocol/phase.mjs';
|
|
12
|
+
|
|
13
|
+
const ECOSYSTEM_API = 'https://raw.githubusercontent.com/zoahdev/dsh-ecosystem/main/docs';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 获取发布兼容性报告
|
|
17
|
+
*/
|
|
18
|
+
async function fetchReleaseCompat() {
|
|
19
|
+
try {
|
|
20
|
+
const response = await fetch(`${ECOSYSTEM_API}/release-compat/`, { signal: AbortSignal.timeout(10000) });
|
|
21
|
+
if (!response.ok) return null;
|
|
22
|
+
// 解析目录列表找最新的兼容性报告
|
|
23
|
+
const text = await response.text();
|
|
24
|
+
const match = text.match(/release-compat-[\d-]+\.md/);
|
|
25
|
+
if (!match) return null;
|
|
26
|
+
|
|
27
|
+
const reportResponse = await fetch(`${ECOSYSTEM_API}/release-compat/${match[0]}`, { signal: AbortSignal.timeout(10000) });
|
|
28
|
+
if (!reportResponse.ok) return null;
|
|
29
|
+
return await reportResponse.text();
|
|
30
|
+
} catch { return null; }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 获取 bug 雷达
|
|
35
|
+
*/
|
|
36
|
+
async function fetchBugRadar() {
|
|
37
|
+
try {
|
|
38
|
+
const response = await fetch(`${ECOSYSTEM_API}/weekly-2026-08-15.md`, { signal: AbortSignal.timeout(10000) });
|
|
39
|
+
if (!response.ok) return null;
|
|
40
|
+
return await response.text();
|
|
41
|
+
} catch { return null; }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function runCheck(profileDir) {
|
|
45
|
+
const id = 'EXT-ECO-1';
|
|
46
|
+
|
|
47
|
+
const releaseNotes = await fetchReleaseCompat();
|
|
48
|
+
const bugRadar = await fetchBugRadar();
|
|
49
|
+
|
|
50
|
+
if (!releaseNotes && !bugRadar) {
|
|
51
|
+
return { id, ok: true, severity: Severity.LOW, detail: 'dsh-ecosystem 数据源不可用,跳过生态兼容性检查' };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const issues = [];
|
|
55
|
+
|
|
56
|
+
// 检查是否有已知的 breaking changes
|
|
57
|
+
if (releaseNotes) {
|
|
58
|
+
const breakingMatch = releaseNotes.match(/breaking|incompatible|migration/gi);
|
|
59
|
+
if (breakingMatch && breakingMatch.length > 0) {
|
|
60
|
+
issues.push({ type: 'breaking-changes', detail: `发布兼容性报告中发现 ${breakingMatch.length} 个 breaking change 提及` });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 检查是否有 critical bugs
|
|
65
|
+
if (bugRadar) {
|
|
66
|
+
const criticalMatch = bugRadar.match(/critical|CRITICAL|严重/gi);
|
|
67
|
+
if (criticalMatch && criticalMatch.length > 0) {
|
|
68
|
+
issues.push({ type: 'critical-bugs', detail: `Bug 雷达中发现 ${criticalMatch.length} 个 critical 级别问题` });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (issues.length === 0) {
|
|
73
|
+
return { id, ok: true, severity: Severity.LOW, detail: 'dsh-ecosystem 生态兼容性检查通过' };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const details = issues.map(i => `${i.type}: ${i.detail}`).join('\n');
|
|
77
|
+
return {
|
|
78
|
+
id,
|
|
79
|
+
ok: false,
|
|
80
|
+
severity: Severity.LOW,
|
|
81
|
+
detail: `dsh-ecosystem 检测到 ${issues.length} 个生态关注点:\n${details}`,
|
|
82
|
+
fix: '查看 dsh-ecosystem 周报获取最新生态状态',
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const ecosystemCheck = {
|
|
87
|
+
id: 'EXT-ECO-1',
|
|
88
|
+
name: 'ecosystem-compat',
|
|
89
|
+
severity: Severity.LOW,
|
|
90
|
+
phase: CheckPhase.LIFECYCLE,
|
|
91
|
+
description: 'dsh-ecosystem 生态兼容性检查',
|
|
92
|
+
src: 'external',
|
|
93
|
+
source: 'dsh-ecosystem',
|
|
94
|
+
runner: (profileDir) => runCheck(profileDir),
|
|
95
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export { poisonGuardCheck, isAvailable as isPoisonGuardAvailable } from './poison-guard.mjs';
|
|
2
|
+
export { sandboxAuditCheck, isAvailable as isSandboxAuditAvailable } from './sandbox-audit.mjs';
|
|
3
|
+
export { ecosystemCheck } from './ecosystem.mjs';
|
|
4
|
+
export { pluginReducerCheck, isAvailable as isPluginReducerAvailable } from './plugin-reducer.mjs';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 获取所有可用的外部集成检查
|
|
8
|
+
*/
|
|
9
|
+
export async function getAvailableIntegrations() {
|
|
10
|
+
const integrations = [];
|
|
11
|
+
|
|
12
|
+
// dsh-poison-guard(需要安装)
|
|
13
|
+
try {
|
|
14
|
+
const mod = await import('./poison-guard.mjs');
|
|
15
|
+
if (mod.isAvailable()) integrations.push(mod.poisonGuardCheck);
|
|
16
|
+
} catch { /* skip */ }
|
|
17
|
+
|
|
18
|
+
// dsh-sandbox-audit(需要安装)
|
|
19
|
+
try {
|
|
20
|
+
const mod = await import('./sandbox-audit.mjs');
|
|
21
|
+
if (mod.isAvailable()) integrations.push(mod.sandboxAuditCheck);
|
|
22
|
+
} catch { /* skip */ }
|
|
23
|
+
|
|
24
|
+
// dsh-ecosystem(总是可用,网络 API)
|
|
25
|
+
try {
|
|
26
|
+
const mod = await import('./ecosystem.mjs');
|
|
27
|
+
integrations.push(mod.ecosystemCheck);
|
|
28
|
+
} catch { /* skip */ }
|
|
29
|
+
|
|
30
|
+
// dsh-plugin-reducer(需要安装)
|
|
31
|
+
try {
|
|
32
|
+
const mod = await import('./plugin-reducer.mjs');
|
|
33
|
+
if (mod.isAvailable()) integrations.push(mod.pluginReducerCheck);
|
|
34
|
+
} catch { /* skip */ }
|
|
35
|
+
|
|
36
|
+
return integrations;
|
|
37
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-plugin-reducer 集成
|
|
3
|
+
*
|
|
4
|
+
* 在检测到故障时,提供最小化故障插件集的能力。
|
|
5
|
+
* 如果 dsh-plugin-reducer 已安装,自动建议运行。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { execSync } from 'node:child_process';
|
|
9
|
+
import { Severity } from '../protocol/severity.mjs';
|
|
10
|
+
import { CheckPhase } from '../protocol/phase.mjs';
|
|
11
|
+
|
|
12
|
+
export function isAvailable() {
|
|
13
|
+
try {
|
|
14
|
+
execSync('which dsh-plugin-reducer', { encoding: 'utf8', stdio: 'pipe' });
|
|
15
|
+
return true;
|
|
16
|
+
} catch {
|
|
17
|
+
try {
|
|
18
|
+
execSync('npx dsh-plugin-reducer --version', { encoding: 'utf8', stdio: 'pipe', timeout: 10000 });
|
|
19
|
+
return true;
|
|
20
|
+
} catch { return false; }
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function runReducer(profileDir, probeType = 'web') {
|
|
25
|
+
const id = 'EXT-RED-1';
|
|
26
|
+
|
|
27
|
+
if (!isAvailable()) {
|
|
28
|
+
return { id, ok: true, severity: Severity.LOW, detail: 'dsh-plugin-reducer 未安装,跳过故障最小化' };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const output = execSync(`npx dsh-plugin-reducer --profile "${profileDir}" --probe ${probeType} --report /tmp/reducer-report.json --json`, {
|
|
33
|
+
encoding: 'utf8',
|
|
34
|
+
timeout: 120000,
|
|
35
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const result = JSON.parse(output);
|
|
39
|
+
|
|
40
|
+
if (result.minimalSet && result.minimalSet.length > 0) {
|
|
41
|
+
return {
|
|
42
|
+
id,
|
|
43
|
+
ok: false,
|
|
44
|
+
severity: Severity.MEDIUM,
|
|
45
|
+
detail: `dsh-plugin-reducer 找到最小故障插件集:${result.minimalSet.join(', ')}`,
|
|
46
|
+
fix: '移除或禁用故障插件集中的插件',
|
|
47
|
+
evidence: result,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return { id, ok: true, severity: Severity.LOW, detail: 'dsh-plugin-reducer 未发现故障插件集' };
|
|
52
|
+
} catch (e) {
|
|
53
|
+
return { id, ok: true, severity: Severity.LOW, detail: `dsh-plugin-reducer 执行失败:${e.message.slice(0, 80)}` };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const pluginReducerCheck = {
|
|
58
|
+
id: 'EXT-RED-1',
|
|
59
|
+
name: 'plugin-reducer',
|
|
60
|
+
severity: Severity.MEDIUM,
|
|
61
|
+
phase: CheckPhase.LIFECYCLE,
|
|
62
|
+
description: 'dsh-plugin-reducer 故障最小化',
|
|
63
|
+
src: 'external',
|
|
64
|
+
source: 'dsh-plugin-reducer',
|
|
65
|
+
runner: (profileDir) => runReducer(profileDir),
|
|
66
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-poison-guard 集成
|
|
3
|
+
*
|
|
4
|
+
* 通过 Plugin Interface 注册投毒扫描检查。
|
|
5
|
+
* 如果 dsh-poison-guard 已安装,自动集成到安全检查流程。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { execSync } from 'node:child_process';
|
|
9
|
+
import { existsSync } from 'node:fs';
|
|
10
|
+
import { Severity } from '../protocol/severity.mjs';
|
|
11
|
+
import { CheckPhase } from '../protocol/phase.mjs';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 检测 dsh-poison-guard 是否可用
|
|
15
|
+
*/
|
|
16
|
+
export function isAvailable() {
|
|
17
|
+
try {
|
|
18
|
+
execSync('which dsh-poison-guard', { encoding: 'utf8', stdio: 'pipe' });
|
|
19
|
+
return true;
|
|
20
|
+
} catch {
|
|
21
|
+
// 也检查 npx 可用性
|
|
22
|
+
try {
|
|
23
|
+
execSync('npx dsh-poison-guard --version', { encoding: 'utf8', stdio: 'pipe', timeout: 10000 });
|
|
24
|
+
return true;
|
|
25
|
+
} catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 运行 dsh-poison-guard 扫描
|
|
33
|
+
* @param {string} targetPath - 要扫描的目录或包路径
|
|
34
|
+
* @returns {Promise<import('../protocol/check.mjs').SecurityCheckResult>}
|
|
35
|
+
*/
|
|
36
|
+
export async function runScan(targetPath) {
|
|
37
|
+
const id = 'EXT-PG-1';
|
|
38
|
+
|
|
39
|
+
if (!isAvailable()) {
|
|
40
|
+
return { id, ok: true, severity: Severity.HIGH, detail: 'dsh-poison-guard 未安装,跳过投毒扫描' };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const output = execSync(`npx dsh-poison-guard scan "${targetPath}" --json`, {
|
|
45
|
+
encoding: 'utf8',
|
|
46
|
+
timeout: 60000,
|
|
47
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const result = JSON.parse(output);
|
|
51
|
+
|
|
52
|
+
if (result.clean || result.vulnerabilities?.length === 0) {
|
|
53
|
+
return { id, ok: true, severity: Severity.HIGH, detail: 'dsh-poison-guard 扫描通过:未检测到投毒模式' };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const vulns = result.vulnerabilities || [];
|
|
57
|
+
const details = vulns.slice(0, 10).map(v => `[${v.severity}] ${v.type}: ${v.message}`).join('\n');
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
id,
|
|
61
|
+
ok: false,
|
|
62
|
+
severity: Severity.HIGH,
|
|
63
|
+
detail: `dsh-poison-guard 检测到 ${vulns.length} 个投毒模式:\n${details}`,
|
|
64
|
+
fix: '检查相关插件的源码,移除恶意代码',
|
|
65
|
+
};
|
|
66
|
+
} catch (e) {
|
|
67
|
+
return { id, ok: true, severity: Severity.HIGH, detail: `dsh-poison-guard 执行失败:${e.message.slice(0, 80)}` };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* dsh-poison-guard 集成检查对象
|
|
73
|
+
*/
|
|
74
|
+
export const poisonGuardCheck = {
|
|
75
|
+
id: 'EXT-PG-1',
|
|
76
|
+
name: 'poison-scan',
|
|
77
|
+
severity: Severity.HIGH,
|
|
78
|
+
phase: CheckPhase.POST_INSTALL,
|
|
79
|
+
description: 'dsh-poison-guard 投毒扫描(AST + 反混淆)',
|
|
80
|
+
src: 'external',
|
|
81
|
+
source: 'dsh-poison-guard',
|
|
82
|
+
runner: (targetPath) => runScan(targetPath),
|
|
83
|
+
};
|