@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
|
@@ -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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"}
|