@jieai/dsh-plugin-vet
v0.2.5
Published
Plugin trust pipeline for deepseek-harness (DSH): deterministic static scan, runtime guard (T1 sentinel + T2 hooks), audit protocol skill, GUI shield. Alarms by default; N7 confirm-block intercepts confirmed destructive fs ops (family 1/2) by throwing.
Maintainers
Readme
@jieai/dsh-plugin-vet — DSH 插件信任流水线
English | 中文
安装任何插件前,先让 dsh-plugin-vet 走一遍:静态规则给出 verdict(确定性、不可伪造), agent 按 vet-audit-protocol 技能排查敏感点与质量问题(谁也无法替代),最终一张评分卡交给人/模型决定。
定位:监控报警器,不是打手。 vet 只做「检查 → 报警 → 给建议」:写时查(静态扫描)、 跑时盯(运行时守卫)、报警面(评分卡 + GUI 盾牌状态灯)。vet 永不替用户动手——不自动卸载、 不自动杀进程、不自动改配置;deny 模式是部署者显式开启的 opt-in,不构成产品身份。最终怎么处置, 由用户在自己的 DSH 上操作决定。
@jieai/dsh-plugin-vet 是 deepseek-harness 生态的信任层插件:占据
下载 → 扫描 → 审计 → 评分 → 决定 → 运行时盯梢 这一整套信任流水线。运行时盯梢内置蜜罐诱饵:谁偷偷翻找密钥文件,当场现形(opt-in,honeypot.enabled)。不做插件市场本体(目录/分发)。
- 📚 架构设计:docs/ARCHITECTURE.md
- 🧾 审查协议:AUDIT_PROTOCOL.md
- 🛡️ 安全政策:SECURITY.md
- 🤝 贡献指南:CONTRIBUTING.md
安装
dsh plugin --profile <profile> add @jieai/dsh-plugin-vet安装即生效链路:pnpm 安装 → reconcilePlugins 读 dsh.bundle → 下次启动 loadProfile
解析 bundle 挂载插件。默认配置见下方 Config(fail-open:只报告不拦截)。
本地 tarball 安装(离线/先验证再发版场景):
dsh plugin --profile <profile> add ./jieai-dsh-plugin-vet-0.1.4.tgz
# 或直接解包到 profile 的 node_modules:
# tar -xzf jieai-dsh-plugin-vet-0.1.4.tgz -C ~/.dsh/profiles/<profile>/node_modules/@jieai/
// 并在 profile 的 cordis.patch.yml 里 insert 挂载条目:
// - insert:
// - id: plugin-vet
// name: '@jieai/dsh-plugin-vet'
// config:
// mode: report
// autoScan: true路径/相对路径/URL 均可(
dsh plugin add走 pnpm 的file:协议兜底,本地 tgz 直接解析)。首次安装耗时提示:
dsh plugin add首次安装到大型 profile 可能耗时数分钟—— 期间 pnpm 会做全量依赖解析、更新 500+ 包的 lockfile 并对整棵依赖树做供应链策略校验 (vet 自身只带 2 个运行时依赖,耗时大头是 profile 已有依赖树的解析/校验,不是 vet)。 校验完成后再次安装/更新只需秒级(复用校验结果)。
兼容性:vet 面向 DSH 0.1.0-rc.6+(peer 范围
^0.1.0-rc.6)。安装时 pnpm 可能提示 unmet peer dependency——这是预期的:profile 模板autoInstallPeers: false,运行期从 DSH 安装闭包 ($DSH_HOME/profiles/node_modules回退层)解析,无需也不能在 profile 里另装一份 cordis 全家桶。
监控范围 = 安装 vet 的 profile。 vet 的守卫是进程内事件(
internal/plugin)—— vet 装进哪个 profile,就只守那个 profile 实例加载的插件。多 profile 部署时, 每个要守的 profile 都要装一份 vet(dsh plugin --profile <name> add @jieai/dsh-plugin-vet), 并把 requireAudit 配到对应 profile 的 cordis.patch.yml。
Config(cordis.yml)
| 键 | 默认 | 说明 |
|---|---|---|
| mode | report | report 只报告不拦截;deny 显式开启拦截 |
| autoScan | true | 新插件(internal/plugin)自动静态扫描 |
| scannerTimeoutMs | 15000 | 静态扫描子进程超时 |
| requireAudit | false | 审计门槛(opt-in):开启后新插件加载时检查 ~/.dsh/vet/audits/ 健康档案——无档案则 report 模式记录黄色 audit-required 告警、deny 模式拦截。档案由 agent 按 vet-audit-protocol 技能审查后手写落盘 |
| rules | {}(全开) | 规则开关(R1-R14) |
| denyOn | critical | mode: deny 时的拦截阈值 |
| allowlist | [] | 包名/插件 id 白名单(跳过扫描) |
| runtimeGuard | off | 运行时守卫(性能/稳定代价 opt-in):off 关;watch 启用 T1 哨兵 + T2 钩子,只报警不动作 |
| runtimeIntervalMs | 2000 | T1 哨兵 /proc 采样间隔 |
| runtimeMemLimitMb | 2048 | T1 内存报警阈值(宿主 VmRSS,超限 → red) |
| runtimeForkBurstN | 5 | T1 子进程突增报警阈值(单轮增量,→ red) |
| runtimeFdLimit | 512 | T1 文件描述符报警阈值(→ yellow) |
| runtimeGrowthMb | 256 | T1 内存持续膨胀报警阈值(完整窗口内 RSS 净增长,→ yellow 疑似泄漏;起窗初期的瞬时尖峰不构成窗口级持续膨胀,不会误报) |
| runtimeGrowthWindowMs | 600000 | 膨胀检测窗口(默认 10 分钟) |
| honeypot.enabled | false | 蜜罐诱饵(需 runtimeGuard: watch):往 honeypot.dir 放假密钥诱饵,T2 对诱饵路径的触碰(读/写/删)单独报 honeypot 类报警。目录/文件名/内容均无蜜罐关键词(反蜜罐),默认位置 ~/.dsh/.local,诱饵值全是格式正确但无效的假凭据 |
| honeypot.dir | '' | 诱饵目录;空 = $HOME/.dsh/.local |
| osvCheck | true | 扫描 package.json 时向 Google OSV 查询已知漏洞(仅精确版本查询:range(*、>=、^、~)与无 version 的主包跳过,P3-1/P3-3——避免陈旧全量历史误报;round-7 起 range 不再剥前缀当下界精确版查询);核对面 = 插件自身 + 直接依赖(上限 8 个,@deepseek-ai/* 官方包跳过,P3-10);间接传递树超出 OSV v1 范围与扫描预算。默认开启会外发包名到 api.osv.dev,网络失败静默降级。介意隐私可设 false |
| contentBaseline | true | 官方包内容哈希基线(P-5):对每个 @deepseek-ai/* 包的文件计算 SHA-256 并与基线比对——同名冒名包(file:/tarball 无 registry 校验)哈希不符时按最严格 plugin 判定。首次见到自动落盘并信任;基线按 name@version 多版本并存(上限:1000 文件 / 50MB / 10s) |
| networkEgress | true | 运行时网络出口观测(P1):包装 http/https/net/http2/tls/dgram/fetch,观测插件发起的出站请求(alarm-only,需 runtimeGuard: watch) |
| transitiveDeps | false | 传递依赖 OSV 核对(P1,opt-in,默认关):调用本地已安装的 upstream-radar CLI(绝不 npx 自动安装);未安装/超时/输出形状不符 → 静默降级为仅直接依赖。命中以 OSV-T medium 呈现 |
| confirmBlock | block | N7 确认拦截(0.1.14,需 runtimeGuard: watch):只拦不可逆破坏。block(默认)族 1/2 确认即拦;alarm 全族只报警;off 关闭。每次拦截抛错并写一条红色 n7-block 报警;黑名单为进程内存(重启即清) |
| confirmBlockFamily3 | alarm | N7 族 3 覆写(系统持久化/提权面写入:bashrc/cron/systemd/ld.so.preload/sudoers.d/profile.d/autostart/authorized_keys/hosts/ssl)。显式 block 为用户自担风险的选择,默认只报警 |
| confirmBlockFamily4 | alarm | N7 族 4 覆写(供应链/安装态写入:node_modules 包文件、cordis.patch.yml / cordis.yml / plugin.json)。显式 block 为用户自担风险的选择,默认只报警 |
@deepseek-ai/* 官方包默认豁免(内置信任)。
工具
scan_plugin— 确定性静态扫描:target=dynamic-code(源码字符串)/package(包目录)/file(单文件)。返回评分卡(verdict + staticScore + findings)。verdict 只由静态规则产出。支持scanBasis:npm(默认,registry tarball 真实发布物)/git(仅源码仓,R12 入口/patch 缺失降 info 不误报)。0.1.21 起评分卡能力块含 R16 幽灵/僵尸依赖字段;扫 vet 本体(realpath 判定,非包名)时额外输出selfScan注解块——本体自扫呈现 Trusted 卡(① token 级能力声明降级 + ② 每版本产物钉扎vet-self-pins.json+ ④ 发布自扫门禁),普通插件路径行为零变化;原始 findings 原样保留可展开(详见 docs/ARCHITECTURE.md §5.12)。vet_diff— 只读、纯本地:输出某包本地记录过的版本历史 + 最近两版的行为差分(N6)。展示 hosts/fsPaths/spawnCmds/imports 的新增|移除与网络/执行能力翻转。不扫描、不联网。vet_label— 只读、纯本地:输出某包的人类可读"能力营养标签"(M2)——访问的文件(标注敏感路径)、引用的网络主机/子进程、第三方依赖(能力未知)、网络/执行能力标志(含 ESM 具名导入盲区标记),以及最近升级差分摘要。数据源 = 同一份本地 N6 能力清单历史;标签反映的是声明侧静态能力——运行时观测/休眠能力属运行中的盾牌。不扫描、不联网。vet-audit-protocol(技能) — 审查流程协议(AUDIT_PROTOCOL.md):agent 按预设步骤审查新插件——scan_plugin 静态判据(含 R12 Cordis/DSH 契约)→ 读清单/源码 → 逐条核实发现 → 主动深挖(网络/文件/进程/凭据/库语义)→ 契约与代码质量审计(4.5 步:入口/Config schema 一致性、错误处理/同步阻塞/资源泄漏/异步正确性等「写得烂」问题——静态干净≠值得装)→ 用系统写入能力手写健康档案到~/.dsh/vet/audits/<plugin>-<version>-<ts>.md。vet 不内置审计工具、不替 agent 调查,只给判据与落盘约定。
自动行为
internal/plugin自动扫描(autoScan: true):新装第三方 npm 包加载时自动静态扫描;deny模式 + verdict ≥denyOn→ 回滚加载。- 审计门槛(
requireAudit: true):无健康档案的第三方插件加载时——report模式记录黄色audit-required告警(进 /vet/status.json 告警列表,插件照常加载);deny模式回滚加载(引用vet-audit-protocol提示先审查)。档案按版本精确匹配(P-1):插件升级后旧版本档案不放行新版本——重新审查才能消除告警/拦截。 tools/execute拦截:cordis_define/run_code/workflow执行前扫描代码字符串(cordis_run的真实 schema 无 code 载荷,保留守卫位不生效,P3-11 同步);report模式仅在非 clean 结果时加VET:前缀(干净执行不污染机器可读输出),deny模式直接拦截(isError)。- 运行时守卫(
runtimeGuard: watch)——alarm-only:- T1 哨兵:旁路子进程每
runtimeIntervalMs读宿主 /proc(VmRSS / 子进程数 / fd 数),报警 JSON 行回传宿主 → 盾牌变黄/红。 - T2 钩子:进程内包装 fs / child_process(含 fs.promises),危险操作(敏感路径写入/删除、读密钥文件、含 shell/下载外联关键词的子进程、蜜罐诱饵触碰、
~/.dsh配置根侦察)取栈归因到插件包名后报警;官方包归因全类降噪(能力授权——官方包是平台本体,高频读写~/.dsh会话/配置/存储不刷屏;第三方无法伪造归因)。从不阻断调用。自伤豁免(实测误报后修复):- node_modules 包目录豁免:包名/包内文件是公开工件——含 credential/secret 等词的包名是正常生态(
@aws-sdk/credential-provider-*、@deepseek-ai/dsh-credentials-local等),宿主模块解析(require.resolve 内部 realpathSync/stat 包内 package.json)与 vet 扫描读取都会高频触碰,不再误报 fs-probe;node_modules 之前的段照常判定(~/.ssh/node_modules/x仍命中 .ssh),写删系统根(/usr 等)仍报警。 - 归因排除 vet 自身:包装器帧永远是报警栈栈顶,vet 根不参与归因映射——宿主/无主报警不再栽到 vet 头上(报警照发,归因到真实调用方)。
- 工具链临时产物(tsc
<源名>.<pid>.<uuid>.tmpdir、*.tmp、*.temp、*.swp等)自动豁免——名字里的 secrets/credentials 只是被编译的源文件名,删它是清理不是破坏;父段照常判定(~/.ssh/config.bak仍报警)。
- node_modules 包目录豁免:包名/包内文件是公开工件——含 credential/secret 等词的包名是正常生态(
- T1 哨兵:旁路子进程每
- GUI 盾牌:浏览器半区注册进
conversation.session.header.actions,轮询 /vet/status.json 显示绿/黄/红灯 + 报警计数。激活需dsh web重启(重启后 client-modules 才扫描到dsh.client声明)。- 交互:可点击——点击展开报警面板(实时指标:内存/CPU/I-O/子进程/fd;守卫状态:未开启时可一键写入 runtimeGuard: watch 配置(重启生效);报警列表含严重度/归因/逐条建议;最近扫描回显、刷新、更新时刻),外部点击自动关闭;有报警时盾牌旁显示计数徽标(绿/黄/红主题色,明暗自适应)。
- 单条忽略:每条报警可点「忽略」——只影响展示(不再计入盾牌等级与计数),记录保留可随时「恢复」;报警停止后忽略自动失效,将来复发会重新可见(可再忽略)。忽略状态与报警存储同生命周期(重启即重置)。鉴权边界(P3-12 记录):dismiss/restore 仅做同源校验(alarm-only 展示层风险——同源页面脚本可隐藏报警,但记录不删、不影响其他能力,体系内可接受)。
- 展示上限:面板展示最近 8 条报警;存储为环形缓冲上限 20 条,同 id 60 秒内去重,24 小时 TTL 过期(持续触发会自然续期)——100 条不会全量展示,也无需展示(新报警会顶掉最旧的)。最近扫描回显(suspicious → 黄灯)同样按 24h TTL 过期(P3-2:一次可疑扫描不再永久黄,持续扫描自然续期)。
静态规则表(R1-R16)
| ID | 名称 | 默认级别 | 适用场景 | 确定性 |
|---|---|---|---|---|
| R1 | constructor 链逃逸 | critical | code + files | certain/likely |
| R2 | 动态执行(eval/Function/import/require) | high(files)/ medium(code;bin 入口降 medium) | both | certain/likely |
| R3 | process 直接访问(按 runtime 分级;只读成员/generic/bin 入口/应用型包 → info) | critical(host)/ high(sandbox) | both | certain |
| R4 | 宿主闭包捕获(agent/TextEncoder…)+ 宿主全局原型污染 | critical(code)/ high(files,与 targetKind 无关) | both | certain/likely |
| R5 | ctx 逃逸尝试信号(withheld 成员/未声明服务;ctx.logger 等官方注入服务白名单放行) | medium | 仅 code | likely |
| R6 | 字符串粗扫兜底(混淆特征需与动态执行组合证据) | info | both | heuristic |
| R7 | 硬编码密钥 | high | both | likely |
| R9 | 资源安全(无界分配/无出口同步循环/循环内 spawn/ReDoS/递归无终止/循环内增长模式) | high(分配/死循环/fork)/ medium(ReDoS/递归/Map.set)/ info(常驻循环/+=/Promise.all) | both | certain/likely/heuristic |
| R10 | 供应链(package.json install 钩子/依赖清单) | high(install 钩子)/ info(依赖清单) | files | likely/heuristic |
| R11 | 破坏性文件操作(fs 删除/敏感路径读写) | high(敏感路径)/ medium(删除) | both | likely |
| R12 | Cordis/DSH 契约(入口文件/bundle patch 声明/name/engines.node) | high(patch 缺失/入口缺失)/ medium(无入口/缺 name)/ info(node 版本低) | files | certain/likely |
| R13 | 网络外联端点(字符串字面量中的 Discord/Telegram/Slack webhook、云元数据端点 169.254.169.254 / metadata.*.internal / 100.100.100.200、.onion 目标) | high | both | likely |
| R14 | 随包分发的非 JS 脚本下载即执行(.sh/.bash/.ps1/.cmd/.bat 中 curl|sh、wget|sh、编码 PowerShell -enc/IEX、certutil/bitsadmin/mshta/regsvr32/rundll32 等;generic → info) | high(plugin)/ info(generic) | files | likely |
| R15 | 动态网络目标(fetch / WebSocket / http(s).request|get / net.connect 的目标参数静态不可解——"刻意遮蔽"目标) | info(观测;叠加 N1 隐能力等信号才抬升) | both | heuristic |
| R16 | 依赖一致性审计:幽灵依赖(代码引用但 package.json 未声明,靠传递依赖提升侥幸可解析)与僵尸依赖(package.json 声明但 node_modules 缺失) | info(观测;永不进 verdict) | files | heuristic |
评分模型
staticScore = max(0, 100 - Σ(severity 权重 × 命中数 × confidence 系数))
verdict(唯一权威判定,heuristic 永不升级):critical ≥ 1 → critical;否则 high ≥ 1 → suspicious;其余 → clean。verdict 只由静态层产出:staticScore 与 verdict 分开呈现,不合成单一总分。
能力边界(诚实清单)
静态扫描是"减速带 + 取证层",不是安全边界。以下按判定影响分两档, 并如实列出明确不检测的形态(均已实测验证)。
能检测 —— 判定级(会改变 verdict)
| 规则 | 检测的问题类 | 命中 → verdict | 验证 |
|---|---|---|---|
| R1 | 构造器链逃逸:x.constructor("return process") / x["constructor"]("return " + "process") / new (globalThis.constructor.constructor)("return process")()(点/元素访问 + new 形态;字符串参数静态可求值:字面量/模板/拼接/const 绑定;new 支持 const 别名追踪) | critical | 矩阵 + 多文件 ✓ |
| R2 | 动态执行:eval() / Function() / new Function/`new AsyncFunction`(含括号形态 new (Function)(...);参数含逃逸串 → critical)/ (async)=>{}.constructor 捕获(round-7.2:new X.constructor 仅 base 为函数字面量才报——new n.constructor(n.type, n) 对象克隆形态不报)/ vm.runInContext/`runInNewContext` / 动态 import() / require() | high(files)/ medium(code,逃逸串 critical);bin 入口文件按通用代码判定降 medium | 矩阵 + round-7/7.2 回归 ✓ |
| R3 | process 直访:getBuiltinModule/`mainModule`/`module`/`exit`(含 reallyExit)→ critical;副作用成员(kill/abort/chdir/umask/setuid/dlopen/binding 等)与未知成员 → high;只读成员(round-7.1):env/cwd/platform/pid/argv/execPath/stdin/stdout/stderr/nextTick/on 等 → info 能力触达面(读 cwd/env/pid 不是逃逸通道,bridges 类无 bin 的 MCP/工具插件不再误伤);runtime='sandbox' 封顶 high;形态降级:generic 包 / bin 入口文件 / 应用型包 → info | critical / high / info | 矩阵 + round-7.1 回归 ✓ |
| R4 | 宿主闭包捕获:agent/parallel/pipeline/phase/log/TextEncoder/TextDecoder/btoa/atob 的 .constructor 读取或 Object.getPrototypeOf 投喂(code 场景);宿主全局原型污染:<内置>.prototype.<成员> = ... 覆盖赋值与 Object.defineProperty(<内置>.prototype, ...)(Object/Array/String/Function/TextEncoder/URL/Buffer 等 40+ 内置,round-7) | critical(code)/ high(files,round-7.1 起与 targetKind 无关——污染语义不分插件/通用包,generic 不再降 info) | 矩阵 + round-7 回归 ✓ |
| R7 | 硬编码密钥:sk- / AKIA / AIza / gh[pousr]_ / xox[baprs]- / 环境变量赋值 / URL 内嵌 key(占位符排除) | high → suspicious | 矩阵 ✓ |
| R9 | 资源安全:new Array(2**31) / Buffer.alloc(1GB) 无界分配(≥1e8)、while(true)/for(;;) 无出口同步循环(卡死宿主;round-7.2:带标签 break 跳出外层循环算出口——outer: for(;;){ ... break outer } 不再误报)、无出口循环内 spawn/exec/fork/new Worker(fork 炸弹) | high → suspicious;ReDoS 嵌套量词 (a+)+ 类与 alternation 分支重叠 (a|aa)+ → medium(分支首字符互斥的 (?:[^']|'')* 类、组后 ? 的 (https?:)? 类线性回溯不报,round-7)、递归无终止(for-of/for-in 集合遍历与带条件循环内的自调用不报,round-7)、循环内 Map.set → medium(不进 verdict);含 await 常驻循环仅 info(§14.1 不短路审查) | 矩阵 + round-7/7.2 回归 ✓ |
| R10 | 供应链:package.json scripts 的 preinstall/install/postinstall/uninstall 钩子(安装期任意代码执行)→ high;依赖清单 → info(已知漏洞核对:OSV 精确版本查询,osvCheck 可关) | high → suspicious(install 钩子) | 矩阵 ✓ |
| R11 | 破坏性文件操作:fs.unlink/rm/rmdir(+Sync) 删除敏感路径(/etc/root/.ssh 等)→ high,普通删除 → medium;fs.writeFile 等写入敏感路径 → high;fs.readdir 遍历敏感目录 → medium | high → suspicious(敏感路径);medium 不进 verdict | 矩阵 ✓ |
| R12 | Cordis/DSH 契约:dsh.bundle.patch 声明的文件缺失 → high;无 入口(无 main/exports["."] 且根无 index.js)→ medium;声明的入口文件缺失 → high;插件意图包缺 name → medium;engines.node 主版本低于 22 → info | high → suspicious(声明即挂载点/入口,缺失必失败);medium/info 不进 verdict | 矩阵 ✓ |
| R13 | 网络外联:字符串字面量中硬编码 Discord/Telegram/Slack webhook、云元数据端点、.onion 目标 | high → suspicious | 矩阵 + R13 测试 ✓ |
| R14 | 非 JS 脚本:.sh/.bash/.ps1/.cmd/.bat 中 curl|sh、wget|sh、PowerShell 下载管道/-enc/IEX、certutil/bitsadmin/mshta/regsvr32/rundll32(generic → info) | high → suspicious(plugin);info 不进 verdict(generic) | 矩阵 + R14 测试 ✓ |
能检测 —— 提示级(只降分,永不改变 verdict)
| 规则 | 检测的问题类 | 说明 |
|---|---|---|
| R5 | ctx 逃逸尝试信号:访问沙箱 withheld 框架成员/未声明服务(ctx.plugin 等) | 仅 code 场景;medium |
| R6 | 字符串粗扫:拼接逃逸特征、getBuiltinModule/`child_process`/危险 require 模块引用、混淆特征(String.fromCharCode/`Buffer.from(base64)`/`atob(`/`charCodeAt`——round-7 起需与同文件动态执行信号(eval/new Function/vm 等)组合才报,终端协议/编码类常规字节处理不再误报) | info/heuristic |
| R8 | 扫描超时/文件过大跳过 | info 元规则 |
运行时监控(runtimeGuard: watch 时启用)——只报警
| 层 | 机制 | 能抓 | 局限 |
|---|---|---|---|
| T1 哨兵 | 子进程轮询宿主 /proc | 内存炸弹(>memLimit)、内存持续膨胀(泄漏,窗口净增长按倍数报警)、fork 炸弹(子进程突增)、fd 激增 | 粒度=宿主全局(插件共用进程,无法归因到插件) |
| T2 钩子 | 进程内包装 fs/child_process(含 fs.promises) | 敏感路径写入/删除(/etc、~/.ssh、.env…)、读密钥文件、含 shell/下载外联关键词的 spawn | 栈归因 best-effort;每次调用包装开销(I/O 密集 <5%,热点 10-20% 级) |
| N1 能力差分(0.1.13) | 声明能力清单(扫描时注册)vs 运行时观测(T2) | 隐能力被执行(观测到敏感动作但静态足迹为零含 imports)→ red n1-hidden;imports 非空 ⇒ 「能力未知」保守覆盖;只敏感动作参与 | 需先扫过该插件(自动扫描注册);静态可见未触发的能力记为休眠,展示于营养标签(M2,0.1.16) |
| N3 外泄/破坏台账(0.1.14) | 按插件字节计数(敏感读/网络写,生命周期累计)+ 10s 破坏签名窗 + 序列签名(读密钥→spawn curl/wget/nc、读密钥→网络写) | 读完就发:双计数>0 → 黄 n3-exfil,量级接近 → 红 n3-exfil-match,序列命中(30s 窗)→ 红;破坏族:批量删/改名加密标记/同路径读后覆盖写/写放大 → 黄,两个以上叠加 → 红 n3-ransom;蜜罐/金丝雀确认(N4)插件阈值降最低 | 不检查会话内容(只字节+操作形态);跨会话/超慢外泄、原生二进制内部、fd 级读取、fetch body 不计(已记边界);归因 best-effort |
| N4 蜜罐水印(0.1.14) | 诱饵值内嵌高熵金丝雀(内存集合);网络 URL/body(write/end)、dgram、fetch URL/body、spawn 参数扫描 | 出站命中 → red canary-leak(100% 外泄确认;直接/URL-decode/一次 base64-decode 变体;涉事插件标记 N3 疑似) | 只确认蜜罐材料外泄;金丝雀分片重组不防(已记边界);需蜜罐诱饵(幂等诱饵保留金丝雀) |
| 完整性金丝雀(0.1.14) | ~/.dsh 下落地少量小文件(固定内容+自身 sha256);写/删 → red kind integrity | 勒索加密 profile/凭据面的最早触发(N3 破坏签名兜底) | 范围仅限 ~/.dsh(拍板);读不报警 |
| N7 确认拦截(0.1.14) | 破坏确认后对破坏类 fs 操作做包装器级拦截(族 1/2)+ 族 3/4 可选升级拦截;护栏:官方归因/无主操作/vet 自身 IO 永不拦、凭据精确文件级匹配、判定 fail-open | 族 1:确认(N3 勒索组合/完整性金丝雀写删/N4 金丝雀泄漏)后该插件破坏类 fs 操作(write/unlink/rename/cp/truncate/createWriteStream)抛错;族 2:凭据本体删除+覆盖已存在文件单次即时拦截(精确文件:~/.ssh/id_*、~/.dsh/.credentials.yaml、~/.aws/credentials、.pgpass、.netrc、.git-credentials、.npmrc);族 3/4:黄 persistence-write/install-write 报警(默认永不拦) | 黑名单为进程内存(重启清);配置变更需重启;可逆写(appendFile、新建文件)永不拦;族 3/4 仅显式覆写 block 才拦 |
| N6 版本行为差分(0.1.15) | 每次自动扫描把 N1 能力清单按 name@version 记入本地 ~/.dsh/vet/capabilities.json(0600,LRU 保留 1000 个版本);升级时与上一记录版本(按 recordedAt 选取,不解析 semver)做清单差分 | 相对上一版的新增能力 → 黄 upgrade-diff(新增网络主机/敏感路径/子进程/依赖/网络或执行能力);新增构成高敏感组合(执行+网络 / 敏感路径+网络 / 敏感路径+执行)→ 红;冷启动(首次安装)只记录,exec+network 双高给黄 upgrade-cold 提示;能力收窄只记录不报警;vet_diff 工具输出本地历史 + 最近两版行为 changelog | 只对比"声明"清单(运行时隐藏/依赖携带的能力变化由 N1 隐能力 + N2 解码覆盖,不在清单差分内);"上一个版本"= 本机实际扫过的最近版本;同版本重装不差分(同版本内容篡改由 content-baseline 哈希覆盖);纯本地、alarm-only |
| 取证模式(0.1.21,P0-2) | N4 金丝雀确认后,被确认恶意插件后续每次 fs/子进程/网络操作追加写入 ~/.dsh/vet/forensics/<plugin>-<ts>.jsonl(0600/0700,fail-open) | 完整微活动时间线:"平时不打扰,确认有鬼布天罗地网" | 武装集在内存(重启清除);不落会话内容(仅操作形状+目标,与 N3 同数据面);取证是增强、永不拦截 |
| 盾牌 | 浏览器 conversation.session.header.actions + /vet/status.json | 绿/黄/红灯 + 报警计数 | 需 dsh web 重启激活 |
明确不检测(实测验证)
| 形态 | 实测结果 |
|---|---|
| 间接引用:别名函数 const f = Function; f(...)、process["getBuiltinModule"]、globalThis.process、间接 eval (0, eval) | 仅 R6 info 或零 finding,verdict=clean |
| 运行时/外部构造载荷:base64 串、hex/charCode 拼装、网络/环境变量/参数读码、自修改代码 | base64 构造器串实测零 finding;0.1.15(N5/R15):网络 sink 的目标参数静态不可解 → 报 info("刻意遮蔽"——运行时目标无法从源码审计) |
| 非源码文件:.jsx/`.tsx`/`.vue`/`.json`/二进制/wasm | 不在扫描面;shell/PowerShell/batch 脚本(.sh/.bash/.ps1/.cmd/.bat)由 R14 覆盖(下载即执行) |
| 依赖链/供应链:import/require 图、依赖版本漏洞、package.json scripts/install 钩子、许可证、作者信誉 | 不解析 |
| 运行时行为:网络外传、动态原型污染链、死循环/资源耗尽、时序、权限滥用 | 无数据流/行为分析;静态的 <内置>.prototype 覆盖赋值已由 R4 检出(round-7) |
| 语义知识:插件实际注入的服务、bundler polyfill 中的 process、遮蔽判定边界 | R5 只认 4 个变量名;遮蔽检查是 v1 启发式(偏少报) |
| 宿主工具包的合法 process 使用(process.env 读配置、process.stdin/stdout 协议、process.execPath spawn) | 已解决:targetKind 分级——非 DSH 插件包/官方包(generic)下 R3/R2/R10/R9 死循环降级为能力触达面/提示(info/medium),不进 verdict;DSH 插件包保持严格。round-7 新增形态降级:应用型包(package.json 声明 bin)与 bin 入口文件同样按能力触达面降级;round-7.1 只读成员分类:cwd/env/platform/pid 等纯只读成员在 plugin 模式也降 info(bridges 类无 bin 的 MCP/工具插件不再误伤),kill/exit 等副作用/逃逸成员保持 high/critical。实测 195 官方包全 clean |
0.1.16 加固批次(代码审查产出)
全面代码审查(三路深挖 + 人工实证验证)后的修复(C1–C4 严重,M5–M9 中/低,规则补丁;ENGINE static-v12):
- C1 — 宿主侧改为单文件 bundle 发布(
lib/index.bundle.js),files白名单不再携带 guard 内部模块 (setRootIndexing/withVetSelfIo/confirmBlock/canaryStore/capabilityDiff全部闭包封闭)—— 恶意插件再也无法经绝对路径 require 关掉 T2/N7/N4/N1(此前实证可达:全进程致盲、N7 关闭、金丝雀清空)。 - C2 — ESM 具名导入(
import { readFileSync } from 'node:fs')按 Node 互操作快照绕过全部 T2 钩子(实证)。 N1 清单新增esmNamedBuiltins标记,runtimeGuard: watch下对这类插件记录黄色esm-guard-coverage—— 覆盖盲区显式可见,不再静默(运行时防线剩 T1 哨兵与审计协议,文档边界)。 - C3 —
DSH_PLUGIN_VET_CACHE_DIR/DSH_PLUGIN_VET_BASELINE_DIR改为模块加载时快照;进程内改 env 无法再重定向存储/缓存。scanner 缓存读取时校验宿主注入的进程内随机 nonce——预写伪造 clean 缓存条目 (已实证的攻法)失效。 - C4 — 检测
Error.prepareStackTrace/stackTraceLimit篡改:归因不可信 → 敏感操作报 redattribution-tampered,且 N7 族 2 凭据破坏经哨兵身份照样拦截(此前 hint 缺失会绕开唯一拦截)。 - M5 — T2 补
symlink/link/chmod/chown/mkdir/mkdtemp/utimes/lutimes(+Sync) 写面与lstat/lstatSync侦察面。 - M6/M7/M8/M9 — R9 fork-bomb 覆盖 sync 变体 · 能力/基线存储自检外部改写(
vet-store-tamper黄灯)·isSensitiveFsPath段级匹配(不再子串误抬)· 侧车终止前核对/proc/<pid>/cmdline(PID 复用防误杀)。 - 规则补丁 — R2 全局/间接 eval 形态 + require 拼接折叠、R3
globalThis.process.*成员口径、 R4Reflect.defineProperty、R9 转义括号组深度、R10prepare钩子、R14 python/ruby/perl 下载即执行、 R15 undici sink(见静态规则表)。 - 会话日志轮换降噪 —
isSessionLogFile现也识别分片会话文件(session.jsonl.zstd.<shard>);~/.dsh/sessions/**下未归因的会话日志删除由 redfs-destroy降为 yellow(宿主自身运维不可能"攻击自己"),归因到插件的删除仍保持 red(可能是在销毁证据)。
信任边界
- verdict 只由确定性静态层产出——规则是正则/AST 判定,不可被提示注入欺骗。
- 静态层与插件代码物理隔离——scanner 是独立进程,AST 只读、从不 eval。
- 审查走 agent 协议——agent 按 vet-audit-protocol 技能步骤复核(静态判据先行,敏感点逐条深挖),verdict 不受审查环节影响。
- 不合成单一总分——禁止把 verdict 与主观评估合并,防止污染 verdict 边界。
- 本产品不是安全边界——定位是"减速带+取证层"(具体可绕过形态见下方 Known Limitations 1,与 DSH 官方立场对齐)。
- fail-open 起步——默认
mode: report,deny由部署者显式开启。 - alarm-only——运行时守卫只 watch 不 kill;vet 的自动行为(deny 拦截)仅存在于部署者显式开启的 opt-in 模式。报警只附建议,处置永远留给用户在 DSH 上操作。
Known Limitations
- 静态扫描不是安全边界:混淆/编码/动态生成代码可绕过 AST 规则;R6 只提供"疑似"信号。
1b. 源码枚举限制:internal/plugin 自动扫描只递归收集 ≤6 层深、非隐藏(非
.开头)的 .js/.ts/.mjs/.cjs 文件——深层或隐藏目录里的源码静默不扫(无提示);需要全量时可手动用 scan_plugin(target=package) 扫整个目录。 - agent 审查可被提示注入:verdict 永不来自审查环节,但 agent 可能漏报——置信度字段让用户知晓。
internal/plugin守卫不覆盖运行时动态挂载逃逸:vm 路径由tools/execute守卫在调用层拦截。- R5 仅 code 场景:files 场景的 ctx 访问默认不报(误报率高)。
- 扫描耗时:大插件包可能超时跳过(R8 info);agent 审查按 vet-audit-protocol 步骤进行。
- verdict 是静态层确定性判定;agent 的主观判断记录在健康档案里,不构成安全保证。
- /vet/status.json 无鉴权:盾牌轮询需要匿名 GET,路由本身不鉴权——若 dsh web 绑定非回环地址,局域网内可读扫描结论/报警目标。vet 是 alarm-only 观测器,不做越权的访问控制;介意就保持回环绑定或信任网络(POST 开关守卫已有同源校验,无 Origin 拒绝)。
@deepseek-ai/*内容哈希基线(P-5):对官方包计算内容哈希(SHA-256),与基线比对,防止包名伪造。首次见到自动落盘基线并信任;后续哈希不一致时撤销豁免并记录 red 报警。基线存储支持多版本共存(key =name@version),资源限制(1000 文件 / 50MB / 10s 超时)防 DoS。已知限制:基线文件存储在~/.dsh/vet/baseline.json(0600 权限),若攻击者已拿到用户权限可篡改基线文件;多进程并发写可能导致记录丢失(下次扫描重新计算)。可通过contentBaseline: false关闭。- R10 已知漏洞核对:直接依赖 OSV 查询(默认开启,
osvCheck: false关闭)+ 传递依赖 upstream-radar 扫描(默认关闭,transitiveDeps: true开启)。OSV 查询把「包名+精确版本」发到 api.osv.dev;网络失败/超时静默降级。传递依赖扫描需要安装 upstream-radar(本地探测,不使用 npx),未安装时静默降级。OSV-T 规则 severity 为 medium(传递依赖利用面小于直接依赖)。 - R11 只认
fs.*形态:解构/别名调用(const { unlinkSync } = require('fs'))与运行时路径漏检(已实测记录,属静态边界)。 - T1/T2 是"防盗摄像头"不是"保险柜":抓明显搞事(内存/fork 炸弹、敏感路径操作、第三方 spawn、网络出口),抓不了 worker 线程/原生插件/低流量慢外联;T2 对 ESM 具名导入快照、
process.binding等旁路不覆盖(0.1.16:N1 新增esmNamedBuiltins标记 +esm-guard-coverage黄灯提示,盲区显式化;归因篡改走attribution-tamperedred)。网络出口观测(networkEgress: true,默认开启):包装 http/https/net/http2/tls/dgram/fetch 模块,观测敏感主机/端口。已知限制:IP 直连绕过域名敏感列表;WebSocket 使用底层 net.Socket 会被捕获,但浏览器 WebSocket 不在观测范围;不观测 DNS 查询(性能开销大)。 - T2 归因与降噪:栈归因是 best-effort(共享服务/定时器跨插件会误归因);官方包 spawn 默认不报警(能力授权)。
- 盾牌激活需要
dsh web重启:client-modules 在启动时扫描dsh.client声明;重启前浏览器不会加载盾牌,但 /vet/status.json 端点与运行时守卫(宿主侧)重启即生效。 - 运行时守卫默认关闭(
runtimeGuard: 'off'):包装 fs/child_process 有性能与稳定代价,opt-in 开启。 process.kill保持 high(有意设计,round-7.1):kill 是副作用成员,不随只读成员降级——但 MCP/桥接器类插件 kill 自己 spawn 的子进程是正常功能面(dsh-bridges 实测:98/134 条已清,剩余 high 全为 run.js/util.js 的 process.kill)。静态区分process.kill(child.pid)(pid 来自本包 spawn 返回值)与任意 pid 需要数据流分析,成本高收益低——维持现状,由 agent 按 vet-audit-protocol 审计时人工排除(结论记入健康档案)。- 平台支持(Linux 优先,非 Linux 优雅降级):静态扫描层(scanner-bin/scan_plugin/R1-R12/OSV)、T2 运行时钩子、蜜罐、GUI 盾牌、审计协议均为纯 JS,跨平台(macOS/Windows 可跑)。T1 哨兵是 Linux 专有——依赖 /proc 读 VmRSS/子进程/fd;0.1.21(P0-6)起在非 Linux 上通过显式平台门整体跳过(不拉哨兵、零 respawn/sentinel-down 噪音),指标面板回退 -1/0(不报错,设计如此)。T2 敏感路径按"路径段名"匹配(反斜杠已归一化,Windows 下
.ssh/.env/credentials等段名同样命中),但"系统根前缀"(/etc /usr /var)是 POSIX 形态——Windows 上写删C:\Windows\System32类路径不受系统根判定(段名/密钥特征判定仍有效);macOS 有 /etc /usr /var,T2 全功能。与 DSH 当前 Linux 优先的适配状态一致。
开发
npm run build # scanner-bin + src 编译到 lib/ + client bundle
npm run typecheck # tsc --noEmit 全量
npm test # 构建 + vitest(633 用例全绿)
npx vitest run --coverage # 覆盖率报告(lines/functions >= 70%,branches >= 50%)目录:scanner-bin/ 静态引擎(独立进程);src/ 插件本体(tools/guards/audit/report/guard);
src/client/ GUI 盾牌;test/ fixtures + 单测 + 对抗矩阵。架构见 docs/ARCHITECTURE.md。
许可证
MIT。
