@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.
package/README.md CHANGED
@@ -1,46 +1,138 @@
1
1
  # dsh-security
2
2
 
3
- [![CI](https://img.shields.io/badge/CI-passing-brightgreen)](#)
4
3
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
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)](#)
6
6
 
7
- **DSH 生态统一安全检查框架** — 覆盖插件全生命周期(发现→安装→运行→更新→退役)。
7
+ **DSH 生态统一安全检查框架** — 覆盖插件全生命周期(发现→安装→运行→更新→退役),17 个内置检查 + 4 个外部工具集成。
8
8
 
9
9
  > Community security tool. Not an official DeepSeek project.
10
10
 
11
- ## 定位
11
+ ---
12
12
 
13
- ```
14
- dsh-doctor ← 执行者(运行检查)
15
- dsh-security ← 检查库(定义检查)
16
- dsh-poison-guard ← 外部集成(投毒扫描)
17
- dsh-sandbox-audit ← 外部集成(沙箱审计)
18
- dsh-redact ← 外部集成(日志脱敏)
19
- ```
13
+ ## 项目背景
14
+
15
+ DeepSeek Harness(DSH)生态在 0.1.0-rc.6 开源后爆发式增长:2958 个插件仓库、8+ 精选列表、新插件每小时出现。但**没有统一的安全检查框架**——安全工具各自为战,用户无法一键获得完整的安全态势感知。
16
+
17
+ dsh-security 的目标:**建立 DSH 生态的统一安全检查层**,让任何工具都能贡献检查,让用户一键发现安全问题。
18
+
19
+ ---
20
+
21
+ ## 开发历程
22
+
23
+ ### Phase 1:基础框架 + 核心检查(Day 1)
24
+
25
+ - 设计 4 层架构(Check Protocol → Static → Runtime → Lifecycle)
26
+ - 实现 Layer 0:Severity/CheckPhase 枚举、SecurityCheck 接口、v1 信封扩展
27
+ - 实现 SP2(密钥扫描)和 SS1(凭据泄露检测)——最高价值的两个检查
28
+ - 50/50 测试全绿
29
+
30
+ ### Phase 2:静态检查完整 + 外部集成(Day 1)
31
+
32
+ - 实现 SP1(依赖链审计)、SP3(沙箱策略一致性)
33
+ - 实现 Plugin Interface——外部工具可通过 JSON 注册检查
34
+ - 集成 dsh-poison-guard、dsh-sandbox-audit、dsh-ecosystem、dsh-plugin-reducer
35
+ - dsh-doctor `--security` 标志集成
36
+
37
+ ### Phase 3:运行时检查(Day 1)
38
+
39
+ - 实现 SR1(沙箱逃逸检测)——检测 mount remount、sudo、/etc 访问等
40
+ - 实现 SR3(数据外泄检测)——检测凭据转发、网络外发
41
+ - 基于会话日志分析的运行时安全检测
42
+
43
+ ### Phase 4:生命周期检查 + 生态集成(Day 1)
44
+
45
+ - 实现 SL1(供应链完整性)、SL4(发布兼容性)
46
+ - 实现 SP4(恶意 entry)、SP5(权限验证)、SP6(漏洞匹配)
47
+ - 实现 SR2(权限提升)、SR4(隔离验证)
48
+ - 实现 SL2(更新完整性)、SL3(信誉评分)
49
+ - 实现 SS2(PII 检测)、SS3(敏感输出检测)
50
+ - `~/.dsh/security.json` 配置文件支持
51
+ - `--security-only` 标志
52
+
53
+ ### 部署验证(Day 1)
54
+
55
+ - 同步到用户 profile,Web GUI 诊断面板显示 🔒 Security 区域
56
+ - 修复 SL3 对 @local/* 包的误报
57
+ - 50/50 测试全绿 + dsh-doctor 54/54 测试全绿
58
+
59
+ ---
20
60
 
21
61
  ## 架构
22
62
 
23
63
  ```
24
- ┌──────────────────────────────────────────────────┐
25
- │ Layer 0: Check Protocol(统一协议)
26
- │ • Severity: CRITICAL/HIGH/MEDIUM/LOW/INFO
27
- │ • Phase: PRE/POST/RUNTIME/LIFECYCLE
28
- │ • Plugin Interface(外部工具注册检查)
29
- ├──────────────────────────────────────────────────┤
30
- │ Layer 1: Static Checks(静态检查)
31
- • SP2: 密钥扫描(硬编码 API key/token/私钥)
32
- • SS1: 凭据泄露检测(会话日志扫描)
33
- ├──────────────────────────────────────────────────┤
34
- Layer 2: Runtime Checks(运行时检查)
35
- • SR1-SR4: 即将实现
36
- ├──────────────────────────────────────────────────┤
37
- Layer 3: Lifecycle Checks(生命周期检查)
38
- • SL1-SL4: 即将实现
39
- └──────────────────────────────────────────────────┘
64
+ ┌──────────────────────────────────────────────────────────┐
65
+ │ Layer 0: Check Protocol(统一协议)
66
+ │ • Severity: CRITICAL / HIGH / MEDIUM / LOW / INFO
67
+ │ • Phase: PRE_INSTALL / POST_INSTALL / RUNTIME / LIFECYCLE│
68
+ │ • Plugin Interface(外部工具注册检查)
69
+ ├──────────────────────────────────────────────────────────┤
70
+ │ Layer 1: Static Checks(静态检查,安装前后)
71
+ SP1: 依赖链审计 (npm audit)
72
+ SP2: 密钥扫描 (正则匹配 API key/token/私钥)
73
+ │ SP3: 沙箱策略一致性 (cordis.patch.yml 审计) │
74
+ SP4: 恶意 entry 注入检测
75
+ SP5: 插件权限声明验证
76
+ │ SP6: 已知漏洞匹配 (OSV/CVE/GHSA) │
77
+ SS1: 凭据泄露检测 (会话日志)
78
+ SS2: PII 数据暴露检测
79
+ │ SS3: 插件输出敏感数据检测 │
80
+ ├──────────────────────────────────────────────────────────┤
81
+ │ Layer 2: Runtime Checks(运行时,基于会话日志) │
82
+ │ SR1: 沙箱逃逸检测 (#1769 mount remount 等) │
83
+ │ SR2: 权限提升检测 (sudo/chmod/chown) │
84
+ │ SR3: 数据外泄检测 (凭据转发/网络外发) │
85
+ │ SR4: 插件隔离验证 (跨插件数据泄漏) │
86
+ ├──────────────────────────────────────────────────────────┤
87
+ │ Layer 3: Lifecycle Checks(生命周期) │
88
+ │ SL1: 供应链完整性 (npm registry 一致性) │
89
+ │ SL2: 更新完整性 (版本回退检测) │
90
+ │ SL3: 插件信誉评分 (维护者/更新频率) │
91
+ │ SL4: 发布兼容性 (dist-tags 一致性) │
92
+ ├──────────────────────────────────────────────────────────┤
93
+ │ External Integrations(外部工具集成) │
94
+ │ EXT-PG-1: dsh-poison-guard (投毒扫描) │
95
+ │ EXT-SA-1: dsh-sandbox-audit (沙箱审计) │
96
+ │ EXT-ECO-1: dsh-ecosystem (生态兼容性) │
97
+ │ EXT-RED-1: dsh-plugin-reducer (故障最小化) │
98
+ └──────────────────────────────────────────────────────────┘
40
99
  ```
41
100
 
101
+ ---
102
+
42
103
  ## 快速开始
43
104
 
105
+ ### CLI 使用
106
+
107
+ ```bash
108
+ # 只跑安全检查(最快)
109
+ dsh-doctor --security --security-only --profile web
110
+
111
+ # 完整诊断 + 安全检查
112
+ dsh-doctor --json --envelope --security --profile web
113
+
114
+ # 检查会话日志
115
+ dsh-doctor --security --session /path/to/session.jsonl
116
+
117
+ # JSON 输出(程序化处理)
118
+ dsh-doctor --json --envelope --security --profile web
119
+ ```
120
+
121
+ ### Web GUI
122
+
123
+ 重启 dsh web 后,设置→诊断面板会显示 🔒 Security 区域:
124
+
125
+ ```
126
+ == 🔒 安全 ==
127
+ ✅ [SP1] 依赖链无已知漏洞
128
+ ✅ [SP2] 未检测到硬编码密钥
129
+ ✅ [SP3] 沙箱策略配置一致
130
+ ❌ [SL1] 检测到 2 个供应链问题
131
+ ...
132
+ ```
133
+
134
+ ### 编程接口
135
+
44
136
  ```javascript
45
137
  import { createDefaultRegistry } from '@moonquake2004/dsh-security';
46
138
 
@@ -49,37 +141,94 @@ const { results, exitCode, summary } = await registry.runAll(
49
141
  (check) => profileDir // 为每个检查提供上下文
50
142
  );
51
143
 
52
- console.log({ exitCode, summary });
53
- // { exitCode: 0, summary: { critical: 0, high: 0, medium: 0, low: 0, info: 0 } }
144
+ // summary: { critical: 0, high: 0, medium: 0, low: 0, info: 0, skipped: 0 }
145
+ // results skipped=true 的项代表"因外部条件未真正执行"(离线、工具缺失等),
146
+ // 不计入失败统计,也不影响 exitCode。
147
+ // exitCode: 0=通过或只有 MEDIUM 及以下失败, 1=有HIGH, 2=有CRITICAL
54
148
  ```
55
149
 
56
- ## 内置检查
150
+ ### 配置注入(可选)
57
151
 
58
- | ID | 名称 | Severity | Phase | 描述 |
59
- |---|---|---|---|---|
60
- | SP2 | secret-scan | HIGH/CRITICAL | POST_INSTALL | 配置文件硬编码密钥检测 |
61
- | SS1 | credential-leak | CRITICAL | POST_INSTALL | 会话日志凭据泄露检测 |
152
+ ```javascript
153
+ import { createDefaultRegistry, loadConfig } from '@moonquake2004/dsh-security';
154
+
155
+ const registry = await createDefaultRegistry();
156
+ registry.setConfig(loadConfig()); // 读 ~/.dsh/security.json;不注入则全部启用
157
+ ```
158
+
159
+ ### 退出码含义
160
+
161
+ | exitCode | 含义 |
162
+ |---|---|
163
+ | 0 | 全部通过或只有 INFO/LOW |
164
+ | 1 | 有 HIGH 级别问题 |
165
+ | 2 | 有 CRITICAL 级别问题 |
166
+
167
+ ---
62
168
 
63
169
  ## 外部工具集成
64
170
 
65
- 通过 `~/.dsh/security/checks.d/*.json` 注册外部检查:
171
+ dsh-security 通过自动探测集成以下优秀开源项目。**探测方式为检查 `PATH` 中的可执行文件**(不会用 `npx` 触发包下载执行);未安装时对应检查返回 skip 并注明原因,网络类数据源离线时同样 skip 而非谎报通过。
172
+
173
+ dsh-security 通过 Plugin Interface 集成以下优秀开源项目:
174
+
175
+ ### [dsh-poison-guard](https://github.com/zoahdev/dsh-poison-guard) — 投毒扫描
176
+
177
+ > **感谢 [@zoahdev](https://github.com/zoahdev)** 开发的投毒扫描工具,提供 AST 分析(JS-X-Ray)+ 反混淆解码能力。
178
+
179
+ dsh-security 自动检测 dsh-poison-guard 是否已安装,如已安装则集成投毒扫描检查(EXT-PG-1)。
180
+
181
+ ### [dsh-sandbox-audit](https://github.com/zoahdev/dsh-sandbox-audit) — 沙箱策略审计
182
+
183
+ > **感谢 [@zoahdev](https://github.com/zoahdev)** 开发的沙箱策略审计工具,提供静态 cordis.patch.yml 策略一致性检查。
184
+
185
+ dsh-security 自动检测 dsh-sandbox-audit 是否已安装,如已安装则集成沙箱审计检查(EXT-SA-1)。
186
+
187
+ ### [dsh-ecosystem](https://github.com/zoahdev/dsh-ecosystem) — 生态兼容性数据
188
+
189
+ > **感谢 [@zoahdev](https://github.com/zoahdev)** 维护的 DSH 生态地图,提供发布兼容性报告、bug 雷达、生态健康数据。
190
+
191
+ dsh-security 通过网络 API 获取生态兼容性数据,自动检查已知 breaking changes 和 critical bugs(EXT-ECO-1)。
192
+
193
+ ### [dsh-plugin-reducer](https://github.com/ArmyWas/dsh-plugin-reducer) — 故障最小化
194
+
195
+ > **感谢 [@ArmyWas](https://github.com/ArmyWas)** 开发的插件故障最小化工具,用 delta debugging 找到最小故障插件集。
196
+
197
+ dsh-security 自动检测 dsh-plugin-reducer 是否已安装,如已安装则在检测到故障时提供最小化建议(EXT-RED-1)。
198
+
199
+ ### [npm audit](https://docs.npmjs.com/cli/v9/commands/npm-audit) — 依赖漏洞扫描
200
+
201
+ SP1 检查集成 npm audit,自动扫描 profile 中依赖的已知漏洞。
202
+
203
+ ### [OSV](https://osv.dev/) — 已知漏洞数据库
204
+
205
+ SP6 检查通过 OSV API 查询 npm 包的已知漏洞(CVE/GHSA)。
206
+
207
+ ---
208
+
209
+ ## 配置
210
+
211
+ 创建 `~/.dsh/security.json` 自定义安全检查行为(需调用方注入 `registry.setConfig(loadConfig())` 才生效;未注入时全部检查启用):
66
212
 
67
213
  ```json
68
214
  {
69
- "source": "dsh-poison-guard",
70
- "version": "0.2.0",
71
- "checks": [
72
- {
73
- "id": "EXT-PG-1",
74
- "name": "poison-scan",
75
- "severity": "high",
76
- "phase": "pre-install",
77
- "command": "dsh-poison-guard scan --json"
78
- }
79
- ]
215
+ "enabled": true,
216
+ "checks": {
217
+ "SP1": { "enabled": true },
218
+ "SR1": { "enabled": false }
219
+ },
220
+ "severityThreshold": "medium"
80
221
  }
81
222
  ```
82
223
 
224
+ - `enabled: false`:停用整个框架
225
+ - `checks.{ID}.enabled: false`:停用单个检查
226
+ - `severityThreshold`(`low`/`medium`/`high`):低于阈值的失败降级为 skip,不进失败统计与退出码
227
+
228
+ 会话日志检查(SS1/SS2/SR1-SR4)支持 zstd 压缩的 `session.jsonl.zstd`,依赖系统 `zstd` 命令。
229
+
230
+ ---
231
+
83
232
  ## 开发
84
233
 
85
234
  ```bash
@@ -90,12 +239,44 @@ node --test test/*.mjs
90
239
  # 1. 创建 src/checks/xx-check-name.mjs
91
240
  # 2. 导出 runner 函数和 check 对象
92
241
  # 3. 在 src/checks/index.mjs 中注册
93
- # 4. 添加测试
242
+ # 4. 在 src/registry.mjs 中添加 import
243
+ # 5. 添加测试
94
244
  ```
95
245
 
96
- ## 设计文档
246
+ ### 项目结构
247
+
248
+ ```
249
+ dsh-security/
250
+ ├── src/
251
+ │ ├── protocol/ # Layer 0: Check Protocol
252
+ │ ├── checks/ # 17 个内置检查
253
+ │ ├── integrations/ # 4 个外部工具集成
254
+ │ ├── session-reader.mjs # 会话日志读取(明文 + zstd)
255
+ │ ├── registry.mjs # Check Registry(支持 setConfig 注入配置)
256
+ │ ├── config.mjs # 配置管理
257
+ │ └── index.mjs # 主入口
258
+ ├── test/ # 测试
259
+ └── docs/plans/design.md # 设计文档
260
+ ```
261
+
262
+ ---
263
+
264
+ ## 致谢
265
+
266
+ 本项目的实现得益于以下开源项目的启发和集成:
267
+
268
+ | 项目 | 作者 | 贡献 |
269
+ |---|---|---|
270
+ | [dsh-poison-guard](https://github.com/zoahdev/dsh-poison-guard) | [@zoahdev](https://github.com/zoahdev) | AST 投毒扫描能力 |
271
+ | [dsh-sandbox-audit](https://github.com/zoahdev/dsh-sandbox-audit) | [@zoahdev](https://github.com/zoahdev) | 沙箱策略审计方法论 |
272
+ | [dsh-ecosystem](https://github.com/zoahdev/dsh-ecosystem) | [@zoahdev](https://github.com/zoahdev) | 生态兼容性数据源 |
273
+ | [dsh-plugin-reducer](https://github.com/ArmyWas/dsh-plugin-reducer) | [@ArmyWas](https://github.com/ArmyWas) | 故障最小化算法 |
274
+ | [dsh-doctor](https://github.com/moonquake2004/dsh-doctor) | [@moonquake2004](https://github.com/moonquake2004) | 执行框架 + P1-P14 检查 |
275
+ | [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) | [@deepseek-ai](https://github.com/deepseek-ai) | DSH 平台 |
276
+
277
+ 特别感谢 DSH 生态的社区贡献者们,你们的工作让这个安全框架成为可能。
97
278
 
98
- - [完整设计](docs/plans/2026-08-18-security-framework-design.md)
279
+ ---
99
280
 
100
281
  ## License
101
282
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moonquake2004/dsh-security",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
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",
@@ -2,9 +2,12 @@
2
2
  * SL1: Supply Chain Integrity — 供应链完整性验证
3
3
  *
4
4
  * 验证 profile 中已安装的包与 npm registry 上的版本一致:
5
- * - 版本号匹配
6
- * - integrity hash 匹配(SHA-512)
7
- * - 检测潜在的包劫持(版本存在但内容不匹配)
5
+ * - 版本号匹配(本地 vs dist-tags.latest)
6
+ * - integrity hash 匹配:以 pnpm-lock.yaml 中锁定的 sha512 为本地基准,
7
+ * registry versions[localVersion].dist.integrity 比对,检测发布被篡改/覆盖。
8
+ * (复审修复:安装产物 package.json 本身不含 integrity 字段,旧实现该分支永不生效)
9
+ *
10
+ * 网络不可达时返回 skip(不再静默当作通过)。
8
11
  *
9
12
  * Severity: HIGH
10
13
  * Phase: LIFECYCLE
@@ -12,17 +15,17 @@
12
15
 
13
16
  import { readFileSync, existsSync } from 'node:fs';
14
17
  import { join } from 'node:path';
15
- import { execSync } from 'node:child_process';
16
18
  import { Severity, maxSeverity } from '../protocol/severity.mjs';
17
19
  import { CheckPhase } from '../protocol/phase.mjs';
18
- import { pass, fail } from '../protocol/check.mjs';
20
+ import { pass, fail, skip } from '../protocol/check.mjs';
19
21
 
20
22
  /**
21
23
  * 获取 npm registry 上的包信息
22
24
  */
23
25
  async function fetchRegistryInfo(pkgName) {
24
26
  try {
25
- const url = `https://registry.npmjs.org/${encodeURIComponent(pkgName).replace(/%2f/g, '/')}`;
27
+ // encodeURIComponent scoped 名产生 %2F,npm registry 接受该规范形式
28
+ const url = `https://registry.npmjs.org/${encodeURIComponent(pkgName)}`;
26
29
  const response = await fetch(url, { signal: AbortSignal.timeout(10000) });
27
30
  if (!response.ok) return null;
28
31
  return await response.json();
@@ -31,10 +34,25 @@ async function fetchRegistryInfo(pkgName) {
31
34
  }
32
35
  }
33
36
 
37
+ /**
38
+ * 从 pnpm-lock.yaml 提取 <name>@<version> 锁定的 integrity(sha512-...)。
39
+ * 零依赖实现:只做定位 key 行 + 向后小窗口找 resolution.integrity。
40
+ */
41
+ export function extractLockIntegrity(lockText, pkgName, version) {
42
+ if (!lockText || !pkgName || !version) return null;
43
+ const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
44
+ const keyRe = new RegExp(`^\\s{2}['"]?${esc(pkgName)}@${esc(version)}['"]?:\\s*$`, 'm');
45
+ const m = keyRe.exec(lockText);
46
+ if (!m) return null;
47
+ const window = lockText.slice(m.index, m.index + 500);
48
+ const im = /integrity:\s*(sha[0-9]+-[A-Za-z0-9+/=]+)/.exec(window);
49
+ return im ? im[1] : null;
50
+ }
51
+
34
52
  /**
35
53
  * 比较本地安装版本与 registry 版本
36
54
  */
37
- function compareVersions(localPkg, registryInfo) {
55
+ export function compareVersions(localPkg, registryInfo, localLockIntegrity = null) {
38
56
  const issues = [];
39
57
  const localVersion = localPkg.version;
40
58
  const latestVersion = registryInfo['dist-tags']?.latest;
@@ -48,16 +66,16 @@ function compareVersions(localPkg, registryInfo) {
48
66
  });
49
67
  }
50
68
 
51
- // integrity 比较
52
- const localIntegrity = localPkg._integrity || localPkg.dist?.integrity;
69
+ // integrity 比较:优先用 pnpm-lock.yaml 锁定值;registry 同版本被篡改/覆盖时会不一致
53
70
  const registryVersion = registryInfo.versions?.[localVersion];
54
71
  const registryIntegrity = registryVersion?.dist?.integrity;
72
+ const localIntegrity = localLockIntegrity || localPkg._integrity || null;
55
73
 
56
74
  if (localIntegrity && registryIntegrity && localIntegrity !== registryIntegrity) {
57
75
  issues.push({
58
76
  type: 'integrity-mismatch',
59
77
  severity: 'critical',
60
- detail: `integrity hash 不匹配:本地 ${localIntegrity.slice(0, 30)}... ≠ registry ${registryIntegrity.slice(0, 30)}...`,
78
+ detail: `integrity hash 不匹配:本地 ${String(localIntegrity).slice(0, 30)}... ≠ registry ${String(registryIntegrity).slice(0, 30)}...`,
61
79
  });
62
80
  }
63
81
 
@@ -85,7 +103,6 @@ export async function run(profileDir) {
85
103
  }
86
104
 
87
105
  const deps = manifest.dependencies || {};
88
- const bundleNames = manifest.dsh?.profile?.bundles || [];
89
106
 
90
107
  // 只检查 dsh 相关的包(减少 API 调用)
91
108
  const dshPackages = Object.keys(deps).filter(name =>
@@ -96,6 +113,12 @@ export async function run(profileDir) {
96
113
  return pass(id, Severity.HIGH, 'profile 无 dsh 相关依赖,跳过供应链验证');
97
114
  }
98
115
 
116
+ const lockPath = join(profileDir, 'pnpm-lock.yaml');
117
+ let lockText = null;
118
+ try {
119
+ if (existsSync(lockPath)) lockText = readFileSync(lockPath, 'utf8');
120
+ } catch { lockText = null; }
121
+
99
122
  const allIssues = [];
100
123
  let checked = 0;
101
124
  let errors = 0;
@@ -113,7 +136,8 @@ export async function run(profileDir) {
113
136
  continue;
114
137
  }
115
138
 
116
- const issues = compareVersions(localPkg, registryInfo);
139
+ const lockIntegrity = lockText ? extractLockIntegrity(lockText, pkgName, localPkg.version) : null;
140
+ const issues = compareVersions(localPkg, registryInfo, lockIntegrity);
117
141
  for (const issue of issues) {
118
142
  allIssues.push({ package: pkgName, ...issue });
119
143
  }
@@ -123,6 +147,11 @@ export async function run(profileDir) {
123
147
  }
124
148
  }
125
149
 
150
+ // 全部查询失败 → skip(离线时不应谎报"无异常")
151
+ if (checked === 0 && errors > 0) {
152
+ return skip(id, Severity.HIGH, `${errors} 个包 registry 查询失败(离线或网络受限),跳过供应链验证`);
153
+ }
154
+
126
155
  if (allIssues.length === 0) {
127
156
  return pass(id, Severity.HIGH,
128
157
  `验证 ${checked} 个包的供应链完整性,未发现异常${errors > 0 ? `(${errors} 个包查询失败)` : ''}`
@@ -130,7 +159,7 @@ export async function run(profileDir) {
130
159
  }
131
160
 
132
161
  const criticalIssues = allIssues.filter(i => i.severity === 'critical');
133
- const overallSeverity = criticalIssues.length > 0 ? Severity.CRITICAL : Severity.HIGH;
162
+ const overallSeverity = criticalIssues.length > 0 ? Severity.CRITICAL : maxSeverity(allIssues.map(i => i.severity === 'critical' ? Severity.CRITICAL : i.severity === 'medium' ? Severity.MEDIUM : Severity.LOW));
134
163
 
135
164
  const details = allIssues
136
165
  .map(i => `[${i.severity}] ${i.package} — ${i.type}: ${i.detail}`)
@@ -141,7 +170,7 @@ export async function run(profileDir) {
141
170
  : '部分包版本落后于 registry latest,建议更新';
142
171
 
143
172
  return fail(id, overallSeverity,
144
- `检测到 ${allIssues.length} 个供应链问题(${checked} 个包已验证):\n${details}`,
173
+ `检测到 ${allIssues.length} 个供应链问题(${checked} 个包已验证${errors > 0 ? `,${errors} 个查询失败` : ''}):\n${details}`,
145
174
  fix
146
175
  );
147
176
  }
@@ -151,7 +180,7 @@ export const sl1Check = {
151
180
  name: 'supply-chain-integrity',
152
181
  severity: Severity.HIGH,
153
182
  phase: CheckPhase.LIFECYCLE,
154
- description: '供应链完整性验证(npm registry 一致性)',
183
+ description: '供应链完整性验证(npm registry 一致性 + pnpm-lock integrity 比对)',
155
184
  src: 'builtin',
156
185
  runner: (profileDir) => run(profileDir),
157
186
  };
@@ -2,10 +2,11 @@
2
2
  * SL3: Reputation Score — 插件信誉评分
3
3
  *
4
4
  * 基于 npm registry 信号评估插件信誉:
5
- * - 下载量
6
5
  * - 维护者数量
7
6
  * - 最近更新时间
8
- * - 依赖数量
7
+ *
8
+ * (对齐实现:下载量/依赖数量信号未落地,不再在注释中宣称)
9
+ * 网络不可达时返回 skip(此前会把"查询失败"当 concern 误报 fail)。
9
10
  *
10
11
  * Severity: LOW
11
12
  * Phase: LIFECYCLE
@@ -15,11 +16,11 @@ import { readFileSync, existsSync } from 'node:fs';
15
16
  import { join } from 'node:path';
16
17
  import { Severity } from '../protocol/severity.mjs';
17
18
  import { CheckPhase } from '../protocol/phase.mjs';
18
- import { pass, fail } from '../protocol/check.mjs';
19
+ import { pass, fail, skip } from '../protocol/check.mjs';
19
20
 
20
21
  async function fetchNpmInfo(pkgName) {
21
22
  try {
22
- const url = `https://registry.npmjs.org/${encodeURIComponent(pkgName).replace(/%2f/g, '/')}`;
23
+ const url = `https://registry.npmjs.org/${encodeURIComponent(pkgName)}`;
23
24
  const response = await fetch(url, { signal: AbortSignal.timeout(10000) });
24
25
  if (!response.ok) return null;
25
26
  return await response.json();
@@ -39,16 +40,20 @@ export async function run(profileDir) {
39
40
  const dshPackages = Object.keys(deps).filter(n => {
40
41
  if (!n.includes('dsh') && !n.includes('deepseek')) return false;
41
42
  if (n.startsWith('@local/')) return false; // 本地包
42
- if (deps[n].startsWith('file:')) return false; // 文件依赖
43
- if (deps[n].startsWith('github:')) return false; // GitHub 依赖
43
+ if (typeof deps[n] === 'string' && deps[n].startsWith('file:')) return false; // 文件依赖
44
+ if (typeof deps[n] === 'string' && deps[n].startsWith('github:')) return false; // GitHub 依赖
44
45
  return true;
45
46
  });
46
47
  if (dshPackages.length === 0) return pass(id, Severity.LOW, '无 npm 上的 dsh 相关依赖,跳过信誉评分');
47
48
 
48
49
  const concerns = [];
50
+ let netFailures = 0;
51
+ let evaluated = 0;
52
+
49
53
  for (const name of dshPackages.slice(0, 10)) {
50
54
  const info = await fetchNpmInfo(name);
51
- if (!info) { concerns.push({ package: name, concern: 'registry 查询失败' }); continue; }
55
+ if (!info) { netFailures++; continue; }
56
+ evaluated++;
52
57
  const latest = info['dist-tags']?.latest;
53
58
  const ver = info.versions?.[latest];
54
59
  if (!ver) continue;
@@ -65,9 +70,17 @@ export async function run(profileDir) {
65
70
  }
66
71
  }
67
72
 
68
- if (concerns.length === 0) return pass(id, Severity.LOW, `评估 ${dshPackages.length} 个包的信誉,无异常`);
73
+ // 全部查询失败 skip(离线时不应把"查不到"当"有 concern")
74
+ if (concerns.length === 0 && evaluated === 0 && netFailures > 0) {
75
+ return skip(id, Severity.LOW, `${netFailures} 个包 registry 查询失败(离线或网络受限),跳过信誉评分`);
76
+ }
77
+
78
+ const failureNote = netFailures > 0 ? `\n(另有 ${netFailures} 个包查询失败未评估)` : '';
79
+ if (concerns.length === 0) {
80
+ return pass(id, Severity.LOW, `评估 ${evaluated} 个包的信誉,无异常${failureNote.trim()}`);
81
+ }
69
82
  const details = concerns.map(c => `${c.package}: ${c.concern}`).join('\n');
70
- return fail(id, Severity.LOW, `检测到 ${concerns.length} 个信誉关注点:\n${details}`, '考虑替换长期未维护的包');
83
+ return fail(id, Severity.LOW, `检测到 ${concerns.length} 个信誉关注点:\n${details}${failureNote}`, '考虑替换长期未维护的包');
71
84
  }
72
85
 
73
86
  export const sl3Check = { id: 'SL3', name: 'reputation-score', severity: Severity.LOW, phase: CheckPhase.LIFECYCLE, description: '插件信誉评分', src: 'builtin', runner: (d) => run(d) };
@@ -1,11 +1,12 @@
1
1
  /**
2
2
  * SL4: Release Compatibility — 发布兼容性验证
3
3
  *
4
- * 验证 profile 中包的 dist-tags 与 DSH 版本的兼容性:
5
- * - 检测 latest 指向 broken 版本
6
- * - 检测 next/rc 标签与本地版本的差异
7
- * - 验证 peerDependencies 兼容性
4
+ * 验证 profile 中包的 dist-tags 与本地版本的差异:
5
+ * - 检测 major 版本落后
6
+ * - 检测 latest 指向 rc/beta 预发布
7
+ * - 提示可用的 next 标签
8
8
  *
9
+ * (对齐实现:peerDependencies 兼容性验证未落地,不再宣称)
9
10
  * Severity: MEDIUM
10
11
  * Phase: LIFECYCLE
11
12
  */
@@ -14,14 +15,14 @@ import { readFileSync, existsSync } from 'node:fs';
14
15
  import { join } from 'node:path';
15
16
  import { Severity, maxSeverity } from '../protocol/severity.mjs';
16
17
  import { CheckPhase } from '../protocol/phase.mjs';
17
- import { pass, fail } from '../protocol/check.mjs';
18
+ import { pass, fail, skip } from '../protocol/check.mjs';
18
19
 
19
20
  /**
20
21
  * 获取 npm registry dist-tags
21
22
  */
22
23
  async function fetchDistTags(pkgName) {
23
24
  try {
24
- const url = `https://registry.npmjs.org/${encodeURIComponent(pkgName).replace(/%2f/g, '/')}`;
25
+ const url = `https://registry.npmjs.org/${encodeURIComponent(pkgName)}`;
25
26
  const response = await fetch(url, { signal: AbortSignal.timeout(10000) });
26
27
  if (!response.ok) return null;
27
28
  const data = await response.json();
@@ -106,6 +107,7 @@ export async function run(profileDir) {
106
107
 
107
108
  const allIssues = [];
108
109
  let checked = 0;
110
+ let errors = 0;
109
111
 
110
112
  for (const pkgName of dshPackages.slice(0, 10)) {
111
113
  const localPkgPath = join(profileDir, 'node_modules', pkgName, 'package.json');
@@ -115,7 +117,7 @@ export async function run(profileDir) {
115
117
  const localPkg = JSON.parse(readFileSync(localPkgPath, 'utf8'));
116
118
  const distTags = await fetchDistTags(pkgName);
117
119
 
118
- if (!distTags) continue;
120
+ if (!distTags) { errors++; continue; }
119
121
 
120
122
  const issues = checkCompatibility(localPkg.version, distTags);
121
123
  for (const issue of issues) {
@@ -123,13 +125,18 @@ export async function run(profileDir) {
123
125
  }
124
126
  checked++;
125
127
  } catch {
126
- // 跳过查询失败的包
128
+ errors++;
127
129
  }
128
130
  }
129
131
 
132
+ // 全部查询失败 → skip(离线时不应谎报"无异常")
133
+ if (checked === 0 && errors > 0) {
134
+ return skip(id, Severity.MEDIUM, `${errors} 个包 registry 查询失败(离线或网络受限),跳过发布兼容性验证`);
135
+ }
136
+
130
137
  if (allIssues.length === 0) {
131
138
  return pass(id, Severity.MEDIUM,
132
- `验证 ${checked} 个包的发布兼容性,无异常`
139
+ `验证 ${checked} 个包的发布兼容性,无异常${errors > 0 ? `(${errors} 个查询失败)` : ''}`
133
140
  );
134
141
  }
135
142
 
@@ -75,7 +75,9 @@ function scanDirectory(dir, profileDir, extensions = ['.yml', '.yaml', '.json',
75
75
 
76
76
  const entries = readdirSync(dir, { withFileTypes: true });
77
77
  for (const entry of entries) {
78
- if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
78
+ // 点文件默认跳过,但 .env / *.env 是密钥重灾区,必须扫描(复审修复:此前 .env 永不被扫)
79
+ const isEnvFile = entry.name === '.env' || entry.name.endsWith('.env');
80
+ if ((entry.name.startsWith('.') && !isEnvFile) || entry.name === 'node_modules') continue;
79
81
  const fullPath = join(dir, entry.name);
80
82
  if (entry.isDirectory()) {
81
83
  findings.push(...scanDirectory(fullPath, profileDir, extensions));
@@ -15,6 +15,7 @@ import { join } from 'node:path';
15
15
  import { Severity } from '../protocol/severity.mjs';
16
16
  import { CheckPhase } from '../protocol/phase.mjs';
17
17
  import { pass, fail } from '../protocol/check.mjs';
18
+ import { maxSeverity } from '../protocol/severity.mjs';
18
19
 
19
20
  /** 已知恶意 entry 模式 */
20
21
  const MALICIOUS_PATTERNS = [
@@ -68,7 +69,9 @@ export async function run(profileDir) {
68
69
  if (allFindings.length === 0) return pass(id, Severity.HIGH, `扫描 ${patchFiles.length} 个 patch 文件,未检测到恶意 entry 模式`);
69
70
 
70
71
  const details = allFindings.slice(0, 10).map(f => `[${f.severity}] ${f.type}: ${f.snippet}`).join('\n');
71
- return fail(id, Severity.HIGH, `检测到 ${allFindings.length} 个可疑 entry 模式:\n${details}`, '检查相关插件的 cordis.patch.yml 内容');
72
+ // 复审修复:按实际命中的最高严重度定级,medium 级命中不再一律拔高成 HIGH
73
+ const overallSeverity = maxSeverity(allFindings.map(f => f.severity));
74
+ return fail(id, overallSeverity, `检测到 ${allFindings.length} 个可疑 entry 模式:\n${details}`, '检查相关插件的 cordis.patch.yml 内容', ['#2066']);
72
75
  }
73
76
 
74
77
  export const sp4Check = { id: 'SP4', name: 'entry-poison', severity: Severity.HIGH, phase: CheckPhase.POST_INSTALL, description: '恶意 entry 注入检测', src: 'builtin', runner: (d) => run(d) };