toby-bridges/api-relay-audit
toby-bridges/api-relay-audit从 DeepSeek Harness 对 AI API 中转站和 LLM 代理运行本地安全审计,生成 Markdown 报告,覆盖提示词注入、模型替换信号、工具调用改写、错误泄漏、流完整性和按 profile 启用的 Web3 风险。
运行时插件防火墙,仅覆盖经其启动器启动的会话(DSH Desktop 桌面应用注入不进去,不在覆盖范围):插件发起的文件、网络、子进程调用会归属到发起它的插件,并用该插件自己声明的需求去衡量——本职范围内的行为零打扰,越出声明才被记录。归因是尽力而为,不是保证:覆盖不到的调用退回 host,不会被记到别的插件头上。附带装前体检卡(能力面、红线组合、PASS/WARN/BLOCK)、越界判定、可选的执行前拦截(含 credential-exfil 规则)、更新 diff 与隔离子进程试装、SARIF/CSV 审计导出与多源汇总。数据只存本机,无遥测;webhook 默认关闭。
Plugin Detection and Response for DeepSeek Harness.
English · 中文
Every plugin you install runs with the same privileges as the host: it can read your credentials, open sockets, and spawn processes. Reviewing its source answers *what it could do* — and only until the next update.
DSH-PDR answers a different question: what did it actually do?
When the host is started through its launcher, it observes the running process, attributes the fs / network / subprocess calls plugins make to the plugin that made them, judges those calls against what the plugin claims to need, and shows you the result. Everything stays on your machine. Attribution is best-effort, not a guarantee — see Limitations.
credential-exfil catches a credential read followed by egress in the same breathDSH_HOME, hardened timeout — a separate process, not a sandbox)json, Slack and DingTalk payload formats# 1. install into a profile
dsh plugin --profile <profile> add file:/path/to/dsh-pdr-plugin
# or, directly from this repository:
dsh plugin --profile <profile> add github:kipozannaki/DSH-PDR-Plugin
# 2. start the host through the launcher (not `dsh` directly — see below)
dsh-pdr --profile <profile> web
# 3. confirm the preload is in place
dsh-pdr --pdr-checkRequirements: Node ≥ 22.12 (for require(esm)), DSH ≥ 0.1.7, pnpm (used by dsh plugin add).
Why the launcher? A DSH host formalises several node:* ESM facades very early, so a probeinstalled from inside a plugin misses a large slice of calls. Measured on one real session:
earlyInstall alone attributed 0 of 3 target-plugin calls (926 events, all fell back tohost); adding the preload attributed 3 of 3 (5 192 events). The preload is a requirement,not an optimisation — docs/DESIGN.md.Where the launcher comes from, and where it works. dsh plugin add links the package'sbininto/node_modules/.bin/, so after installing you can call it as
npx dsh-pdr …from that profile's context, ornode /node_modules/dsh-pdr-plugin/bin/dsh-pdr.cjs …
directly — either way it sets NODE_OPTIONS=--require …/preload.cjs and forwards everythingelse to dsh. The DSH Desktop app does not go through a shell, so the launcher cannotinject into it; for Desktop, attribute nothing rather than assume this plugin is watching —
the health card and the offlineaudit/aggregatecommands still work, and a
--import-based hook (host-level, no launcher) is the tracked next step(#1).
Nothing installed? The UI runs on demo data:
node src/client/serve.js # http://127.0.0.1:3123/firewall/Stage · Capability · Where Before install · Health card: capability surface, red-line combinations, PASS / WARN / BLOCK · src/inspector/ Before install · Trust score: weighted, auditable formula (red lines −30 each, credential access −5, network −3, subprocess −2) → bands 良好 / 注意 / 谨慎 / 高危 · src/inspector/trust-score.js Runtime · Behaviour probe: network / fs / subprocess, attributed per plugin · src/probe/ Runtime · Red-line alerts: credentials+network, credentials+subprocess, cleartext egress · src/probe/ Runtime · Boundary judge: contract × path class × baseline → normal / observe / suspicious / boundary · src/judge/ Runtime · Contract inference: observe a cold start → draft → human confirmation; an inference never launders an observed red line · src/judge/infer.js Runtime · Block mode: pre-execution check, violation stopped in place (opt-in; alarm is the default) · src/guard/ On change · Snapshot & rollback of the profile manifest trio (package.json, pnpm-lock.yaml, cordis.patch.yml) · src/store/snapshot.js On change · Update guard: capability diff + trial install in an isolated child process · src/update/ Overview · Security-centre UI: plugin list, trust score, red-line banner, filterable timeline · src/client/ Overview · Audit report, SARIF 2.1.0 / CSV compliance export, multi-source aggregation · src/report/ Overview · Red-line webhook push — off by default, deduped, rate-limited, never self-recording · src/alert/
node src/client/serve.js # demo data
node src/client/serve.js --jsonl <path> # replay a real probe log ($DSH_HOME/firewall/events.jsonl)Plain index.html + app.js + style.css — no framework, no build step. The standalone server and the in-host mount share one route table (src/client/api-handlers.mjs), so the preview behaves exactly like the mounted panel.
config materialises ── earlyInstall expression installs the probe
│
preload ────────────── dsh-pdr sets NODE_OPTIONS=--require …/preload.cjs ← beats ESM facades
│
probe ──────────────── layer 1 property patch + layer 2 binding patch
│ attribution: ALS → stack walk → 'host'
judge / guard ──────── contract × path class × baseline → allow / alarm / block
│
store / report ─────── JSONL on disk, audit export, optional webhookThe probe wraps RegistryService.prototype.plugin before any plugin is instantiated, so plugin entry points run inside an AsyncLocalStorage context and the calls they start are attributed — including calls made through injected host services. This is the cheap path: measured on a real session (~5 600 hooks), stack-based attribution costs ≈ 1.9 % of wall clock against ALS's ≈ 0.05 % (per-op: 114 µs vs 2.9 µs), so als is the default. What als does not cover — calls that never cross a wrapped entry point, the named-import gaps listed under Limitations — falls back to host rather than being silently misattributed.
Full architecture, hook-coverage matrix and the honest list of residual gaps: docs/DESIGN.md.
# the profile's cordis.patch.yml
- id: dsh-pdr-plugin
config:
dataDir: ~/.dsh/firewall # default
probeMode: als # als (default) | stack (slower) | both
maxBytes: 8388608 # rotate events.jsonl at 8 MB, keep 3
policy:
mode: alarm # alarm (default) | block
rules:
credential-exfil: block
alerts:
enabled: false # off: not one byte leaves the machine
url: https://your-webhook.example/hook
rules: [] # empty = every red line
format: json # json | slack | dingtalk// package.json
"firewall": {
"hosts": ["api.example.com"], // declared destinations; going outside → scope violation
"grants": ["global-credentials"] // explicit authorisation for sensitive targets
}A contract is declared freedom: staying inside it is silent, leaving it is recorded. When nothing is declared, the contract is derived from the health card's capability surface.
dsh-pdr web # = dsh web, with the preload
dsh-pdr headless "run tests" # any dsh subcommand
dsh-pdr --pdr-check # print which preload is in use
# offline export — no host, no hooks, just reads the JSONL
dsh-pdr audit --format md
dsh-pdr audit --format sarif --out audit.sarif
dsh-pdr audit --format csv --out audit.csv
dsh-pdr audit --format json --out audit.json
# multi-source aggregation (several profiles / machines / archives)
dsh-pdr aggregate --format md
dsh-pdr aggregate --source prod=/m1/.dsh/firewall --source dev=/m2/.dsh/firewallaggregate reads rotated files too (events.jsonl.1/.2/.3), de-duplicates across sources and reports the counts, and counts unparsable lines instead of dropping them silently. Measured on 4 files / 127 000 events: 1 252 duplicates found.
Other plugins can inject the firewall service:
const api = ctx.firewall;
api.getEvents({ actor: 'some-plugin', flag: 'credentials-access', limit: 100 });
api.getRedFlags(); // red-line alerts
api.getSummary(); // per-plugin counts + red lines + probe health
api.healthCard(pluginDir); // static card: PASS / WARN / BLOCK
api.getVerdicts(); // boundary verdicts + contracts
api.inferContracts(); // drafts from observed behaviour (never auto-applied)
api.adoptContract(name); // confirm a draft
api.setPolicy({ rules: { 'credential-exfil': 'block' } });
api.getUpdateDiff(before, after); // what changed in this update
api.trialInstall(pluginDir); // isolated trial, crash/hang contained
api.getAuditReport(); // JSON report
api.getSarif(); api.getCsv(); // compliance formats
api.eventLogPath; // local JSONL — nothing is uploaded$DSH_HOME/firewall/. No telemetry.events only — never the full timeline, never file contents, never credentials. The audit report records only the webhook's origin (scheme + host), never the URL with any token in it.
capability surface, as scanned by its own health card, is part of the test suite.
Stated plainly, because a security tool that overstates its coverage is worse than none:
what the runtime layer is for. On 154 real ecosystem plugins the static pass recalls 66.1 % at 17.2 % false positives. Being listed on a plugin marketplace is not a security review, and neither is this README: treat every plugin, including this one, as untrusted until observed.
(native modules, process.binding, patching the probe) can evade it. The goal is continuous, attributable visibility.
dsh-pdr (or another host-level preload), thein-plugin probe installs too late and attribution collapses — measured 0 of 3 on a real session with everything falling back to host. The DSH Desktop app cannot be launched through the launcher, so its sessions are currently outside attribution; the health card and the offline audit / aggregate commands are unaffected. A host-level --import hook is the tracked next step.
child_process / http / net when the facade predates theprobe. fs is covered on both hook layers.
DSH_HOME,a hard timeout and SIGKILL on hang, so a crashing or hung plugin cannot take the host down — but a plugin is still running with your user's privileges inside it.
dsh-h3 adapter follows a contract measured on a livecommunity plugin and is covered by 11 injected-runtime assertions, but dsh-h3 is a Typert virtual module that cannot be resolved offline. Until it is verified in a live host the panel falls back to serve.js.
dsh-pdr-compat/1; their schema is notpublic, so mapping it is still outstanding.
npm test # all 22 suites (PASS / SKIP / FAIL; any failure exits 1)
npm run test:list # what would run
node test/smoke.mjs # one suiteSuites that need a real environment (a real plugin, a profile, an external baseline) print SKIP: and how to restore them, and exit 0. They never pretend to pass. Any failing assertion exits 1.
What each suite covers: docs/TESTING.md.
file: dependencies are copied into the profile, not symlinked — changing code or bumping the version does not reach it, and add answers "Already up to date". Remove then add:
dsh plugin --profile <p> remove dsh-pdr-plugin
dsh plugin --profile <p> add file:/path/to/dsh-pdr-pluginDocument · Contents docs/DESIGN.md · architecture, attribution, hook coverage, residual gaps docs/TESTING.md · every suite, what it asserts, environment variables docs/LESSONS.md · findings that changed the implementation CHANGELOG.md · what changed, when
For plugin marketplaces: screenshots are declared in screenshots.json and shipped in assets/. The package publishes no postinstall / prepare script, so a source install never needs a pnpm build approval.
MIT — see LICENSE.