@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 +147 -3
- package/README-es.md +10 -4
- package/README-fr.md +10 -4
- package/README-ru.md +10 -4
- package/README-zh.md +10 -4
- package/README.md +10 -4
- package/dist/glob.d.ts +81 -0
- package/dist/glob.d.ts.map +1 -0
- package/dist/glob.js +180 -0
- package/dist/glob.js.map +1 -0
- package/dist/index.d.ts +42 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +84 -29
- package/dist/index.js.map +1 -1
- package/dist/origin.d.ts.map +1 -1
- package/dist/origin.js +7 -2
- package/dist/origin.js.map +1 -1
- package/dist/pinning.d.ts +18 -0
- package/dist/pinning.d.ts.map +1 -1
- package/dist/pinning.js +59 -11
- package/dist/pinning.js.map +1 -1
- package/dist/sandbox.d.ts.map +1 -1
- package/dist/sandbox.js +10 -7
- package/dist/sandbox.js.map +1 -1
- package/dist/sanitize.d.ts +38 -0
- package/dist/sanitize.d.ts.map +1 -0
- package/dist/sanitize.js +53 -0
- package/dist/sanitize.js.map +1 -0
- package/dist/static-scan.d.ts +46 -2
- package/dist/static-scan.d.ts.map +1 -1
- package/dist/static-scan.js +358 -34
- package/dist/static-scan.js.map +1 -1
- package/dist/threat-feed.d.ts +3 -0
- package/dist/threat-feed.d.ts.map +1 -1
- package/dist/threat-feed.js +118 -29
- package/dist/threat-feed.js.map +1 -1
- package/dist/types.d.ts +15 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -2
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,
|
|
11
|
-
|
|
12
|
-
|
|
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="
|
|
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`
|
|
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: "
|
|
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 #
|
|
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="
|
|
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,
|
|
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: "
|
|
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 #
|
|
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="
|
|
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 правил, версия
|
|
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: "
|
|
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 #
|
|
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="
|
|
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 条规则(
|
|
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: "
|
|
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 #
|
|
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="
|
|
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,
|
|
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: "
|
|
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 #
|
|
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"}
|