@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.
@@ -0,0 +1,135 @@
1
+ import type { McpServerRef, ThreatRecord, ToolDef, WardenFinding, WardenGate, WardenGateInput, WardenGateResult, WardenLogger } from "./types.js";
2
+ /**
3
+ * Threat-intel feed for known-bad MCP servers/tools.
4
+ *
5
+ * Reputation answers "is this server trusted?"; the threat feed answers "is this
6
+ * server a *known* bad actor?". It ships with a small built-in deny-list of
7
+ * patterns (credential-stealing, destructive commands, crypto-drainer keywords,
8
+ * typosquat-style names) and can be topped up from a signed remote feed.
9
+ *
10
+ * **In the shipping configuration there is no remote feed.** `warden.threatFeedUrl`
11
+ * and `warden.feedPublicKey` default to `undefined` (see `src/config.ts`), so
12
+ * unless an operator sets both, this gate *is* the {@link BUILTIN} list — a fixed
13
+ * floor of 11 patterns, not live intel. That is a deliberate default: ARGUS ships
14
+ * no feed endpoint and no publisher key, because a feed URL baked into the binary
15
+ * is a single point every install would have to trust.
16
+ *
17
+ * When a feed *is* configured, three properties are enforced fail-closed — any of
18
+ * them failing keeps the built-in floor and changes nothing else:
19
+ *
20
+ * - **Authenticity.** An Ed25519 signature over the canonical form of
21
+ * `{records, timestamp}`, verified with a pre-configured public key. Unsigned
22
+ * feeds are refused outright.
23
+ * - **Freshness.** The signed `timestamp` must be within {@link
24
+ * DEFAULT_FEED_MAX_AGE_MS} (configurable). Without this, whoever serves the feed
25
+ * URL can replay a months-old snapshot forever and silently erase every threat
26
+ * record added since — a signature says *who* wrote a document, never *when you
27
+ * were handed it*.
28
+ * - **Determinism.** The signed bytes are RFC 8785 canonical (§4 of
29
+ * `awr/SPEC.md`), so publisher and verifier agree on them regardless of the key
30
+ * order the wire used.
31
+ *
32
+ * The remote fetch degrades silently otherwise: a feed outage must never weaken
33
+ * the built-in floor or crash a connection check.
34
+ */
35
+ /**
36
+ * Default freshness window for a signed remote feed: 24 h.
37
+ *
38
+ * Rationale for a *default* rather than an opt-in: a feed whose age is unchecked
39
+ * is indistinguishable from a feed that is being replayed. An operator publishing
40
+ * on a slower cadence should raise `warden.feedMaxAgeMs`, not disable the check.
41
+ *
42
+ * (Note the deliberate difference from AWR documents, where age is *policy, not
43
+ * validity* — SPEC §11.3. A work receipt from two years ago is still a true
44
+ * statement about the past; a deny-list from two years ago is a false statement
45
+ * about the present.)
46
+ */
47
+ export declare const DEFAULT_FEED_MAX_AGE_MS: number;
48
+ /**
49
+ * Tolerance for a feed timestamp in the future — publisher/host clock skew.
50
+ * Beyond it the feed is refused: a future-dated snapshot would otherwise pass the
51
+ * freshness check for as long as the date it claims.
52
+ */
53
+ export declare const FEED_CLOCK_SKEW_MS: number;
54
+ export interface ThreatFeedOptions {
55
+ /** Ed25519 public key (hex-encoded SPKI DER) of the feed publisher. */
56
+ feedPublicKey?: string;
57
+ /**
58
+ * Maximum age of a signed feed's `timestamp`. Defaults to
59
+ * {@link DEFAULT_FEED_MAX_AGE_MS}; a non-finite or non-positive value falls back
60
+ * to the default rather than disabling the check.
61
+ */
62
+ maxAgeMs?: number;
63
+ log?: WardenLogger;
64
+ /** Clock source. Injectable so freshness is testable without waiting a day. */
65
+ now?: () => number;
66
+ }
67
+ export declare class ThreatFeed {
68
+ private records;
69
+ private feedPublicKey?;
70
+ private readonly maxAgeMs;
71
+ private readonly now;
72
+ private readonly log?;
73
+ constructor(opts?: ThreatFeedOptions);
74
+ /** The built-in floor, always present regardless of remote load state. */
75
+ get builtins(): ThreatRecord[];
76
+ all(): ThreatRecord[];
77
+ /**
78
+ * Fetch and verify a signed remote threat feed.
79
+ *
80
+ * Feed format: `{ records: ThreatRecord[], timestamp: number, signature: string }`
81
+ * where `timestamp` is **epoch milliseconds** and `signature` is a hex-encoded
82
+ * Ed25519 signature over the RFC 8785 (JCS) canonical form of
83
+ * `{records, timestamp}` — see {@link canonicalize}. A publisher that signs
84
+ * `JSON.stringify` output instead will not verify here, and that is the point:
85
+ * `JSON.stringify` bytes depend on the key order of whatever object the
86
+ * publisher happened to build, so the same logical feed could be signed and then
87
+ * fail to verify.
88
+ *
89
+ * - Without a configured `feedPublicKey`, remote feeds are refused.
90
+ * - Signature failure logs a warning and preserves the built-in floor.
91
+ * - A missing/non-integer `timestamp`, a snapshot older than `maxAgeMs`, or one
92
+ * dated further ahead than {@link FEED_CLOCK_SKEW_MS} is refused — a replayed
93
+ * old snapshot would otherwise erase every newer threat record.
94
+ * - Network/parse errors degrade silently — built-ins remain.
95
+ */
96
+ load(feedUrl?: string): Promise<void>;
97
+ /**
98
+ * Match a server — and its advertised tool definitions — against every record.
99
+ * One finding per matched record per surface.
100
+ *
101
+ * Two surfaces, because a threat record about an MCP server is not only a
102
+ * statement about the config the user wrote:
103
+ *
104
+ * - **Server identity** (`id`, `name`, `url`, `command`, `args`): the local
105
+ * config. This is the only surface a pattern matches when its `scope` is
106
+ * `"server"`.
107
+ * - **Tool definitions** (`name`, `description`, `inputSchema`): attacker-
108
+ * controlled text the *server* chose, which is where a poisoned tool actually
109
+ * lives. Matched for records scoped `"any"` (the default) or `"tool"`.
110
+ *
111
+ * Findings from the tool surface carry `tool`, so `Warden.partitionTools()` can
112
+ * quarantine the one tool instead of attributing the match to the whole
113
+ * connection. The connection is still blocked when the severity reaches
114
+ * `policy.blockAtSeverity` — per-tool attribution narrows the *blame*, it does
115
+ * not soften the policy.
116
+ */
117
+ match(server: McpServerRef, tools?: ToolDef[]): WardenFinding[];
118
+ }
119
+ /**
120
+ * Gate wrapper around a ThreatFeed. Any match is disqualifying for the score; a
121
+ * critical-severity match against the *server's identity* short-circuits the chain
122
+ * (fatal), because nothing about that connection is salvageable.
123
+ *
124
+ * A critical match against a single tool definition is deliberately not fatal: it
125
+ * still scores 0 and still blocks under the default `blockAtSeverity`, but letting
126
+ * the remaining gates run means the verdict names every problem and every affected
127
+ * tool instead of only the first one found.
128
+ */
129
+ export declare class ThreatGate implements WardenGate {
130
+ private readonly feed;
131
+ readonly name = "threat-feed";
132
+ constructor(feed: ThreatFeed);
133
+ evaluate(input: WardenGateInput): Promise<WardenGateResult>;
134
+ }
135
+ //# sourceMappingURL=threat-feed.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"threat-feed.d.ts","sourceRoot":"","sources":["../src/threat-feed.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,YAAY,EAEZ,YAAY,EAEZ,OAAO,EACP,aAAa,EACb,UAAU,EACV,eAAe,EACf,gBAAgB,EAChB,YAAY,EACb,MAAM,YAAY,CAAC;AAEpB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,uBAAuB,QAAsB,CAAC;AAE3D;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,QAAgB,CAAC;AA+GhD,MAAM,WAAW,iBAAiB;IAChC,uEAAuE;IACvE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,YAAY,CAAC;IACnB,+EAA+E;IAC/E,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,qBAAa,UAAU;IACrB,OAAO,CAAC,OAAO,CAAgC;IAC/C,OAAO,CAAC,aAAa,CAAC,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAe;gBAExB,IAAI,CAAC,EAAE,iBAAiB;IAUpC,0EAA0E;IAC1E,IAAI,QAAQ,IAAI,YAAY,EAAE,CAE7B;IAED,GAAG,IAAI,YAAY,EAAE;IAIrB;;;;;;;;;;;;;;;;;;OAkBG;IACG,IAAI,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgH3C;;;;;;;;;;;;;;;;;;;OAmBG;IACH,KAAK,CAAC,MAAM,EAAE,YAAY,EAAE,KAAK,GAAE,OAAO,EAAO,GAAG,aAAa,EAAE;CA6CpE;AAED;;;;;;;;;GASG;AACH,qBAAa,UAAW,YAAW,UAAU;IAG/B,OAAO,CAAC,QAAQ,CAAC,IAAI;IAFjC,QAAQ,CAAC,IAAI,iBAAiB;gBAED,IAAI,EAAE,UAAU;IAEvC,QAAQ,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,gBAAgB,CAAC;CAMlE"}
@@ -0,0 +1,457 @@
1
+ import { verify, createPublicKey } from "node:crypto";
2
+ import { canonicalize, parseJsonStrict, CanonicalizationError } from "./jcs.js";
3
+ /**
4
+ * Threat-intel feed for known-bad MCP servers/tools.
5
+ *
6
+ * Reputation answers "is this server trusted?"; the threat feed answers "is this
7
+ * server a *known* bad actor?". It ships with a small built-in deny-list of
8
+ * patterns (credential-stealing, destructive commands, crypto-drainer keywords,
9
+ * typosquat-style names) and can be topped up from a signed remote feed.
10
+ *
11
+ * **In the shipping configuration there is no remote feed.** `warden.threatFeedUrl`
12
+ * and `warden.feedPublicKey` default to `undefined` (see `src/config.ts`), so
13
+ * unless an operator sets both, this gate *is* the {@link BUILTIN} list — a fixed
14
+ * floor of 11 patterns, not live intel. That is a deliberate default: ARGUS ships
15
+ * no feed endpoint and no publisher key, because a feed URL baked into the binary
16
+ * is a single point every install would have to trust.
17
+ *
18
+ * When a feed *is* configured, three properties are enforced fail-closed — any of
19
+ * them failing keeps the built-in floor and changes nothing else:
20
+ *
21
+ * - **Authenticity.** An Ed25519 signature over the canonical form of
22
+ * `{records, timestamp}`, verified with a pre-configured public key. Unsigned
23
+ * feeds are refused outright.
24
+ * - **Freshness.** The signed `timestamp` must be within {@link
25
+ * DEFAULT_FEED_MAX_AGE_MS} (configurable). Without this, whoever serves the feed
26
+ * URL can replay a months-old snapshot forever and silently erase every threat
27
+ * record added since — a signature says *who* wrote a document, never *when you
28
+ * were handed it*.
29
+ * - **Determinism.** The signed bytes are RFC 8785 canonical (§4 of
30
+ * `awr/SPEC.md`), so publisher and verifier agree on them regardless of the key
31
+ * order the wire used.
32
+ *
33
+ * The remote fetch degrades silently otherwise: a feed outage must never weaken
34
+ * the built-in floor or crash a connection check.
35
+ */
36
+ /**
37
+ * Default freshness window for a signed remote feed: 24 h.
38
+ *
39
+ * Rationale for a *default* rather than an opt-in: a feed whose age is unchecked
40
+ * is indistinguishable from a feed that is being replayed. An operator publishing
41
+ * on a slower cadence should raise `warden.feedMaxAgeMs`, not disable the check.
42
+ *
43
+ * (Note the deliberate difference from AWR documents, where age is *policy, not
44
+ * validity* — SPEC §11.3. A work receipt from two years ago is still a true
45
+ * statement about the past; a deny-list from two years ago is a false statement
46
+ * about the present.)
47
+ */
48
+ export const DEFAULT_FEED_MAX_AGE_MS = 24 * 60 * 60 * 1000;
49
+ /**
50
+ * Tolerance for a feed timestamp in the future — publisher/host clock skew.
51
+ * Beyond it the feed is refused: a future-dated snapshot would otherwise pass the
52
+ * freshness check for as long as the date it claims.
53
+ */
54
+ export const FEED_CLOCK_SKEW_MS = 5 * 60 * 1000;
55
+ /** Hard cap on a feed response body (OOM guard). */
56
+ const MAX_FEED_BYTES = 512_000;
57
+ /**
58
+ * Built-in deny-list. Small but real — patterns seen in poisoned MCP servers.
59
+ *
60
+ * `scope` says where each pattern means something (see {@link ThreatFeed.match}):
61
+ *
62
+ * - `server` for the **command-line and identity** signatures. `rm -rf`, a fork
63
+ * bomb or a typosquatted name are evidence in `server.command`/`args` or in a
64
+ * server's name; in prose they are as likely to be documentation as attack — a
65
+ * security tool's own description may well list `offical-mcp`.
66
+ * - `any` for the **content** signatures. A credential path, a seed phrase or a
67
+ * drainer keyword appearing in a tool name, description or input schema is
68
+ * exactly the tool-poisoning shape `static-scan` also looks for, so those
69
+ * patterns are matched against tool definitions as well as server identity.
70
+ */
71
+ const BUILTIN = [
72
+ {
73
+ pattern: "*~/.ssh*",
74
+ severity: "critical",
75
+ code: "THREAT_SSH_KEY_READ",
76
+ reason: "Server references the user's SSH key directory.",
77
+ source: "builtin",
78
+ scope: "any",
79
+ },
80
+ {
81
+ pattern: "*id_rsa*",
82
+ severity: "critical",
83
+ code: "THREAT_SSH_KEY_READ",
84
+ reason: "Server references a private SSH key file.",
85
+ source: "builtin",
86
+ scope: "any",
87
+ },
88
+ {
89
+ pattern: "*rm -rf*",
90
+ severity: "critical",
91
+ code: "THREAT_DESTRUCTIVE_CMD",
92
+ reason: "Server command performs a destructive recursive delete.",
93
+ source: "builtin",
94
+ scope: "server",
95
+ },
96
+ {
97
+ pattern: "*:(){ :|:&};:*",
98
+ severity: "critical",
99
+ code: "THREAT_FORK_BOMB",
100
+ reason: "Server command contains a shell fork bomb.",
101
+ source: "builtin",
102
+ scope: "server",
103
+ },
104
+ {
105
+ pattern: "*drain*wallet*",
106
+ severity: "critical",
107
+ code: "THREAT_CRYPTO_DRAINER",
108
+ reason: "Crypto-drainer keyword in server identity.",
109
+ source: "builtin",
110
+ scope: "any",
111
+ },
112
+ {
113
+ pattern: "*seed*phrase*",
114
+ severity: "high",
115
+ code: "THREAT_SEED_PHRASE",
116
+ reason: "Server references wallet seed phrases.",
117
+ source: "builtin",
118
+ scope: "any",
119
+ },
120
+ {
121
+ pattern: "*sweep*funds*",
122
+ severity: "critical",
123
+ code: "THREAT_CRYPTO_DRAINER",
124
+ reason: "Crypto-drainer keyword in server identity.",
125
+ source: "builtin",
126
+ scope: "any",
127
+ },
128
+ {
129
+ pattern: "*.env*exfil*",
130
+ severity: "critical",
131
+ code: "THREAT_ENV_EXFIL",
132
+ reason: "Server references exfiltrating environment files.",
133
+ source: "builtin",
134
+ scope: "any",
135
+ },
136
+ // Typosquat-style names mimicking the official reference servers.
137
+ {
138
+ pattern: "*offical-mcp*",
139
+ severity: "high",
140
+ code: "THREAT_TYPOSQUAT",
141
+ reason: "Typosquat of an official MCP server name.",
142
+ source: "builtin",
143
+ scope: "server",
144
+ },
145
+ {
146
+ pattern: "*modelcontextprotocoll*",
147
+ severity: "high",
148
+ code: "THREAT_TYPOSQUAT",
149
+ reason: "Typosquat of modelcontextprotocol.",
150
+ source: "builtin",
151
+ scope: "server",
152
+ },
153
+ {
154
+ pattern: "*filesytem*",
155
+ severity: "medium",
156
+ code: "THREAT_TYPOSQUAT",
157
+ reason: "Typosquat of the filesystem reference server.",
158
+ source: "builtin",
159
+ scope: "server",
160
+ },
161
+ ];
162
+ export class ThreatFeed {
163
+ records = [...BUILTIN];
164
+ feedPublicKey;
165
+ maxAgeMs;
166
+ now;
167
+ log;
168
+ constructor(opts) {
169
+ this.feedPublicKey = opts?.feedPublicKey;
170
+ this.maxAgeMs =
171
+ opts?.maxAgeMs != null && Number.isFinite(opts.maxAgeMs) && opts.maxAgeMs > 0
172
+ ? opts.maxAgeMs
173
+ : DEFAULT_FEED_MAX_AGE_MS;
174
+ this.now = opts?.now ?? (() => Date.now());
175
+ this.log = opts?.log;
176
+ }
177
+ /** The built-in floor, always present regardless of remote load state. */
178
+ get builtins() {
179
+ return [...BUILTIN];
180
+ }
181
+ all() {
182
+ return [...this.records];
183
+ }
184
+ /**
185
+ * Fetch and verify a signed remote threat feed.
186
+ *
187
+ * Feed format: `{ records: ThreatRecord[], timestamp: number, signature: string }`
188
+ * where `timestamp` is **epoch milliseconds** and `signature` is a hex-encoded
189
+ * Ed25519 signature over the RFC 8785 (JCS) canonical form of
190
+ * `{records, timestamp}` — see {@link canonicalize}. A publisher that signs
191
+ * `JSON.stringify` output instead will not verify here, and that is the point:
192
+ * `JSON.stringify` bytes depend on the key order of whatever object the
193
+ * publisher happened to build, so the same logical feed could be signed and then
194
+ * fail to verify.
195
+ *
196
+ * - Without a configured `feedPublicKey`, remote feeds are refused.
197
+ * - Signature failure logs a warning and preserves the built-in floor.
198
+ * - A missing/non-integer `timestamp`, a snapshot older than `maxAgeMs`, or one
199
+ * dated further ahead than {@link FEED_CLOCK_SKEW_MS} is refused — a replayed
200
+ * old snapshot would otherwise erase every newer threat record.
201
+ * - Network/parse errors degrade silently — built-ins remain.
202
+ */
203
+ async load(feedUrl) {
204
+ if (!feedUrl)
205
+ return;
206
+ if (!this.feedPublicKey) {
207
+ this.log?.warn("threat feed URL configured but no feedPublicKey set — remote feed REFUSED (unsigned feeds not allowed)");
208
+ return;
209
+ }
210
+ try {
211
+ // AbortController with a 10 s timeout — a hanging feed must not block startup.
212
+ const ctrl = new AbortController();
213
+ const timer = setTimeout(() => ctrl.abort(), 10_000);
214
+ const res = await fetch(feedUrl, { signal: ctrl.signal }).finally(() => clearTimeout(timer));
215
+ if (!res.ok) {
216
+ this.log?.warn(`threat feed fetch returned ${res.status} — keeping built-in floor`);
217
+ return;
218
+ }
219
+ // Reject oversized responses before parsing (OOM guard).
220
+ const cl = res.headers.get("content-length");
221
+ if (cl && Number(cl) > MAX_FEED_BYTES) {
222
+ this.log?.warn(`threat feed: content-length ${cl} exceeds ${MAX_FEED_BYTES} byte limit — rejected`);
223
+ return;
224
+ }
225
+ // Read text, not res.json(): the received characters are what the canonical
226
+ // parser needs for its duplicate-key and number-literal checks.
227
+ const body = await res.text();
228
+ if (Buffer.byteLength(body, "utf8") > MAX_FEED_BYTES) {
229
+ this.log?.warn(`threat feed: body exceeds the ${MAX_FEED_BYTES} byte limit (content-length absent or wrong) — rejected`);
230
+ return;
231
+ }
232
+ let data;
233
+ try {
234
+ data = parseJsonStrict(body);
235
+ }
236
+ catch (err) {
237
+ this.log?.warn(`threat feed: body rejected by the canonical JSON parser (${err.message})`);
238
+ return;
239
+ }
240
+ if (!data || typeof data !== "object")
241
+ return;
242
+ const pkg = data;
243
+ const records = pkg.records;
244
+ const signature = pkg.signature;
245
+ const timestamp = pkg.timestamp;
246
+ if (!Array.isArray(records) || typeof signature !== "string") {
247
+ this.log?.warn("threat feed: missing or invalid fields (records, signature) — rejected");
248
+ return;
249
+ }
250
+ // The timestamp is REQUIRED: it is what the freshness check is made of, and
251
+ // an optional one would mean "a feed that omits it is fresh forever".
252
+ if (typeof timestamp !== "number" || !Number.isInteger(timestamp)) {
253
+ this.log?.warn("threat feed: missing or non-integer `timestamp` (epoch ms) — rejected, freshness cannot be checked");
254
+ return;
255
+ }
256
+ // Ed25519 signature over the CANONICAL JSON of {records, timestamp}.
257
+ let payload;
258
+ try {
259
+ payload = canonicalize({ records, timestamp });
260
+ }
261
+ catch (err) {
262
+ const detail = err instanceof CanonicalizationError ? err.message : err.message;
263
+ this.log?.warn(`threat feed: payload has no canonical form (${detail}) — rejected`);
264
+ return;
265
+ }
266
+ const sigBuf = Buffer.from(signature, "hex");
267
+ let pubKey;
268
+ try {
269
+ pubKey = createPublicKey({
270
+ key: Buffer.from(this.feedPublicKey, "hex"),
271
+ format: "der",
272
+ type: "spki",
273
+ });
274
+ }
275
+ catch (err) {
276
+ // A misconfigured key must say so out loud: silently degrading here would
277
+ // look exactly like "the feed had nothing new".
278
+ this.log?.warn(`threat feed: feedPublicKey is not a valid hex SPKI DER key (${err.message}) — feed REFUSED`);
279
+ return;
280
+ }
281
+ const ok = verify(null, Buffer.from(payload, "utf8"), pubKey, sigBuf);
282
+ if (!ok) {
283
+ this.log?.warn("threat feed: Ed25519 signature INVALID — feed rejected, built-in floor preserved");
284
+ return;
285
+ }
286
+ // Freshness is checked only now: until the signature verifies, `timestamp`
287
+ // is a number an attacker chose, and refusing on it would prove nothing.
288
+ const age = this.now() - timestamp;
289
+ if (age > this.maxAgeMs) {
290
+ this.log?.warn(`threat feed: signed snapshot is ${formatDuration(age)} old, past the ${formatDuration(this.maxAgeMs)} limit — ` +
291
+ `REJECTED as stale (possible replay of a superseded feed); built-in floor preserved`);
292
+ return;
293
+ }
294
+ if (age < -FEED_CLOCK_SKEW_MS) {
295
+ this.log?.warn(`threat feed: signed snapshot is dated ${formatDuration(-age)} in the future, beyond the ` +
296
+ `${formatDuration(FEED_CLOCK_SKEW_MS)} clock-skew allowance — REJECTED; built-in floor preserved`);
297
+ return;
298
+ }
299
+ const remote = records.filter(isThreatRecord);
300
+ this.records = [...BUILTIN, ...remote];
301
+ this.log?.info(`threat feed loaded: ${this.records.length} records (${BUILTIN.length} builtin + ${remote.length} remote, ` +
302
+ `signature valid, snapshot ${formatDuration(age)} old)`);
303
+ }
304
+ catch (err) {
305
+ // Degrade gracefully: built-ins remain in effect.
306
+ this.log?.debug(`threat feed load error: ${err.message}`);
307
+ }
308
+ }
309
+ /**
310
+ * Match a server — and its advertised tool definitions — against every record.
311
+ * One finding per matched record per surface.
312
+ *
313
+ * Two surfaces, because a threat record about an MCP server is not only a
314
+ * statement about the config the user wrote:
315
+ *
316
+ * - **Server identity** (`id`, `name`, `url`, `command`, `args`): the local
317
+ * config. This is the only surface a pattern matches when its `scope` is
318
+ * `"server"`.
319
+ * - **Tool definitions** (`name`, `description`, `inputSchema`): attacker-
320
+ * controlled text the *server* chose, which is where a poisoned tool actually
321
+ * lives. Matched for records scoped `"any"` (the default) or `"tool"`.
322
+ *
323
+ * Findings from the tool surface carry `tool`, so `Warden.partitionTools()` can
324
+ * quarantine the one tool instead of attributing the match to the whole
325
+ * connection. The connection is still blocked when the severity reaches
326
+ * `policy.blockAtSeverity` — per-tool attribution narrows the *blame*, it does
327
+ * not soften the policy.
328
+ */
329
+ match(server, tools = []) {
330
+ const serverHay = [
331
+ server.id,
332
+ server.name,
333
+ server.url ?? "",
334
+ server.command ?? "",
335
+ (server.args ?? []).join(" "),
336
+ ]
337
+ .join("\n")
338
+ .toLowerCase();
339
+ // Built once: the record loop below would otherwise re-stringify every schema.
340
+ const toolHays = tools.map((tool) => ({
341
+ name: tool.name,
342
+ hay: [tool.name, tool.description ?? "", stringifySchema(tool.inputSchema)].join("\n").toLowerCase(),
343
+ }));
344
+ const findings = [];
345
+ for (const rec of this.records) {
346
+ const scope = rec.scope ?? "any";
347
+ if (scope !== "tool" && patternMatches(rec.pattern, serverHay)) {
348
+ findings.push({
349
+ gate: "threat-feed",
350
+ severity: rec.severity,
351
+ code: rec.code,
352
+ message: `${rec.reason} (matched "${rec.pattern}" in server identity, source: ${rec.source})`,
353
+ });
354
+ }
355
+ if (scope === "server")
356
+ continue;
357
+ for (const tool of toolHays) {
358
+ if (patternMatches(rec.pattern, tool.hay)) {
359
+ findings.push({
360
+ gate: "threat-feed",
361
+ severity: rec.severity,
362
+ code: rec.code,
363
+ message: `Tool "${tool.name}": ${rec.reason} (matched "${rec.pattern}" in the tool definition, source: ${rec.source})`,
364
+ tool: tool.name,
365
+ });
366
+ }
367
+ }
368
+ }
369
+ return findings;
370
+ }
371
+ }
372
+ /**
373
+ * Gate wrapper around a ThreatFeed. Any match is disqualifying for the score; a
374
+ * critical-severity match against the *server's identity* short-circuits the chain
375
+ * (fatal), because nothing about that connection is salvageable.
376
+ *
377
+ * A critical match against a single tool definition is deliberately not fatal: it
378
+ * still scores 0 and still blocks under the default `blockAtSeverity`, but letting
379
+ * the remaining gates run means the verdict names every problem and every affected
380
+ * tool instead of only the first one found.
381
+ */
382
+ export class ThreatGate {
383
+ feed;
384
+ name = "threat-feed";
385
+ constructor(feed) {
386
+ this.feed = feed;
387
+ }
388
+ async evaluate(input) {
389
+ const findings = this.feed.match(input.server, input.tools);
390
+ if (findings.length === 0)
391
+ return { findings, score: 1 };
392
+ const fatal = findings.some((f) => f.severity === "critical" && !f.tool);
393
+ return { findings, score: 0, fatal };
394
+ }
395
+ }
396
+ /**
397
+ * Case-insensitive match. `*` is a wildcard (glob); a pattern with no `*` is a
398
+ * plain substring test. `hay` is expected to be pre-lowercased.
399
+ */
400
+ function patternMatches(pattern, hay) {
401
+ const p = pattern.toLowerCase();
402
+ if (!p.includes("*"))
403
+ return hay.includes(p);
404
+ const re = globToRegExp(p);
405
+ return re.test(hay);
406
+ }
407
+ /**
408
+ * Translate a `*`-glob into an anchored regex, escaping all other metachars.
409
+ * The `s` (dotAll) flag is essential: a haystack joins several fields with
410
+ * newlines, and without it `.*` would not cross a newline — silently defeating
411
+ * every `*…*` pattern. (Regression-tested in test/warden.test.ts.)
412
+ */
413
+ function globToRegExp(glob) {
414
+ const escaped = glob
415
+ .split("*")
416
+ .map((seg) => seg.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
417
+ .join(".*");
418
+ return new RegExp(`^${escaped}$`, "is");
419
+ }
420
+ /**
421
+ * Flatten a tool's input schema for pattern matching only. Key order is
422
+ * irrelevant here (a glob over a lowercased blob), so plain `JSON.stringify` is
423
+ * the right tool — unlike the signed and hashed paths, which need
424
+ * {@link canonicalize}.
425
+ */
426
+ function stringifySchema(schema) {
427
+ try {
428
+ return JSON.stringify(schema) ?? "";
429
+ }
430
+ catch {
431
+ return String(schema ?? "");
432
+ }
433
+ }
434
+ /** Human-readable duration for log lines: minutes under an hour, else hours. */
435
+ function formatDuration(ms) {
436
+ const minutes = Math.round(ms / 60_000);
437
+ if (Math.abs(minutes) < 60)
438
+ return `${minutes} min`;
439
+ return `${Math.round(ms / 360_000) / 10} h`;
440
+ }
441
+ const THREAT_SCOPES = ["server", "tool", "any"];
442
+ function isThreatRecord(v) {
443
+ if (!v || typeof v !== "object")
444
+ return false;
445
+ const r = v;
446
+ const severities = ["info", "low", "medium", "high", "critical"];
447
+ return (typeof r.pattern === "string" && r.pattern.length > 0 && r.pattern.length <= 2000 &&
448
+ typeof r.code === "string" && r.code.length > 0 && r.code.length <= 200 &&
449
+ typeof r.reason === "string" && r.reason.length > 0 && r.reason.length <= 2000 &&
450
+ typeof r.source === "string" && r.source.length > 0 && r.source.length <= 200 &&
451
+ typeof r.severity === "string" &&
452
+ severities.includes(r.severity) &&
453
+ // An unrecognised scope drops the record instead of defaulting it: a publisher
454
+ // that meant "server" must not have its pattern silently widened to tool text.
455
+ (r.scope === undefined || (typeof r.scope === "string" && THREAT_SCOPES.includes(r.scope))));
456
+ }
457
+ //# sourceMappingURL=threat-feed.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"threat-feed.js","sourceRoot":"","sources":["../src/threat-feed.ts"],"names":[],"mappings":"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"}