@aimarket/warden 0.8.2 → 0.13.0
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/CHANGELOG.md +55 -1
- package/README-es.md +45 -10
- package/README-fr.md +45 -10
- package/README-ru.md +47 -10
- package/README-zh.md +45 -10
- package/README.md +47 -10
- package/dist/classifier.d.ts +79 -0
- package/dist/classifier.d.ts.map +1 -0
- package/dist/classifier.js +360 -0
- package/dist/classifier.js.map +1 -0
- package/dist/claude-hook.d.ts +53 -0
- package/dist/claude-hook.d.ts.map +1 -0
- package/dist/claude-hook.js +192 -0
- package/dist/claude-hook.js.map +1 -0
- package/dist/encoded-text.d.ts +7 -0
- package/dist/encoded-text.d.ts.map +1 -0
- package/dist/encoded-text.js +63 -0
- package/dist/encoded-text.js.map +1 -0
- package/dist/fold.d.ts +1 -2
- package/dist/fold.d.ts.map +1 -1
- package/dist/fold.js +3 -2
- package/dist/fold.js.map +1 -1
- package/dist/histor.d.ts +104 -0
- package/dist/histor.d.ts.map +1 -0
- package/dist/histor.js +86 -0
- package/dist/histor.js.map +1 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -4
- package/dist/index.js.map +1 -1
- package/dist/mcp-client.d.ts +58 -0
- package/dist/mcp-client.d.ts.map +1 -0
- package/dist/mcp-client.js +451 -0
- package/dist/mcp-client.js.map +1 -0
- package/dist/mcp-rpc.js +1 -1
- package/dist/mcp-server.js +8 -0
- package/dist/mcp-server.js.map +1 -1
- package/dist/mcp-tools.d.ts.map +1 -1
- package/dist/mcp-tools.js +29 -4
- package/dist/mcp-tools.js.map +1 -1
- package/dist/mtl.d.ts +28 -0
- package/dist/mtl.d.ts.map +1 -0
- package/dist/mtl.js +77 -0
- package/dist/mtl.js.map +1 -0
- package/dist/pin-store.d.ts.map +1 -1
- package/dist/pin-store.js +1 -0
- package/dist/pin-store.js.map +1 -1
- package/dist/pinning.d.ts +1 -1
- package/dist/pinning.d.ts.map +1 -1
- package/dist/pinning.js +12 -4
- package/dist/pinning.js.map +1 -1
- package/dist/result-screen.d.ts +19 -0
- package/dist/result-screen.d.ts.map +1 -0
- package/dist/result-screen.js +86 -0
- package/dist/result-screen.js.map +1 -0
- package/dist/scan-cli.d.ts +3 -0
- package/dist/scan-cli.d.ts.map +1 -0
- package/dist/scan-cli.js +36 -0
- package/dist/scan-cli.js.map +1 -0
- package/dist/scan-config.d.ts +72 -0
- package/dist/scan-config.d.ts.map +1 -0
- package/dist/scan-config.js +239 -0
- package/dist/scan-config.js.map +1 -0
- package/dist/scan-lock.d.ts +46 -0
- package/dist/scan-lock.d.ts.map +1 -0
- package/dist/scan-lock.js +113 -0
- package/dist/scan-lock.js.map +1 -0
- package/dist/scan-report.d.ts +21 -0
- package/dist/scan-report.d.ts.map +1 -0
- package/dist/scan-report.js +288 -0
- package/dist/scan-report.js.map +1 -0
- package/dist/scan.d.ts +177 -0
- package/dist/scan.d.ts.map +1 -0
- package/dist/scan.js +754 -0
- package/dist/scan.js.map +1 -0
- package/dist/static-scan.d.ts +4 -3
- package/dist/static-scan.d.ts.map +1 -1
- package/dist/static-scan.js +227 -11
- package/dist/static-scan.js.map +1 -1
- package/dist/tool-list.d.ts +34 -0
- package/dist/tool-list.d.ts.map +1 -0
- package/dist/tool-list.js +57 -0
- package/dist/tool-list.js.map +1 -0
- package/dist/toxic-flow.d.ts +33 -0
- package/dist/toxic-flow.d.ts.map +1 -0
- package/dist/toxic-flow.js +117 -0
- package/dist/toxic-flow.js.map +1 -0
- package/dist/types.d.ts +12 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/wrap-cli.d.ts +3 -0
- package/dist/wrap-cli.d.ts.map +1 -1
- package/dist/wrap-cli.js +17 -6
- package/dist/wrap-cli.js.map +1 -1
- package/dist/wrap-state.js +2 -2
- package/dist/wrap-state.js.map +1 -1
- package/dist/wrap.d.ts.map +1 -1
- package/dist/wrap.js +22 -35
- package/dist/wrap.js.map +1 -1
- package/docs/gates.es.md +40 -7
- package/docs/gates.fr.md +39 -6
- package/docs/gates.md +38 -7
- package/docs/gates.ru.md +40 -7
- package/docs/gates.zh.md +10 -5
- package/docs/scan.es.md +199 -0
- package/docs/scan.fr.md +199 -0
- package/docs/scan.md +199 -0
- package/docs/scan.ru.md +199 -0
- package/docs/scan.zh.md +199 -0
- package/docs/scanner-comparison.es.md +209 -0
- package/docs/scanner-comparison.fr.md +213 -0
- package/docs/scanner-comparison.md +200 -0
- package/docs/scanner-comparison.ru.md +205 -0
- package/docs/scanner-comparison.zh.md +131 -0
- package/docs/security-hardening-language-boundary.md +306 -0
- package/package.json +10 -5
- package/scripts/check-quality-gate.mjs +64 -0
- package/scripts/quality-public-key.pem +3 -0
package/docs/scan.md
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# Scan the servers your MCP clients start
|
|
2
|
+
|
|
3
|
+
> 🌐 **English** · [Русский](scan.ru.md) · [Español](scan.es.md) · [Français](scan.fr.md) · [中文](scan.zh.md)
|
|
4
|
+
|
|
5
|
+
`warden-mcp scan` reads the MCP configuration you already have, connects to every server it starts, and vets the tool definitions before a model sees them. It is the same gate chain as [`wrap`](../README.md#protect-claude-desktop-or-cursor-with-one-wrapper) and the [library](integration.md), run once over your configs instead of in front of one server.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx -y @aimarket/warden@0.11.0 scan
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
WARDEN scan 0.10.0 · ruleset 10 sha256-lJuKKKV5mtru… · block at high
|
|
13
|
+
read .mcp.json (claude-code, 3 servers)
|
|
14
|
+
|
|
15
|
+
✓ allow notes claude-code 1 tool · score 0.90
|
|
16
|
+
✗ BLOCK evil-notes claude-code 1 tool · score 0.00 · TOOL_DEF_EXFIL(notes) TOOL_DEF_SECRET_REQUEST(notes) THREAT_SSH_KEY_READ(notes)
|
|
17
|
+
! error broken claude-code could not start: spawn /nonexistent/bin/server ENOENT
|
|
18
|
+
|
|
19
|
+
3 servers: 1 allowed, 1 blocked, 1 not checked, 0 skipped.
|
|
20
|
+
Details: warden-mcp scan --json, or --markdown FILE. To review a changed server: warden-mcp scan --lock warden.lock.json --update-lock.
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
No account, no API key, no model. The only network traffic is to the servers in your config, and to the HISTOR log if you pass `--histor`.
|
|
24
|
+
|
|
25
|
+
## Where it looks
|
|
26
|
+
|
|
27
|
+
Without arguments, every file below that exists. Pass files to scan only those, `--project` for the project files in the working directory, or `--client NAME` for one client.
|
|
28
|
+
|
|
29
|
+
| Client | Project file | User file |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| Claude Code | `.mcp.json` | `~/.claude.json` (user servers, and this project's) |
|
|
32
|
+
| Claude Desktop | — | `claude_desktop_config.json` in the app's config directory |
|
|
33
|
+
| Cursor | `.cursor/mcp.json` | `~/.cursor/mcp.json` |
|
|
34
|
+
| VS Code | `.vscode/mcp.json` (JSONC) | `mcp.json` in the user settings directory |
|
|
35
|
+
| Windsurf | — | `~/.codeium/windsurf/mcp_config.json` |
|
|
36
|
+
|
|
37
|
+
A server the config starts through `warden-mcp wrap` is scanned as the server behind the wrapper, under the pin id `wrap` uses, so drift against the approved snapshot is reported. Entries that are disabled, need an `${input:…}` value the client prompts for, or have neither `command` nor `url` are listed as skipped, never dropped silently.
|
|
38
|
+
|
|
39
|
+
## Starting servers
|
|
40
|
+
|
|
41
|
+
To read a stdio server's tools, `scan` starts it with the command, arguments and environment from the config, exactly as your client would, and stops it after `tools/list`. It never calls a tool. That still runs the program. Where you do not want that, such as CI on a pull request, `--no-launch` vets the launch line only: the threat feed's command records, and the lock's launch identity.
|
|
42
|
+
|
|
43
|
+
Remote servers (`url`, streamable HTTP or the older HTTP+SSE) are asked over the network with the headers from the config. `--public-only` refuses any that resolve to loopback, private, link-local or cloud-metadata addresses, checked on the address actually dialled. Redirects are not followed.
|
|
44
|
+
|
|
45
|
+
## Output and exit codes
|
|
46
|
+
|
|
47
|
+
| Option | Writes |
|
|
48
|
+
|---|---|
|
|
49
|
+
| (default) | a table on stdout |
|
|
50
|
+
| `--json` | the JSON report on stdout |
|
|
51
|
+
| `--json-file FILE` | the JSON report to a file, alongside the table |
|
|
52
|
+
| `--sarif FILE` | SARIF 2.1.0 for GitHub code scanning; blocking findings only, located at the server's line in the config |
|
|
53
|
+
| `--markdown FILE` | a summary with collapsible details, for `$GITHUB_STEP_SUMMARY` or a PR comment |
|
|
54
|
+
|
|
55
|
+
Exit `0`: nothing blocked. `1`: a server was blocked, or with `--fail-on-error` could not be checked. `2`: a usage or config error. `--fail-on SEVERITY` moves the block threshold; `--policy FILE` takes the same strict policy file as `wrap`.
|
|
56
|
+
|
|
57
|
+
Credentials in launch lines and URLs are shown as `***`. Tool descriptions in the Markdown report are inside code blocks that the text cannot close.
|
|
58
|
+
|
|
59
|
+
## The lock file: review the definitions, not just the command
|
|
60
|
+
|
|
61
|
+
A config line says which program starts. It does not say what that program will tell your model. The lock records the latter, so that a pull request shows it.
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
warden-mcp scan --project --lock warden.lock.json --update-lock # after reading what changed
|
|
65
|
+
git add .mcp.json warden.lock.json
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`--update-lock` writes each server's launch identity and full tool definitions, and refuses to record a server WARDEN blocks. Re-running it on unchanged servers changes nothing in the file. With `--lock` alone:
|
|
69
|
+
|
|
70
|
+
- a server the lock does not know is blocked (`LOCK_MISSING`);
|
|
71
|
+
- a server started differently is blocked (`SERVER_IDENTITY_DRIFT`);
|
|
72
|
+
- a server that now advertises different tools is blocked (`TOOL_DEF_DRIFT`), and the Markdown report shows the change as a diff of names, descriptions and schemas.
|
|
73
|
+
|
|
74
|
+
Lock entries no config starts are listed, and pruned on the next update.
|
|
75
|
+
|
|
76
|
+
## GitHub Action
|
|
77
|
+
|
|
78
|
+
```yaml
|
|
79
|
+
name: MCP servers
|
|
80
|
+
on: [pull_request]
|
|
81
|
+
permissions:
|
|
82
|
+
contents: read
|
|
83
|
+
security-events: write # only for upload-sarif
|
|
84
|
+
jobs:
|
|
85
|
+
warden:
|
|
86
|
+
runs-on: ubuntu-latest
|
|
87
|
+
steps:
|
|
88
|
+
- uses: actions/checkout@v4
|
|
89
|
+
- uses: alexar76/warden@v0.11.0
|
|
90
|
+
with:
|
|
91
|
+
upload-sarif: 'true'
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
By default it reads the project files, does not start stdio servers (a pull request chooses that program), refuses non-public addresses, uses `warden.lock.json` when it exists, writes the job summary, and fails on a blocked server. Inputs: `config`, `working-directory`, `lock`, `launch-stdio`, `public-only`, `fail-on`, `histor`, `classifier-url`, `classifier-model`, `classifier-blocks`, `sarif`, `upload-sarif`, `version`. Outputs: `blocked`, `servers`, `sarif`. Pin the action by commit SHA in production workflows.
|
|
95
|
+
|
|
96
|
+
To be told when a server you use changes, run the same job on a schedule as well (`on: { schedule: [{ cron: '17 6 * * *' }] }`): with a committed `warden.lock.json`, a server whose tool definitions changed fails the run with `TOOL_DEF_DRIFT` and the summary shows the diff.
|
|
97
|
+
|
|
98
|
+
## pre-commit
|
|
99
|
+
|
|
100
|
+
```yaml
|
|
101
|
+
repos:
|
|
102
|
+
- repo: https://github.com/alexar76/warden
|
|
103
|
+
rev: v0.10.0
|
|
104
|
+
hooks:
|
|
105
|
+
- id: warden-scan # changed configs; does not start stdio servers
|
|
106
|
+
- id: warden-lock # the project matches warden.lock.json; starts stdio servers
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Both run the published package through `npx`, so Node 20 or later must be on `PATH`.
|
|
110
|
+
|
|
111
|
+
## Claude Code plugin
|
|
112
|
+
|
|
113
|
+
```text
|
|
114
|
+
/plugin marketplace add alexar76/warden
|
|
115
|
+
/plugin install warden@warden
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
At session start it scans the servers Claude Code starts for the project, tells you which are blocked, and tells the model by name and finding code only. A blocked description never enters the model's context. Calls to tools of a blocked server, or to a blocked tool, are then denied by a `PreToolUse` hook that reads one small file and starts nothing. Details and limits: [claude-plugin/README.md](../claude-plugin/README.md).
|
|
119
|
+
|
|
120
|
+
## Toxic flows: safe one by one, dangerous together
|
|
121
|
+
|
|
122
|
+
A client hands every server's tools to one model. When the servers it starts include a tool that reads your private data, one that lets text from outside in (a web page, an issue, an email) and one that can send data out, text arriving through the second can tell the model to read with the first and send with the third. No server is at fault, so no per-server verdict shows it. `scan` reports it per client:
|
|
123
|
+
|
|
124
|
+
```text
|
|
125
|
+
⚠ toxic flow claude-code can read private data (filesystem.read_file, …), take in text from outside (fetch.fetch) and send data out (fetch.fetch). …
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The capabilities are read from tool names and parameter names (`read_file`, `send_email`, a `url` or `to` parameter), not from descriptions: the reading does not depend on the description's language, and a description cannot talk its way out of it. A flow is advisory: it never blocks and never changes the exit code. It is in `--json` (`flows`) and in the Markdown summary. To break one, drop a leg — a `fetch` server you do not need, or a filesystem server scoped to one folder in a separate client.
|
|
129
|
+
|
|
130
|
+
## HISTOR: is this server serving you what it serves everyone?
|
|
131
|
+
|
|
132
|
+
[HISTOR](https://histor.modelmarket.dev) is a public log that records, every day, the tool definitions of every remote server in the official MCP registry. `--histor` asks it one question per remote server: is the tool set you were just served the one HISTOR observes?
|
|
133
|
+
|
|
134
|
+
What is sent: the endpoint with scheme, host and path only (no query, no user name or password), and the [MTL/1](https://github.com/alexar76/awr) digest of the tool set. No tool description, no header. Endpoints on private hosts or addresses, and paths that look like they carry a key, are not sent at all; the report says why.
|
|
135
|
+
|
|
136
|
+
A stdio server started from the public npm or PyPI registry (`npx`, `bunx`, `npm exec`, `pnpm dlx`, `uvx`, `uv tool run`, `pipx run`) is named by its package — `npm:<name>` or `pypi:<name>`, without the version — and the same digest. HISTOR installs each published version of such a package in a gVisor sandbox (decoy credentials, no route out; each tool called once with canary arguments, against nothing real) and records its tools and what it did and records its tools, so `previously-observed` here means you run an older version, and `different` a version HISTOR has not reached yet or a package that is not the published one. A local path, a git or tarball URL, a private registry (`--registry`, `--index-url`) or a plain `node`/`python` launch is never sent. For a package HISTOR also says, as advisory findings, whether its name imitates a popular one (`HISTOR_PACKAGE_LOOKALIKE`: one character away, the same name under another scope, or other separators, from a package downloaded at least 100 times more) and whether the version it observed carries the marks of a stolen publishing token: `HISTOR_PACKAGE_PROVENANCE_LOST` (no attested CI build where the previous version had one), `HISTOR_PACKAGE_INSTALL_SCRIPTS` (new install scripts), `HISTOR_PACKAGE_PUBLISHER_CHANGED`, `HISTOR_PACKAGE_NEW_DEPENDENCIES`. And what the package did in the sandbox: `HISTOR_PACKAGE_READS_SECRETS` (it opened decoy credentials — SSH keys, cloud or registry tokens, a wallet — at install, at startup or when a tool was called), `HISTOR_PACKAGE_PERSISTENCE` (it wrote where it would outlive the session, such as `.bashrc` or `authorized_keys`), `HISTOR_PACKAGE_STARTUP_NETWORK` (it reached out before any tool was called), `HISTOR_PACKAGE_STARTS_PROGRAMS`, `HISTOR_PACKAGE_INSTALL_BEHAVIOUR` (what its install scripts do). Loading `.env` from the working directory and calling its own API during a tool call are not findings. With `--histor`, the report also gives a link to a HISTOR feed of changes to the scanned servers HISTOR knows (`historWatch` in `--json`): subscribe in any feed reader.
|
|
137
|
+
|
|
138
|
+
| Answer | Meaning |
|
|
139
|
+
|---|---|
|
|
140
|
+
| `same` | You were served the set HISTOR currently observes |
|
|
141
|
+
| `different` | HISTOR has never seen this set: it changed after the last daily crawl, or the server serves you something it does not serve the public crawler. Reported as advisory `HISTOR_UNSEEN_TOOLSET` |
|
|
142
|
+
| `previously-observed` | A set HISTOR saw earlier, not the current one. Advisory `HISTOR_OLDER_TOOLSET` |
|
|
143
|
+
| `not-listed`, `not-observed` | HISTOR does not know this endpoint, or has not read it successfully |
|
|
144
|
+
|
|
145
|
+
HISTOR answers never block. An outage is reported and the scan continues.
|
|
146
|
+
|
|
147
|
+
## The optional classifier
|
|
148
|
+
|
|
149
|
+
WARDEN's rules are offline and deterministic, and they miss what no rule names: a paraphrase, an instruction in another language. `--classifier-url` and `--classifier-model` add a second opinion from a model you choose, through any OpenAI-compatible endpoint: a local Ollama, vLLM or LM Studio, or a hosted API.
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
warden-mcp scan --classifier-url http://localhost:11434/v1 --classifier-model qwen2.5:14b
|
|
153
|
+
WARDEN_CLASSIFIER_API_KEY=… warden-mcp scan --classifier-url https://api.deepseek.com --classifier-model deepseek-flash --classifier-reasoning-effort none
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
- **Opt-in; sends every advertised field.** Name, title, description, both schemas, annotations and extension metadata go to the endpoint, together with a normalized reading when needed. The key comes only from `WARDEN_CLASSIFIER_API_KEY`.
|
|
157
|
+
- **Advisory by default; fail closed with `--classifier-blocks`.** Findings use `TOOL_DEF_CLASSIFIER`. Enforcing mode also refuses incomplete coverage, truncation, uncertainty, malformed evidence, timeouts and provider errors (`CLASSIFIER_INCOMPLETE`). These refusals are inspection failures, not detected attacks. They cannot enter a lock through `--update-lock`.
|
|
158
|
+
- **Authority, in any language.** Categories are `instruction_to_model`, `exfiltration`, `secret_request`, `concealment` and `cross_tool`. The prompt separates legitimate tool requirements from attempts to override the user, invent consent or control another tool. Random fences help delimit untrusted data; they are not a proof against injection. The field-reference protocol requires one decision per input and host-validated evidence field IDs. WARDEN resolves source previews itself and retries each incomplete input once in isolation. Provider outages do not fan out into per-tool requests.
|
|
159
|
+
- **Review is not approval.** Model language coverage varies. For a language-independent admission boundary use [`wrap --require-approval`](security-hardening-language-boundary.md). An LLM cannot create that approval. Historical HISTOR classifier results remain advisory and use their historical protocol.
|
|
160
|
+
|
|
161
|
+
Measured on 2026-10-09 with `deepseek-flash`, the model HISTOR uses, on MCPTox (the published benchmark described in the comparison below) and on our own sets:
|
|
162
|
+
|
|
163
|
+
| | Rules (v10) | Rules + classifier, blocking at `high` | Rules + any classifier flag (advisory) |
|
|
164
|
+
|---|---|---|---|
|
|
165
|
+
| MCPTox held-out half, 218 poisoned tools caught | 171 | 191 | 218 |
|
|
166
|
+
| 45 clean MCPTox servers, blocked or flagged | 0 | 0 | 4 flagged |
|
|
167
|
+
| 200 corpus servers drawn at random, blocked or flagged | 2 blocked | 2 blocked | 2 blocked, 12 more flagged |
|
|
168
|
+
| 23 attacks we wrote / 12 hard benign cases | 20 / 0 | 22 / 0 | 23 / 0 |
|
|
169
|
+
|
|
170
|
+
The two the rules block are the identity service and the deploy tool from the comparison below. The 13 servers the classifier flagged were flagged at `medium` or `low`, never `high`. Six of them are worth a human look: one tells the model to make an irreversible ENS name transfer "as the first and only action" without asking the user, another tells it not to disclose where its data comes from. The model also flags the official Fetch server, whose description tells the model it now has internet access and should not refuse. That is an instruction to the model, though not an attack. The model missed what the rules catch: an injection in annotations, a private key or seed phrase asked for as a parameter, and `rm -rf ~`.
|
|
171
|
+
|
|
172
|
+
## How it compares
|
|
173
|
+
|
|
174
|
+
On 2026-10-09 we ran WARDEN, mcp-audit 0.18.2 (`--connect`) and mcp-shield 1.0.4 over the same servers. Each server was replayed over stdio, so every scanner connected to it as it would in the field. The harness, the sets and every judgment are in [`scripts/scanner-comparison`](../scripts/scanner-comparison/). The full write-up, with the method and every caveat, is the [scanner comparison](scanner-comparison.md).
|
|
175
|
+
|
|
176
|
+
The last three rows come from MCPTox (Wang et al., AAAI 2026), a published benchmark of 485 poisoned tools written for 45 real MCP servers. We split its servers in two by a fixed hash. Ruleset v10 was written from 22 servers; the table reports the other 23, with 218 poisoned tools no rule was fitted to.
|
|
177
|
+
|
|
178
|
+
| Servers | WARDEN 0.8.2 (v8) | WARDEN 0.9.0 (v10) | mcp-audit 0.18.2 | mcp-shield 1.0.4 |
|
|
179
|
+
|---|---|---|---|---|
|
|
180
|
+
| 23 attacks we wrote, blocked | 14 | 20 | 10 | 6 |
|
|
181
|
+
| 10 attacks from mcp-audit's and mcp-shield's own fixtures, blocked | 7 | 10 | 10 | 8 |
|
|
182
|
+
| 12 hard benign cases, blocked | 0 | 0 | 1 | 1 |
|
|
183
|
+
| 986 public servers, blocked | 3 | 3 | 33 | 343 |
|
|
184
|
+
| …blocks that hold up when the text is read | 1, and 1 arguable | 1, and 1 arguable | 0 | 0 of 20 drawn at random |
|
|
185
|
+
| 218 poisoned MCPTox tools on the held-out servers, blocked | 26 | 171 | 25 | 41 |
|
|
186
|
+
| 225 MCPTox tools with an `<IMPORTANT>` or "Ignore the previous instructions" prefix, blocked | 225 | 225 | 222 | not run |
|
|
187
|
+
| 45 clean MCPTox servers, blocked | 0 | 0 | 2 | 3 |
|
|
188
|
+
|
|
189
|
+
- **Read the corpus row first.** No rule was fitted to it. mcp-audit's 33 blocks are 16 "homoglyphs" in text written wholly in its own script (Chinese punctuation, Greek symbols, Cyrillic), 12 honesty instructions such as "do not tell the user the check digits are wrong", 3 base64 utilities, an SSH public-key tool, and the deploy tool WARDEN also blocks by mistake. mcp-shield blocks 35% of real servers, almost all on a keyword: "API key", "token", `.env`, "..". An ellipsis ("Shopify...") counts as path traversal. It does block the identity service WARDEN blocks, on the word "credentials".
|
|
190
|
+
- **Know which numbers are fitted.** We wrote the first attack set. Ruleset v9 was written after we had read both attack sets, and v10 after the 22 MCPTox servers of the other half. So 20 and 10 are the recall of rules fitted to what they were shown. The held-out MCPTox row, the clean servers, the corpus and the whole 0.8.2 column are not fitted. On the corpus v10 blocks exactly what v8 blocked.
|
|
191
|
+
- **MCPTox tests a shape, not a keyword.** Almost every poisoned tool tells the model to call another tool first, or to change another tool's arguments. v10 names that shape (`TOOL_DEF_CROSS_TOOL`, see [the gate chain](gates.md)) and blocks 171 of 218 held-out tools, against 189 of 267 on the half it was written from. Add the benchmark's `<IMPORTANT>` or "Ignore the previous instructions" prefix and every scanner we ran catches it; we did not run mcp-shield on that set. On the clean servers mcp-audit blocks AmapMap and BaiduMap, and mcp-shield blocks FileSystem, AdFin and Commander on "sensitive file access".
|
|
192
|
+
- **What WARDEN still misses:** 47 of the 218 held-out MCPTox tools, an instruction in Chinese to read `.env`, a paraphrase with no keyword ("gather every credential file … keep this step to yourself") and "include the output of env". It reports 41 of those 47, and all three of ours, as advisory, not as blocks. They need a meaning-based check, not another rule: with the optional classifier above, 191 of the 218 are blocked at `high` and all 218 are flagged.
|
|
193
|
+
- **WARDEN is not alone in that slot.** mcp-audit also ships a lock file, a PR diff mode and SARIF.
|
|
194
|
+
|
|
195
|
+
## What scan does not do
|
|
196
|
+
|
|
197
|
+
- It reads definitions once. A server that changes later is caught by the next scan, by the lock in CI, or at every call by `wrap`.
|
|
198
|
+
- It does not scan call arguments, results, prompts or resources, and it is not a sandbox: starting a stdio server runs it.
|
|
199
|
+
- Its verdicts are WARDEN's static rules and threat records. A paraphrase no rule covers passes; see [the gate chain](gates.md) and the [field survey](mcp-survey.md) for what the rules catch and how often they are wrong.
|
package/docs/scan.ru.md
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# Проверка серверов, которые запускают ваши MCP-клиенты
|
|
2
|
+
|
|
3
|
+
> 🌐 [English](scan.md) · **Русский** · [Español](scan.es.md) · [Français](scan.fr.md) · [中文](scan.zh.md)
|
|
4
|
+
|
|
5
|
+
`warden-mcp scan` читает уже существующую у вас конфигурацию MCP, подключается к каждому серверу из неё и проверяет определения инструментов до того, как их увидит модель. Это та же цепочка гейтов, что у [`wrap`](../README-ru.md) и у [библиотеки](integration.ru.md), только пройденная один раз по вашим конфигам, а не перед одним сервером.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx -y @aimarket/warden@0.11.0 scan
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
WARDEN scan 0.10.0 · ruleset 10 sha256-lJuKKKV5mtru… · block at high
|
|
13
|
+
read .mcp.json (claude-code, 3 servers)
|
|
14
|
+
|
|
15
|
+
✓ allow notes claude-code 1 tool · score 0.90
|
|
16
|
+
✗ BLOCK evil-notes claude-code 1 tool · score 0.00 · TOOL_DEF_EXFIL(notes) TOOL_DEF_SECRET_REQUEST(notes) THREAT_SSH_KEY_READ(notes)
|
|
17
|
+
! error broken claude-code could not start: spawn /nonexistent/bin/server ENOENT
|
|
18
|
+
|
|
19
|
+
3 servers: 1 allowed, 1 blocked, 1 not checked, 0 skipped.
|
|
20
|
+
Details: warden-mcp scan --json, or --markdown FILE. To review a changed server: warden-mcp scan --lock warden.lock.json --update-lock.
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Без аккаунта, без API-ключа, без модели. Сетевой трафик идёт только к серверам из вашего конфига и, если передан `--histor`, к журналу HISTOR.
|
|
24
|
+
|
|
25
|
+
## Где он ищет
|
|
26
|
+
|
|
27
|
+
Без аргументов — каждый существующий файл из таблицы. Передайте файлы, чтобы проверить только их; `--project` — только файлы проекта в рабочем каталоге; `--client NAME` — один клиент.
|
|
28
|
+
|
|
29
|
+
| Клиент | Файл проекта | Файл пользователя |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| Claude Code | `.mcp.json` | `~/.claude.json` (серверы пользователя и этого проекта) |
|
|
32
|
+
| Claude Desktop | — | `claude_desktop_config.json` в каталоге конфигурации приложения |
|
|
33
|
+
| Cursor | `.cursor/mcp.json` | `~/.cursor/mcp.json` |
|
|
34
|
+
| VS Code | `.vscode/mcp.json` (JSONC) | `mcp.json` в каталоге пользовательских настроек |
|
|
35
|
+
| Windsurf | — | `~/.codeium/windsurf/mcp_config.json` |
|
|
36
|
+
|
|
37
|
+
Сервер, который конфиг запускает через `warden-mcp wrap`, проверяется как сервер за обёрткой и под тем же id пина, что использует `wrap`, поэтому дрейф относительно одобренного снимка попадает в отчёт. Выключенные записи, записи с значением `${input:…}`, которое клиент спрашивает у пользователя, и записи без `command` и `url` показываются как пропущенные и никогда не исчезают молча.
|
|
38
|
+
|
|
39
|
+
## Запуск серверов
|
|
40
|
+
|
|
41
|
+
Чтобы прочитать инструменты stdio-сервера, `scan` запускает его с командой, аргументами и окружением из конфига — ровно так, как это сделал бы клиент, — и останавливает после `tools/list`. Инструменты он не вызывает никогда. Но программа при этом всё равно выполняется. Там, где это нежелательно, например в CI на pull request, `--no-launch` проверяет только строку запуска: по записям threat feed о командах и по идентичности запуска из лок-файла.
|
|
42
|
+
|
|
43
|
+
Удалённые серверы (`url`, streamable HTTP или старый HTTP+SSE) опрашиваются по сети с заголовками из конфига. `--public-only` отказывает тем, что разрешаются в loopback, частные, link-local адреса или адрес облачных метаданных; проверяется адрес, к которому реально идёт подключение. Редиректы не выполняются.
|
|
44
|
+
|
|
45
|
+
## Вывод и коды выхода
|
|
46
|
+
|
|
47
|
+
| Опция | Что пишет |
|
|
48
|
+
|---|---|
|
|
49
|
+
| (по умолчанию) | таблицу в stdout |
|
|
50
|
+
| `--json` | JSON-отчёт в stdout |
|
|
51
|
+
| `--json-file FILE` | JSON-отчёт в файл, вместе с таблицей |
|
|
52
|
+
| `--sarif FILE` | SARIF 2.1.0 для GitHub code scanning; только блокирующие находки, с указанием строки сервера в конфиге |
|
|
53
|
+
| `--markdown FILE` | сводку со сворачиваемыми подробностями для `$GITHUB_STEP_SUMMARY` или комментария к PR |
|
|
54
|
+
|
|
55
|
+
Код `0`: ничего не заблокировано. `1`: сервер заблокирован или, с `--fail-on-error`, его не удалось проверить. `2`: ошибка использования или конфига. `--fail-on SEVERITY` сдвигает порог блокировки; `--policy FILE` принимает тот же строгий файл политики, что и `wrap`.
|
|
56
|
+
|
|
57
|
+
Учётные данные в строках запуска и URL показываются как `***`. Описания инструментов в Markdown-отчёте стоят внутри блоков кода, которые этот текст не может закрыть.
|
|
58
|
+
|
|
59
|
+
## Лок-файл: ревьюим определения, а не только команду
|
|
60
|
+
|
|
61
|
+
Строка конфига говорит, какая программа запускается. Она не говорит, что эта программа скажет вашей модели. Лок-файл записывает второе, чтобы pull request это показал.
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
warden-mcp scan --project --lock warden.lock.json --update-lock # после того как прочли, что изменилось
|
|
65
|
+
git add .mcp.json warden.lock.json
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`--update-lock` записывает для каждого сервера идентичность запуска и полные определения инструментов и отказывается записывать сервер, который WARDEN блокирует. Повторный запуск на неизменных серверах файл не меняет. С одним `--lock`:
|
|
69
|
+
|
|
70
|
+
- сервер, которого нет в лок-файле, блокируется (`LOCK_MISSING`);
|
|
71
|
+
- сервер, запускаемый иначе, блокируется (`SERVER_IDENTITY_DRIFT`);
|
|
72
|
+
- сервер, который теперь объявляет другие инструменты, блокируется (`TOOL_DEF_DRIFT`), а Markdown-отчёт показывает изменение диффом имён, описаний и схем.
|
|
73
|
+
|
|
74
|
+
Записи лок-файла, которые не запускает ни один конфиг, перечисляются и удаляются при следующем обновлении.
|
|
75
|
+
|
|
76
|
+
## GitHub Action
|
|
77
|
+
|
|
78
|
+
```yaml
|
|
79
|
+
name: MCP servers
|
|
80
|
+
on: [pull_request]
|
|
81
|
+
permissions:
|
|
82
|
+
contents: read
|
|
83
|
+
security-events: write # нужно только для upload-sarif
|
|
84
|
+
jobs:
|
|
85
|
+
warden:
|
|
86
|
+
runs-on: ubuntu-latest
|
|
87
|
+
steps:
|
|
88
|
+
- uses: actions/checkout@v4
|
|
89
|
+
- uses: alexar76/warden@v0.11.0
|
|
90
|
+
with:
|
|
91
|
+
upload-sarif: 'true'
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
По умолчанию Action читает файлы проекта, не запускает stdio-серверы (эту программу выбирает pull request), отказывает непубличным адресам, использует `warden.lock.json`, если он есть, пишет сводку задания и падает на заблокированном сервере. Входы: `config`, `working-directory`, `lock`, `launch-stdio`, `public-only`, `fail-on`, `histor`, `classifier-url`, `classifier-model`, `classifier-blocks`, `sarif`, `upload-sarif`, `version`. Выходы: `blocked`, `servers`, `sarif`. В боевых workflow закрепляйте Action по SHA коммита.
|
|
95
|
+
|
|
96
|
+
Чтобы узнавать, когда меняется сервер, которым вы пользуетесь, запускайте ту же задачу ещё и по расписанию (`on: { schedule: [{ cron: '17 6 * * *' }] }`): при закоммиченном `warden.lock.json` сервер, у которого изменились определения инструментов, роняет запуск с `TOOL_DEF_DRIFT`, а в сводке виден diff.
|
|
97
|
+
|
|
98
|
+
## pre-commit
|
|
99
|
+
|
|
100
|
+
```yaml
|
|
101
|
+
repos:
|
|
102
|
+
- repo: https://github.com/alexar76/warden
|
|
103
|
+
rev: v0.10.0
|
|
104
|
+
hooks:
|
|
105
|
+
- id: warden-scan # изменённые конфиги; stdio-серверы не запускаются
|
|
106
|
+
- id: warden-lock # проект соответствует warden.lock.json; stdio-серверы запускаются
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Оба хука запускают опубликованный пакет через `npx`, поэтому в `PATH` нужен Node 20 или новее.
|
|
110
|
+
|
|
111
|
+
## Плагин Claude Code
|
|
112
|
+
|
|
113
|
+
```text
|
|
114
|
+
/plugin marketplace add alexar76/warden
|
|
115
|
+
/plugin install warden@warden
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
В начале сессии плагин проверяет серверы, которые Claude Code запускает для проекта, сообщает вам, какие из них заблокированы, а модели — только имя и код находки. Описание заблокированного инструмента никогда не попадает в контекст модели. Вызовы инструментов заблокированного сервера или заблокированного инструмента затем запрещает хук `PreToolUse`: он читает один небольшой файл и ничего не запускает. Подробности и ограничения: [claude-plugin/README.md](../claude-plugin/README.md).
|
|
119
|
+
|
|
120
|
+
## Опасные связки: по отдельности безопасно, вместе — нет
|
|
121
|
+
|
|
122
|
+
Клиент отдаёт инструменты всех серверов одной модели. Если среди серверов есть инструмент, который читает ваши приватные данные, инструмент, через который приходит чужой текст (веб-страница, задача, письмо), и инструмент, который может отправить данные наружу, то текст, пришедший через второй, может велеть модели прочитать первым и отправить третьим. Ни один сервер не виноват, поэтому вердикт по отдельному серверу этого не покажет. `scan` сообщает об этом по каждому клиенту:
|
|
123
|
+
|
|
124
|
+
```text
|
|
125
|
+
⚠ toxic flow claude-code can read private data (filesystem.read_file, …), take in text from outside (fetch.fetch) and send data out (fetch.fetch). …
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Возможности определяются по именам инструментов и параметров (`read_file`, `send_email`, параметр `url` или `to`), а не по описаниям: язык описания не важен, и описание не может себя оправдать. Связка — рекомендательная находка: она никогда не блокирует и не меняет код выхода. Она есть в `--json` (`flows`) и в сводке Markdown. Чтобы разорвать связку, уберите одну из ног — например, ненужный сервер `fetch` или вынесите файловый сервер с доступом к одной папке в отдельный клиент.
|
|
129
|
+
|
|
130
|
+
## HISTOR: отдаёт ли сервер вам то же, что всем?
|
|
131
|
+
|
|
132
|
+
[HISTOR](https://histor.modelmarket.dev) — публичный журнал, который каждый день записывает определения инструментов всех удалённых серверов из официального реестра MCP. `--histor` задаёт ему один вопрос о каждом удалённом сервере: тот ли это набор инструментов, который видит HISTOR?
|
|
133
|
+
|
|
134
|
+
Что отправляется: адрес эндпоинта — только схема, хост и путь (без query, без имени пользователя и пароля) — и дайджест набора инструментов по [MTL/1](https://github.com/alexar76/awr). Ни описаний инструментов, ни заголовков. Эндпоинты на частных хостах и адресах, а также пути, похожие на содержащие ключ, не отправляются вовсе; отчёт объясняет почему.
|
|
135
|
+
|
|
136
|
+
Stdio-сервер, запущенный из публичного реестра npm или PyPI (`npx`, `bunx`, `npm exec`, `pnpm dlx`, `uvx`, `uv tool run`, `pipx run`), называется по пакету — `npm:<имя>` или `pypi:<имя>`, без версии — с тем же дайджестом. HISTOR ставит каждую опубликованную версию такого пакета в песочнице gVisor (приманки вместо секретов, без маршрута наружу; каждый инструмент вызывается один раз с подставными аргументами, ни на что настоящее не действуя) и записывает его инструменты и то, что он делал и записывает его инструменты, поэтому `previously-observed` здесь значит, что у вас более старая версия, а `different` — версия, до которой HISTOR ещё не дошёл, или пакет, который не совпадает с опубликованным. Локальный путь, git- или tarball-адрес, частный реестр (`--registry`, `--index-url`) или простой запуск `node`/`python` не отправляются никогда. Для пакета HISTOR также сообщает, рекомендательными находками, не подражает ли его имя популярному (`HISTOR_PACKAGE_LOOKALIKE`: на один символ, то же имя под другим scope или с другими разделителями, при том что популярный скачивают хотя бы в 100 раз чаще) и нет ли у прочитанной версии признаков украденного токена публикации: `HISTOR_PACKAGE_PROVENANCE_LOST` (нет подтверждённой сборки в CI, хотя у предыдущей версии была), `HISTOR_PACKAGE_INSTALL_SCRIPTS` (появились установочные скрипты), `HISTOR_PACKAGE_PUBLISHER_CHANGED`, `HISTOR_PACKAGE_NEW_DEPENDENCIES`. И что пакет делал в песочнице: `HISTOR_PACKAGE_READS_SECRETS` (открыл приманки — SSH-ключи, токены облаков или реестров, кошелёк — при установке, при запуске или при вызове инструмента), `HISTOR_PACKAGE_PERSISTENCE` (писал туда, где пережил бы сессию, например в `.bashrc` или `authorized_keys`), `HISTOR_PACKAGE_STARTUP_NETWORK` (выходил в сеть до вызова любого инструмента), `HISTOR_PACKAGE_STARTS_PROGRAMS`, `HISTOR_PACKAGE_INSTALL_BEHAVIOUR` (что делают его установочные скрипты). Загрузка `.env` из рабочей папки и обращения к собственному API во время вызова инструмента находками не считаются. С `--histor` отчёт также даёт ссылку на ленту HISTOR с изменениями тех просканированных серверов, которые HISTOR знает (`historWatch` в `--json`): на неё можно подписаться в любом читателе лент.
|
|
137
|
+
|
|
138
|
+
| Ответ | Значение |
|
|
139
|
+
|---|---|
|
|
140
|
+
| `same` | Вам отдали тот набор, который HISTOR видит сейчас |
|
|
141
|
+
| `different` | HISTOR никогда не видел этот набор: он изменился после последнего ежедневного обхода, или сервер отдаёт вам не то, что публичному краулеру. Попадает в отчёт как рекомендательная находка `HISTOR_UNSEEN_TOOLSET` |
|
|
142
|
+
| `previously-observed` | Набор, который HISTOR видел раньше, но не текущий. Рекомендательная находка `HISTOR_OLDER_TOOLSET` |
|
|
143
|
+
| `not-listed`, `not-observed` | HISTOR не знает этот эндпоинт или ещё ни разу не прочитал его успешно |
|
|
144
|
+
|
|
145
|
+
Ответы HISTOR никогда не блокируют. Если HISTOR недоступен, это попадает в отчёт, а проверка продолжается.
|
|
146
|
+
|
|
147
|
+
## Необязательный классификатор
|
|
148
|
+
|
|
149
|
+
Правила WARDEN работают офлайн и детерминированно, и они пропускают то, что не названо ни одним правилом: перефразировку, инструкцию на другом языке. `--classifier-url` и `--classifier-model` добавляют второе мнение модели, которую выбираете вы, через любой OpenAI-совместимый адрес: локальный Ollama, vLLM или LM Studio либо облачный API.
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
warden-mcp scan --classifier-url http://localhost:11434/v1 --classifier-model qwen2.5:14b
|
|
153
|
+
WARDEN_CLASSIFIER_API_KEY=… warden-mcp scan --classifier-url https://api.deepseek.com --classifier-model deepseek-flash --classifier-reasoning-effort none
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
- **Включается явно и отправляет все объявленные поля.** Имя, заголовок, описание, обе схемы, annotations и метаданные расширений, а при необходимости и нормализованное прочтение. Ключ берётся только из `WARDEN_CLASSIFIER_API_KEY`.
|
|
157
|
+
- **По умолчанию — рекомендация; с `--classifier-blocks` неполная проверка запрещает допуск.** Находки имеют код `TOOL_DEF_CLASSIFIER`. Ошибки провайдера, таймаут, усечение текста, неопределённость, неполный или некорректный ответ дают `CLASSIFIER_INCOMPLETE`. Это отказ проверки, а не обнаруженная атака. Такой результат нельзя утвердить через `--update-lock`.
|
|
158
|
+
- **Проверка полномочий на любом языке.** Пять категорий: `instruction_to_model`, `exfiltration`, `secret_request`, `concealment`, `cross_tool`. Промпт отличает требования самого инструмента от подмены указаний пользователя, выдуманного согласия и управления чужими инструментами. Случайные разделители не гарантируют защиту от инъекции. Протокол требует решение по каждому инструменту и ссылки на номера полей. WARDEN сам извлекает доказательства из исходника. Неполное решение повторно проверяется один раз отдельно; сбои API не размножают запросы.
|
|
159
|
+
- **Проверка не даёт доверия.** Владение языками зависит от модели. Независимую от языка границу допуска даёт [`wrap --require-approval`](security-hardening-language-boundary.md); LLM не может выдать это утверждение. Старые ответы HISTOR остаются рекомендательными и используют прежний протокол.
|
|
160
|
+
|
|
161
|
+
Замер 9 октября 2026 года на `deepseek-flash`, той же модели, что у HISTOR, на MCPTox (опубликованный бенчмарк, он описан в сравнении ниже) и на наших наборах:
|
|
162
|
+
|
|
163
|
+
| | Правила (v10) | Правила + классификатор, блокировка на `high` | Правила + любые пометки классификатора (рекомендательно) |
|
|
164
|
+
|---|---|---|---|
|
|
165
|
+
| Отложенная половина MCPTox, поймано из 218 отравленных инструментов | 171 | 191 | 218 |
|
|
166
|
+
| 45 чистых серверов MCPTox, заблокировано или помечено | 0 | 0 | 4 помечено |
|
|
167
|
+
| 200 случайных серверов корпуса, заблокировано или помечено | 2 заблокировано | 2 заблокировано | 2 заблокировано, ещё 12 помечено |
|
|
168
|
+
| 23 наши атаки / 12 трудных безобидных случаев | 20 / 0 | 22 / 0 | 23 / 0 |
|
|
169
|
+
|
|
170
|
+
Два сервера, которые блокируют правила, — это сервис идентичности и деплой-инструмент из сравнения ниже. 13 серверов, помеченных классификатором, получили `medium` или `low`, ни одного `high`. Шесть из них стоит показать человеку: один велит модели провести необратимую передачу ENS-имени «первым и единственным действием», не спрашивая пользователя, другой велит не раскрывать, откуда его данные. Модель помечает и официальный сервер Fetch: его описание говорит модели, что у неё теперь есть интернет и отказываться не нужно. Это инструкция модели, но не атака. Модель пропустила то, что ловят правила: инъекцию в аннотациях, запрос приватного ключа или seed-фразы как параметра и `rm -rf ~`.
|
|
171
|
+
|
|
172
|
+
## Сравнение
|
|
173
|
+
|
|
174
|
+
9 октября 2026 года мы прогнали WARDEN, mcp-audit 0.18.2 (`--connect`) и mcp-shield 1.0.4 по одним и тем же серверам. Каждый сервер воспроизводился по stdio, так что каждый сканер подключался к нему так же, как в жизни. Стенд, наборы и разметка каждого случая лежат в [`scripts/scanner-comparison`](../scripts/scanner-comparison/). Полный разбор с методом и всеми оговорками — в [сравнении сканеров](scanner-comparison.ru.md).
|
|
175
|
+
|
|
176
|
+
Последние три строки взяты из MCPTox (Wang et al., AAAI 2026) — опубликованного бенчмарка из 485 отравленных инструментов, написанных для 45 настоящих MCP-серверов. Мы разделили его серверы пополам по фиксированному хешу. Ruleset v10 написан по 22 серверам; в таблице — остальные 23, с 218 отравленными инструментами, под которые не подгонялось ни одно правило.
|
|
177
|
+
|
|
178
|
+
| Серверы | WARDEN 0.8.2 (v8) | WARDEN 0.9.0 (v10) | mcp-audit 0.18.2 | mcp-shield 1.0.4 |
|
|
179
|
+
|---|---|---|---|---|
|
|
180
|
+
| 23 атаки, написанные нами, заблокировано | 14 | 20 | 10 | 6 |
|
|
181
|
+
| 10 атак из фикстур самих mcp-audit и mcp-shield, заблокировано | 7 | 10 | 10 | 8 |
|
|
182
|
+
| 12 трудных безобидных случаев, заблокировано | 0 | 0 | 1 | 1 |
|
|
183
|
+
| 986 публичных серверов, заблокировано | 3 | 3 | 33 | 343 |
|
|
184
|
+
| …из них по делу, если прочитать текст | 1 и 1 спорный | 1 и 1 спорный | 0 | 0 из 20 случайно выбранных |
|
|
185
|
+
| 218 отравленных инструментов MCPTox на отложенных серверах, заблокировано | 26 | 171 | 25 | 41 |
|
|
186
|
+
| 225 инструментов MCPTox с префиксом `<IMPORTANT>` или «Ignore the previous instructions», заблокировано | 225 | 225 | 222 | не запускали |
|
|
187
|
+
| 45 чистых серверов MCPTox, заблокировано | 0 | 0 | 2 | 3 |
|
|
188
|
+
|
|
189
|
+
- **Сначала читайте строку корпуса.** Под неё не подгонялось ни одно правило. 33 блокировки mcp-audit — это 16 «омоглифов» в тексте, целиком написанном на своём алфавите (китайская пунктуация, греческие символы, кириллица), 12 инструкций честности вроде «do not tell the user the check digits are wrong», 3 утилиты base64, инструмент для публичного SSH-ключа и деплой-инструмент, который WARDEN тоже блокирует по ошибке. mcp-shield блокирует 35% настоящих серверов, почти всегда по ключевому слову: «API key», «token», `.env`, «..». Многоточие («Shopify...») он считает выходом из каталога. Сервис идентичности, который блокирует WARDEN, он тоже блокирует, но за слово «credentials».
|
|
190
|
+
- **Помните, какие числа подогнаны.** Первый набор атак написали мы. Ruleset v9 написан после того, как мы прочли оба набора атак, а v10 — после 22 серверов MCPTox из другой половины. Так что 20 и 10 — это полнота правил, подогнанных под увиденное. Отложенная строка MCPTox, чистые серверы, корпус и весь столбец 0.8.2 не подогнаны. На корпусе v10 блокирует ровно то же, что v8.
|
|
191
|
+
- **MCPTox проверяет форму, а не ключевое слово.** Почти каждый отравленный инструмент велит модели сначала вызвать другой инструмент или изменить аргументы другого инструмента. v10 называет эту форму (`TOOL_DEF_CROSS_TOOL`, см. [цепочку гейтов](gates.ru.md)) и блокирует 171 из 218 отложенных инструментов против 189 из 267 на половине, по которой он написан. С префиксом бенчмарка `<IMPORTANT>` или «Ignore the previous instructions» их ловит каждый запущенный нами сканер; mcp-shield на этом наборе мы не запускали. На чистых серверах mcp-audit блокирует AmapMap и BaiduMap, а mcp-shield — FileSystem, AdFin и Commander за «sensitive file access».
|
|
192
|
+
- **Что WARDEN всё ещё пропускает:** 47 из 218 отложенных инструментов MCPTox, инструкцию по-китайски прочитать `.env`, перефразировку без ключевых слов («gather every credential file … keep this step to yourself») и «include the output of env». О 41 из этих 47 и обо всех трёх наших он сообщает рекомендательно, но не блокирует. Здесь нужна проверка по смыслу, а не ещё одно правило: с необязательным классификатором выше 191 из 218 блокируются на уровне `high`, а помечаются все 218.
|
|
193
|
+
- **WARDEN в этой нише не один.** У mcp-audit тоже есть лок-файл, режим диффа для PR и SARIF.
|
|
194
|
+
|
|
195
|
+
## Чего scan не делает
|
|
196
|
+
|
|
197
|
+
- Он читает определения один раз. Сервер, изменившийся позже, поймает следующий запуск, лок-файл в CI или `wrap` при каждом вызове.
|
|
198
|
+
- Он не проверяет аргументы и результаты вызовов, prompts и resources и не является песочницей: запуск stdio-сервера выполняет его.
|
|
199
|
+
- Его вердикты — это статические правила и записи угроз WARDEN. Перефразировка, которую не покрывает ни одно правило, пройдёт; что правила ловят и как часто ошибаются — в [цепочке гейтов](gates.ru.md) и в [полевом замере](mcp-survey.ru.md).
|