@aimarket/warden 0.3.0 → 0.5.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.
Files changed (51) hide show
  1. package/CHANGELOG.md +155 -3
  2. package/README-es.md +82 -11
  3. package/README-fr.md +82 -11
  4. package/README-ru.md +82 -11
  5. package/README-zh.md +79 -10
  6. package/README.md +91 -14
  7. package/dist/glob.d.ts +81 -0
  8. package/dist/glob.d.ts.map +1 -0
  9. package/dist/glob.js +180 -0
  10. package/dist/glob.js.map +1 -0
  11. package/dist/index.d.ts +42 -9
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +84 -29
  14. package/dist/index.js.map +1 -1
  15. package/dist/mcp-rpc.d.ts +40 -0
  16. package/dist/mcp-rpc.d.ts.map +1 -0
  17. package/dist/mcp-rpc.js +117 -0
  18. package/dist/mcp-rpc.js.map +1 -0
  19. package/dist/mcp-server.d.ts +10 -0
  20. package/dist/mcp-server.d.ts.map +1 -0
  21. package/dist/mcp-server.js +89 -0
  22. package/dist/mcp-server.js.map +1 -0
  23. package/dist/mcp-tools.d.ts +42 -0
  24. package/dist/mcp-tools.d.ts.map +1 -0
  25. package/dist/mcp-tools.js +652 -0
  26. package/dist/mcp-tools.js.map +1 -0
  27. package/dist/origin.d.ts.map +1 -1
  28. package/dist/origin.js +7 -2
  29. package/dist/origin.js.map +1 -1
  30. package/dist/pinning.d.ts +18 -0
  31. package/dist/pinning.d.ts.map +1 -1
  32. package/dist/pinning.js +59 -11
  33. package/dist/pinning.js.map +1 -1
  34. package/dist/sandbox.d.ts.map +1 -1
  35. package/dist/sandbox.js +10 -7
  36. package/dist/sandbox.js.map +1 -1
  37. package/dist/sanitize.d.ts +38 -0
  38. package/dist/sanitize.d.ts.map +1 -0
  39. package/dist/sanitize.js +53 -0
  40. package/dist/sanitize.js.map +1 -0
  41. package/dist/static-scan.d.ts +46 -2
  42. package/dist/static-scan.d.ts.map +1 -1
  43. package/dist/static-scan.js +358 -34
  44. package/dist/static-scan.js.map +1 -1
  45. package/dist/threat-feed.d.ts +3 -0
  46. package/dist/threat-feed.d.ts.map +1 -1
  47. package/dist/threat-feed.js +118 -29
  48. package/dist/threat-feed.js.map +1 -1
  49. package/dist/types.d.ts +15 -0
  50. package/dist/types.d.ts.map +1 -1
  51. package/package.json +11 -4
package/README-zh.md CHANGED
@@ -1,32 +1,96 @@
1
- # WARDEN — MCP 安全防火墙
1
+ # WARDEN — MCP 服务器
2
+
3
+ <!-- mcp-name: io.github.alexar76/warden -->
2
4
 
3
5
  <!-- aicom-readme-badges -->
4
6
  <p align="center">
5
7
  <a href="https://github.com/alexar76/warden/actions/workflows/ci.yml"><img src="docs/badges/ci.svg" alt="CI" /></a>
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>
8
+ <a href="https://glama.ai/mcp/servers/alexar76/warden"><img src="https://glama.ai/mcp/servers/alexar76/warden/badges/score.svg" alt="warden MCP server" /></a>
9
+ <a href="https://warden.modelmarket.dev/"><img src="https://img.shields.io/npm/v/@aimarket/warden?color=cb3837&label=npm" alt="npm 版本" /></a>
7
10
  <img src="docs/badges/deps.svg" alt="零运行时依赖" />
8
- <img src="docs/badges/tests.svg" alt="95 项测试通过" />
11
+ <img src="docs/badges/tests.svg" alt="166 项测试通过" />
9
12
  <img src="docs/badges/node.svg" alt="Node >= 20" />
10
13
  <a href="LICENSE"><img src="docs/badges/license.svg" alt="许可证:MIT" /></a>
11
14
  </p>
12
15
  <!-- /aicom-readme-badges -->
13
16
 
