Files
dsh_shenxian/dsh-server-docs/04-调整方案/66-业务插件P0误报与admin显式信任.md
T
admin 5ad755116e chore(docs): 文档库并入代码仓(R4 选 a)+ 索引/台账跟进
1) dsh-server-docs/ 从工作区(原 E:\...\aliyun-dsh-server\dsh-server-docs)**整体并入本仓**,
   保留目录名 ⇒ 仓库内 dsh-server-docs/... 的相对引用天然继续有效;旧目录(含其 .git)已归档到
   工作区 _中间产物_待清理/,未随本提交带入。
2) .gitattributes:新增 `dsh-server-docs/** -text` —— 原文档库是 `* -text` + autocrlf=false,
   必须保持纯 LF,否则会被本仓的 CRLF 规则翻掉。
3) 活引用里的绝对路径已全部改到新位置(docs 的 INDEX / README / scripts / skills + 用户级 skills
   + ~/.workbuddy/settings.json 的 hooks);历史档案(04-调整方案/、archive/)按「只增不改」未动。
   ⚠️ hooks 路径改动需「完全重启会话」才生效(配置是会话启动快照)。
4) 交接单/T08:新增 §16「生产整体切换执行记录」(形态 / 落地动作 / **4 个只有真上线才暴露的真 bug** /
   验收证据 / 回滚命令 / 残留项);台账 T08 行 → 已完成并归档;03-路线图 §二 登记 T08 收尾项。
5) 统一称谓:**「本机」只指跑 WorkBuddy 的开发机**,47 / 106 一律写「远程服务器」。
2026-09-15 18:47:13 +08:00

