@aimarket/warden 0.0.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +206 -0
- package/LICENSE +21 -0
- package/README-es.md +190 -0
- package/README-fr.md +190 -0
- package/README-ru.md +189 -0
- package/README-zh.md +173 -0
- package/README.md +179 -6
- package/dist/glob.d.ts +81 -0
- package/dist/glob.d.ts.map +1 -0
- package/dist/glob.js +180 -0
- package/dist/glob.js.map +1 -0
- package/dist/index.d.ts +106 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +218 -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 +49 -0
- package/dist/origin.js.map +1 -0
- package/dist/pinning.d.ts +91 -0
- package/dist/pinning.d.ts.map +1 -0
- package/dist/pinning.js +227 -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 +79 -0
- package/dist/sandbox.js.map +1 -0
- package/dist/sanitize.d.ts +38 -0
- package/dist/sanitize.d.ts.map +1 -0
- package/dist/sanitize.js +53 -0
- package/dist/sanitize.js.map +1 -0
- package/dist/static-scan.d.ts +110 -0
- package/dist/static-scan.d.ts.map +1 -0
- package/dist/static-scan.js +539 -0
- package/dist/static-scan.js.map +1 -0
- package/dist/threat-feed.d.ts +138 -0
- package/dist/threat-feed.d.ts.map +1 -0
- package/dist/threat-feed.js +546 -0
- package/dist/threat-feed.js.map +1 -0
- package/dist/types.d.ts +173 -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 +67 -9
- package/index.js +0 -6
package/dist/glob.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `*`-glob matching, without a regular expression.
|
|
3
|
+
*
|
|
4
|
+
* WARDEN matches two kinds of glob against attacker-controlled text: threat-feed
|
|
5
|
+
* patterns (a signed publisher chooses the pattern, a hostile server chooses the
|
|
6
|
+
* haystack) and `policy.sensitiveToolPatterns` (the operator chooses the pattern,
|
|
7
|
+
* the server chooses the tool name). Both used to compile `*` into `.*` and run a
|
|
8
|
+
* regex, which is a denial of service:
|
|
9
|
+
*
|
|
10
|
+
* pattern "*a*a*a*a*a*a*a*a*a*a*a*a*a*a*zzz" (32 characters)
|
|
11
|
+
* haystack "aaaa..." (220 characters)
|
|
12
|
+
* -> 112 SECONDS inside one RegExp.test
|
|
13
|
+
*
|
|
14
|
+
* The engine has to try every way of distributing the wildcards over the haystack
|
|
15
|
+
* before it can conclude "no match" - exponential in the number of `*`. That
|
|
16
|
+
* mattered more than an ordinary performance bug: the threat feed's whole trust
|
|
17
|
+
* story is that a compromised publisher can only ever ADD protection, never
|
|
18
|
+
* remove it, and a record that hangs `vet()` forever removes all of it. The same
|
|
19
|
+
* shape let a hostile server freeze the agent inspecting it, by advertising a
|
|
20
|
+
* long enough tool name against an operator's own `*a*b*c*` pattern.
|
|
21
|
+
*
|
|
22
|
+
* The two-pointer algorithm below is the standard wildcard matcher: it walks the
|
|
23
|
+
* haystack once and remembers only the most recent `*` to fall back to, so the
|
|
24
|
+
* work is bounded by `pattern.length x value.length` with no backtracking tree to
|
|
25
|
+
* explode. The same 32/220 case returns in microseconds.
|
|
26
|
+
*
|
|
27
|
+
* Semantics, matching what the regex did: `*` matches any run of zero or more
|
|
28
|
+
* UTF-16 code units (including newlines - the old regex needed the `s` flag for
|
|
29
|
+
* that), every other character is literal, and the match is anchored at both
|
|
30
|
+
* ends. Case folding is the caller's job: both sides arrive already lowercased.
|
|
31
|
+
*/
|
|
32
|
+
export function wildcardMatch(pattern, value) {
|
|
33
|
+
const STAR = 0x2a;
|
|
34
|
+
let p = 0;
|
|
35
|
+
let v = 0;
|
|
36
|
+
let star = -1;
|
|
37
|
+
let resume = 0;
|
|
38
|
+
while (v < value.length) {
|
|
39
|
+
if (p < pattern.length && pattern.charCodeAt(p) === value.charCodeAt(v)) {
|
|
40
|
+
p++;
|
|
41
|
+
v++;
|
|
42
|
+
}
|
|
43
|
+
else if (p < pattern.length && pattern.charCodeAt(p) === STAR) {
|
|
44
|
+
// Remember this wildcard and let it match nothing for now.
|
|
45
|
+
star = p++;
|
|
46
|
+
resume = v;
|
|
47
|
+
}
|
|
48
|
+
else if (star >= 0) {
|
|
49
|
+
// Backtrack: hand one more character to the last wildcard. There is only
|
|
50
|
+
// ever one fallback point, which is what keeps this linear-bounded.
|
|
51
|
+
p = star + 1;
|
|
52
|
+
v = ++resume;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Trailing wildcards may still match the empty remainder.
|
|
59
|
+
while (p < pattern.length && pattern.charCodeAt(p) === STAR)
|
|
60
|
+
p++;
|
|
61
|
+
return p === pattern.length;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* How many wildcards a pattern may carry.
|
|
65
|
+
*
|
|
66
|
+
* The matcher above is not exponential, but `*`-heavy patterns still cost
|
|
67
|
+
* `pattern x value` per match and buy nothing: a real threat pattern names a
|
|
68
|
+
* command, a path or a keyword. A record with more than this many wildcards is
|
|
69
|
+
* refused rather than run - refusing an *addition* to the deny-list is the safe
|
|
70
|
+
* direction, and it is reported loudly.
|
|
71
|
+
*/
|
|
72
|
+
export const MAX_GLOB_WILDCARDS = 12;
|
|
73
|
+
/** Count `*` in a pattern, for the record validator. */
|
|
74
|
+
export function countWildcards(pattern) {
|
|
75
|
+
let n = 0;
|
|
76
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
77
|
+
if (pattern.charCodeAt(i) === 0x2a)
|
|
78
|
+
n++;
|
|
79
|
+
}
|
|
80
|
+
return n;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Largest gap an INTERIOR `*` in a threat pattern may span.
|
|
84
|
+
*
|
|
85
|
+
* A field survey of 1 108 public MCP servers (see docs/mcp-survey.md) found every
|
|
86
|
+
* multi-segment built-in firing on unrelated words that merely happened to appear
|
|
87
|
+
* in the same paragraph: `*drain*wallet*` matched an anti-drainer scanner because
|
|
88
|
+
* it says "drainer" in one clause and "wallet" in another, and `*seed*phrase*`
|
|
89
|
+
* matched "for a **seed** topic, returns suggested search **phrases**". A real
|
|
90
|
+
* threat pattern names something whose parts sit next to each other; 24 characters
|
|
91
|
+
* is roughly "a few words apart" and comfortably covers "wallet seed or recovery
|
|
92
|
+
* phrase".
|
|
93
|
+
*/
|
|
94
|
+
export const THREAT_MAX_GAP = 24;
|
|
95
|
+
/**
|
|
96
|
+
* Threat-pattern matching: `wildcardMatch` plus two constraints that only make
|
|
97
|
+
* sense for a deny-list.
|
|
98
|
+
*
|
|
99
|
+
* 1. **Interior gaps are bounded** to {@link THREAT_MAX_GAP}. Leading and trailing
|
|
100
|
+
* `*` stay unbounded — `*id_rsa*` must still find `id_rsa` anywhere.
|
|
101
|
+
* 2. **A segment that starts with a word character must start at a word boundary.**
|
|
102
|
+
* `*sweep*funds*` matched an ENS floor-sweeping tool because `funds` sits inside
|
|
103
|
+
* "re**funds**". Only the START is anchored: a pattern author writing `*drain*`
|
|
104
|
+
* means "drainer" too, so anchoring the end as well would break ordinary
|
|
105
|
+
* stemming.
|
|
106
|
+
*
|
|
107
|
+
* Kept separate from {@link wildcardMatch} on purpose. That function is also what
|
|
108
|
+
* `policy.sensitiveToolPatterns` runs on, where the operator wrote the pattern
|
|
109
|
+
* against their own tool names and plain glob semantics are what they expect.
|
|
110
|
+
*
|
|
111
|
+
* Completeness matters here in a way it does not for a display filter: a missed
|
|
112
|
+
* match is a deny-list entry that silently stopped working. So this explores every
|
|
113
|
+
* reachable position per segment (deduplicated, so the work stays bounded by
|
|
114
|
+
* `segments x value.length`) rather than taking the leftmost occurrence greedily,
|
|
115
|
+
* which would report "no match" for a pattern that does match further along.
|
|
116
|
+
*/
|
|
117
|
+
export function threatMatch(pattern, value) {
|
|
118
|
+
if (!pattern.includes("*"))
|
|
119
|
+
return value.includes(pattern);
|
|
120
|
+
const parts = pattern.split("*");
|
|
121
|
+
const openStart = parts[0] === "";
|
|
122
|
+
const openEnd = parts[parts.length - 1] === "";
|
|
123
|
+
const segments = parts.filter((s) => s !== "");
|
|
124
|
+
if (segments.length === 0)
|
|
125
|
+
return true;
|
|
126
|
+
// Reachable end positions after matching the segments consumed so far.
|
|
127
|
+
let ends = [0];
|
|
128
|
+
for (let i = 0; i < segments.length; i++) {
|
|
129
|
+
const seg = segments[i];
|
|
130
|
+
// The first segment is anchored at 0 unless the pattern opened with `*`.
|
|
131
|
+
const unbounded = i === 0 ? openStart : false;
|
|
132
|
+
const next = new Set();
|
|
133
|
+
for (const from of ends) {
|
|
134
|
+
const limit = unbounded ? value.length : Math.min(value.length, from + THREAT_MAX_GAP + seg.length);
|
|
135
|
+
let at = from;
|
|
136
|
+
for (;;) {
|
|
137
|
+
const found = value.indexOf(seg, at);
|
|
138
|
+
if (found < 0 || found + seg.length > limit)
|
|
139
|
+
break;
|
|
140
|
+
if (i === 0 && !openStart && found !== 0)
|
|
141
|
+
break;
|
|
142
|
+
if (startsOnBoundary(seg, value, found))
|
|
143
|
+
next.add(found + seg.length);
|
|
144
|
+
at = found + 1;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (next.size === 0)
|
|
148
|
+
return false;
|
|
149
|
+
ends = [...next];
|
|
150
|
+
}
|
|
151
|
+
return openEnd || ends.some((e) => e === value.length);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Is `seg` at `at` starting on a word boundary?
|
|
155
|
+
*
|
|
156
|
+
* Only asked when the segment's own first character is a word character — a
|
|
157
|
+
* pattern starting with `~`, `.` or `/` has no boundary to respect.
|
|
158
|
+
*/
|
|
159
|
+
function startsOnBoundary(seg, value, at) {
|
|
160
|
+
if (!isWordChar(seg.charCodeAt(0)))
|
|
161
|
+
return true;
|
|
162
|
+
if (at === 0)
|
|
163
|
+
return true;
|
|
164
|
+
return !isWordChar(value.charCodeAt(at - 1));
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Letters and digits only — `_` and `-` are BOUNDARIES here, not word characters.
|
|
168
|
+
*
|
|
169
|
+
* Tool schemas are written in snake_case and kebab-case, so a field named
|
|
170
|
+
* `seed_phrase` must still satisfy `*seed*phrase*`; treating `_` as a word
|
|
171
|
+
* character silently switched that built-in off. "re**funds**" is still rejected,
|
|
172
|
+
* because there the preceding character is a letter.
|
|
173
|
+
*/
|
|
174
|
+
function isWordChar(c) {
|
|
175
|
+
return ((c >= 0x30 && c <= 0x39) || // 0-9
|
|
176
|
+
(c >= 0x41 && c <= 0x5a) || // A-Z
|
|
177
|
+
(c >= 0x61 && c <= 0x7a) // a-z
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
//# sourceMappingURL=glob.js.map
|
package/dist/glob.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"glob.js","sourceRoot":"","sources":["../src/glob.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
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, serverIdentityHash, 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 { displaySafe, DEFAULT_DISPLAY_MAX } from "./sanitize.js";
|
|
14
|
+
export { wildcardMatch } from "./glob.js";
|
|
15
|
+
export * from "./types.js";
|
|
16
|
+
export interface WardenInit {
|
|
17
|
+
gates: WardenGate[];
|
|
18
|
+
policy: WardenPolicy;
|
|
19
|
+
/** Where decisions are reported. Defaults to `silentLogger()`. */
|
|
20
|
+
log?: WardenLogger;
|
|
21
|
+
}
|
|
22
|
+
export interface WardenCreateDeps {
|
|
23
|
+
store: PinStore;
|
|
24
|
+
policy: WardenPolicy;
|
|
25
|
+
/**
|
|
26
|
+
* The known-bad feed. Required, not defaulted: a `ThreatFeed` the host never
|
|
27
|
+
* loaded still carries the built-in floor, but silently constructing one here
|
|
28
|
+
* would hide from the host that no external intel is in play.
|
|
29
|
+
*/
|
|
30
|
+
threatFeed: ThreatFeed;
|
|
31
|
+
log?: WardenLogger;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* WARDEN — the MCP security firewall.
|
|
35
|
+
*
|
|
36
|
+
* Every MCP server is vetted through an ordered gate chain before its tools are
|
|
37
|
+
* exposed to the agent: cheap static scanning first, then the known-bad threat
|
|
38
|
+
* feed, then origin (declared by the operator, or offered by a remote catalog),
|
|
39
|
+
* then drift/pinning. Findings accumulate across gates; the verdict is allow/block
|
|
40
|
+
* plus a composite 0..1 safety score and a per-tool partition so a mostly-trusted
|
|
41
|
+
* server can have one poisoned tool quarantined without severing the whole
|
|
42
|
+
* connection.
|
|
43
|
+
*
|
|
44
|
+
* The score is WARDEN's OWN — the product of what each gate contributed, computed
|
|
45
|
+
* from local facts. No reputation oracle is consulted and no socket is opened while
|
|
46
|
+
* vetting. Slot three used to be a reputation gate that asked an oracle for a score
|
|
47
|
+
* it had no data to compute; it is gone, and test/no-phantom-gate.test.ts fails if
|
|
48
|
+
* anything like it comes back.
|
|
49
|
+
*/
|
|
50
|
+
export declare class Warden {
|
|
51
|
+
private readonly gates;
|
|
52
|
+
private readonly policy;
|
|
53
|
+
private readonly log;
|
|
54
|
+
constructor(init: WardenInit);
|
|
55
|
+
/**
|
|
56
|
+
* Guard against config typos that silently disable blocking, on a COPY.
|
|
57
|
+
*
|
|
58
|
+
* If `blockAtSeverity` isn't a valid Severity key, `SEVERITY_RANK[bad]` is
|
|
59
|
+
* undefined and every comparison `number >= undefined` is false — zero blocks,
|
|
60
|
+
* zero warnings — so the value is repaired. Repairing it in place used to write
|
|
61
|
+
* into the host's own policy object, which is both a surprise (their config
|
|
62
|
+
* silently changes under them, and any other Warden sharing the object sees it)
|
|
63
|
+
* and a crash: `Object.freeze(policy)` is a reasonable thing for a host to do,
|
|
64
|
+
* and assigning to a frozen property throws a TypeError out of the constructor
|
|
65
|
+
* under ESM strict mode. A firewall that refuses to be constructed because the
|
|
66
|
+
* config was immutable is not a firewall.
|
|
67
|
+
*/
|
|
68
|
+
private normalizePolicy;
|
|
69
|
+
/** Build the standard gate chain: static → threat → origin → pinning. */
|
|
70
|
+
static create(deps: WardenCreateDeps): Warden;
|
|
71
|
+
/**
|
|
72
|
+
* Vet a server and its advertised tools. Runs every gate in order, short-
|
|
73
|
+
* circuiting only on a fatal gate result. A connection is blocked if any gate
|
|
74
|
+
* is fatal or any finding reaches policy.blockAtSeverity.
|
|
75
|
+
*/
|
|
76
|
+
vet(server: McpServerRef, tools: ToolDef[]): Promise<WardenVerdict>;
|
|
77
|
+
/**
|
|
78
|
+
* Run one gate, converting a thrown error into a blocking finding.
|
|
79
|
+
*
|
|
80
|
+
* A gate that crashed did not clear the server, so the honest result is a
|
|
81
|
+
* finding at `high` with a zero score — which blocks at the default threshold
|
|
82
|
+
* and, under a report-only policy (`blockAtSeverity: "critical"`), is reported
|
|
83
|
+
* without blocking, exactly like every other high-severity finding.
|
|
84
|
+
*
|
|
85
|
+
* Letting the exception escape instead was worse than it looks: `vet()`
|
|
86
|
+
* rejected, so the host got no verdict at all — not the findings the earlier
|
|
87
|
+
* gates had already produced, not the per-tool partition — and whether that
|
|
88
|
+
* ended in a blocked connection or an unhandled rejection was up to the host.
|
|
89
|
+
* A store with a full disk or a custom gate with a bug should not be able to
|
|
90
|
+
* decide that.
|
|
91
|
+
*/
|
|
92
|
+
private runGate;
|
|
93
|
+
/**
|
|
94
|
+
* Record the user's approval: pin the current tool defs so future drift is
|
|
95
|
+
* detected. Idempotent — re-approving just refreshes the snapshot.
|
|
96
|
+
*/
|
|
97
|
+
approve(server: McpServerRef, tools: ToolDef[]): Promise<void>;
|
|
98
|
+
/**
|
|
99
|
+
* A tool is blocked if a finding naming it reaches the block threshold.
|
|
100
|
+
* Sensitive tools (per policy) stay allowed but are surfaced as flagged so the
|
|
101
|
+
* agent loop can demand per-call approval at run time.
|
|
102
|
+
*/
|
|
103
|
+
private partitionTools;
|
|
104
|
+
private sensitiveCount;
|
|
105
|
+
}
|
|
106
|
+
//# 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,EAEV,YAAY,EACZ,YAAY,EACZ,aAAa,EACd,MAAM,YAAY,CAAC;AAIpB,OAAO,EAAE,UAAU,EAAc,MAAM,kBAAkB,CAAC;AAK1D,OAAO,EACL,cAAc,EACd,iBAAiB,EACjB,oBAAoB,EACpB,2BAA2B,GAC5B,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,WAAW,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAClI,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,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACjE,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC1C,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;;;;;;;;;;;;;;;;GAgBG;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;IAM5B;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,eAAe;IAWvB,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;IAqDzE;;;;;;;;;;;;;;OAcG;YACW,OAAO;IAiCrB;;;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,218 @@
|
|
|
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
|
+
import { displaySafe } from "./sanitize.js";
|
|
8
|
+
export { StaticScanGate, staticScanRuleset, staticScanRulesetRef, STATIC_SCAN_RULESET_VERSION, } from "./static-scan.js";
|
|
9
|
+
export { PinningGate, canonicalToolsHash, tryCanonicalToolsHash, serverIdentityHash, UNCANONICAL_TOOLS_HASH } from "./pinning.js";
|
|
10
|
+
export { OriginGate } from "./origin.js";
|
|
11
|
+
export { ThreatFeed, ThreatGate, DEFAULT_FEED_MAX_AGE_MS, FEED_CLOCK_SKEW_MS } from "./threat-feed.js";
|
|
12
|
+
export { EgressGuard, isSensitiveTool, classifyTools } from "./sandbox.js";
|
|
13
|
+
export { canonicalize, parseJsonStrict, CanonicalizationError, MAX_SAFE_JSON_INTEGER } from "./jcs.js";
|
|
14
|
+
export { silentLogger } from "./logger.js";
|
|
15
|
+
export { displaySafe, DEFAULT_DISPLAY_MAX } from "./sanitize.js";
|
|
16
|
+
export { wildcardMatch } from "./glob.js";
|
|
17
|
+
export * from "./types.js";
|
|
18
|
+
const SEVERITY_RANK = {
|
|
19
|
+
info: 0,
|
|
20
|
+
low: 1,
|
|
21
|
+
medium: 2,
|
|
22
|
+
high: 3,
|
|
23
|
+
critical: 4,
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* WARDEN — the MCP security firewall.
|
|
27
|
+
*
|
|
28
|
+
* Every MCP server is vetted through an ordered gate chain before its tools are
|
|
29
|
+
* exposed to the agent: cheap static scanning first, then the known-bad threat
|
|
30
|
+
* feed, then origin (declared by the operator, or offered by a remote catalog),
|
|
31
|
+
* then drift/pinning. Findings accumulate across gates; the verdict is allow/block
|
|
32
|
+
* plus a composite 0..1 safety score and a per-tool partition so a mostly-trusted
|
|
33
|
+
* server can have one poisoned tool quarantined without severing the whole
|
|
34
|
+
* connection.
|
|
35
|
+
*
|
|
36
|
+
* The score is WARDEN's OWN — the product of what each gate contributed, computed
|
|
37
|
+
* from local facts. No reputation oracle is consulted and no socket is opened while
|
|
38
|
+
* vetting. Slot three used to be a reputation gate that asked an oracle for a score
|
|
39
|
+
* it had no data to compute; it is gone, and test/no-phantom-gate.test.ts fails if
|
|
40
|
+
* anything like it comes back.
|
|
41
|
+
*/
|
|
42
|
+
export class Warden {
|
|
43
|
+
gates;
|
|
44
|
+
policy;
|
|
45
|
+
log;
|
|
46
|
+
constructor(init) {
|
|
47
|
+
this.gates = [...init.gates];
|
|
48
|
+
this.log = (init.log ?? silentLogger()).child("warden");
|
|
49
|
+
this.policy = this.normalizePolicy(init.policy);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Guard against config typos that silently disable blocking, on a COPY.
|
|
53
|
+
*
|
|
54
|
+
* If `blockAtSeverity` isn't a valid Severity key, `SEVERITY_RANK[bad]` is
|
|
55
|
+
* undefined and every comparison `number >= undefined` is false — zero blocks,
|
|
56
|
+
* zero warnings — so the value is repaired. Repairing it in place used to write
|
|
57
|
+
* into the host's own policy object, which is both a surprise (their config
|
|
58
|
+
* silently changes under them, and any other Warden sharing the object sees it)
|
|
59
|
+
* and a crash: `Object.freeze(policy)` is a reasonable thing for a host to do,
|
|
60
|
+
* and assigning to a frozen property throws a TypeError out of the constructor
|
|
61
|
+
* under ESM strict mode. A firewall that refuses to be constructed because the
|
|
62
|
+
* config was immutable is not a firewall.
|
|
63
|
+
*/
|
|
64
|
+
normalizePolicy(policy) {
|
|
65
|
+
const valid = ["info", "low", "medium", "high", "critical"];
|
|
66
|
+
if (valid.includes(policy.blockAtSeverity))
|
|
67
|
+
return { ...policy };
|
|
68
|
+
const fallback = "high";
|
|
69
|
+
this.log.warn(`WARDEN: invalid blockAtSeverity "${displaySafe(policy.blockAtSeverity, 40)}" ` +
|
|
70
|
+
`(expected one of ${valid.join(", ")}) — falling back to "${fallback}" to keep blocking enabled`);
|
|
71
|
+
return { ...policy, blockAtSeverity: fallback };
|
|
72
|
+
}
|
|
73
|
+
/** Build the standard gate chain: static → threat → origin → pinning. */
|
|
74
|
+
static create(deps) {
|
|
75
|
+
const gates = [
|
|
76
|
+
new StaticScanGate(deps.log),
|
|
77
|
+
new ThreatGate(deps.threatFeed),
|
|
78
|
+
new OriginGate(),
|
|
79
|
+
new PinningGate(deps.store),
|
|
80
|
+
];
|
|
81
|
+
return new Warden({ gates, policy: deps.policy, log: deps.log });
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Vet a server and its advertised tools. Runs every gate in order, short-
|
|
85
|
+
* circuiting only on a fatal gate result. A connection is blocked if any gate
|
|
86
|
+
* is fatal or any finding reaches policy.blockAtSeverity.
|
|
87
|
+
*/
|
|
88
|
+
async vet(server, tools) {
|
|
89
|
+
const findings = [];
|
|
90
|
+
const scores = [];
|
|
91
|
+
let allow = true;
|
|
92
|
+
let decidedBy;
|
|
93
|
+
const blockThreshold = SEVERITY_RANK[this.policy.blockAtSeverity];
|
|
94
|
+
for (const gate of this.gates) {
|
|
95
|
+
const result = await this.runGate(gate, server, tools, findings);
|
|
96
|
+
findings.push(...result.findings);
|
|
97
|
+
scores.push(clamp01(result.score));
|
|
98
|
+
// Advisory findings never block, at any threshold — see WardenFinding.advisory.
|
|
99
|
+
const tripped = result.findings.some((f) => !f.advisory && SEVERITY_RANK[f.severity] >= blockThreshold);
|
|
100
|
+
if (result.fatal || tripped) {
|
|
101
|
+
if (allow) {
|
|
102
|
+
allow = false;
|
|
103
|
+
decidedBy = gate.name;
|
|
104
|
+
}
|
|
105
|
+
if (result.fatal) {
|
|
106
|
+
this.log.warn(`gate "${displaySafe(gate.name, 60)}" returned fatal for server ${displaySafe(server.id)}`);
|
|
107
|
+
break; // short-circuit only on an explicit fatal
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Composite score: product of gate contributions (one bad gate drags it down).
|
|
112
|
+
const score = scores.reduce((acc, s) => acc * s, 1);
|
|
113
|
+
const { allowedTools, blockedTools } = this.partitionTools(tools, findings, blockThreshold);
|
|
114
|
+
if (!allow) {
|
|
115
|
+
this.log.warn(`BLOCK ${displaySafe(server.id)} (decidedBy=${displaySafe(decidedBy ?? "-", 60)}, ` +
|
|
116
|
+
`score=${score.toFixed(3)}, findings=${findings.length})`);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
this.log.info(`ALLOW ${displaySafe(server.id)} (score=${score.toFixed(3)}, sensitive=${blockedTools.length === 0 ? this.sensitiveCount(tools) : "?"})`);
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
allow,
|
|
123
|
+
score: clamp01(score),
|
|
124
|
+
decidedBy,
|
|
125
|
+
findings,
|
|
126
|
+
allowedTools,
|
|
127
|
+
blockedTools,
|
|
128
|
+
rulesets: { staticScan: staticScanRulesetRef() },
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Run one gate, converting a thrown error into a blocking finding.
|
|
133
|
+
*
|
|
134
|
+
* A gate that crashed did not clear the server, so the honest result is a
|
|
135
|
+
* finding at `high` with a zero score — which blocks at the default threshold
|
|
136
|
+
* and, under a report-only policy (`blockAtSeverity: "critical"`), is reported
|
|
137
|
+
* without blocking, exactly like every other high-severity finding.
|
|
138
|
+
*
|
|
139
|
+
* Letting the exception escape instead was worse than it looks: `vet()`
|
|
140
|
+
* rejected, so the host got no verdict at all — not the findings the earlier
|
|
141
|
+
* gates had already produced, not the per-tool partition — and whether that
|
|
142
|
+
* ended in a blocked connection or an unhandled rejection was up to the host.
|
|
143
|
+
* A store with a full disk or a custom gate with a bug should not be able to
|
|
144
|
+
* decide that.
|
|
145
|
+
*/
|
|
146
|
+
async runGate(gate, server, tools, prior) {
|
|
147
|
+
try {
|
|
148
|
+
const result = await gate.evaluate({ server, tools, prior: [...prior], policy: this.policy });
|
|
149
|
+
// A third-party gate is not obliged to be well-behaved either.
|
|
150
|
+
return {
|
|
151
|
+
findings: Array.isArray(result?.findings) ? result.findings : [],
|
|
152
|
+
score: typeof result?.score === "number" ? result.score : 0,
|
|
153
|
+
...(result?.fatal ? { fatal: true } : {}),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
158
|
+
this.log.error(`gate "${displaySafe(gate.name, 60)}" threw: ${displaySafe(detail, 300)}`);
|
|
159
|
+
return {
|
|
160
|
+
findings: [
|
|
161
|
+
{
|
|
162
|
+
gate: gate.name,
|
|
163
|
+
severity: "high",
|
|
164
|
+
code: "GATE_ERROR",
|
|
165
|
+
message: `Gate "${displaySafe(gate.name, 60)}" failed to complete (${displaySafe(detail, 300)}), so this ` +
|
|
166
|
+
`server was not checked by it.`,
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
score: 0,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Record the user's approval: pin the current tool defs so future drift is
|
|
175
|
+
* detected. Idempotent — re-approving just refreshes the snapshot.
|
|
176
|
+
*/
|
|
177
|
+
async approve(server, tools) {
|
|
178
|
+
const pinning = this.gates.find((g) => g instanceof PinningGate);
|
|
179
|
+
if (!pinning) {
|
|
180
|
+
this.log.warn("approve() called but no PinningGate in the chain; nothing to pin");
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
await pinning.pin(server, tools);
|
|
184
|
+
this.log.info(`pinned tool defs for ${displaySafe(server.id)} (${tools.length} tools)`);
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* A tool is blocked if a finding naming it reaches the block threshold.
|
|
188
|
+
* Sensitive tools (per policy) stay allowed but are surfaced as flagged so the
|
|
189
|
+
* agent loop can demand per-call approval at run time.
|
|
190
|
+
*/
|
|
191
|
+
partitionTools(tools, findings, blockThreshold) {
|
|
192
|
+
const blockedByFinding = new Set();
|
|
193
|
+
for (const f of findings) {
|
|
194
|
+
if (f.advisory)
|
|
195
|
+
continue; // report-only, never costs a tool
|
|
196
|
+
if (f.tool && SEVERITY_RANK[f.severity] >= blockThreshold)
|
|
197
|
+
blockedByFinding.add(f.tool);
|
|
198
|
+
}
|
|
199
|
+
const allowedTools = [];
|
|
200
|
+
const blockedTools = [];
|
|
201
|
+
for (const tool of tools) {
|
|
202
|
+
if (blockedByFinding.has(tool.name))
|
|
203
|
+
blockedTools.push(tool.name);
|
|
204
|
+
else
|
|
205
|
+
allowedTools.push(tool.name);
|
|
206
|
+
}
|
|
207
|
+
return { allowedTools, blockedTools };
|
|
208
|
+
}
|
|
209
|
+
sensitiveCount(tools) {
|
|
210
|
+
return classifyTools(tools, this.policy).sensitive.length;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function clamp01(n) {
|
|
214
|
+
if (!Number.isFinite(n))
|
|
215
|
+
return 0;
|
|
216
|
+
return Math.max(0, Math.min(1, n));
|
|
217
|
+
}
|
|
218
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/jcs.d.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 8785 (JSON Canonicalization Scheme) as profiled by AWR/2 (`awr/SPEC.md` §4).
|
|
3
|
+
*
|
|
4
|
+
* Two places in WARDEN turn a JSON value into bytes that a *different*
|
|
5
|
+
* implementation has to reproduce exactly: the signed threat feed (a publisher
|
|
6
|
+
* signs, ARGUS verifies) and the tool-def pin (a sha256 quoted in receipts and
|
|
7
|
+
* re-checked by `argus verify`). `JSON.stringify` cannot do that job — its output
|
|
8
|
+
* depends on the key order the wire happened to use, so one logical document
|
|
9
|
+
* serialises to different bytes on the two sides and the signature fails for no
|
|
10
|
+
* security reason at all.
|
|
11
|
+
*
|
|
12
|
+
* The profile implemented here, rule by rule:
|
|
13
|
+
*
|
|
14
|
+
* 1. **Property names sort as arrays of UTF-16 code units** (RFC 8785 §3.2.3).
|
|
15
|
+
* JavaScript's default `Array.prototype.sort()` on strings compares exactly
|
|
16
|
+
* that, so this is the one rule the platform gives away for free.
|
|
17
|
+
* `String.prototype.localeCompare` does *not*: it depends on the host locale
|
|
18
|
+
* and ICU version, which makes it unusable as a digest input.
|
|
19
|
+
* 2. **No Unicode normalization, ever** (SPEC §4.1 item 2). NFC would collide two
|
|
20
|
+
* property names that RFC 8785 keeps distinct.
|
|
21
|
+
* 3. **Two-character escapes plus lowercase `\uXXXX` for the remaining C0
|
|
22
|
+
* controls** (§3.2.2.2) — which is precisely what `JSON.stringify` emits for a
|
|
23
|
+
* string, so string bodies are delegated to it rather than re-implemented.
|
|
24
|
+
* 4. **Lone surrogates terminate with an error** instead of being substituted
|
|
25
|
+
* (§4.1 item 4). `JSON.stringify` would quietly escape them into
|
|
26
|
+
* valid-looking bytes, so they are rejected before it is called.
|
|
27
|
+
* 5. **Integers only, within ±(2^53−1)**; every non-integer number is refused
|
|
28
|
+
* rather than arbitrated (§4.3), because whether `1` parses as an integer or
|
|
29
|
+
* as a double is a language accident that silently changes the signed bytes.
|
|
30
|
+
*
|
|
31
|
+
* Scope: this module is *only* the byte-level canonicalization. It knows nothing
|
|
32
|
+
* about AWR documents, Data Integrity proofs, `did:key` or profiles.
|
|
33
|
+
*/
|
|
34
|
+
/** Reason codes from the SPEC §11.2 `AWR-CANON-*` registry that this module raises. */
|
|
35
|
+
export type CanonicalizationCode =
|
|
36
|
+
/** Non-integer JSON number present. */
|
|
37
|
+
"AWR-CANON-001"
|
|
38
|
+
/** Integer outside ±(2^53−1). */
|
|
39
|
+
| "AWR-CANON-002"
|
|
40
|
+
/** Invalid Unicode (lone surrogate) in string data. */
|
|
41
|
+
| "AWR-CANON-003"
|
|
42
|
+
/** Duplicate object property name. */
|
|
43
|
+
| "AWR-CANON-004"
|
|
44
|
+
/** Input is not well-formed JSON / not representable in JSON. */
|
|
45
|
+
| "AWR-CANON-005";
|
|
46
|
+
/**
|
|
47
|
+
* A value could not be canonicalized (or parsed) under the AWR profile.
|
|
48
|
+
*
|
|
49
|
+
* Callers are expected to *handle* this rather than let it escape: refusing to
|
|
50
|
+
* produce bytes is the honest outcome, but it must not crash a connection check.
|
|
51
|
+
*/
|
|
52
|
+
export declare class CanonicalizationError extends Error {
|
|
53
|
+
readonly code: CanonicalizationCode;
|
|
54
|
+
constructor(code: CanonicalizationCode, detail: string);
|
|
55
|
+
}
|
|
56
|
+
/** Largest magnitude of a JSON integer permitted in canonical bytes (2^53−1). */
|
|
57
|
+
export declare const MAX_SAFE_JSON_INTEGER = 9007199254740991;
|
|
58
|
+
/**
|
|
59
|
+
* Return the RFC 8785 canonical serialization of `value` as a UTF-8-ready string.
|
|
60
|
+
*
|
|
61
|
+
* Throws {@link CanonicalizationError} rather than emitting bytes it cannot
|
|
62
|
+
* defend: a non-integer number, an integer beyond 2^53−1, a lone surrogate, an
|
|
63
|
+
* `undefined` property (which `JSON.stringify` would silently drop), a
|
|
64
|
+
* non-plain object, or a cycle.
|
|
65
|
+
*/
|
|
66
|
+
export declare function canonicalize(value: unknown): string;
|
|
67
|
+
/**
|
|
68
|
+
* Parse JSON with the profile's parser requirements, for input whose bytes were
|
|
69
|
+
* signed by someone else.
|
|
70
|
+
*
|
|
71
|
+
* `JSON.parse` alone is not enough on a verification path:
|
|
72
|
+
*
|
|
73
|
+
* - **Duplicate property names** (§4.1 item 5, `AWR-CANON-004`) are resolved
|
|
74
|
+
* last-wins and silently, which would let the parser — not the signer — decide
|
|
75
|
+
* which of two records was the signed one.
|
|
76
|
+
* - **Non-integer number literals** (§4.3, `AWR-CANON-001`) are rejected
|
|
77
|
+
* *lexically*, not by value: `2340.0` and `2340` parse to the same JavaScript
|
|
78
|
+
* number, and a publisher written in a language that distinguishes them would
|
|
79
|
+
* have signed different bytes.
|
|
80
|
+
*
|
|
81
|
+
* Both checks run over the received text after `JSON.parse` has confirmed it is
|
|
82
|
+
* well-formed, so the scanner below only has to find tokens, never validate
|
|
83
|
+
* grammar.
|
|
84
|
+
*/
|
|
85
|
+
export declare function parseJsonStrict(text: string): unknown;
|
|
86
|
+
//# sourceMappingURL=jcs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jcs.d.ts","sourceRoot":"","sources":["../src/jcs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,uFAAuF;AACvF,MAAM,MAAM,oBAAoB;AAC9B,uCAAuC;AACrC,eAAe;AACjB,iCAAiC;GAC/B,eAAe;AACjB,uDAAuD;GACrD,eAAe;AACjB,sCAAsC;GACpC,eAAe;AACjB,iEAAiE;GAC/D,eAAe,CAAC;AAEpB;;;;;GAKG;AACH,qBAAa,qBAAsB,SAAQ,KAAK;IAClC,QAAQ,CAAC,IAAI,EAAE,oBAAoB;gBAA1B,IAAI,EAAE,oBAAoB,EAAE,MAAM,EAAE,MAAM;CAIhE;AAED,iFAAiF;AACjF,eAAO,MAAM,qBAAqB,mBAAmB,CAAC;AAStD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAInD;AA8HD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CASrD"}
|