@aimarket/warden 0.0.1 → 0.3.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 +62 -0
- package/LICENSE +21 -0
- package/README-es.md +184 -0
- package/README-fr.md +184 -0
- package/README-ru.md +183 -0
- package/README-zh.md +167 -0
- package/README.md +173 -6
- package/dist/index.d.ts +73 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +163 -0
- package/dist/index.js.map +1 -0
- package/dist/jcs.d.ts +86 -0
- package/dist/jcs.d.ts.map +1 -0
- package/dist/jcs.js +296 -0
- package/dist/jcs.js.map +1 -0
- package/dist/logger.d.ts +10 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +19 -0
- package/dist/logger.js.map +1 -0
- package/dist/origin.d.ts +25 -0
- package/dist/origin.d.ts.map +1 -0
- package/dist/origin.js +44 -0
- package/dist/origin.js.map +1 -0
- package/dist/pinning.d.ts +73 -0
- package/dist/pinning.d.ts.map +1 -0
- package/dist/pinning.js +179 -0
- package/dist/pinning.js.map +1 -0
- package/dist/sandbox.d.ts +38 -0
- package/dist/sandbox.d.ts.map +1 -0
- package/dist/sandbox.js +76 -0
- package/dist/sandbox.js.map +1 -0
- package/dist/static-scan.d.ts +66 -0
- package/dist/static-scan.d.ts.map +1 -0
- package/dist/static-scan.js +215 -0
- package/dist/static-scan.js.map +1 -0
- package/dist/threat-feed.d.ts +135 -0
- package/dist/threat-feed.d.ts.map +1 -0
- package/dist/threat-feed.js +457 -0
- package/dist/threat-feed.js.map +1 -0
- package/dist/types.d.ts +158 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +11 -0
- package/dist/types.js.map +1 -0
- package/package.json +66 -9
- package/index.js +0 -6
package/README-zh.md
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# WARDEN — MCP 安全防火墙
|
|
2
|
+
|
|
3
|
+
<!-- aicom-readme-badges -->
|
|
4
|
+
<p align="center">
|
|
5
|
+
<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>
|
|
7
|
+
<img src="docs/badges/deps.svg" alt="零运行时依赖" />
|
|
8
|
+
<img src="docs/badges/tests.svg" alt="95 项测试通过" />
|
|
9
|
+
<img src="docs/badges/node.svg" alt="Node >= 20" />
|
|
10
|
+
<a href="LICENSE"><img src="docs/badges/license.svg" alt="许可证:MIT" /></a>
|
|
11
|
+
</p>
|
|
12
|
+
<!-- /aicom-readme-badges -->
|
|
13
|
+
|
|
14
|
+
> 🌐 [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
|
+
|
|
16
|
+
MCP 服务器自己告诉你的智能体,它的工具是做什么的。智能体就信了——而这句话正是攻击面。工具描述就是第三方直接
|
|
17
|
+
投递进模型上下文的提示词文本;而一个名叫 `api_key` 的 schema 字段,就是以 API 形式写出来的索要密钥的请求。
|
|
18
|
+
|
|
19
|
+
WARDEN 在**该服务器的任何工具到达模型之前**审查它,并返回一份可以留档的裁定:允许/阻止、0..1 的评分、导致该
|
|
20
|
+
评分的各项发现、按工具的划分,以及当时生效的确切规则表。
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install @aimarket/warden
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**零运行时依赖。** 整个包里唯一的 import 是 `node:crypto`。它就是 [ARGUS](https://github.com/alexar76/argus)
|
|
27
|
+
里的那个防火墙,被单独抽出来,好让你把它放在自己的 MCP 宿主前面,而不必换用一个智能体。
|
|
28
|
+
|
|
29
|
+
## 快速开始
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { Warden, ThreatFeed, silentLogger } from "@aimarket/warden";
|
|
33
|
+
|
|
34
|
+
const threatFeed = new ThreatFeed({ feedPublicKey: process.env.FEED_PUBKEY });
|
|
35
|
+
await threatFeed.load(process.env.FEED_URL); // 不传 URL → 只用内置拒绝列表,不联网
|
|
36
|
+
|
|
37
|
+
const pins = new Map();
|
|
38
|
+
const warden = Warden.create({
|
|
39
|
+
policy: {
|
|
40
|
+
blockAtSeverity: "high",
|
|
41
|
+
sensitiveToolPatterns: ["*delete*", "*transfer*", "*key*"],
|
|
42
|
+
allowUnknownServers: false, // 失败即关闭:只允许你声明过的服务器
|
|
43
|
+
pinToolDefs: true,
|
|
44
|
+
},
|
|
45
|
+
threatFeed,
|
|
46
|
+
store: {
|
|
47
|
+
getPin: async (id) => pins.get(id),
|
|
48
|
+
putPin: async (p) => void pins.set(p.serverId, p),
|
|
49
|
+
},
|
|
50
|
+
log: silentLogger(), // 或者你自己的 logger
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const verdict = await warden.vet(server, await client.listTools());
|
|
54
|
+
|
|
55
|
+
if (!verdict.allow) throw new Error(`被 ${verdict.decidedBy} 阻止`);
|
|
56
|
+
const usable = verdict.allowedTools; // 被投毒的工具可以单独隔离
|
|
57
|
+
await warden.approve(server, tools); // 把用户认可的内容固定(pin)下来
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`vet()` **不发起任何网络请求**。WARDEN 唯一会发出的请求,是你把 URL 传给 `load()` 时主动要求的威胁情报
|
|
61
|
+
(threat feed)下载。
|
|
62
|
+
|
|
63
|
+
## 门控链
|
|
64
|
+
|
|
65
|
+
```mermaid
|
|
66
|
+
flowchart LR
|
|
67
|
+
T["服务器给出的<br/>工具定义"] --> S["static scan<br/>25 条规则"]
|
|
68
|
+
S --> F["threat feed<br/>11 条内置 + 已签名"]
|
|
69
|
+
F --> O["origin<br/>已声明 vs 来自目录"]
|
|
70
|
+
O --> P["pinning<br/>漂移 vs 已批准"]
|
|
71
|
+
P --> V["裁定<br/>allow · score · findings<br/>allowedTools / blockedTools"]
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
| 门控 | 判定什么 | 网络 | 是否 fatal |
|
|
75
|
+
|---|---|---|---|
|
|
76
|
+
| **static-scan** | `description` 与 `inputSchema` 中的注入、外泄、索要凭据,以及隐藏 Unicode/base64 迹象——25 条规则(v2),其中 18 条可阻止、7 条仅提示 | 无 | 否 |
|
|
77
|
+
| **threat-feed** | 已知恶意的服务器身份或工具:11 条内置记录,外加可选的已签名 feed | 仅 feed 下载 | 是,服务器范围的 `critical` |
|
|
78
|
+
| **origin** | 该服务器是运营者声明的,还是来自远端目录 | 无 | 是,当 `allowUnknownServers: false` |
|
|
79
|
+
| **pinning** | 工具定义是否仍与用户批准过的一致 | 无 | 是,当 `pinToolDefs: true` |
|
|
80
|
+
|
|
81
|
+
综合评分是各门控贡献的**乘积**:一个门控出问题会把整台服务器拉下来,而不是被平均掉。严重级别与是否阻止是两
|
|
82
|
+
个独立的维度:带 `advisory` 的发现会被报告,但在任何 `blockAtSeverity` 下都不会阻止连接、也不会牺牲任何工
|
|
83
|
+
具——因为「这值得多少注意」和「这到底是不是缺陷」是两个不同的问题,而把后者编码成一个较低的严重级别,会让
|
|
84
|
+
它对任何收紧阈值的人重新变成阻止性的。
|
|
85
|
+
|
|
86
|
+
## 裁定就是为了留档
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
{
|
|
90
|
+
allow: false,
|
|
91
|
+
score: 0,
|
|
92
|
+
decidedBy: "threat-feed",
|
|
93
|
+
findings: [{ gate, severity, code: "THREAT_TOOL_MATCH", message, tool, advisory? }],
|
|
94
|
+
allowedTools: ["add"],
|
|
95
|
+
blockedTools: ["sweeper"],
|
|
96
|
+
rulesets: { staticScan: { version: "2", digest: "sha256-gWC14PR4…" } }
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`rulesets` 不是装饰。同一台服务器在更晚的规则表下会得到不同评分;如果既没有版本号、也没有对规则内容的摘要,
|
|
101
|
+
就无法把这种情况与「服务器本身变了」区分开。缺了它们,留档的扫描结果不可复现。
|
|
102
|
+
|
|
103
|
+
## 已签名的威胁情报 feed
|
|
104
|
+
|
|
105
|
+
WARDEN 不会读取未签名的远端 feed。这个契约刻意做得很枯燥:
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
GET <你的 feed url>
|
|
109
|
+
{ "records": [ {pattern, severity, code, reason, source, scope}, … ],
|
|
110
|
+
"timestamp": 1786205907380, // epoch 毫秒,整数——必填
|
|
111
|
+
"signature": "f588d5a4…" // 对 {records, timestamp} 的 RFC 8785
|
|
112
|
+
} // 规范化形式做的 Ed25519 签名(hex)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
会检查三项性质,且**任何一项失败都保留内置底线**,而不是退化成毫无防护:
|
|
116
|
+
|
|
117
|
+
1. **真实性**——用你事先固定的公钥(`feedPublicKey`)做 Ed25519 验签;
|
|
118
|
+
2. **新鲜度**——*被签名的* timestamp 必须落在 `maxAgeMs` 窗口内(默认 24 小时),这样提供该 URL 的一方就无法
|
|
119
|
+
重放几个月前的快照、悄悄抹掉此后新增的每一条记录。签名说明的是谁写了这份文件,从不说明它是何时交到你手上
|
|
120
|
+
的;
|
|
121
|
+
3. **确定性**——RFC 8785 规范化字节,使发布方与验证方无论 JSON 键顺序如何都能对上。
|
|
122
|
+
|
|
123
|
+
如果你需要一个可以让 `load()` 指向的目标,[MOMUS](https://github.com/alexar76/momus) 是该契约的参考发布方
|
|
124
|
+
(`/warden/threat-feed`)。
|
|
125
|
+
|
|
126
|
+
## 还包含
|
|
127
|
+
|
|
128
|
+
- **`EgressGuard`**——出网白名单,用来包住工具发出的任何请求。工具去访问你从未列入的主机,正是典型的
|
|
129
|
+
phone-home 迹象。`*.example.com` 覆盖子域;空白名单会阻止一切,而不是放行一切。
|
|
130
|
+
- **`isSensitiveTool` / `classifyTools`**——对必须逐次调用都要审批的工具做 glob 分类。敏感工具仍然是*已公布*
|
|
131
|
+
的,只是不能在无人看管的情况下运行。
|
|
132
|
+
- **`canonicalize` / `parseJsonStrict`**——严格的 RFC 8785(JCS)实现,同时以 `@aimarket/warden/jcs` 导出,
|
|
133
|
+
便于把另一份实现逐字节对照验证。超出 `MAX_SAFE_JSON_INTEGER` 只接受整数、遇到孤立代理项直接拒绝(而不是转
|
|
134
|
+
义),且每次拒绝都带原因码。
|
|
135
|
+
|
|
136
|
+
## 文档
|
|
137
|
+
|
|
138
|
+
| | |
|
|
139
|
+
|---|---|
|
|
140
|
+
| [门控链](docs/gates.zh.md) | 每一个规则层级、每一个发现码、综合评分如何构成,以及如何新增一个门控 |
|
|
141
|
+
| [已签名的威胁情报 feed](docs/threat-feed.zh.md) | 线上契约、三项检查,以及如何发布一个 WARDEN 会接受的 feed |
|
|
142
|
+
| [集成指南](docs/integration.zh.md) | 如何把 WARDEN 接入自己的 MCP 宿主、策略取舍,以及应当留档什么 |
|
|
143
|
+
|
|
144
|
+
## 它不是什么
|
|
145
|
+
|
|
146
|
+
- **不是沙箱。** 这些都是进程内的 JS 判定。操作系统层面对 MCP 子进程的约束(seccomp/Landlock、
|
|
147
|
+
`sandbox-exec`)不在这里。
|
|
148
|
+
- **不是模型。** 整条链上任何位置都不调用 LLM。这正是 `vet()` 快速、离线且确定的原因——也正因如此,静态扫描
|
|
149
|
+
是正则形态的,会漏掉任何规则都没覆盖的改写说法。
|
|
150
|
+
- **不是信誉服务。** 早期版本里有一个门控,会向信任预言机索要一个它根本没有数据可算的评分,然后在一个请求都
|
|
151
|
+
没发出的情况下报告预言机不可达。该门控已被移除,并且只要有任何门控再次声称不可达,
|
|
152
|
+
`test/no-phantom-gate.test.ts` 就会失败。
|
|
153
|
+
- **不能替代你亲自读工具定义。** 11 条内置威胁记录是底线,不是目录。
|
|
154
|
+
|
|
155
|
+
## 开发
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
npm install && npm run build && npm test # 95 项测试
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
`test/packaging.test.ts` 正是让标题保持诚实的东西:一旦出现运行时依赖、任何源文件从包外 import、或者入口点不
|
|
162
|
+
再导出执行面,它就会失败。
|
|
163
|
+
|
|
164
|
+
被 [ARGUS](https://github.com/alexar76/argus)(参考宿主)、[MOMUS](https://github.com/alexar76/momus)
|
|
165
|
+
(发布方一侧)以及 AICOM 的 MCP 安全课程使用。
|
|
166
|
+
|
|
167
|
+
MIT © AICOM (alexar76)
|
package/README.md
CHANGED
|
@@ -1,12 +1,179 @@
|
|
|
1
|
-
#
|
|
1
|
+
# WARDEN — MCP security firewall
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<!-- aicom-readme-badges -->
|
|
4
|
+
<p align="center">
|
|
5
|
+
<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>
|
|
7
|
+
<img src="docs/badges/deps.svg" alt="Zero runtime dependencies" />
|
|
8
|
+
<img src="docs/badges/tests.svg" alt="95 tests passing" />
|
|
9
|
+
<img src="docs/badges/node.svg" alt="Node >= 20" />
|
|
10
|
+
<a href="LICENSE"><img src="docs/badges/license.svg" alt="License: MIT" /></a>
|
|
11
|
+
</p>
|
|
12
|
+
<!-- /aicom-readme-badges -->
|
|
4
13
|
|
|
5
|
-
**
|
|
14
|
+
> 🌐 **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
|
+
|
|
16
|
+
An MCP server tells your agent what its tools do. The agent believes it — that sentence is the
|
|
17
|
+
attack surface. A tool description is prompt text delivered by a third party straight into your
|
|
18
|
+
model's context, and a schema field named `api_key` is a request for your secrets phrased as an API.
|
|
19
|
+
|
|
20
|
+
WARDEN vets a server **before any of its tools reach the model**, and returns a verdict you can
|
|
21
|
+
record: allow/block, a 0..1 score, the findings that produced it, a per-tool partition, and the
|
|
22
|
+
exact rule table that was in force.
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install @aimarket/warden
|
|
26
|
+
```
|
|
27
|
+
|
|
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.
|
|
31
|
+
|
|
32
|
+
## Quick start
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { Warden, ThreatFeed, silentLogger } from "@aimarket/warden";
|
|
36
|
+
|
|
37
|
+
const threatFeed = new ThreatFeed({ feedPublicKey: process.env.FEED_PUBKEY });
|
|
38
|
+
await threatFeed.load(process.env.FEED_URL); // omit → built-in deny-list only, no network
|
|
39
|
+
|
|
40
|
+
const pins = new Map();
|
|
41
|
+
const warden = Warden.create({
|
|
42
|
+
policy: {
|
|
43
|
+
blockAtSeverity: "high",
|
|
44
|
+
sensitiveToolPatterns: ["*delete*", "*transfer*", "*key*"],
|
|
45
|
+
allowUnknownServers: false, // fail-closed: only servers you declared
|
|
46
|
+
pinToolDefs: true,
|
|
47
|
+
},
|
|
48
|
+
threatFeed,
|
|
49
|
+
store: {
|
|
50
|
+
getPin: async (id) => pins.get(id),
|
|
51
|
+
putPin: async (p) => void pins.set(p.serverId, p),
|
|
52
|
+
},
|
|
53
|
+
log: silentLogger(), // or your own logger
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const verdict = await warden.vet(server, await client.listTools());
|
|
57
|
+
|
|
58
|
+
if (!verdict.allow) throw new Error(`blocked by ${verdict.decidedBy}`);
|
|
59
|
+
const usable = verdict.allowedTools; // a poisoned tool can be quarantined alone
|
|
60
|
+
await warden.approve(server, tools); // pin what the user accepted
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`vet()` performs **no network I/O**. The only request WARDEN ever makes is the threat-feed fetch you
|
|
64
|
+
asked for by passing a URL to `load()`.
|
|
65
|
+
|
|
66
|
+
## The gate chain
|
|
67
|
+
|
|
68
|
+
```mermaid
|
|
69
|
+
flowchart LR
|
|
70
|
+
T["tool defs<br/>from the server"] --> S["static scan<br/>25 rules"]
|
|
71
|
+
S --> F["threat feed<br/>11 built-ins + signed"]
|
|
72
|
+
F --> O["origin<br/>declared vs catalog"]
|
|
73
|
+
O --> P["pinning<br/>drift vs approval"]
|
|
74
|
+
P --> V["verdict<br/>allow · score · findings<br/>allowedTools / blockedTools"]
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
| Gate | What it decides | Network | Fatal? |
|
|
78
|
+
|---|---|---|---|
|
|
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 |
|
|
80
|
+
| **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
|
+
| **origin** | Whether the operator declared this server or it arrived from a remote catalog | none | yes, under `allowUnknownServers: false` |
|
|
82
|
+
| **pinning** | Whether the tool defs still match what the user approved | none | yes, under `pinToolDefs: true` |
|
|
83
|
+
|
|
84
|
+
The composite score is the **product** of gate contributions, so one bad gate drags the whole server
|
|
85
|
+
down rather than being averaged away. Severity and blocking are separate axes: an `advisory` finding
|
|
86
|
+
is reported and never blocks and never costs a tool, at any `blockAtSeverity` — because "how much
|
|
87
|
+
attention does this deserve" and "is this a defect at all" are different questions, and encoding the
|
|
88
|
+
second as a low severity made it blocking again for anyone who tightened the threshold.
|
|
89
|
+
|
|
90
|
+
## The verdict is meant to be recorded
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
{
|
|
94
|
+
allow: false,
|
|
95
|
+
score: 0,
|
|
96
|
+
decidedBy: "threat-feed",
|
|
97
|
+
findings: [{ gate, severity, code: "THREAT_TOOL_MATCH", message, tool, advisory? }],
|
|
98
|
+
allowedTools: ["add"],
|
|
99
|
+
blockedTools: ["sweeper"],
|
|
100
|
+
rulesets: { staticScan: { version: "2", digest: "sha256-gWC14PR4…" } }
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`rulesets` is not decoration. The same server scores differently under a later rule table, and
|
|
105
|
+
without the version *and* a digest over the rules there is no way to tell that apart from the server
|
|
106
|
+
having changed. A stored scan without them is not reproducible.
|
|
107
|
+
|
|
108
|
+
## Signed threat feed
|
|
109
|
+
|
|
110
|
+
WARDEN will not read an unsigned remote feed. The contract is deliberately boring:
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
GET <your feed url>
|
|
114
|
+
{ "records": [ {pattern, severity, code, reason, source, scope}, … ],
|
|
115
|
+
"timestamp": 1786205907380, // epoch ms, integer — required
|
|
116
|
+
"signature": "f588d5a4…" // Ed25519 (hex) over the RFC 8785 canonical
|
|
117
|
+
} // form of {records, timestamp}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Three properties are checked, and **any failure keeps the built-in floor** rather than degrading to
|
|
121
|
+
no protection:
|
|
122
|
+
|
|
123
|
+
1. **authenticity** — Ed25519 against the key you pinned in advance (`feedPublicKey`);
|
|
124
|
+
2. **freshness** — the *signed* timestamp must be inside `maxAgeMs` (24 h by default), so whoever
|
|
125
|
+
serves the URL cannot replay a months-old snapshot and silently erase every record added since.
|
|
126
|
+
A signature says who wrote a document, never when you were handed it;
|
|
127
|
+
3. **determinism** — RFC 8785 canonical bytes, so publisher and verifier agree regardless of JSON
|
|
128
|
+
key order.
|
|
129
|
+
|
|
130
|
+
[MOMUS](https://github.com/alexar76/momus) is a reference publisher of this contract
|
|
131
|
+
(`/warden/threat-feed`) if you want something to point `load()` at.
|
|
132
|
+
|
|
133
|
+
## Also in the box
|
|
134
|
+
|
|
135
|
+
- **`EgressGuard`** — an outbound allowlist to wrap any request a tool makes. A tool reaching a host
|
|
136
|
+
you never listed is the classic phone-home tell. `*.example.com` matches subdomains; an empty
|
|
137
|
+
allowlist blocks everything rather than allowing everything.
|
|
138
|
+
- **`isSensitiveTool` / `classifyTools`** — glob classification of tools that must require per-call
|
|
139
|
+
approval. Sensitive tools stay *advertised*; they just cannot run unattended.
|
|
140
|
+
- **`canonicalize` / `parseJsonStrict`** — a strict RFC 8785 (JCS) implementation, also exported as
|
|
141
|
+
`@aimarket/warden/jcs` so another implementation can be byte-checked against it. Integers only
|
|
142
|
+
beyond `MAX_SAFE_JSON_INTEGER`, refusal (not escaping) on lone surrogates, and a reason code on
|
|
143
|
+
every refusal.
|
|
144
|
+
|
|
145
|
+
## Documentation
|
|
146
|
+
|
|
147
|
+
| | |
|
|
148
|
+
|---|---|
|
|
149
|
+
| [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
|
+
| [The signed threat feed](docs/threat-feed.md) | The wire contract, the three checks, and how to publish a feed WARDEN will accept |
|
|
151
|
+
| [Integration guide](docs/integration.md) | Wiring WARDEN into your own MCP host, policy choices, and what to record |
|
|
152
|
+
|
|
153
|
+
## What this is not
|
|
154
|
+
|
|
155
|
+
- **Not a sandbox.** These are in-process JS decisions. OS-level confinement of the MCP child
|
|
156
|
+
process (seccomp/Landlock, `sandbox-exec`) is not here.
|
|
157
|
+
- **Not a model.** No LLM is called anywhere in the chain. That is why `vet()` is fast, offline and
|
|
158
|
+
deterministic — and why the static scan is regex-shaped and will miss a paraphrase no rule covers.
|
|
159
|
+
- **Not a reputation service.** An earlier version had a gate that asked a trust oracle for a score
|
|
160
|
+
it had no data to compute, then reported the oracle as unreachable without having sent a request.
|
|
161
|
+
It was removed, and `test/no-phantom-gate.test.ts` fails if any gate ever claims unreachability
|
|
162
|
+
again.
|
|
163
|
+
- **Not a substitute for reading the tool defs.** 11 built-in threat records is a floor, not a
|
|
164
|
+
catalog.
|
|
165
|
+
|
|
166
|
+
## Development
|
|
6
167
|
|
|
7
168
|
```bash
|
|
8
|
-
npm install
|
|
169
|
+
npm install && npm run build && npm test # 95 tests
|
|
9
170
|
```
|
|
10
171
|
|
|
11
|
-
|
|
12
|
-
|
|
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.
|
|
175
|
+
|
|
176
|
+
Used by [ARGUS](https://github.com/alexar76/argus) (the reference host), [MOMUS](https://github.com/alexar76/momus)
|
|
177
|
+
(the publisher side), and the AICOM MCP-security course.
|
|
178
|
+
|
|
179
|
+
MIT © AICOM (alexar76)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { McpServerRef, PinStore, ToolDef, WardenGate, WardenLogger, WardenPolicy, WardenVerdict } from "./types.js";
|
|
2
|
+
import { ThreatFeed } from "./threat-feed.js";
|
|
3
|
+
export { StaticScanGate, staticScanRuleset, staticScanRulesetRef, STATIC_SCAN_RULESET_VERSION, } from "./static-scan.js";
|
|
4
|
+
export { PinningGate, canonicalToolsHash, tryCanonicalToolsHash, UNCANONICAL_TOOLS_HASH } from "./pinning.js";
|
|
5
|
+
export { OriginGate } from "./origin.js";
|
|
6
|
+
export { ThreatFeed, ThreatGate, DEFAULT_FEED_MAX_AGE_MS, FEED_CLOCK_SKEW_MS } from "./threat-feed.js";
|
|
7
|
+
export { EgressGuard, isSensitiveTool, classifyTools } from "./sandbox.js";
|
|
8
|
+
export { canonicalize, parseJsonStrict, CanonicalizationError, MAX_SAFE_JSON_INTEGER } from "./jcs.js";
|
|
9
|
+
export type { CanonicalizationCode } from "./jcs.js";
|
|
10
|
+
export type { StaticScanRule, StaticScanRuleset } from "./static-scan.js";
|
|
11
|
+
export type { ThreatFeedOptions } from "./threat-feed.js";
|
|
12
|
+
export { silentLogger } from "./logger.js";
|
|
13
|
+
export * from "./types.js";
|
|
14
|
+
export interface WardenInit {
|
|
15
|
+
gates: WardenGate[];
|
|
16
|
+
policy: WardenPolicy;
|
|
17
|
+
/** Where decisions are reported. Defaults to `silentLogger()`. */
|
|
18
|
+
log?: WardenLogger;
|
|
19
|
+
}
|
|
20
|
+
export interface WardenCreateDeps {
|
|
21
|
+
store: PinStore;
|
|
22
|
+
policy: WardenPolicy;
|
|
23
|
+
/**
|
|
24
|
+
* The known-bad feed. Required, not defaulted: a `ThreatFeed` the host never
|
|
25
|
+
* loaded still carries the built-in floor, but silently constructing one here
|
|
26
|
+
* would hide from the host that no external intel is in play.
|
|
27
|
+
*/
|
|
28
|
+
threatFeed: ThreatFeed;
|
|
29
|
+
log?: WardenLogger;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* WARDEN — the MCP security firewall.
|
|
33
|
+
*
|
|
34
|
+
* Every MCP server is vetted through an ordered gate chain before its tools are
|
|
35
|
+
* exposed to the agent: cheap static scanning first, then the known-bad threat
|
|
36
|
+
* feed, then network reputation, then drift/pinning. Findings accumulate across
|
|
37
|
+
* gates; the verdict is allow/block plus a composite 0..1 safety score and a
|
|
38
|
+
* per-tool partition so a mostly-trusted server can have one poisoned tool
|
|
39
|
+
* quarantined without severing the whole connection.
|
|
40
|
+
*/
|
|
41
|
+
export declare class Warden {
|
|
42
|
+
private readonly gates;
|
|
43
|
+
private readonly policy;
|
|
44
|
+
private readonly log;
|
|
45
|
+
constructor(init: WardenInit);
|
|
46
|
+
/**
|
|
47
|
+
* Guard against config typos that silently disable blocking. If blockAtSeverity
|
|
48
|
+
* isn't a valid Severity key, SEVERITY_RANK[bad] is undefined and every
|
|
49
|
+
* comparison `number >= undefined` is false — zero blocks, zero warnings.
|
|
50
|
+
*/
|
|
51
|
+
private validatePolicy;
|
|
52
|
+
/** Build the standard gate chain: static → threat → origin → pinning. */
|
|
53
|
+
static create(deps: WardenCreateDeps): Warden;
|
|
54
|
+
/**
|
|
55
|
+
* Vet a server and its advertised tools. Runs every gate in order, short-
|
|
56
|
+
* circuiting only on a fatal gate result. A connection is blocked if any gate
|
|
57
|
+
* is fatal or any finding reaches policy.blockAtSeverity.
|
|
58
|
+
*/
|
|
59
|
+
vet(server: McpServerRef, tools: ToolDef[]): Promise<WardenVerdict>;
|
|
60
|
+
/**
|
|
61
|
+
* Record the user's approval: pin the current tool defs so future drift is
|
|
62
|
+
* detected. Idempotent — re-approving just refreshes the snapshot.
|
|
63
|
+
*/
|
|
64
|
+
approve(server: McpServerRef, tools: ToolDef[]): Promise<void>;
|
|
65
|
+
/**
|
|
66
|
+
* A tool is blocked if a finding naming it reaches the block threshold.
|
|
67
|
+
* Sensitive tools (per policy) stay allowed but are surfaced as flagged so the
|
|
68
|
+
* agent loop can demand per-call approval at run time.
|
|
69
|
+
*/
|
|
70
|
+
private partitionTools;
|
|
71
|
+
private sensitiveCount;
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,YAAY,EACZ,QAAQ,EAER,OAAO,EAEP,UAAU,EACV,YAAY,EACZ,YAAY,EACZ,aAAa,EACd,MAAM,YAAY,CAAC;AAIpB,OAAO,EAAE,UAAU,EAAc,MAAM,kBAAkB,CAAC;AAI1D,OAAO,EACL,cAAc,EACd,iBAAiB,EACjB,oBAAoB,EACpB,2BAA2B,GAC5B,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,WAAW,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAC9G,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,uBAAuB,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACvG,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC3E,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AACvG,YAAY,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AACrD,YAAY,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAC1E,YAAY,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,cAAc,YAAY,CAAC;AAU3B,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,MAAM,EAAE,YAAY,CAAC;IACrB,kEAAkE;IAClE,GAAG,CAAC,EAAE,YAAY,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,QAAQ,CAAC;IAChB,MAAM,EAAE,YAAY,CAAC;IACrB;;;;OAIG;IACH,UAAU,EAAE,UAAU,CAAC;IACvB,GAAG,CAAC,EAAE,YAAY,CAAC;CACpB;AAED;;;;;;;;;GASG;AACH,qBAAa,MAAM;IACjB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAe;IACrC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;IACtC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;gBAEvB,IAAI,EAAE,UAAU;IAO5B;;;;OAIG;IACH,OAAO,CAAC,cAAc;IAYtB,yEAAyE;IACzE,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,gBAAgB,GAAG,MAAM;IAU7C;;;;OAIG;IACG,GAAG,CAAC,MAAM,EAAE,YAAY,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,aAAa,CAAC;IAuDzE;;;OAGG;IACG,OAAO,CAAC,MAAM,EAAE,YAAY,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAUpE;;;;OAIG;IACH,OAAO,CAAC,cAAc;IAoBtB,OAAO,CAAC,cAAc;CAGvB"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { StaticScanGate, staticScanRulesetRef } from "./static-scan.js";
|
|
2
|
+
import { PinningGate } from "./pinning.js";
|
|
3
|
+
import { OriginGate } from "./origin.js";
|
|
4
|
+
import { ThreatGate } from "./threat-feed.js";
|
|
5
|
+
import { classifyTools } from "./sandbox.js";
|
|
6
|
+
import { silentLogger } from "./logger.js";
|
|
7
|
+
export { StaticScanGate, staticScanRuleset, staticScanRulesetRef, STATIC_SCAN_RULESET_VERSION, } from "./static-scan.js";
|
|
8
|
+
export { PinningGate, canonicalToolsHash, tryCanonicalToolsHash, UNCANONICAL_TOOLS_HASH } from "./pinning.js";
|
|
9
|
+
export { OriginGate } from "./origin.js";
|
|
10
|
+
export { ThreatFeed, ThreatGate, DEFAULT_FEED_MAX_AGE_MS, FEED_CLOCK_SKEW_MS } from "./threat-feed.js";
|
|
11
|
+
export { EgressGuard, isSensitiveTool, classifyTools } from "./sandbox.js";
|
|
12
|
+
export { canonicalize, parseJsonStrict, CanonicalizationError, MAX_SAFE_JSON_INTEGER } from "./jcs.js";
|
|
13
|
+
export { silentLogger } from "./logger.js";
|
|
14
|
+
export * from "./types.js";
|
|
15
|
+
const SEVERITY_RANK = {
|
|
16
|
+
info: 0,
|
|
17
|
+
low: 1,
|
|
18
|
+
medium: 2,
|
|
19
|
+
high: 3,
|
|
20
|
+
critical: 4,
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* WARDEN — the MCP security firewall.
|
|
24
|
+
*
|
|
25
|
+
* Every MCP server is vetted through an ordered gate chain before its tools are
|
|
26
|
+
* exposed to the agent: cheap static scanning first, then the known-bad threat
|
|
27
|
+
* feed, then network reputation, then drift/pinning. Findings accumulate across
|
|
28
|
+
* gates; the verdict is allow/block plus a composite 0..1 safety score and a
|
|
29
|
+
* per-tool partition so a mostly-trusted server can have one poisoned tool
|
|
30
|
+
* quarantined without severing the whole connection.
|
|
31
|
+
*/
|
|
32
|
+
export class Warden {
|
|
33
|
+
gates;
|
|
34
|
+
policy;
|
|
35
|
+
log;
|
|
36
|
+
constructor(init) {
|
|
37
|
+
this.gates = init.gates;
|
|
38
|
+
this.policy = init.policy;
|
|
39
|
+
this.log = (init.log ?? silentLogger()).child("warden");
|
|
40
|
+
this.validatePolicy();
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Guard against config typos that silently disable blocking. If blockAtSeverity
|
|
44
|
+
* isn't a valid Severity key, SEVERITY_RANK[bad] is undefined and every
|
|
45
|
+
* comparison `number >= undefined` is false — zero blocks, zero warnings.
|
|
46
|
+
*/
|
|
47
|
+
validatePolicy() {
|
|
48
|
+
const valid = ["info", "low", "medium", "high", "critical"];
|
|
49
|
+
if (!valid.includes(this.policy.blockAtSeverity)) {
|
|
50
|
+
const fallback = "high";
|
|
51
|
+
this.log.warn(`WARDEN: invalid blockAtSeverity "${String(this.policy.blockAtSeverity)}" ` +
|
|
52
|
+
`(expected one of ${valid.join(", ")}) — falling back to "${fallback}" to keep blocking enabled`);
|
|
53
|
+
this.policy.blockAtSeverity = fallback;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Build the standard gate chain: static → threat → origin → pinning. */
|
|
57
|
+
static create(deps) {
|
|
58
|
+
const gates = [
|
|
59
|
+
new StaticScanGate(),
|
|
60
|
+
new ThreatGate(deps.threatFeed),
|
|
61
|
+
new OriginGate(),
|
|
62
|
+
new PinningGate(deps.store),
|
|
63
|
+
];
|
|
64
|
+
return new Warden({ gates, policy: deps.policy, log: deps.log });
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Vet a server and its advertised tools. Runs every gate in order, short-
|
|
68
|
+
* circuiting only on a fatal gate result. A connection is blocked if any gate
|
|
69
|
+
* is fatal or any finding reaches policy.blockAtSeverity.
|
|
70
|
+
*/
|
|
71
|
+
async vet(server, tools) {
|
|
72
|
+
const findings = [];
|
|
73
|
+
const scores = [];
|
|
74
|
+
let allow = true;
|
|
75
|
+
let decidedBy;
|
|
76
|
+
const blockThreshold = SEVERITY_RANK[this.policy.blockAtSeverity];
|
|
77
|
+
for (const gate of this.gates) {
|
|
78
|
+
const result = await gate.evaluate({
|
|
79
|
+
server,
|
|
80
|
+
tools,
|
|
81
|
+
prior: [...findings],
|
|
82
|
+
policy: this.policy,
|
|
83
|
+
});
|
|
84
|
+
findings.push(...result.findings);
|
|
85
|
+
scores.push(clamp01(result.score));
|
|
86
|
+
// Advisory findings never block, at any threshold — see WardenFinding.advisory.
|
|
87
|
+
const tripped = result.findings.some((f) => !f.advisory && SEVERITY_RANK[f.severity] >= blockThreshold);
|
|
88
|
+
if (result.fatal || tripped) {
|
|
89
|
+
if (allow) {
|
|
90
|
+
allow = false;
|
|
91
|
+
decidedBy = gate.name;
|
|
92
|
+
}
|
|
93
|
+
if (result.fatal) {
|
|
94
|
+
this.log.warn(`gate "${gate.name}" returned fatal for server ${server.id}`);
|
|
95
|
+
break; // short-circuit only on an explicit fatal
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Composite score: product of gate contributions (one bad gate drags it down).
|
|
100
|
+
const score = scores.reduce((acc, s) => acc * s, 1);
|
|
101
|
+
const { allowedTools, blockedTools } = this.partitionTools(tools, findings, blockThreshold);
|
|
102
|
+
if (!allow) {
|
|
103
|
+
this.log.warn(`BLOCK ${server.id} (decidedBy=${decidedBy}, score=${score.toFixed(3)}, findings=${findings.length})`);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
this.log.info(`ALLOW ${server.id} (score=${score.toFixed(3)}, sensitive=${blockedTools.length === 0 ? this.sensitiveCount(tools) : "?"})`);
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
allow,
|
|
110
|
+
score: clamp01(score),
|
|
111
|
+
decidedBy,
|
|
112
|
+
findings,
|
|
113
|
+
allowedTools,
|
|
114
|
+
blockedTools,
|
|
115
|
+
rulesets: { staticScan: staticScanRulesetRef() },
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Record the user's approval: pin the current tool defs so future drift is
|
|
120
|
+
* detected. Idempotent — re-approving just refreshes the snapshot.
|
|
121
|
+
*/
|
|
122
|
+
async approve(server, tools) {
|
|
123
|
+
const pinning = this.gates.find((g) => g instanceof PinningGate);
|
|
124
|
+
if (!pinning) {
|
|
125
|
+
this.log.warn("approve() called but no PinningGate in the chain; nothing to pin");
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
await pinning.pin(server, tools);
|
|
129
|
+
this.log.info(`pinned tool defs for ${server.id} (${tools.length} tools)`);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* A tool is blocked if a finding naming it reaches the block threshold.
|
|
133
|
+
* Sensitive tools (per policy) stay allowed but are surfaced as flagged so the
|
|
134
|
+
* agent loop can demand per-call approval at run time.
|
|
135
|
+
*/
|
|
136
|
+
partitionTools(tools, findings, blockThreshold) {
|
|
137
|
+
const blockedByFinding = new Set();
|
|
138
|
+
for (const f of findings) {
|
|
139
|
+
if (f.advisory)
|
|
140
|
+
continue; // report-only, never costs a tool
|
|
141
|
+
if (f.tool && SEVERITY_RANK[f.severity] >= blockThreshold)
|
|
142
|
+
blockedByFinding.add(f.tool);
|
|
143
|
+
}
|
|
144
|
+
const allowedTools = [];
|
|
145
|
+
const blockedTools = [];
|
|
146
|
+
for (const tool of tools) {
|
|
147
|
+
if (blockedByFinding.has(tool.name))
|
|
148
|
+
blockedTools.push(tool.name);
|
|
149
|
+
else
|
|
150
|
+
allowedTools.push(tool.name);
|
|
151
|
+
}
|
|
152
|
+
return { allowedTools, blockedTools };
|
|
153
|
+
}
|
|
154
|
+
sensitiveCount(tools) {
|
|
155
|
+
return classifyTools(tools, this.policy).sensitive.length;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function clamp01(n) {
|
|
159
|
+
if (!Number.isFinite(n))
|
|
160
|
+
return 0;
|
|
161
|
+
return Math.max(0, Math.min(1, n));
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|