94 lines
5.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 66 · 业务插件上传的「P0 误报」与 admin 显式信任
- 日期:2026-09-12
- 触发:把 AnySearch 插件投放进候选池时被安全检测 **P0 阻断**(命中 `package/README.md` 等 4 个文档);用户提出「让 admin 在插件管理里手动标注信任」
- 结论一句话:**安全检测从「命中即 400、命中详情只进日志」改成「默认拒绝但逐条回显 + admin 显式声明信任后放行并留痕」** —— 防线不动,把判断责任交给本就可信、且能看到证据的 admin。
- 状态:✅ 已实现、已验证、已部署(2026-09-12 11:15)
> **TL;DR**|**结论**:P0 规则 `/\.credentials\.yaml/` 是**纯字面量匹配、扫所有文本文件**,第三方插件的 README 只要教用户把 key 写进凭据文件就会被判「读取实例会话密钥」—— 这是第二次同类误报(档案 29 记过 `dsh-memory` 的 `docs/…example.yml`)。
> **关键**:不降低规则强度(不改规则),改为 **fail-closed + 可显式覆盖 + 全程留痕**。
> **状态**:✅ 已上线
---
## 一、问题
### 1.1 现象
投放 `@anysearch/[email protected]` 时:
```
安全检测未通过(P0 阻断):读取实例会话密钥(package/README.md)
statusCode: 400
```
按 P0 规则全集扫描该包,命中 **4 处,全部是文档**、**代码文件 0 命中**:
| 文件 | 命中内容 |
|---|---|
| `README.md` | "Store the key in `$DSH_HOME/.credentials.yaml`" |
| `README.zh-CN.md` | 中文版同句 |
| `docs/user-guide.zh-CN.md` | 凭据解析顺序说明 |
| `docs/agent-installation.md` | 指导 agent 让用户配置 key |
即:**教用户正确配置凭据**被当成了**读取凭据**。
### 1.2 根因
`src/web/security-scan.ts` 的 P0 规则 `{ re: /\.credentials\.yaml/, why: '读取实例会话密钥' }`:
- **纯字面量匹配**,无上下文判断
- **扫描所有文本文件**(`SCAN_TEXT_EXT` 含 `.md`)
- 命中即 `throw httpError(400, ...)`,**调用方无法裁决**
- 命中详情只能通过异常消息带出**一条**,且旧文案是脱敏的一句话 —— **admin 看不到"到底哪里命中了什么"**
### 1.3 为什么不能靠收窄规则解决
两个候选方案都被否:
| 方案 | 否决理由 |
|---|---|
| 把 P0 规则限定在代码类文件(`.md`/`.txt` 降级为 P1) | 这是**降低所有人的防线**(虽然实际防护力损失很小),且以后每遇到一类新误报都要再动一次规则 |
| 手工把 tgz 放进候选池(绕过扫描) | 绕过安全机制,且**不可复现** —— admin 将来通过门户重新上传同一个包仍会被挡 |
**用户提出的方案更优**:把判断责任交给 **admin**(上传本就仅 admin 可做,责任边界清楚),前提是**把证据给足**。
## 二、方案
**三层改动,规则本身一行未动:**
| 层 | 改动 |
|---|---|
| 扫描 | 新增 `scanDirDetailed()`:**收集 P0 命中而不抛**(`walk()` 抽出为共享遍历);**`scanDir()` 行为完全不变**(技能上传等路径仍"命中即 400") |
| 上传 API | P0 命中且未声明信任 → **409** + `blocked[]`(文件 + 原因)+ `warnings[]`;带 `trust.confirmed=true` → 放行 + 写两条 audit |
| 门户 | 被挡时**逐条列出命中**(文件 + 规则)+ 信任理由输入框 +「我已逐条确认,信任并投放」按钮 |
**三条设计底线**(写进代码注释与 UI 文案):
1. **必须显式** —— 缺省 fail-closed,没有"默认放过"的路径;
2. **必须留痕** —— `upload_business_plugin`(含 `trustedOverride`)+ `trust_business_plugin`(含逐条命中与理由)两条审计;
3. **必须展示证据** —— 让 admin 真的看到自己信任了什么,而不是点一个盲确认。
## 三、实现
| 文件 | 改动 |
|---|---|
| `src/web/security-scan.ts` | 新增 `ScanBlock` / `ScanResult`;`walk()` 抽出共享;`scanDir()` 保持抛错语义;新增 `scanDirDetailed()` |
| `src/web/routes/business-plugins.ts` | `StagedPlugin` 增 `blocked`/`warnings`;`stageTgzArchive()` 改用收集模式;POST 接受 `trust` 并裁决 + 留痕 |
| `web/portal.html` | 抽出 `doUpload(file, trust)`;新增 `renderScanBlocked()` 渲染命中详情 + 信任入口 |
**自检**:`npm run typecheck` / `npm run build` 均 exit 0。
## 四、验证(2026-09-12 部署后实测)
| 场景 | 期望 | 实测 |
|---|---|---|
| 不带信任投放 AnySearch | fail-closed 拒绝 + 列 4 处命中 | ✅ 列出 `README.md` / `README.zh-CN.md` / `docs/agent-installation.md` / `docs/user-guide.zh-CN.md`,exit 2 |
| 带 `--trust` 投放 | 放行 + 落库 + 留痕 | ✅ 投放成功;`business_plugins` 已有 `@anysearch/anysearch-dsh @ 0.1.4` |
| 审计留痕 | 两条 | ✅ `upload_business_plugin{trustedOverride:true, via:…}` + `trust_business_plugin{blocked:[4条], reason:…}` |
| 既有行为未受影响 | 技能上传仍 fail-closed | ✅ `scanDir()` 未改语义(仅内部重构) |
## 五、待办
1. **官方目录批量导入路径(`/api/plugins/whitelist/import`)未接信任入口** —— 它仍会因 P0 直接失败(结果是 `{ok:false, error}`)。走那条路的插件目前**无法信任投放**;需要同样的「命中详情 + 信任」UI(多选批量的交互要单独设计)。
2. **信任状态未持久化** —— 本轮未加 DB 列,列表页暂不显示「已信任」徽标。建议与「默认开启」的字段**合并成一次 migration v6**(避免两次迁移 + 两次重启)。
3. **`scripts/ensure-anysearch-admin.cjs` 的手工覆写段待退役** —— 其 web provider 覆写职责已由档案 65 的平台托管段接管;两段并存时「整体替换」语义会互相覆盖。