@aimarket/warden 0.3.0 → 0.4.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 CHANGED
@@ -2,14 +2,105 @@
2
2
 
3
3
  All notable changes to `@aimarket/warden`.
4
4
 
5
+ ## 0.4.0 — 2026-08-24
6
+
7
+ Calibrated against the ecosystem, not against fixtures. WARDEN was pointed at every public MCP
8
+ server it could legitimately reach — 1 108 of them answered a live `tools/list` with 17 491 tool
9
+ definitions — and the result is written up in [`docs/mcp-survey.md`](docs/mcp-survey.md).
10
+
11
+ Ruleset `v3` blocked 50 of those 1 108 servers. Four held up on review. The other 46 were blocked
12
+ for saying the right thing: *"Never send a private key"*, *"the private key never leaves your
13
+ machine"*, a security scanner listing the attacks it detects, a Persian tool description spelled with
14
+ the ZERO WIDTH NON-JOINER its language requires. A scanner with that false-positive profile does not
15
+ get tuned by its users; it gets uninstalled.
16
+
17
+ Ruleset **v4** brings blocking from 50 servers down to **6** on the same corpus, with all four real
18
+ findings still caught.
19
+
20
+ ### Rules read context now
21
+
22
+ Rules gained **guards** — named context checks that decide whether a match is the thing the rule is
23
+ looking for. A guard is part of the published rule table and therefore of the digest: the same regex
24
+ with and without `polarity` is a different ruleset, and a recorded verdict has to be able to say
25
+ which one it was.
26
+
27
+ - **`polarity`** — a credential noun inside a refusal is a promise, not a request. This one
28
+ distinction accounted for 390 of the 492 blocking findings in the survey.
29
+ - **`mention`** — a phrase in quotes, in backticks, or as a bare JSON `enum` value is cited, not said.
30
+ - **`detection`** — a secret named as the object of `detect`/`scan`/`find`/`leaked` is what a scanner
31
+ looks for, not what it wants.
32
+ - **`identifierFragment`** — `mnemonic` inside `bip39-mnemonic-checksum` is that identifier's name.
33
+ - **`harvestTarget`** — a harvest instruction says *whose* secret or *where* it lives. Without it,
34
+ "Obtain a permanent anonymous API key" read as theft rather than issuance.
35
+ - **`uri`** — `javascript:` is now matched case-**sensitively** and needs a payload. Under `/i` it
36
+ matched the word *JavaScript* followed by a colon, i.e. every language list ever written.
37
+ - **`payload`** — a `data:…;base64,` URI with fewer than 32 characters behind the comma is the format
38
+ being documented.
39
+ - **`blob`** — `/` is in the base64 alphabet, so a JSON Schema `$ref` pointer read as a hidden blob.
40
+ Now gated on entropy and on schema keywords.
41
+ - **`zeroWidth`** — U+200C/U+200D adjacent to Arabic, Persian or Indic script is orthography. U+200B,
42
+ U+FEFF and the bidi overrides still block.
43
+ - **`publicKeyPath`** — `authorized_keys`, `known_hosts` and `*.pub` are public by definition.
44
+
45
+ ### Rules demoted from block to advise
46
+
47
+ Each of these was measured selecting for honest servers. They are still reported.
48
+
49
+ - **`exfiltrat*`** as a bare noun. An attacker does not name the attack; a defender names it in every
50
+ sentence. All three hits were defensive tools. The anchored *"send X to <external destination>"*
51
+ rules keep the blocking weight.
52
+ - **`system prompt` / `developer message`**. 15 findings across 6 servers, every one an LLM proxy,
53
+ persona manager or agent-configuration tool that declares a `system` parameter because setting a
54
+ system prompt is its job.
55
+ - **`do not tell the user`**. Four real uses, four of them the opposite of concealment — *"no refund
56
+ is issued automatically … do not tell the user a refund is coming"*. A blocking rule needs a
57
+ concealment target that refers to the tool's own action; the bare phrase carries none.
58
+ - The **credential nouns** keep blocking but drop from `critical` to `high`: still over the default
59
+ threshold, no longer zeroing the gate score. One noun in a schema template shared by 377 tools
60
+ should not read as "maximally compromised".
61
+
62
+ ### Threat feed
63
+
64
+ - Wildcard threat patterns now use `threatMatch`: **interior gaps are bounded** to 24 characters and
65
+ a segment starting with a letter must **start on a word boundary**. `*sweep*funds*` was matching
66
+ `funds` inside "re**funds**", and `*drain*wallet*` was joining two words from different clauses.
67
+ Leading and trailing `*` stay unbounded, and `_`/`-` count as boundaries so a `seed_phrase` schema
68
+ field still matches. `policy.sensitiveToolPatterns` keeps plain glob semantics — that is the
69
+ operator's own pattern against their own tool names.
70
+
71
+ ### Findings say what they matched
72
+
73
+ - Every finding message now quotes the matched text: `… signature (\b(?:read|extract|…) at "obtain
74
+ the redacted credential"`. Without it a reviewer cannot tell which alternative of an alternation
75
+ fired, or on what. Recovering that by hand is most of the work of judging a finding, and it cost
76
+ hours in the survey itself.
77
+ - Dropped matches are logged at `debug` with the guard's reason, because a guard silently discarding a
78
+ finding is the one behaviour in this gate that a verdict cannot show.
79
+
80
+ ### Release gate
81
+
82
+ - `npm run check:ruleset` fails if the version in `package.json` is already on the registry with a
83
+ different ruleset ref, and runs in CI and in `prepublishOnly`. **0.3.0 was published carrying
84
+ ruleset v2 sixty-four seconds before the extraction commit, v3 landed in the source 52 minutes
85
+ later, and nothing republished** — so `npm install @aimarket/warden@0.3.0` handed strangers a
86
+ scanner with no rules on the tool-name surface while the README in that same tarball documented v3.
87
+ Measured after the fact, that cost nothing on the 1 108-server corpus; the ambiguity in every
88
+ recorded verdict was the real defect.
89
+
90
+ ### Tests
91
+
92
+ - `test/field-survey-regression.test.ts` — 19 cases built from the survey's real text. Both
93
+ directions: the 46 false positives must not block, and the four real findings must.
94
+
5
95
  ## 0.3.0 — 2026-08-24