17
+ <p align="center">
18
+ <a href="https://warden.modelmarket.dev/">
19
+ <img src="docs/screenshots/readme/hero-3d.png" alt="WARDEN — 3D 门链:tools/list 经 static-scan、threat-feed、origin、pinning 到可记录的裁定" width="100%" />
20
+ </a>
21
+ </p>
22
+
23
+
14
24
  > 🌐 [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
25
 
26
+ **一个 MCP 服务器。针对已广告工具定义的安全防火墙。附带库。**
27
+
28
+ 传输:**stdio**(`npx -y @aimarket/warden` / `node dist/mcp-server.js`)。兼容主机:Claude Desktop、
29
+ Cursor、Glama,以及任何支持 stdio 的 MCP 客户端。不需要密钥。
30
+
31
+ | | |
32
+ |------|----------|
33
+ | MCP 入口(stdio) | `warden-mcp` → [`src/mcp-server.ts`](src/mcp-server.ts) |
34
+ | 工具 | `vet_mcp_server`、`static_scan_tools`、`classify_sensitive_tools`、`check_egress_url`、`canonicalize_json`、`list_scan_rules` |
35
+ | 库 | `import { Warden } from "@aimarket/warden"` |
36
+ | Glama / Docker(stdio) | [`Dockerfile`](Dockerfile)、[`glama.json`](glama.json) |
37
+ | Official MCP Registry | [`server.json`](server.json) → `io.github.alexar76/warden` |
38
+ | Smithery | [`smithery.yaml`](smithery.yaml) |
39
+
16
40
  MCP 服务器自己告诉你的智能体,它的工具是做什么的。智能体就信了——而这句话正是攻击面。工具描述就是第三方直接
17
41
  投递进模型上下文的提示词文本;而一个名叫 `api_key` 的 schema 字段,就是以 API 形式写出来的索要密钥的请求。
18
42
 
19
43
  WARDEN 在**该服务器的任何工具到达模型之前**审查它,并返回一份可以留档的裁定:允许/阻止、0..1 的评分、导致该
20
44
  评分的各项发现、按工具的划分,以及当时生效的确切规则表。
21
45
 
46
+ **零 npm 运行时依赖。** 库本身唯一的 import 是 `node:crypto`。stdio MCP 服务器会再用到其他 `node:`
47
+ 内建模块(`fs`、`path`、`process`),仍然不拉任何包。它就是 [ARGUS](https://github.com/alexar76/argus)
48
+ 里的那个防火墙,被单独抽出来,好让你把它放在自己的 MCP 宿主前面,而不必换用一个智能体。
49
+
50
+ ## 作为 MCP 服务器运行(stdio)
51
+
22
52
  ```bash
23
- npm install @aimarket/warden
53
+ npx -y @aimarket/warden
54
+ npm run build && node dist/mcp-server.js
24
55
  ```
25
56
 
26
- **零运行时依赖。** 整个包里唯一的 import 是 `node:crypto`。它就是 [ARGUS](https://github.com/alexar76/argus)
27
- 里的那个防火墙,被单独抽出来,好让你把它放在自己的 MCP 宿主前面,而不必换用一个智能体。
57
+ Claude Desktop / Cursor(`mcpServers`):
28
58
 
29
- ## 快速开始
59
+ ```json
60
+ {
61
+ "mcpServers": {
62
+ "warden": {
63
+ "command": "npx",
64
+ "args": ["-y", "@aimarket/warden"]
65
+ }
66
+ }
67
+ }
68
+ ```
69
+
70
+ 进程**不会**启动、代理或沙箱化另一个 MCP 服务器:你传入 `tools/list` 转储,得到一份裁决。
71
+
72
+ | 工具 | 何时使用 |
73
+ |---|---|
74
+ | `vet_mcp_server` | 完整门控链 |
75
+ | `static_scan_tools` | 仅静态扫描 |
76
+ | `classify_sensitive_tools` | 运营方 glob 分类 |
77
+ | `check_egress_url` | 主机 allowlist(空列表拒绝全部) |
78
+ | `canonicalize_json` | RFC 8785 字节 |
79
+ | `list_scan_rules` | 已发布的规则表 |
80
+
81
+ ### 发布到 Glama
82
+
83
+ 列表页:**[glama.ai/mcp/servers/alexar76/warden](https://glama.ai/mcp/servers/alexar76/warden)**
84
+
85
+ 与 [ARGUS](https://github.com/alexar76/argus)、[aimarket-mcp](https://github.com/alexar76/aimarket-mcp)
86
+ 相同:[`glama.json`](glama.json) + [`Dockerfile`](Dockerfile) + `node dist/mcp-server.js`。
87
+ 表单:[`docs/GLAMA.md`](docs/GLAMA.md)。
88
+
89
+ ## 库(嵌入你的宿主)
90
+
91
+ ```bash
92
+ npm install @aimarket/warden
93
+ ```
30
94
 
31
95
  ```ts
32
96
  import { Warden, ThreatFeed, silentLogger } from "@aimarket/warden";
@@ -73,7 +137,7 @@ flowchart LR
73
137
 
74
138
  | 门控 | 判定什么 | 网络 | 是否 fatal |
75
139
  |---|---|---|---|
76
- | **static-scan** | `description` 与 `inputSchema` 中的注入、外泄、索要凭据,以及隐藏 Unicode/base64 迹象——25 条规则(v2),其中 18 条可阻止、7 条仅提示 | 无 | 否 |
140
+ | **static-scan** | 工具 `name`、`description` 与 `inputSchema` 中的注入、外泄、索要凭据,以及隐藏 Unicode/base64 迹象——25 条规则(v4),其中 15 条可阻止、10 条仅提示,17 条同时覆盖名称,12 条带有上下文 guard | 无 | 否 |
77
141
  | **threat-feed** | 已知恶意的服务器身份或工具:11 条内置记录,外加可选的已签名 feed | 仅 feed 下载 | 是,服务器范围的 `critical` |
78
142
  | **origin** | 该服务器是运营者声明的,还是来自远端目录 | 无 | 是,当 `allowUnknownServers: false` |
79
143
  | **pinning** | 工具定义是否仍与用户批准过的一致 | 无 | 是,当 `pinToolDefs: true` |
@@ -93,7 +157,7 @@ flowchart LR
93
157
  findings: [{ gate, severity, code: "THREAT_TOOL_MATCH", message, tool, advisory? }],
94
158
  allowedTools: ["add"],
95
159
  blockedTools: ["sweeper"],
96
- rulesets: { staticScan: { version: "2", digest: "sha256-gWC14PR4…" } }
160
+ rulesets: { staticScan: { version: "4", digest: "sha256-klRyTiD3…" } }
97
161
  }
98
162
  ```
99
163
 
@@ -140,6 +204,10 @@ GET <你的 feed url>
140
204
  | [门控链](docs/gates.zh.md) | 每一个规则层级、每一个发现码、综合评分如何构成,以及如何新增一个门控 |
141
205
  | [已签名的威胁情报 feed](docs/threat-feed.zh.md) | 线上契约、三项检查,以及如何发布一个 WARDEN 会接受的 feed |
142
206
  | [集成指南](docs/integration.zh.md) | 如何把 WARDEN 接入自己的 MCP 宿主、策略取舍,以及应当留档什么 |
207
+ | [实地普查:1 108 个公开 MCP 服务器](docs/mcp-survey.zh.md) | WARDEN 在真实的第三方工具定义上判了什么——拦截 50 个服务器,4 项成立,其余判错的六种方式 |
208
+ | [Glama / Docker](docs/GLAMA.md) | stdio MCP、健康检查、Build steps / CMD |
209
+ | [Security](SECURITY.md) | 如何报告防火墙绕过 |
210
+ | [Contributing](CONTRIBUTING.md) | 零依赖规则、规则表 PR |
143
211
 
144
212
  ## 它不是什么
145
213
 
@@ -151,11 +219,12 @@ GET <你的 feed url>
151
219
  没发出的情况下报告预言机不可达。该门控已被移除,并且只要有任何门控再次声称不可达,
152
220
  `test/no-phantom-gate.test.ts` 就会失败。
153
221
  - **不能替代你亲自读工具定义。** 11 条内置威胁记录是底线,不是目录。
222
+ - **不是代理。** stdio MCP 入口检查的是你传入的定义。它不会连接、下载或执行被扫描的服务器。
154
223
 
155
224
  ## 开发
156
225
 
157
226
  ```bash
158
- npm install && npm run build && npm test # 95 项测试
227
+ npm install && npm run build && npm test # 166 项测试
159
228
  ```
160
229
 
161
230
  `test/packaging.test.ts` 正是让标题保持诚实的东西:一旦出现运行时依赖、任何源文件从包外 import、或者入口点不
package/README.md CHANGED
@@ -1,18 +1,42 @@
1
- # WARDEN — MCP security firewall
1
+ # WARDEN — MCP server
2
+
3
+ <!-- mcp-name: io.github.alexar76/warden -->
2
4
 
3
5
  <!-- aicom-readme-badges -->
4
6
  <p align="center">
5
7
  <a href="https://github.com/alexar76/warden/actions/workflows/ci.yml"><img src="docs/badges/ci.svg" alt="CI" /></a>
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>
8
+ <a href="https://glama.ai/mcp/servers/alexar76/warden"><img src="https://glama.ai/mcp/servers/alexar76/warden/badges/score.svg" alt="warden MCP server" /></a>
9
+ <a href="https://warden.modelmarket.dev/"><img src="https://img.shields.io/npm/v/@aimarket/warden?color=cb3837&label=npm" alt="npm version" /></a>
7
10
  <img src="docs/badges/deps.svg" alt="Zero runtime dependencies" />
8
- <img src="docs/badges/tests.svg" alt="95 tests passing" />
11
+ <img src="docs/badges/tests.svg" alt="166 tests passing" />
9
12
  <img src="docs/badges/node.svg" alt="Node >= 20" />
10
13
  <a href="LICENSE"><img src="docs/badges/license.svg" alt="License: MIT" /></a>
11
14
  </p>
12
15
  <!-- /aicom-readme-badges -->
13
16
 
17
+ <p align="center">
18
+ <a href="https://warden.modelmarket.dev/">
19
+ <img src="docs/screenshots/readme/hero-3d.png" alt="WARDEN — 3D gate chain: tools/list through static-scan, threat-feed, origin, and pinning to a recorded verdict" width="100%" />
20
+ </a>
21
+ </p>
22
+
23
+
14
24
  > 🌐 **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
25
 
26
+ **One MCP server. Security firewall for advertised tool definitions. Library included.**
27
+
28
+ Transport: **stdio** (`npx -y @aimarket/warden` / `node dist/mcp-server.js`). Compatible hosts:
29
+ Claude Desktop, Cursor, Glama, and any MCP client that speaks stdio. No API keys.
30
+
31
+ | Item | Location |
32
+ |------|----------|
33
+ | MCP entrypoint (stdio) | `warden-mcp` → [`src/mcp-server.ts`](src/mcp-server.ts) |
34
+ | Tools | `vet_mcp_server`, `static_scan_tools`, `classify_sensitive_tools`, `check_egress_url`, `canonicalize_json`, `list_scan_rules` |
35
+ | Library | `import { Warden } from "@aimarket/warden"` |
36
+ | Glama / Docker (stdio) | [`Dockerfile`](Dockerfile), [`glama.json`](glama.json) |
37
+ | Official MCP Registry | [`server.json`](server.json) → `io.github.alexar76/warden` |
38
+ | Smithery | [`smithery.yaml`](smithery.yaml) |
39
+
16
40
  An MCP server tells your agent what its tools do. The agent believes it — that sentence is the
17
41
  attack surface. A tool description is prompt text delivered by a third party straight into your
18
42
  model's context, and a schema field named `api_key` is a request for your secrets phrased as an API.
@@ -21,15 +45,60 @@ WARDEN vets a server **before any of its tools reach the model**, and returns a
21
45
  record: allow/block, a 0..1 score, the findings that produced it, a per-tool partition, and the
22
46
  exact rule table that was in force.
23
47
 
48
+ **Zero npm runtime dependencies.** The library's only import is `node:crypto`. The stdio MCP
49
+ server adds other `node:` builtins (`fs`, `path`, `process`) and still pulls in no packages. It is
50
+ the firewall out of [ARGUS](https://github.com/alexar76/argus), extracted so you can put it in front
51
+ of your own MCP host without adopting an agent.
52
+
53
+ ## Run as MCP server (stdio)
54
+
24
55
  ```bash
25
- npm install @aimarket/warden
56
+ npx -y @aimarket/warden # bin: warden-mcp
57
+ # from this repo:
58
+ npm run build && node dist/mcp-server.js
59
+ ```
60
+
61
+ Claude Desktop / Cursor (`mcpServers` entry):
62
+
63
+ ```json
64
+ {
65
+ "mcpServers": {
66
+ "warden": {
67
+ "command": "npx",
68
+ "args": ["-y", "@aimarket/warden"]
69
+ }
70
+ }
71
+ }
26
72
  ```
27
73
 
28
- **Zero runtime dependencies.** The only import in the whole package is `node:crypto`. It is the
29
- firewall out of [ARGUS](https://github.com/alexar76/argus), extracted so you can put it in front of
30
- your own MCP host without adopting an agent.
74
+ The process never starts, proxies, or sandboxes another MCP server — you pass a `tools/list` dump
75
+ in, you get a verdict out.
76
+
77
+ | Tool | When to use |
78
+ |---|---|
79
+ | `vet_mcp_server` | Full gate chain on a server identity + advertised tools |
80
+ | `static_scan_tools` | Injection / exfil scan only (no origin / pinning / threat feed) |
81
+ | `classify_sensitive_tools` | Operator glob split — not an injection scan |
82
+ | `check_egress_url` | Hostname allowlist (empty list denies every host) |
83
+ | `canonicalize_json` | RFC 8785 bytes for feeds and pins |
84
+ | `list_scan_rules` | Published rule table + digest |
85
+
86
+ Glama TDQS: MCP `annotations` (readOnly / destructive / idempotent / openWorld), when-to-use /
87
+ when-not naming siblings, every `inputSchema` property described, `outputSchema` on every tool.
88
+
89
+ ### Publish on Glama
31
90
 
32
- ## Quick start
91
+ Listing: **[glama.ai/mcp/servers/alexar76/warden](https://glama.ai/mcp/servers/alexar76/warden)**
92
+
93
+ Same pattern as **[ARGUS](https://github.com/alexar76/argus)** and
94
+ **[aimarket-mcp](https://github.com/alexar76/aimarket-mcp)**: repo-root [`glama.json`](glama.json) +
95
+ [`Dockerfile`](Dockerfile) + `node dist/mcp-server.js`. Admin form values: [`docs/GLAMA.md`](docs/GLAMA.md).
96
+
97
+ ## Library (embed in your host)
98
+
99
+ ```bash
100
+ npm install @aimarket/warden
101
+ ```
33
102
 
34
103
  ```ts
35
104
  import { Warden, ThreatFeed, silentLogger } from "@aimarket/warden";
@@ -76,7 +145,7 @@ flowchart LR
76
145
 
77
146
  | Gate | What it decides | Network | Fatal? |
78
147
  |---|---|---|---|
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 |
148
+ | **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
149
  | **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
150
  | **origin** | Whether the operator declared this server or it arrived from a remote catalog | none | yes, under `allowUnknownServers: false` |
82
151
  | **pinning** | Whether the tool defs still match what the user approved | none | yes, under `pinToolDefs: true` |
@@ -97,7 +166,7 @@ second as a low severity made it blocking again for anyone who tightened the thr
97
166
  findings: [{ gate, severity, code: "THREAT_TOOL_MATCH", message, tool, advisory? }],
98
167
  allowedTools: ["add"],
99
168
  blockedTools: ["sweeper"],
100
- rulesets: { staticScan: { version: "2", digest: "sha256-gWC14PR4…" } }
169
+ rulesets: { staticScan: { version: "4", digest: "sha256-klRyTiD3…" } }
101
170
  }
102
171
  ```
103
172
 
@@ -149,6 +218,11 @@ no protection:
149
218
  | [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
219
  | [The signed threat feed](docs/threat-feed.md) | The wire contract, the three checks, and how to publish a feed WARDEN will accept |
151
220
  | [Integration guide](docs/integration.md) | Wiring WARDEN into your own MCP host, policy choices, and what to record |
221
+ | [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 |
222
+ | [Glama / Docker](docs/GLAMA.md) | stdio MCP server, health check, admin Build steps / CMD |
223
+ | [MCP registries](docs/REGISTRIES.md) | Official Registry, Smithery, mcp.so / Pulse |
224
+ | [Security](SECURITY.md) | How to report a firewall bypass |
225
+ | [Contributing](CONTRIBUTING.md) | Zero-dep rule, ruleset PRs |
152
226
 
153
227
  ## What this is not
154
228
 
@@ -162,16 +236,19 @@ no protection:
162
236
  again.
163
237
  - **Not a substitute for reading the tool defs.** 11 built-in threat records is a floor, not a
164
238
  catalog.
239
+ - **Not a proxy.** The stdio MCP entry inspects advertised definitions you pass it. It does not
240
+ connect to, fetch, or execute the server under scan.
165
241
 
166
242
  ## Development
167
243
 
168
244
  ```bash
169
- npm install && npm run build && npm test # 95 tests
245
+ npm install && npm run build && npm test # 166 tests
170
246
  ```
171
247
 
172
- `test/packaging.test.ts` is what keeps the headline honest: it fails if a runtime dependency appears,
173
- if any source file imports outside the package, or if the entry point stops exporting the
174
- enforcement surface.
248
+ `test/packaging.test.ts` is what keeps the headline honest: it fails if an npm runtime dependency
249
+ appears, if any source file imports outside the package (except `node:` builtins), or if the entry
250
+ point stops exporting the enforcement surface. `test/mcp-server.test.ts` is the Glama health
251
+ check: `initialize` + `tools/list` + a `tools/call`.
175
252
 
176
253
  Used by [ARGUS](https://github.com/alexar76/argus) (the reference host), [MOMUS](https://github.com/alexar76/momus)
177
254
  (the publisher side), and the AICOM MCP-security course.
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"}
package/dist/glob.js ADDED
@@ -0,0 +1,180 @@
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 function wildcardMatch(pattern, value) {
33
+ const STAR = 0x2a;
34
+ let p = 0;
35
+ let v = 0;
36
+ let star = -1;
37
+ let resume = 0;
38
+ while (v < value.length) {
39
+ if (p < pattern.length && pattern.charCodeAt(p) === value.charCodeAt(v)) {
40
+ p++;
41
+ v++;
42
+ }
43
+ else if (p < pattern.length && pattern.charCodeAt(p) === STAR) {
44
+ // Remember this wildcard and let it match nothing for now.
45
+ star = p++;
46
+ resume = v;
47
+ }
48
+ else if (star >= 0) {
49
+ // Backtrack: hand one more character to the last wildcard. There is only
50
+ // ever one fallback point, which is what keeps this linear-bounded.
51
+ p = star + 1;
52
+ v = ++resume;
53
+ }
54
+ else {
55
+ return false;
56
+ }
57
+ }
58
+ // Trailing wildcards may still match the empty remainder.
59
+ while (p < pattern.length && pattern.charCodeAt(p) === STAR)
60
+ p++;
61
+ return p === pattern.length;
62
+ }
63
+ /**
64
+ * How many wildcards a pattern may carry.
65
+ *
66
+ * The matcher above is not exponential, but `*`-heavy patterns still cost
67
+ * `pattern x value` per match and buy nothing: a real threat pattern names a
68
+ * command, a path or a keyword. A record with more than this many wildcards is
69
+ * refused rather than run - refusing an *addition* to the deny-list is the safe
70
+ * direction, and it is reported loudly.
71
+ */
72
+ export const MAX_GLOB_WILDCARDS = 12;
73
+ /** Count `*` in a pattern, for the record validator. */
74
+ export function countWildcards(pattern) {
75
+ let n = 0;
76
+ for (let i = 0; i < pattern.length; i++) {
77
+ if (pattern.charCodeAt(i) === 0x2a)
78
+ n++;
79
+ }
80
+ return n;
81
+ }
82
+ /**
83
+ * Largest gap an INTERIOR `*` in a threat pattern may span.
84
+ *
85
+ * A field survey of 1 108 public MCP servers (see docs/mcp-survey.md) found every
86
+ * multi-segment built-in firing on unrelated words that merely happened to appear
87
+ * in the same paragraph: `*drain*wallet*` matched an anti-drainer scanner because
88
+ * it says "drainer" in one clause and "wallet" in another, and `*seed*phrase*`
89
+ * matched "for a **seed** topic, returns suggested search **phrases**". A real
90
+ * threat pattern names something whose parts sit next to each other; 24 characters
91
+ * is roughly "a few words apart" and comfortably covers "wallet seed or recovery
92
+ * phrase".
93
+ */
94
+ export const THREAT_MAX_GAP = 24;
95
+ /**
96
+ * Threat-pattern matching: `wildcardMatch` plus two constraints that only make
97
+ * sense for a deny-list.
98
+ *
99
+ * 1. **Interior gaps are bounded** to {@link THREAT_MAX_GAP}. Leading and trailing
100
+ * `*` stay unbounded — `*id_rsa*` must still find `id_rsa` anywhere.
101
+ * 2. **A segment that starts with a word character must start at a word boundary.**
102
+ * `*sweep*funds*` matched an ENS floor-sweeping tool because `funds` sits inside
103
+ * "re**funds**". Only the START is anchored: a pattern author writing `*drain*`
104
+ * means "drainer" too, so anchoring the end as well would break ordinary
105
+ * stemming.
106
+ *
107
+ * Kept separate from {@link wildcardMatch} on purpose. That function is also what
108
+ * `policy.sensitiveToolPatterns` runs on, where the operator wrote the pattern
109
+ * against their own tool names and plain glob semantics are what they expect.
110
+ *
111
+ * Completeness matters here in a way it does not for a display filter: a missed
112
+ * match is a deny-list entry that silently stopped working. So this explores every
113
+ * reachable position per segment (deduplicated, so the work stays bounded by
114
+ * `segments x value.length`) rather than taking the leftmost occurrence greedily,
115
+ * which would report "no match" for a pattern that does match further along.
116
+ */
117
+ export function threatMatch(pattern, value) {
118
+ if (!pattern.includes("*"))
119
+ return value.includes(pattern);
120
+ const parts = pattern.split("*");
121
+ const openStart = parts[0] === "";
122
+ const openEnd = parts[parts.length - 1] === "";
123
+ const segments = parts.filter((s) => s !== "");
124
+ if (segments.length === 0)
125
+ return true;
126
+ // Reachable end positions after matching the segments consumed so far.
127
+ let ends = [0];
128
+ for (let i = 0; i < segments.length; i++) {
129
+ const seg = segments[i];
130
+ // The first segment is anchored at 0 unless the pattern opened with `*`.
131
+ const unbounded = i === 0 ? openStart : false;
132
+ const next = new Set();
133
+ for (const from of ends) {
134
+ const limit = unbounded ? value.length : Math.min(value.length, from + THREAT_MAX_GAP + seg.length);
135
+ let at = from;
136
+ for (;;) {
137
+ const found = value.indexOf(seg, at);
138
+ if (found < 0 || found + seg.length > limit)
139
+ break;
140
+ if (i === 0 && !openStart && found !== 0)
141
+ break;
142
+ if (startsOnBoundary(seg, value, found))
143
+ next.add(found + seg.length);
144
+ at = found + 1;
145
+ }
146
+ }
147
+ if (next.size === 0)
148
+ return false;
149
+ ends = [...next];
150
+ }
151
+ return openEnd || ends.some((e) => e === value.length);
152
+ }
153
+ /**
154
+ * Is `seg` at `at` starting on a word boundary?
155
+ *
156
+ * Only asked when the segment's own first character is a word character — a
157
+ * pattern starting with `~`, `.` or `/` has no boundary to respect.
158
+ */
159
+ function startsOnBoundary(seg, value, at) {
160
+ if (!isWordChar(seg.charCodeAt(0)))
161
+ return true;
162
+ if (at === 0)
163
+ return true;
164
+ return !isWordChar(value.charCodeAt(at - 1));
165
+ }
166
+ /**
167
+ * Letters and digits only — `_` and `-` are BOUNDARIES here, not word characters.
168
+ *
169
+ * Tool schemas are written in snake_case and kebab-case, so a field named
170
+ * `seed_phrase` must still satisfy `*seed*phrase*`; treating `_` as a word
171
+ * character silently switched that built-in off. "re**funds**" is still rejected,
172
+ * because there the preceding character is a letter.
173
+ */
174
+ function isWordChar(c) {
175
+ return ((c >= 0x30 && c <= 0x39) || // 0-9
176
+ (c >= 0x41 && c <= 0x5a) || // A-Z
177
+ (c >= 0x61 && c <= 0x7a) // a-z
178
+ );
179
+ }
180
+ //# sourceMappingURL=glob.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"glob.js","sourceRoot":"","sources":["../src/glob.ts"],"names":[],"mappings":"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"}