@moonquake2004/dsh-security 0.1.0 → 0.1.1

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.
@@ -1,19 +1,21 @@
1
1
  /**
2
2
  * SS1: Credential Leak — 会话日志凭据泄露检测
3
3
  *
4
- * 扫描 session.jsonl 中的敏感凭据(API key/token/私钥)。
5
- * 检查是否已通过 dsh-redact 脱敏。
4
+ * 扫描 session.jsonl(或 .zstd 压缩)中的敏感凭据(API key/token/私钥)。
5
+ *
6
+ * 复审修复:
7
+ * - snippet 不再回显凭据本体(旧实现输出前 20+后 4 字符,检测器自己泄露凭据)
8
+ * - 移除"前几行含 [REDACTED]/*** 即整体跳过"的弱启发(一行 markdown 分隔线就能让整个文件免检)
6
9
  *
7
10
  * Severity: CRITICAL
8
11
  * Phase: POST_INSTALL
9
12
  */
10
13
 
11
- import { readFileSync, existsSync } from 'node:fs';
12
- import { createReadStream } from 'node:fs';
13
- import { createInterface } from 'node:readline';
14
+ import { existsSync } from 'node:fs';
14
15
  import { Severity } from '../protocol/severity.mjs';
15
16
  import { CheckPhase } from '../protocol/phase.mjs';
16
- import { pass, fail } from '../protocol/check.mjs';
17
+ import { pass, fail, skip } from '../protocol/check.mjs';
18
+ import { scanSessionLines } from '../session-reader.mjs';
17
19
 
18
20
  /** 凭据模式 */