6
96
 
7
97
  First standalone release. The gates, the threat feed, the pinning store contract and the RFC 8785
8
98
  canonicalizer were extracted from [ARGUS](https://github.com/alexar76/argus) 0.3.0, where they had
9
99
  lived as `src/warden/`. The version number is deliberately continuous with the agent that shipped
10
- them, not reset to 0.1.0: this is the same enforcement code, at the same ruleset version (`v2`,
11
- digest `sha256-gWC14PR4kUylkJaAGMnIYYX6tPhZTJ60cSB61UZxuWc=`), with the same 85 behavioural tests
12
- that guarded it there.
100
+ them, not reset to 0.1.0: this is the same enforcement code, with the same behavioural tests that
101
+ guarded it there. The extraction itself changed no rule — it moved ruleset `v2` byte for byte — and
102
+ the security review below, run before this package was ever published, is what took the rules to
103
+ `v3` and closed six holes in the machinery around them.
13
104
 
14
105
  ### Why it moved
15
106
 
@@ -35,6 +126,59 @@ nothing but `node:crypto`.
35
126
  - No behavioural change to any gate. The rule table, the severities, the tiers, the score arithmetic
36
127
  and every finding code are the ones ARGUS 0.3.0 shipped.
37
128
 
129
+ ### Security review before first release
130
+
131
+ The extracted code was read end to end and the findings reproduced. Six were real; all six are fixed
132
+ here and each has a regression test in `test/hardening.test.ts` that fails on the code as extracted.
133
+
134
+ - **Denial of service through glob matching (critical).** Threat-feed patterns and
135
+ `policy.sensitiveToolPatterns` compiled `*` into `.*` and ran a regex. A 32-character pattern
136
+ against a 220-character haystack took **112 seconds**; the sensitive-tool path took **89 seconds**
137
+ from a long tool name. A signed feed record could therefore hang every connection check, which is
138
+ precisely what the feed's trust model says a publisher must not be able to do — "a compromised
139
+ publisher can only add protection" is false if one record stops the firewall answering. Replaced
140
+ with a linear two-pointer matcher (`src/glob.ts`): the same case now returns in about a
141
+ millisecond. Feed records are additionally refused past `MAX_GLOB_WILDCARDS` (12), and a feed is
142
+ refused past 2000 records.
143
+ - **Terminal injection through finding messages (high).** Tool names, server ids, catalog names and
144
+ a feed's `reason` string were interpolated raw into `finding.message`, which hosts print to a TTY
145
+ and store in receipts. A tool named `<ESC>[2K<ESC>[1A…` overwrote the BLOCK line WARDEN had just
146
+ written. Control characters and invisible characters are now escaped visibly (`src/sanitize.ts`,
147
+ exported as `displaySafe`) — escaped rather than stripped, because a name containing `U+202E`
148
+ should look suspicious in the report, not clean. `finding.tool` deliberately keeps the raw name:
149
+ it is the key a host filters its tool list with.
150
+ - **The tool NAME was scanned by nothing (high).** An injection phrase, a zero-width character or a
151
+ base64 blob in the first field the model reads produced zero findings. Ruleset **v3** gives every
152
+ rule a `surfaces` list and adds the name to 17 of the 25 — every phrase-keyed and hidden-payload
153
+ rule. The three noun-keyed codes stay off the name on purpose (`sign_with_private_key` is a
154
+ plausible tool, and refusing it would be the v1 calibration error on a new surface).
155
+ - **A gate that threw took the whole verdict with it (high).** `vet()` rejected, so the host got no
156
+ verdict at all — not even the findings the earlier gates had already produced — and a
157
+ full-disk pin store or a bug in a custom gate decided whether the connection was blocked. A throw
158
+ is now a `GATE_ERROR` finding at `high` with a zero score: a gate that crashed cleared nothing.
159
+ - **A frozen policy crashed the constructor (medium).** The `blockAtSeverity` typo fallback assigned
160
+ into the caller's own object, so `Object.freeze(policy)` — a reasonable thing for a host to do —
161
+ raised a `TypeError` out of `new Warden()` under ESM strict mode. The policy is now normalized
162
+ into a private copy and the caller's object is never touched.
163
+ - **Pinning covered the advertisement but not the program (medium).** Tool-def pinning caught a
164
+ server that changed what it advertises, not one that kept the advertisement and changed the
165
+ command behind it — which a remote **catalog** can do to an already-approved id. `PinnedServer`
166
+ gained an optional `identityHash` (transport, command, args, url, name) and a
167
+ `SERVER_IDENTITY_DRIFT` finding. `env` is excluded on purpose: it holds secrets, and the pin is
168
+ written to disk. Pins from before the field exists stay silent — absent is "not recorded", never
169
+ "changed".
170
+
171
+ Two smaller fixes came out of the same pass: a `feedPublicKey` that parses but is not an Ed25519 key
172
+ is now refused with a warning instead of failing inside `verify()` and being logged at `debug`
173
+ (indistinguishable from "the feed had nothing new"), and an oversized feed body is now streamed and
174
+ aborted at the cap instead of being buffered whole by `res.text()` — `content-length` is advisory,
175
+ so the old check was one missing header away from being no check at all.
176
+
177
+ Two things the review deliberately did **not** change: `EgressGuard` passed every case put to it
178
+ (userinfo tricks, `file:`/`data:` schemes, suffix confusion, case folding), and the scan's cost over
179
+ large tool definitions is linear (500 tools × 240 KB in 1.3 s), so no truncation was introduced for
180
+ a problem that does not exist.
181
+
38
182
  ### Added
39
183
 
40
184
  - `test/packaging.test.ts` — fails if a runtime dependency appears, if any source file imports outside
package/README-es.md CHANGED
@@ -5,12 +5,17 @@
5
5
  <a href="https://github.com/alexar76/warden/actions/workflows/ci.yml"><img src="docs/badges/ci.svg" alt="CI" /></a>
6
6
  <a href="https://www.npmjs.com/package/@aimarket/warden"><img src="https://img.shields.io/npm/v/@aimarket/warden?color=cb3837&label=npm" alt="npm version" /></a>
7
7
  <img src="docs/badges/deps.svg" alt="Cero dependencias de ejecución" />
8
- <img src="docs/badges/tests.svg" alt="95 pruebas en verde" />
8
+ <img src="docs/badges/tests.svg" alt="141 pruebas en verde" />
9
9
  <img src="docs/badges/node.svg" alt="Node >= 20" />
10
10
  <a href="LICENSE"><img src="docs/badges/license.svg" alt="Licencia: MIT" /></a>
11
11
  </p>
12
12
  <!-- /aicom-readme-badges -->
13
13
 
14
+ <p align="center">
15
+ <img src="docs/assets/hero.svg" alt="WARDEN — MCP security firewall: tool defs pass static-scan, threat-feed, origin, and pinning before a recorded verdict" width="920" />
16
+ </p>
17
+
18
+
14
19
  > 🌐 [English](README.md) · [Русский](README-ru.md) · **Español** · [Français](README-fr.md) · [中文](README-zh.md) · [Glosario](https://github.com/alexar76/aicom/blob/main/docs/localization-glossary.md)
15
20
 
16
21
  Un servidor MCP le dice a tu agente qué hacen sus herramientas. El agente se lo cree — y esa frase
@@ -77,7 +82,7 @@ flowchart LR
77
82
 
78
83
  | Puerta | Qué decide | Red | ¿Fatal? |
79
84
  |---|---|---|---|
80
- | **static-scan** | Inyección, exfiltración, peticiones de credenciales y señales de Unicode oculto/base64 en `description` e `inputSchema` — 25 reglas, v2, de las cuales 18 pueden bloquear y 7 son solo de aviso | ninguna | no |
85
+ | **static-scan** | Inyección, exfiltración, peticiones de credenciales y señales de Unicode oculto/base64 en el `name`, la `description` y el `inputSchema` de la herramienta — 25 reglas, v4, de las cuales 15 pueden bloquear y 10 son solo de aviso, 17 cubren también el nombre y 12 llevan un guard de contexto | ninguna | no |
81
86
  | **threat-feed** | Identidad de servidor o herramienta conocida como maliciosa: 11 registros integrados más un feed firmado opcional | solo la descarga del feed | sí, para un `critical` con alcance de servidor |
82
87
  | **origin** | Si el operador declaró este servidor o llegó desde un catálogo remoto | ninguna | sí, con `allowUnknownServers: false` |
83
88
  | **pinning** | Si las definiciones de herramientas siguen coincidiendo con lo que el usuario aprobó | ninguna | sí, con `pinToolDefs: true` |
@@ -99,7 +104,7 @@ endureciera el umbral.
99
104
  findings: [{ gate, severity, code: "THREAT_TOOL_MATCH", message, tool, advisory? }],
100
105
  allowedTools: ["add"],
101
106
  blockedTools: ["sweeper"],
102
- rulesets: { staticScan: { version: "2", digest: "sha256-gWC14PR4…" } }
107
+ rulesets: { staticScan: { version: "4", digest: "sha256-klRyTiD3…" } }
103
108
  }
104
109
  ```
105
110
 
@@ -153,6 +158,7 @@ degradar a ninguna protección:
153
158
  | [La cadena de puertas](docs/gates.es.md) | Cada nivel de regla, cada código de hallazgo, cómo se construye la puntuación compuesta y cómo añadir una puerta |
154
159
  | [El threat feed firmado](docs/threat-feed.es.md) | El contrato en el cable, las tres comprobaciones y cómo publicar un feed que WARDEN acepte |
155
160
  | [Guía de integración](docs/integration.es.md) | Cómo conectar WARDEN a tu propio host MCP, elecciones de política y qué registrar |
161
+ | [Estudio de campo: 1 108 servidores MCP públicos](docs/mcp-survey.es.md) | Qué decidió WARDEN sobre definiciones de herramienta reales de terceros — 50 servidores bloqueados, 4 sustentados, y las seis formas en que el resto se equivocó |
156
162
 
157
163
  ## Lo que esto no es
158
164
 
@@ -171,7 +177,7 @@ degradar a ninguna protección:
171
177
  ## Desarrollo
172
178
 
173
179
  ```bash
174
- npm install && npm run build && npm test # 95 pruebas
180
+ npm install && npm run build && npm test # 141 pruebas
175
181
  ```
176
182
 
177
183
  `test/packaging.test.ts` es lo que mantiene honesto el titular: falla si aparece una dependencia de
package/README-fr.md CHANGED
@@ -5,12 +5,17 @@
5
5
  <a href="https://github.com/alexar76/warden/actions/workflows/ci.yml"><img src="docs/badges/ci.svg" alt="CI" /></a>
6
6
  <a href="https://www.npmjs.com/package/@aimarket/warden"><img src="https://img.shields.io/npm/v/@aimarket/warden?color=cb3837&label=npm" alt="version npm" /></a>
7
7
  <img src="docs/badges/deps.svg" alt="Zéro dépendance d'exécution" />
8
- <img src="docs/badges/tests.svg" alt="95 tests au vert" />
8
+ <img src="docs/badges/tests.svg" alt="141 tests au vert" />
9
9
  <img src="docs/badges/node.svg" alt="Node >= 20" />
10
10
  <a href="LICENSE"><img src="docs/badges/license.svg" alt="Licence : MIT" /></a>
11
11
  </p>
12
12
  <!-- /aicom-readme-badges -->
13
13
 
14
+ <p align="center">
15
+ <img src="docs/assets/hero.svg" alt="WARDEN — MCP security firewall: tool defs pass static-scan, threat-feed, origin, and pinning before a recorded verdict" width="920" />
16
+ </p>
17
+
18
+
14
19
  > 🌐 [English](README.md) · [Русский](README-ru.md) · [Español](README-es.md) · **Français** · [中文](README-zh.md) · [Glossaire](https://github.com/alexar76/aicom/blob/main/docs/localization-glossary.md)
15
20
 
16
21
  Un serveur MCP indique lui-même à votre agent ce que font ses outils. L'agent le croit — et cette
@@ -77,7 +82,7 @@ flowchart LR
77
82
 
78
83
  | Porte | Ce qu'elle décide | Réseau | Fatale ? |
79
84
  |---|---|---|---|
80
- | **static-scan** | Injection, exfiltration, demandes d'identifiants et indices d'Unicode masqué/base64 dans `description` et `inputSchema` — 25 règles, v2, dont 18 peuvent bloquer et 7 sont purement indicatives | aucun | non |
85
+ | **static-scan** | Injection, exfiltration, demandes d'identifiants et indices d'Unicode masqué/base64 dans le `name`, la `description` et l'`inputSchema` de l'outil — 25 règles, v4, dont 15 peuvent bloquer et 10 sont purement indicatives, 17 couvrent aussi le nom et 12 portent un guard de contexte | aucun | non |
81
86
  | **threat-feed** | Identité de serveur ou outil connu comme malveillant : 11 enregistrements intégrés plus un feed signé optionnel | seulement le téléchargement du feed | oui, pour un `critical` de portée serveur |
82
87
  | **origin** | Si l'opérateur a déclaré ce serveur ou s'il provient d'un catalogue distant | aucun | oui, avec `allowUnknownServers: false` |
83
88
  | **pinning** | Si les définitions d'outils correspondent encore à ce que l'utilisateur a approuvé | aucun | oui, avec `pinToolDefs: true` |
@@ -99,7 +104,7 @@ bloquante pour quiconque durcissait le seuil.
99
104
  findings: [{ gate, severity, code: "THREAT_TOOL_MATCH", message, tool, advisory? }],
100
105
  allowedTools: ["add"],
101
106
  blockedTools: ["sweeper"],
102
- rulesets: { staticScan: { version: "2", digest: "sha256-gWC14PR4…" } }
107
+ rulesets: { staticScan: { version: "4", digest: "sha256-klRyTiD3…" } }
103
108
  }
104
109
  ```
105
110
 
@@ -153,6 +158,7 @@ une absence de protection :
153
158
  | [La chaîne de portes](docs/gates.fr.md) | Chaque niveau de règle, chaque code de constat, la construction du score composite et comment ajouter une porte |
154
159
  | [Le threat feed signé](docs/threat-feed.fr.md) | Le contrat sur le fil, les trois vérifications, et comment publier un feed que WARDEN acceptera |
155
160
  | [Guide d'intégration](docs/integration.fr.md) | Brancher WARDEN sur votre propre hôte MCP, choix de politique, et quoi consigner |
161
+ | [Étude de terrain : 1 108 serveurs MCP publics](docs/mcp-survey.fr.md) | Ce que WARDEN a décidé sur de vraies définitions d'outil tierces — 50 serveurs bloqués, 4 étayés, et les six façons dont le reste était faux |
156
162
 
157
163
  ## Ce que ce n'est pas
158
164
 
@@ -171,7 +177,7 @@ une absence de protection :
171
177
  ## Développement
172
178
 
173
179
  ```bash
174
- npm install && npm run build && npm test # 95 tests
180
+ npm install && npm run build && npm test # 141 tests
175
181
  ```
176
182
 
177
183
  `test/packaging.test.ts` est ce qui tient l'accroche honnête : il échoue si une dépendance d'exécution
package/README-ru.md CHANGED
@@ -5,12 +5,17 @@
5
5
  <a href="https://github.com/alexar76/warden/actions/workflows/ci.yml"><img src="docs/badges/ci.svg" alt="CI" /></a>
6
6
  <a href="https://www.npmjs.com/package/@aimarket/warden"><img src="https://img.shields.io/npm/v/@aimarket/warden?color=cb3837&label=npm" alt="npm version" /></a>
7
7
  <img src="docs/badges/deps.svg" alt="Zero runtime dependencies" />
8
- <img src="docs/badges/tests.svg" alt="95 tests passing" />
8
+ <img src="docs/badges/tests.svg" alt="141 tests passing" />
9
9
  <img src="docs/badges/node.svg" alt="Node >= 20" />
10
10
  <a href="LICENSE"><img src="docs/badges/license.svg" alt="License: MIT" /></a>
11
11
  </p>
12
12
  <!-- /aicom-readme-badges -->
13
13
 
14
+ <p align="center">
15
+ <img src="docs/assets/hero.svg" alt="WARDEN — MCP security firewall: tool defs pass static-scan, threat-feed, origin, and pinning before a recorded verdict" width="920" />
16
+ </p>
17
+
18
+
14
19
  > 🌐 [English](README.md) · **Русский** · [Español](README-es.md) · [Français](README-fr.md) · [中文](README-zh.md) · [Глоссарий](https://github.com/alexar76/aicom/blob/main/docs/localization-glossary.md)
15
20
 
16
21
  MCP-сервер сам сообщает вашему агенту, что делают его инструменты. Агент этому верит — и вот эта
@@ -77,7 +82,7 @@ flowchart LR
77
82
 
78
83
  | Гейт | Что решает | Сеть | Fatal? |
79
84
  |---|---|---|---|
80
- | **static-scan** | Инъекции, эксфильтрация, запросы учётных данных, скрытый Unicode и base64-признаки в `description` и `inputSchema` — 25 правил, версия 2, из них 18 могут блокировать и 7 только сообщают | нет | нет |
85
+ | **static-scan** | Инъекции, эксфильтрация, запросы учётных данных, скрытый Unicode и base64-признаки в `name`, `description` и `inputSchema` инструмента — 25 правил, версия 4, из них 15 могут блокировать и 10 только сообщают, 17 покрывают и имя, а у 12 есть контекстный guard | нет | нет |
81
86
  | **threat-feed** | Известный плохой сервер или инструмент: 11 встроенных записей плюс опциональный подписанный feed | только загрузка feed | да, для `critical` на уровне сервера |
82
87
  | **origin** | Объявил ли оператор этот сервер, или он пришёл из удалённого каталога | нет | да, при `allowUnknownServers: false` |
83
88
  | **pinning** | Совпадают ли определения инструментов с тем, что подтвердил пользователь | нет | да, при `pinToolDefs: true` |
@@ -98,7 +103,7 @@ severity снова делало находку блокирующей для в
98
103
  findings: [{ gate, severity, code: "THREAT_TOOL_MATCH", message, tool, advisory? }],
99
104
  allowedTools: ["add"],
100
105
  blockedTools: ["sweeper"],
101
- rulesets: { staticScan: { version: "2", digest: "sha256-gWC14PR4…" } }
106
+ rulesets: { staticScan: { version: "4", digest: "sha256-klRyTiD3…" } }
102
107
  }
103
108
  ```
104
109
 
@@ -152,6 +157,7 @@ GET <ваш feed url>
152
157
  | [Цепочка гейтов](docs/gates.ru.md) | Все уровни правил, все коды находок, как складывается composite score и как добавить свой гейт |
153
158
  | [Подписанный threat feed](docs/threat-feed.ru.md) | Контракт на проводе, три проверки и как публиковать feed, который WARDEN примет |
154
159
  | [Руководство по интеграции](docs/integration.ru.md) | Как встроить WARDEN в свой MCP-хост, выбор политики и что записывать |
160
+ | [Полевой обзор: 1 108 публичных MCP-серверов](docs/mcp-survey.ru.md) | Что WARDEN решил на настоящих чужих определениях инструментов — 50 серверов заблокировано, 4 подтверждено, и шесть способов, которыми остальные оказались ошибкой |
155
161
 
156
162
  ## Чем это не является
157
163
 
@@ -170,7 +176,7 @@ GET <ваш feed url>
170
176
  ## Разработка
171
177
 
172
178
  ```bash
173
- npm install && npm run build && npm test # 95 тестов
179
+ npm install && npm run build && npm test # 141 тестов
174
180
  ```
175
181
 
176
182
  `test/packaging.test.ts` — то, что удерживает заголовок честным: он падает, если появляется
package/README-zh.md CHANGED
@@ -5,12 +5,17 @@
5
5
  <a href="https://github.com/alexar76/warden/actions/workflows/ci.yml"><img src="docs/badges/ci.svg" alt="CI" /></a>
6
6
  <a href="https://www.npmjs.com/package/@aimarket/warden"><img src="https://img.shields.io/npm/v/@aimarket/warden?color=cb3837&label=npm" alt="npm 版本" /></a>
7
7
  <img src="docs/badges/deps.svg" alt="零运行时依赖" />
8
- <img src="docs/badges/tests.svg" alt="95 项测试通过" />
8
+ <img src="docs/badges/tests.svg" alt="141 项测试通过" />
9
9
  <img src="docs/badges/node.svg" alt="Node >= 20" />
10
10
  <a href="LICENSE"><img src="docs/badges/license.svg" alt="许可证:MIT" /></a>
11
11
  </p>
12
12
  <!-- /aicom-readme-badges -->
13
13
 
14
+ <p align="center">
15
+ <img src="docs/assets/hero.svg" alt="WARDEN — MCP security firewall: tool defs pass static-scan, threat-feed, origin, and pinning before a recorded verdict" width="920" />
16
+ </p>
17
+
18
+
14
19
  > 🌐 [English](README.md) · [Русский](README-ru.md) · [Español](README-es.md) · [Français](README-fr.md) · **中文** · [术语表](https://github.com/alexar76/aicom/blob/main/docs/localization-glossary.md)
15
20
 
16
21
  MCP 服务器自己告诉你的智能体,它的工具是做什么的。智能体就信了——而这句话正是攻击面。工具描述就是第三方直接
@@ -73,7 +78,7 @@ flowchart LR
73
78
 
74
79
  | 门控 | 判定什么 | 网络 | 是否 fatal |
75
80
  |---|---|---|---|
76
- | **static-scan** | `description` 与 `inputSchema` 中的注入、外泄、索要凭据,以及隐藏 Unicode/base64 迹象——25 条规则(v2),其中 18 条可阻止、7 条仅提示 | 无 | 否 |
81
+ | **static-scan** | 工具 `name`、`description` 与 `inputSchema` 中的注入、外泄、索要凭据,以及隐藏 Unicode/base64 迹象——25 条规则(v4),其中 15 条可阻止、10 条仅提示,17 条同时覆盖名称,12 条带有上下文 guard | 无 | 否 |
77
82
  | **threat-feed** | 已知恶意的服务器身份或工具:11 条内置记录,外加可选的已签名 feed | 仅 feed 下载 | 是,服务器范围的 `critical` |
78
83
  | **origin** | 该服务器是运营者声明的,还是来自远端目录 | 无 | 是,当 `allowUnknownServers: false` |
79
84
  | **pinning** | 工具定义是否仍与用户批准过的一致 | 无 | 是,当 `pinToolDefs: true` |
@@ -93,7 +98,7 @@ flowchart LR
93
98
  findings: [{ gate, severity, code: "THREAT_TOOL_MATCH", message, tool, advisory? }],
94
99
  allowedTools: ["add"],
95
100
  blockedTools: ["sweeper"],
96
- rulesets: { staticScan: { version: "2", digest: "sha256-gWC14PR4…" } }
101
+ rulesets: { staticScan: { version: "4", digest: "sha256-klRyTiD3…" } }
97
102
  }
98
103
  ```
99
104
 
@@ -140,6 +145,7 @@ GET <你的 feed url>
140
145
  | [门控链](docs/gates.zh.md) | 每一个规则层级、每一个发现码、综合评分如何构成,以及如何新增一个门控 |
141
146
  | [已签名的威胁情报 feed](docs/threat-feed.zh.md) | 线上契约、三项检查,以及如何发布一个 WARDEN 会接受的 feed |
142
147
  | [集成指南](docs/integration.zh.md) | 如何把 WARDEN 接入自己的 MCP 宿主、策略取舍,以及应当留档什么 |
148
+ | [实地普查:1 108 个公开 MCP 服务器](docs/mcp-survey.zh.md) | WARDEN 在真实的第三方工具定义上判了什么——拦截 50 个服务器,4 项成立,其余判错的六种方式 |
143
149
 
144
150
  ## 它不是什么
145
151
 
@@ -155,7 +161,7 @@ GET <你的 feed url>
155
161
  ## 开发
156
162
 
157
163
  ```bash
158
- npm install && npm run build && npm test # 95 项测试
164
+ npm install && npm run build && npm test # 141 项测试
159
165
  ```
160
166
 
161
167
  `test/packaging.test.ts` 正是让标题保持诚实的东西:一旦出现运行时依赖、任何源文件从包外 import、或者入口点不
package/README.md CHANGED
@@ -5,12 +5,17 @@
5
5
  <a href="https://github.com/alexar76/warden/actions/workflows/ci.yml"><img src="docs/badges/ci.svg" alt="CI" /></a>
6
6
  <a href="https://www.npmjs.com/package/@aimarket/warden"><img src="https://img.shields.io/npm/v/@aimarket/warden?color=cb3837&label=npm" alt="npm version" /></a>
7
7
  <img src="docs/badges/deps.svg" alt="Zero runtime dependencies" />
8
- <img src="docs/badges/tests.svg" alt="95 tests passing" />
8
+ <img src="docs/badges/tests.svg" alt="141 tests passing" />
9
9
  <img src="docs/badges/node.svg" alt="Node >= 20" />
10
10
  <a href="LICENSE"><img src="docs/badges/license.svg" alt="License: MIT" /></a>
11
11
  </p>
12
12
  <!-- /aicom-readme-badges -->
13
13
 
14
+ <p align="center">
15
+ <img src="docs/assets/hero.svg" alt="WARDEN — MCP security firewall: tool defs pass static-scan, threat-feed, origin, and pinning before a recorded verdict" width="920" />
16
+ </p>
17
+
18
+
14
19
  > 🌐 **English** · [Русский](README-ru.md) · [Español](README-es.md) · [Français](README-fr.md) · [中文](README-zh.md) · [Glossary](https://github.com/alexar76/aicom/blob/main/docs/localization-glossary.md)
15
20
 
16
21
  An MCP server tells your agent what its tools do. The agent believes it — that sentence is the
@@ -76,7 +81,7 @@ flowchart LR
76
81
 
77
82
  | Gate | What it decides | Network | Fatal? |
78
83
  |---|---|---|---|
79
- | **static-scan** | Injection, exfiltration, credential requests and hidden-Unicode/base64 tells in `description` and `inputSchema` — 25 rules, v2, of which 18 can block and 7 are advisory-only | none | no |
84
+ | **static-scan** | Injection, exfiltration, credential requests and hidden-Unicode/base64 tells in the tool `name`, its `description` and its `inputSchema` — 25 rules, v4, of which 15 can block and 10 are advisory-only, 17 also cover the name, and 12 carry a context guard | none | no |
80
85
  | **threat-feed** | Known-bad server identity or tool, from 11 built-in records plus an optional signed feed | only the feed fetch | yes, for a server-scoped `critical` |
81
86
  | **origin** | Whether the operator declared this server or it arrived from a remote catalog | none | yes, under `allowUnknownServers: false` |
82
87
  | **pinning** | Whether the tool defs still match what the user approved | none | yes, under `pinToolDefs: true` |
@@ -97,7 +102,7 @@ second as a low severity made it blocking again for anyone who tightened the thr
97
102
  findings: [{ gate, severity, code: "THREAT_TOOL_MATCH", message, tool, advisory? }],
98
103
  allowedTools: ["add"],
99
104
  blockedTools: ["sweeper"],
100
- rulesets: { staticScan: { version: "2", digest: "sha256-gWC14PR4…" } }
105
+ rulesets: { staticScan: { version: "4", digest: "sha256-klRyTiD3…" } }
101
106
  }
102
107
  ```
103
108
 
@@ -149,6 +154,7 @@ no protection:
149
154
  | [The gate chain](docs/gates.md) | Every rule tier, every finding code, how the composite score is built, and how to add a gate |
150
155
  | [The signed threat feed](docs/threat-feed.md) | The wire contract, the three checks, and how to publish a feed WARDEN will accept |
151
156
  | [Integration guide](docs/integration.md) | Wiring WARDEN into your own MCP host, policy choices, and what to record |
157
+ | [Field survey: 1 108 public MCP servers](docs/mcp-survey.md) | What WARDEN decided on real third-party tool definitions — 50 servers blocked, 4 substantiated, and the six ways the rest were wrong |
152
158
 
153
159
  ## What this is not
154
160
 
@@ -166,7 +172,7 @@ no protection:
166
172
  ## Development
167
173
 
168
174
  ```bash
169
- npm install && npm run build && npm test # 95 tests
175
+ npm install && npm run build && npm test # 141 tests
170
176
  ```
171
177
 
172
178
  `test/packaging.test.ts` is what keeps the headline honest: it fails if a runtime dependency appears,
package/dist/glob.d.ts ADDED
@@ -0,0 +1,81 @@
1
+ /**
2
+ * `*`-glob matching, without a regular expression.
3
+ *
4
+ * WARDEN matches two kinds of glob against attacker-controlled text: threat-feed
5
+ * patterns (a signed publisher chooses the pattern, a hostile server chooses the
6
+ * haystack) and `policy.sensitiveToolPatterns` (the operator chooses the pattern,
7
+ * the server chooses the tool name). Both used to compile `*` into `.*` and run a
8
+ * regex, which is a denial of service:
9
+ *
10
+ * pattern "*a*a*a*a*a*a*a*a*a*a*a*a*a*a*zzz" (32 characters)
11
+ * haystack "aaaa..." (220 characters)
12
+ * -> 112 SECONDS inside one RegExp.test
13
+ *
14
+ * The engine has to try every way of distributing the wildcards over the haystack
15
+ * before it can conclude "no match" - exponential in the number of `*`. That
16
+ * mattered more than an ordinary performance bug: the threat feed's whole trust
17
+ * story is that a compromised publisher can only ever ADD protection, never
18
+ * remove it, and a record that hangs `vet()` forever removes all of it. The same
19
+ * shape let a hostile server freeze the agent inspecting it, by advertising a
20
+ * long enough tool name against an operator's own `*a*b*c*` pattern.
21
+ *
22
+ * The two-pointer algorithm below is the standard wildcard matcher: it walks the
23
+ * haystack once and remembers only the most recent `*` to fall back to, so the
24
+ * work is bounded by `pattern.length x value.length` with no backtracking tree to
25
+ * explode. The same 32/220 case returns in microseconds.
26
+ *
27
+ * Semantics, matching what the regex did: `*` matches any run of zero or more
28
+ * UTF-16 code units (including newlines - the old regex needed the `s` flag for
29
+ * that), every other character is literal, and the match is anchored at both
30
+ * ends. Case folding is the caller's job: both sides arrive already lowercased.
31
+ */
32
+ export declare function wildcardMatch(pattern: string, value: string): boolean;
33
+ /**
34
+ * How many wildcards a pattern may carry.
35
+ *
36
+ * The matcher above is not exponential, but `*`-heavy patterns still cost
37
+ * `pattern x value` per match and buy nothing: a real threat pattern names a
38
+ * command, a path or a keyword. A record with more than this many wildcards is
39
+ * refused rather than run - refusing an *addition* to the deny-list is the safe
40
+ * direction, and it is reported loudly.
41
+ */
42
+ export declare const MAX_GLOB_WILDCARDS = 12;
43
+ /** Count `*` in a pattern, for the record validator. */
44
+ export declare function countWildcards(pattern: string): number;
45
+ /**
46
+ * Largest gap an INTERIOR `*` in a threat pattern may span.
47
+ *
48
+ * A field survey of 1 108 public MCP servers (see docs/mcp-survey.md) found every
49
+ * multi-segment built-in firing on unrelated words that merely happened to appear
50
+ * in the same paragraph: `*drain*wallet*` matched an anti-drainer scanner because
51
+ * it says "drainer" in one clause and "wallet" in another, and `*seed*phrase*`
52
+ * matched "for a **seed** topic, returns suggested search **phrases**". A real
53
+ * threat pattern names something whose parts sit next to each other; 24 characters
54
+ * is roughly "a few words apart" and comfortably covers "wallet seed or recovery
55
+ * phrase".
56
+ */
57
+ export declare const THREAT_MAX_GAP = 24;
58
+ /**
59
+ * Threat-pattern matching: `wildcardMatch` plus two constraints that only make
60
+ * sense for a deny-list.
61
+ *
62
+ * 1. **Interior gaps are bounded** to {@link THREAT_MAX_GAP}. Leading and trailing
63
+ * `*` stay unbounded — `*id_rsa*` must still find `id_rsa` anywhere.
64
+ * 2. **A segment that starts with a word character must start at a word boundary.**
65
+ * `*sweep*funds*` matched an ENS floor-sweeping tool because `funds` sits inside
66
+ * "re**funds**". Only the START is anchored: a pattern author writing `*drain*`
67
+ * means "drainer" too, so anchoring the end as well would break ordinary
68
+ * stemming.
69
+ *
70
+ * Kept separate from {@link wildcardMatch} on purpose. That function is also what
71
+ * `policy.sensitiveToolPatterns` runs on, where the operator wrote the pattern
72
+ * against their own tool names and plain glob semantics are what they expect.
73
+ *
74
+ * Completeness matters here in a way it does not for a display filter: a missed
75
+ * match is a deny-list entry that silently stopped working. So this explores every
76
+ * reachable position per segment (deduplicated, so the work stays bounded by
77
+ * `segments x value.length`) rather than taking the leftmost occurrence greedily,
78
+ * which would report "no match" for a pattern that does match further along.
79
+ */
80
+ export declare function threatMatch(pattern: string, value: string): boolean;
81
+ //# sourceMappingURL=glob.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"glob.d.ts","sourceRoot":"","sources":["../src/glob.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CA4BrE;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAErC,wDAAwD;AACxD,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAMtD;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,cAAc,KAAK,CAAC;AAEjC;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CA+BnE"}