19
21
  const CREDENTIAL_PATTERNS = [
@@ -28,13 +30,18 @@ const CREDENTIAL_PATTERNS = [
28
30
  { name: 'Basic Auth', regex: /Basic\s+[a-zA-Z0-9+/=]{20,}/g },
29
31
  ];
30
32
 
33
+ /** 掩码:只保留极短前缀 + 长度信息,绝不回显凭据内容 */
34
+ function maskSecret(s) {
35
+ if (s.length <= 8) return `***(${s.length} chars)`;
36
+ return `${s.slice(0, 4)}***(${s.length} chars)`;
37
+ }
38
+
31
39
  /**
32
40
  * 从一行 JSONL 中提取文本内容
33
41
  */
34
42
  function extractTextFromLine(line) {
35
43
  try {
36
44
  const event = JSON.parse(line);
37
- // 递归提取所有字符串值
38
45
  const texts = [];
39
46
  function extract(obj) {
40
47
  if (typeof obj === 'string') {
@@ -52,9 +59,6 @@ function extractTextFromLine(line) {
52
59
  }
53
60
  }
54
61
 
55
- /**
56
- * 扫描单行中的凭据
57
- */
58
62
  function scanLine(text) {
59
63
  const findings = [];
60
64
  for (const pattern of CREDENTIAL_PATTERNS) {
@@ -62,7 +66,7 @@ function scanLine(text) {
62
66
  for (const match of matches) {
63
67
  findings.push({
64
68
  type: pattern.name,
65
- snippet: match[0].slice(0, 20) + '...' + match[0].slice(-4),
69
+ snippet: maskSecret(match[0]),
66
70
  });
67
71
  }
68
72
  }
@@ -77,49 +81,27 @@ function scanLine(text) {
77
81
  export async function run(sessionFile) {
78
82
  const id = 'SS1';
79
83
 
80
- if (!existsSync(sessionFile)) {
84
+ if (!sessionFile || !existsSync(sessionFile)) {
81
85
  return pass(id, Severity.CRITICAL, '会话文件不存在,跳过检查');
82
86
  }
83
87
 
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
88
  const findings = [];
109
89
  let lineCount = 0;
110
90
 
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 });
91
+ try {
92
+ lineCount = await scanSessionLines(sessionFile, (line, lineNo) => {
93
+ if (!line.trim()) return;
94
+ const text = extractTextFromLine(line);
95
+ if (!text) return;
96
+ for (const f of scanLine(text)) {
97
+ findings.push({ ...f, line: lineNo });
98
+ }
99
+ });
100
+ } catch (e) {
101
+ if (e.code === 'ZSTD_UNAVAILABLE') {
102
+ return skip(id, Severity.CRITICAL, 'zstd 命令不可用,无法解压压缩会话日志,跳过凭据泄露检测');
122
103
  }
104
+ throw e;
123
105
  }
124
106
 
125
107
  if (findings.length === 0) {
@@ -152,7 +134,7 @@ export const ss1Check = {
152
134
  name: 'credential-leak',
153
135
  severity: Severity.CRITICAL,
154
136
  phase: CheckPhase.POST_INSTALL,
155
- description: '会话日志凭据泄露检测',
137
+ description: '会话日志凭据泄露检测(支持 zstd,结果自动掩码)',
156
138
  src: 'builtin',
157
139
  runner: (sessionFile) => run(sessionFile),
158
140
  };
@@ -11,11 +11,11 @@
11
11
  * Phase: POST_INSTALL
12
12
  */
13
13
 
14
- import { existsSync, createReadStream } from 'node:fs';
15
- import { createInterface } from 'node:readline';
14
+ import { existsSync } from 'node:fs';
16
15
  import { Severity } from '../protocol/severity.mjs';
17
16
  import { CheckPhase } from '../protocol/phase.mjs';
18
- import { pass, fail } from '../protocol/check.mjs';
17
+ import { pass, fail, skip } from '../protocol/check.mjs';
18
+ import { scanSessionLines } from '../session-reader.mjs';
19
19
 
20
20
  const PII_PATTERNS = [
21
21
  { name: 'email', regex: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, severity: 'medium' },
@@ -24,6 +24,12 @@ const PII_PATTERNS = [
24
24
  { name: 'IPv4', regex: /\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b/g, severity: 'low' },
25
25
  ];
26
26
 
27
+ /** PII 掩码:保留类型可辨识度,不回显完整个人信息 */
28
+ function maskPII(s) {
29
+ if (s.length <= 6) return `***(${s.length} chars)`;
30
+ return `${s.slice(0, 2)}***${s.slice(-2)}(${s.length} chars)`;
31
+ }
32
+
27
33
  function extractTextFromLine(line) {
28
34
  try {
29
35
  const event = JSON.parse(line);
@@ -41,21 +47,25 @@ function extractTextFromLine(line) {
41
47
  export async function run(sessionFile) {
42
48
  const id = 'SS2';
43
49
  if (!sessionFile || !existsSync(sessionFile)) return pass(id, Severity.MEDIUM, '无会话文件,跳过 PII 检测');
44
- if (sessionFile.endsWith('.zstd')) return pass(id, Severity.MEDIUM, 'zstd 文件需先解压');
45
50
 
46
51
  const findings = [];
47
52
  let lineCount = 0;
48
- const rl = createInterface({ input: createReadStream(sessionFile, { encoding: 'utf8' }), crlfDelay: Infinity });
49
53
 
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) });
54
+ try {
55
+ lineCount = await scanSessionLines(sessionFile, (line, lineNo) => {
56
+ if (!line.trim()) return;
57
+ const text = extractTextFromLine(line);
58
+ for (const pattern of PII_PATTERNS) {
59
+ for (const match of text.matchAll(new RegExp(pattern.regex.source, 'g'))) {
60
+ findings.push({ type: pattern.name, severity: pattern.severity, line: lineNo, snippet: maskPII(match[0]) });
61
+ }
57
62
  }
63
+ });
64
+ } catch (e) {
65
+ if (e.code === 'ZSTD_UNAVAILABLE') {
66
+ return skip(id, Severity.MEDIUM, 'zstd 命令不可用,无法解压压缩会话日志,跳过 PII 检测');
58
67
  }
68
+ throw e;
59
69
  }
60
70
 
61
71
  if (findings.length === 0) return pass(id, Severity.MEDIUM, `扫描 ${lineCount} 行,未检测到 PII 暴露`);
@@ -10,11 +10,11 @@
10
10
  * Phase: POST_INSTALL
11
11
  */
12
12
 
13
- import { existsSync, createReadStream } from 'node:fs';
14
- import { createInterface } from 'node:readline';
13
+ import { existsSync } from 'node:fs';
15
14
  import { Severity } from '../protocol/severity.mjs';
16
15
  import { CheckPhase } from '../protocol/phase.mjs';
17
- import { pass, fail } from '../protocol/check.mjs';
16
+ import { pass, fail, skip } from '../protocol/check.mjs';
17
+ import { scanSessionLines } from '../session-reader.mjs';
18
18
 
19
19
  const SENSITIVE_OUTPUT_PATTERNS = [
20
20
  { name: 'env dump', regex: /(?:process\.env|ENV|env)\s*[=:]\s*\{[^}]{50,}/gi, severity: 'medium' },
@@ -35,23 +35,26 @@ function extractToolOutputs(line) {
35
35
  export async function run(sessionFile) {
36
36
  const id = 'SS3';
37
37
  if (!sessionFile || !existsSync(sessionFile)) return pass(id, Severity.LOW, '无会话文件,跳过敏感输出检测');
38
- if (sessionFile.endsWith('.zstd')) return pass(id, Severity.LOW, 'zstd 文件需先解压');
39
38
 
40
39
  const findings = [];
41
40
  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) });
41
+
42
+ try {
43
+ lineCount = await scanSessionLines(sessionFile, (line, lineNo) => {
44
+ if (!line.trim()) return;
45
+ for (const output of extractToolOutputs(line)) {
46
+ for (const pattern of SENSITIVE_OUTPUT_PATTERNS) {
47
+ for (const match of output.matchAll(new RegExp(pattern.regex.source, 'g'))) {
48
+ findings.push({ type: pattern.name, severity: pattern.severity, line: lineNo, snippet: match[0].slice(0, 60) });
49
+ }
52
50
  }
53
51
  }
52
+ });
53
+ } catch (e) {
54
+ if (e.code === 'ZSTD_UNAVAILABLE') {
55
+ return skip(id, Severity.LOW, 'zstd 命令不可用,无法解压压缩会话日志,跳过敏感输出检测');
54
56
  }
57
+ throw e;
55
58
  }
56
59
 
57
60
  if (findings.length === 0) return pass(id, Severity.LOW, `扫描 ${lineCount} 行,未检测到敏感输出`);
package/src/config.mjs CHANGED
@@ -2,42 +2,47 @@
2
2
  * DSH Security Framework — 配置管理
3
3
  *
4
4
  * 支持 ~/.dsh/security.json 配置文件:
5
- * - 启用/禁用特定检查
6
- * - 覆盖 severity 阈值
7
- * - 配置外部工具集成
5
+ * - enabled: false → 停用整个安全框架(缺省/文件不存在 = 全部启用)
6
+ * - checks.{ID}.enabled: false → 停用单个检查
7
+ * - severityThreshold: "low"|"medium"|"high" → 低于阈值的失败降级为 skip
8
+ *
9
+ * 注意:配置只在显式 setConfig() 注入 registry 后生效;未注入时所有检查启用。
8
10
  */
9
11
 
10
12
  import { readFileSync, existsSync } from 'node:fs';
11
13
  import { join } from 'node:path';
12
14
  import { homedir } from 'node:os';
13
15
 
14
- const DEFAULT_CONFIG = {
15
- enabled: false,
16
+ const DEFAULT_CONFIG = Object.freeze({
17
+ enabled: true,
16
18
  checks: {},
17
- external: {},
18
- severityThreshold: 'info',
19
- autoRedact: true,
20
- };
19
+ severityThreshold: null,
20
+ });
21
21
 
22
22
  export function loadConfig(dshHome) {
23
23
  const configPath = join(dshHome || join(homedir(), '.dsh'), 'security.json');
24
- if (!existsSync(configPath)) return DEFAULT_CONFIG;
24
+ if (!existsSync(configPath)) return { ...DEFAULT_CONFIG };
25
25
  try {
26
26
  const raw = JSON.parse(readFileSync(configPath, 'utf8'));
27
- return { ...DEFAULT_CONFIG, ...raw };
27
+ return {
28
+ ...DEFAULT_CONFIG,
29
+ ...raw,
30
+ checks: raw.checks && typeof raw.checks === 'object' ? raw.checks : {},
31
+ };
28
32
  } catch {
29
- return DEFAULT_CONFIG;
33
+ return { ...DEFAULT_CONFIG };
30
34
  }
31
35
  }
32
36
 
33
37
  export function isCheckEnabled(config, checkId) {
34
- if (!config.enabled) return false;
35
- const checkConfig = config.checks[checkId];
38
+ if (!config) return true;
39
+ if (config.enabled === false) return false;
40
+ const checkConfig = config.checks && config.checks[checkId];
36
41
  if (checkConfig && checkConfig.enabled === false) return false;
37
42
  return true;
38
43
  }
39
44
 
40
45
  export function getCheckSeverity(config, checkId, defaultSeverity) {
41
- const checkConfig = config.checks[checkId];
42
- return checkConfig?.severity || defaultSeverity;
46
+ const checkConfig = config && config.checks ? config.checks[checkId] : undefined;
47
+ return (checkConfig && checkConfig.severity) || defaultSeverity;
43
48
  }
package/src/index.mjs CHANGED
@@ -12,7 +12,8 @@
12
12
  // Protocol
13
13
  export { Severity, severityToExitCode, severityGte, maxSeverity } from './protocol/severity.mjs';
14
14
  export { CheckPhase } from './protocol/phase.mjs';
15
- export { createResult, pass, fail } from './protocol/check.mjs';
15
+ export { createResult, pass, fail, skip } from './protocol/check.mjs';
16
+ export { isZstdFile, scanSessionLines } from './session-reader.mjs';
16
17
 
17
18
  // Layer 1: Static Checks
18
19
  export { sp1Check } from './checks/sp1-dependency-audit.mjs';
@@ -5,50 +5,87 @@
5
5
  * - 已知 bug 状态
6
6
  * - 发布兼容性报告
7
7
  * - 生态健康信号
8
+ *
9
+ * 复审修复:
10
+ * - 旧实现 fetch raw.githubusercontent.com 的目录 URL——raw 不提供目录列表,必然 404,
11
+ * release-compat 半边永远拿不到数据;现改走 GitHub contents API 列目录并取最新文件。
12
+ * - bug 雷达不再硬编码 weekly-2026-08-15.md,自动取 docs/ 下最新的 weekly-*.md。
13
+ * - 数据源不可用返回 skip 而不是伪装通过。
8
14
  */
9
15
 
10
16
  import { Severity } from '../protocol/severity.mjs';
11
17
  import { CheckPhase } from '../protocol/phase.mjs';
18
+ import { skip } from '../protocol/check.mjs';
19
+
20
+ const REPO_DOCS_API = 'https://api.github.com/repos/zoahdev/dsh-ecosystem/contents/docs';
21
+ const RAW_BASE = 'https://raw.githubusercontent.com/zoahdev/dsh-ecosystem/main/docs';
22
+
23
+ const GH_HEADERS = {
24
+ 'User-Agent': 'dsh-security',
25
+ 'Accept': 'application/vnd.github+json',
26
+ };
27
+
28
+ async function ghFetch(url, accept) {
29
+ const response = await fetch(url, {
30
+ headers: { ...GH_HEADERS, ...(accept ? { Accept: accept } : {}) },
31
+ signal: AbortSignal.timeout(10000),
32
+ });
33
+ if (!response.ok) return null;
34
+ return response;
35
+ }
12
36
 
13
- const ECOSYSTEM_API = 'https://raw.githubusercontent.com/zoahdev/dsh-ecosystem/main/docs';
37
+ /** docs/<sub> 目录下的 .md 文件名(GitHub contents API),按名称倒序 */
38
+ async function listMarkdownFiles(sub = '') {
39
+ try {
40
+ const response = await ghFetch(sub ? `${REPO_DOCS_API}/${sub}` : REPO_DOCS_API);
41
+ if (!response || !response.ok) return null;
42
+ const entries = await response.json();
43
+ if (!Array.isArray(entries)) return null;
44
+ return entries
45
+ .filter(e => e.type === 'file' && e.name.endsWith('.md'))
46
+ .map(e => e.name)
47
+ .sort()
48
+ .reverse();
49
+ } catch { return null; }
50
+ }
51
+
52
+ async function fetchRaw(sub, name) {
53
+ try {
54
+ const response = await ghFetch(`${RAW_BASE}/${sub ? sub + '/' : ''}${name}`);
55
+ if (!response || !response.ok) return null;
56
+ return await response.text();
57
+ } catch { return null; }
58
+ }
14
59
 
15
60
  /**
16
- * 获取发布兼容性报告
61
+ * 获取最新发布兼容性报告
17
62
  */
18
63
  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; }
64
+ const names = await listMarkdownFiles('release-compat');
65
+ if (!names || names.length === 0) return null;
66
+ const latest = names.find(n => /^release-compat-\d[\d-]*\.md$/.test(n));
67
+ if (!latest) return null;
68
+ return fetchRaw('release-compat', latest);
31
69
  }
32
70
 
33
71
  /**
34
- * 获取 bug 雷达
72
+ * 获取最新一期 bug 雷达周报
35
73
  */
36
74
  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; }
75
+ const names = await listMarkdownFiles('');
76
+ if (!names) return null;
77
+ const latest = names.find(n => /^weekly-\d{4}-\d{2}-\d{2}\.md$/.test(n));
78
+ if (!latest) return null;
79
+ return fetchRaw('', latest);
42
80
  }
43
81
 
44
82
  export async function runCheck(profileDir) {
45
83
  const id = 'EXT-ECO-1';
46
84
 
47
- const releaseNotes = await fetchReleaseCompat();
48
- const bugRadar = await fetchBugRadar();
85
+ const [releaseNotes, bugRadar] = await Promise.all([fetchReleaseCompat(), fetchBugRadar()]);
49
86
 
50
87
  if (!releaseNotes && !bugRadar) {
51
- return { id, ok: true, severity: Severity.LOW, detail: 'dsh-ecosystem 数据源不可用,跳过生态兼容性检查' };
88
+ return skip(id, Severity.LOW, 'dsh-ecosystem 数据源不可达(离线或仓库无数据),跳过生态兼容性检查');
52
89
  }
53
90
 
54
91
  const issues = [];
@@ -2,22 +2,21 @@
2
2
  * dsh-plugin-reducer 集成
3
3
  *
4
4
  * 在检测到故障时,提供最小化故障插件集的能力。
5
- * 如果 dsh-plugin-reducer 已安装,自动建议运行。
5
+ * 如果 dsh-plugin-reducer 已安装(PATH 中可执行),自动建议运行。
6
+ * 复审修复:不再用 `npx 包名` 探测/执行;执行失败返回 skip 而不是伪装通过。
6
7
  */
7
8
 
8
- import { execSync } from 'node:child_process';
9
+ import { execFileSync } from 'node:child_process';
9
10
  import { Severity } from '../protocol/severity.mjs';
10
11
  import { CheckPhase } from '../protocol/phase.mjs';
12
+ import { skip } from '../protocol/check.mjs';
11
13
 
12
14
  export function isAvailable() {
13
15
  try {
14
- execSync('which dsh-plugin-reducer', { encoding: 'utf8', stdio: 'pipe' });
16
+ execFileSync('which', ['dsh-plugin-reducer'], { encoding: 'utf8', stdio: 'pipe' });
15
17
  return true;
16
18
  } catch {
17
- try {
18
- execSync('npx dsh-plugin-reducer --version', { encoding: 'utf8', stdio: 'pipe', timeout: 10000 });
19
- return true;
20
- } catch { return false; }
19
+ return false;
21
20
  }
22
21
  }
23
22
 
@@ -25,15 +24,19 @@ export async function runReducer(profileDir, probeType = 'web') {
25
24
  const id = 'EXT-RED-1';
26
25
 
27
26
  if (!isAvailable()) {
28
- return { id, ok: true, severity: Severity.LOW, detail: 'dsh-plugin-reducer 未安装,跳过故障最小化' };
27
+ return skip(id, Severity.LOW, 'dsh-plugin-reducer 未安装,跳过故障最小化');
29
28
  }
30
29
 
31
30
  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
- });
31
+ const output = execFileSync(
32
+ 'dsh-plugin-reducer',
33
+ ['--profile', String(profileDir), '--probe', String(probeType), '--report', '/tmp/reducer-report.json', '--json'],
34
+ {
35
+ encoding: 'utf8',
36
+ timeout: 120000,
37
+ stdio: ['pipe', 'pipe', 'pipe'],
38
+ }
39
+ );
37
40
 
38
41
  const result = JSON.parse(output);
39
42
 
@@ -50,7 +53,7 @@ export async function runReducer(profileDir, probeType = 'web') {
50
53
 
51
54
  return { id, ok: true, severity: Severity.LOW, detail: 'dsh-plugin-reducer 未发现故障插件集' };
52
55
  } catch (e) {
53
- return { id, ok: true, severity: Severity.LOW, detail: `dsh-plugin-reducer 执行失败:${e.message.slice(0, 80)}` };
56
+ return skip(id, Severity.LOW, `dsh-plugin-reducer 执行失败,跳过:${e.message.slice(0, 80)}`);
54
57
  }
55
58
  }
56
59
 
@@ -1,30 +1,24 @@
1
1
  /**
2
2
  * dsh-poison-guard 集成
3
3
  *
4
- * 通过 Plugin Interface 注册投毒扫描检查。
5
- * 如果 dsh-poison-guard 已安装,自动集成到安全检查流程。
4
+ * 如果 dsh-poison-guard 已安装(PATH 中可执行),自动集成到安全检查流程。
5
+ * 复审修复:不再用 `npx 包名` 探测/执行;执行失败返回 skip 而不是伪装通过。
6
6
  */
7
7
 
8
- import { execSync } from 'node:child_process';
9
- import { existsSync } from 'node:fs';
8
+ import { execFileSync } from 'node:child_process';
10
9
  import { Severity } from '../protocol/severity.mjs';
11
10
  import { CheckPhase } from '../protocol/phase.mjs';
11
+ import { skip } from '../protocol/check.mjs';
12
12
 
13
13
  /**
14
14
  * 检测 dsh-poison-guard 是否可用
15
15
  */
16
16
  export function isAvailable() {
17
17
  try {
18
- execSync('which dsh-poison-guard', { encoding: 'utf8', stdio: 'pipe' });
18
+ execFileSync('which', ['dsh-poison-guard'], { encoding: 'utf8', stdio: 'pipe' });
19
19
  return true;
20
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
- }
21
+ return false;
28
22
  }
29
23
  }
30
24
 
@@ -37,11 +31,11 @@ export async function runScan(targetPath) {
37
31
  const id = 'EXT-PG-1';
38
32
 
39
33
  if (!isAvailable()) {
40
- return { id, ok: true, severity: Severity.HIGH, detail: 'dsh-poison-guard 未安装,跳过投毒扫描' };
34
+ return skip(id, Severity.HIGH, 'dsh-poison-guard 未安装,跳过投毒扫描');
41
35
  }
42
36
 
43
37
  try {
44
- const output = execSync(`npx dsh-poison-guard scan "${targetPath}" --json`, {
38
+ const output = execFileSync('dsh-poison-guard', ['scan', String(targetPath), '--json'], {
45
39
  encoding: 'utf8',
46
40
  timeout: 60000,
47
41
  stdio: ['pipe', 'pipe', 'pipe'],
@@ -64,7 +58,7 @@ export async function runScan(targetPath) {
64
58
  fix: '检查相关插件的源码,移除恶意代码',
65
59
  };
66
60
  } catch (e) {
67
- return { id, ok: true, severity: Severity.HIGH, detail: `dsh-poison-guard 执行失败:${e.message.slice(0, 80)}` };
61
+ return skip(id, Severity.HIGH, `dsh-poison-guard 执行失败,跳过:${e.message.slice(0, 80)}`);
68
62
  }
69
63
  }
70
64
 
@@ -1,23 +1,22 @@
1
1
  /**
2
2
  * dsh-sandbox-audit 集成
3
3
  *
4
- * 通过 Plugin Interface 注册沙箱策略审计检查。
5
- * 如果 dsh-sandbox-audit 已安装,自动集成到安全检查流程。
4
+ * 如果 dsh-sandbox-audit 已安装(PATH 中可执行),自动集成到安全检查流程。
5
+ * 复审修复:不再用 `npx 包名` 探测可用性——那会在用户机器上触发任意包的下载执行;
6
+ * 执行失败现在返回 skip 而不是伪装成通过。
6
7
  */
7
8
 
8
- import { execSync } from 'node:child_process';
9
+ import { execFileSync } from 'node:child_process';
9
10
  import { Severity } from '../protocol/severity.mjs';
10
11
  import { CheckPhase } from '../protocol/phase.mjs';
12
+ import { skip } from '../protocol/check.mjs';
11
13
 
12
14
  export function isAvailable() {
13
15
  try {
14
- execSync('which dsh-sandbox-audit', { encoding: 'utf8', stdio: 'pipe' });
16
+ execFileSync('which', ['dsh-sandbox-audit'], { encoding: 'utf8', stdio: 'pipe' });
15
17
  return true;
16
18
  } catch {
17
- try {
18
- execSync('npx dsh-sandbox-audit --version', { encoding: 'utf8', stdio: 'pipe', timeout: 10000 });
19
- return true;
20
- } catch { return false; }
19
+ return false;
21
20
  }
22
21
  }
23
22
 
@@ -25,11 +24,11 @@ export async function runAudit(profileDir) {
25
24
  const id = 'EXT-SA-1';
26
25
 
27
26
  if (!isAvailable()) {
28
- return { id, ok: true, severity: Severity.MEDIUM, detail: 'dsh-sandbox-audit 未安装,跳过沙箱策略审计' };
27
+ return skip(id, Severity.MEDIUM, 'dsh-sandbox-audit 未安装,跳过沙箱策略审计');
29
28
  }
30
29
 
31
30
  try {
32
- const output = execSync(`npx dsh-sandbox-audit "${profileDir}" --json`, {
31
+ const output = execFileSync('dsh-sandbox-audit', [String(profileDir), '--json'], {
33
32
  encoding: 'utf8',
34
33
  timeout: 60000,
35
34
  stdio: ['pipe', 'pipe', 'pipe'],
@@ -52,7 +51,7 @@ export async function runAudit(profileDir) {
52
51
  fix: '参考 dsh-sandbox-audit 文档修复沙箱配置',
53
52
  };
54
53
  } catch (e) {
55
- return { id, ok: true, severity: Severity.MEDIUM, detail: `dsh-sandbox-audit 执行失败:${e.message.slice(0, 80)}` };
54
+ return skip(id, Severity.MEDIUM, `dsh-sandbox-audit 执行失败,跳过:${e.message.slice(0, 80)}`);
56
55
  }
57
56
  }
58
57
 
@@ -47,3 +47,12 @@ export function pass(id, severity, detail) {
47
47
  export function fail(id, severity, detail, fix, references = []) {
48
48
  return createResult(id, false, severity, detail, fix, references);
49
49
  }
50
+
51
+ /**
52
+ * 跳过结果——检查因外部条件不满足而未真正执行(如网络不可达、依赖工具缺失)。
53
+ * skipped=true 的结果不计入失败统计,也不影响退出码;
54
+ * detail 必须说明原因(对齐 #1719 r5 词汇表「skip 必须带 reason」)。
55
+ */
56
+ export function skip(id, severity, detail) {
57
+ return { id, ok: true, skipped: true, severity, detail };
58
+ }