@aimarket/warden 0.3.0 → 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 +147 -3
- package/README-es.md +10 -4
- package/README-fr.md +10 -4
- package/README-ru.md +10 -4
- package/README-zh.md +10 -4
- package/README.md +10 -4
- 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 +42 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +84 -29
- package/dist/index.js.map +1 -1
- package/dist/origin.d.ts.map +1 -1
- package/dist/origin.js +7 -2
- package/dist/origin.js.map +1 -1
- package/dist/pinning.d.ts +18 -0
- package/dist/pinning.d.ts.map +1 -1
- package/dist/pinning.js +59 -11
- package/dist/pinning.js.map +1 -1
- package/dist/sandbox.d.ts.map +1 -1
- package/dist/sandbox.js +10 -7
- package/dist/sandbox.js.map +1 -1
- 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 +46 -2
- package/dist/static-scan.d.ts.map +1 -1
- package/dist/static-scan.js +358 -34
- package/dist/static-scan.js.map +1 -1
- package/dist/threat-feed.d.ts +3 -0
- package/dist/threat-feed.d.ts.map +1 -1
- package/dist/threat-feed.js +118 -29
- package/dist/threat-feed.js.map +1 -1
- package/dist/types.d.ts +15 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -2
package/dist/pinning.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { canonicalize, CanonicalizationError } from "./jcs.js";
|
|
3
|
+
import { displaySafe } from "./sanitize.js";
|
|
3
4
|
/**
|
|
4
5
|
* Tool-definition pinning + drift detection ("rug-pull" defence).
|
|
5
6
|
*
|
|
@@ -27,6 +28,7 @@ export class PinningGate {
|
|
|
27
28
|
throw err;
|
|
28
29
|
return this.uncanonical(input, pin, err);
|
|
29
30
|
}
|
|
31
|
+
const identityDrift = this.identityDrift(input, pin);
|
|
30
32
|
if (!pin) {
|
|
31
33
|
const finding = {
|
|
32
34
|
gate: this.name,
|
|
@@ -43,22 +45,47 @@ export class PinningGate {
|
|
|
43
45
|
// Neutral-to-good: unpinned isn't unsafe, it's just unestablished.
|
|
44
46
|
return { findings: [finding], score: 0.9 };
|
|
45
47
|
}
|
|
48
|
+
const findings = [];
|
|
46
49
|
if (pin.toolsHash !== hash) {
|
|
47
|
-
|
|
50
|
+
findings.push({
|
|
48
51
|
gate: this.name,
|
|
49
52
|
severity: "high",
|
|
50
53
|
code: "TOOL_DEF_DRIFT",
|
|
51
|
-
message: `Tool definitions for "${input.server.id}" changed since approval ` +
|
|
54
|
+
message: `Tool definitions for "${displaySafe(input.server.id)}" changed since approval ` +
|
|
52
55
|
`(pinned ${short(pin.toolsHash)} → now ${short(hash)}). Possible rug-pull; re-approval required.`,
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
// Both are reported when both changed: "the tools moved AND so did the
|
|
59
|
+
// program serving them" is a different picture from either one alone, and
|
|
60
|
+
// returning on the first one found used to hide the second.
|
|
61
|
+
if (identityDrift)
|
|
62
|
+
findings.push(identityDrift);
|
|
63
|
+
if (findings.length > 0) {
|
|
64
|
+
return { findings, score: 0, fatal: input.policy.pinToolDefs === true };
|
|
59
65
|
}
|
|
60
66
|
return { findings: [], score: 1 };
|
|
61
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Has the program behind this server changed since approval?
|
|
70
|
+
*
|
|
71
|
+
* Silent when the pin predates {@link PinnedServer.identityHash}: an absent
|
|
72
|
+
* value is "not recorded", never "changed". The next `approve()` records it.
|
|
73
|
+
*/
|
|
74
|
+
identityDrift(input, pin) {
|
|
75
|
+
if (!pin?.identityHash)
|
|
76
|
+
return undefined;
|
|
77
|
+
const now = serverIdentityHash(input.server);
|
|
78
|
+
if (now === pin.identityHash)
|
|
79
|
+
return undefined;
|
|
80
|
+
return {
|
|
81
|
+
gate: this.name,
|
|
82
|
+
severity: "high",
|
|
83
|
+
code: "SERVER_IDENTITY_DRIFT",
|
|
84
|
+
message: `The launch identity of "${displaySafe(input.server.id)}" changed since approval ` +
|
|
85
|
+
`(pinned ${short(pin.identityHash)} → now ${short(now)}): transport, command, args, url or name ` +
|
|
86
|
+
`is not what was approved. The advertised tools may be identical while the program serving them is not.`,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
62
89
|
/**
|
|
63
90
|
* The tool-def set has no canonical form (see {@link canonicalToolsHash}), so no
|
|
64
91
|
* hash can be produced for it. What that means depends on whether a pin exists:
|
|
@@ -78,7 +105,7 @@ export class PinningGate {
|
|
|
78
105
|
gate: this.name,
|
|
79
106
|
severity: "medium",
|
|
80
107
|
code: "TOOL_DEF_UNCANONICAL",
|
|
81
|
-
message: `Tool definitions for "${input.server.id}" have no canonical form (${err.message}), ` +
|
|
108
|
+
message: `Tool definitions for "${displaySafe(input.server.id)}" have no canonical form (${err.message}), ` +
|
|
82
109
|
`so no reproducible pin can be taken — drift detection is unavailable for this server.`,
|
|
83
110
|
},
|
|
84
111
|
],
|
|
@@ -91,7 +118,7 @@ export class PinningGate {
|
|
|
91
118
|
gate: this.name,
|
|
92
119
|
severity: "high",
|
|
93
120
|
code: "TOOL_DEF_UNCANONICAL",
|
|
94
|
-
message: `Tool definitions for "${input.server.id}" have no canonical form (${err.message}), so the pinned ` +
|
|
121
|
+
message: `Tool definitions for "${displaySafe(input.server.id)}" have no canonical form (${err.message}), so the pinned ` +
|
|
95
122
|
`snapshot ${short(pin.toolsHash)} cannot be re-verified. Treated as drift; re-approval required.`,
|
|
96
123
|
},
|
|
97
124
|
],
|
|
@@ -112,7 +139,8 @@ export class PinningGate {
|
|
|
112
139
|
serverId: server.id,
|
|
113
140
|
toolsHash: canonicalToolsHash(tools),
|
|
114
141
|
approvedAt: new Date().toISOString(),
|
|
115
|
-
toolNames: [...tools.map((t) => t.name)].sort(),
|
|
142
|
+
toolNames: [...tools.map((t) => t.name)].sort(compareCodeUnits),
|
|
143
|
+
identityHash: serverIdentityHash(server),
|
|
116
144
|
};
|
|
117
145
|
await this.store.putPin(pinned);
|
|
118
146
|
}
|
|
@@ -169,6 +197,26 @@ export function tryCanonicalToolsHash(tools) {
|
|
|
169
197
|
throw err;
|
|
170
198
|
}
|
|
171
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* sha256 over the server's launch identity: transport, command, args, url, name.
|
|
202
|
+
*
|
|
203
|
+
* Deliberately NOT the whole `McpServerRef`: `catalog` is provenance rather than
|
|
204
|
+
* identity (the origin gate is what has an opinion about it), and `env` holds
|
|
205
|
+
* secrets that must not be hashed into a value stored on disk next to the pin.
|
|
206
|
+
*
|
|
207
|
+
* Missing fields serialize as empty rather than being omitted, so a server that
|
|
208
|
+
* gains a `url` reads as a change instead of hashing the same as before.
|
|
209
|
+
*/
|
|
210
|
+
export function serverIdentityHash(server) {
|
|
211
|
+
const identity = {
|
|
212
|
+
transport: server.transport,
|
|
213
|
+
command: server.command ?? "",
|
|
214
|
+
args: server.args ?? [],
|
|
215
|
+
url: server.url ?? "",
|
|
216
|
+
name: server.name,
|
|
217
|
+
};
|
|
218
|
+
return createHash("sha256").update(canonicalize(identity), "utf8").digest("hex");
|
|
219
|
+
}
|
|
172
220
|
/** RFC 8785 §3.2.3 ordering: arrays of UTF-16 code units as unsigned integers. */
|
|
173
221
|
function compareCodeUnits(a, b) {
|
|
174
222
|
return a < b ? -1 : a > b ? 1 : 0;
|
package/dist/pinning.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pinning.js","sourceRoot":"","sources":["../src/pinning.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,YAAY,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"pinning.js","sourceRoot":"","sources":["../src/pinning.ts"],"names":[],"mappings":"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"}
|
package/dist/sandbox.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sandbox.d.ts","sourceRoot":"","sources":["../src/sandbox.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"sandbox.d.ts","sourceRoot":"","sources":["../src/sandbox.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAExD;;;;;;;;;GASG;AAEH;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,CAG/E;AAED,2EAA2E;AAC3E,wBAAgB,aAAa,CAC3B,KAAK,EAAE,OAAO,EAAE,EAChB,MAAM,EAAE,YAAY,GACnB;IAAE,SAAS,EAAE,MAAM,EAAE,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,CAQzC;AAED;;;;;GAKG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAW;gBAErB,SAAS,EAAE,MAAM,EAAE;IAI/B,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE;CAmB1D"}
|
package/dist/sandbox.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { wildcardMatch } from "./glob.js";
|
|
1
2
|
/**
|
|
2
3
|
* Runtime policy layer for tool execution.
|
|
3
4
|
*
|
|
@@ -64,13 +65,15 @@ export class EgressGuard {
|
|
|
64
65
|
return { allowed: false, reason: `Host not in egress allowlist: ${host}` };
|
|
65
66
|
}
|
|
66
67
|
}
|
|
67
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* `*`-glob match (case-insensitive). Inputs are matched as whole strings.
|
|
70
|
+
*
|
|
71
|
+
* The pattern comes from the operator's policy, the value from the server being
|
|
72
|
+
* inspected. Compiling `*` into `.*` and running a regex made that pair a denial
|
|
73
|
+
* of service: an operator pattern with a dozen wildcards plus a 200-character
|
|
74
|
+
* tool name took 89 SECONDS to answer "not sensitive". See ./glob.ts.
|
|
75
|
+
*/
|
|
68
76
|
function globMatch(pattern, value) {
|
|
69
|
-
|
|
70
|
-
.toLowerCase()
|
|
71
|
-
.split("*")
|
|
72
|
-
.map((seg) => seg.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
|
73
|
-
.join(".*");
|
|
74
|
-
return new RegExp(`^${re}$`, "i").test(value);
|
|
77
|
+
return wildcardMatch(pattern.toLowerCase(), value);
|
|
75
78
|
}
|
|
76
79
|
//# sourceMappingURL=sandbox.js.map
|
package/dist/sandbox.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sandbox.js","sourceRoot":"","sources":["../src/sandbox.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"sandbox.js","sourceRoot":"","sources":["../src/sandbox.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAG1C;;;;;;;;;GASG;AAEH;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,QAAgB,EAAE,MAAoB;IACpE,MAAM,IAAI,GAAG,QAAQ,CAAC,WAAW,EAAE,CAAC;IACpC,OAAO,MAAM,CAAC,qBAAqB,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC;AAC1E,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,aAAa,CAC3B,KAAgB,EAChB,MAAoB;IAEpB,MAAM,SAAS,GAAa,EAAE,CAAC;IAC/B,MAAM,IAAI,GAAa,EAAE,CAAC;IAC1B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,eAAe,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC;YAAE,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;;YAC7D,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC;IACD,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;AAC7B,CAAC;AAED;;;;;GAKG;AACH,MAAM,OAAO,WAAW;IACL,KAAK,CAAW;IAEjC,YAAY,SAAmB;QAC7B,IAAI,CAAC,KAAK,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAC5E,CAAC;IAED,KAAK,CAAC,GAAW;QACf,IAAI,IAAY,CAAC;QACjB,IAAI,CAAC;YACH,IAAI,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC;QAC7C,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,oBAAoB,GAAG,EAAE,EAAE,CAAC;QAC/D,CAAC;QACD,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC5B,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,qCAAqC,IAAI,EAAE,EAAE,CAAC;QACjF,CAAC;QACD,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YAC/B,IAAI,KAAK,KAAK,IAAI;gBAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;YAC7C,IAAI,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC3B,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,iBAAiB;gBAChD,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM;oBAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;YACrF,CAAC;QACH,CAAC;QACD,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,iCAAiC,IAAI,EAAE,EAAE,CAAC;IAC7E,CAAC;CACF;AAED;;;;;;;GAOG;AACH,SAAS,SAAS,CAAC,OAAe,EAAE,KAAa;IAC/C,OAAO,aAAa,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,KAAK,CAAC,CAAC;AACrD,CAAC"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Make untrusted text safe to *display*.
|
|
3
|
+
*
|
|
4
|
+
* Every finding WARDEN emits quotes something the other side chose: a tool name,
|
|
5
|
+
* a server id, a catalog name, the `reason` string out of a signed threat feed.
|
|
6
|
+
* Those strings are then printed to a terminal by the host CLI, rendered in a
|
|
7
|
+
* dashboard, and stored in receipts. Interpolating them raw makes the firewall
|
|
8
|
+
* itself the delivery vehicle for a terminal-control payload:
|
|
9
|
+
*
|
|
10
|
+
* tool name: <ESC>[2K<ESC>[1A + "overwritten ~/.ssh"
|
|
11
|
+
* finding: Tool "<ESC>[2K<ESC>[1Aoverwritten ~/.ssh": Server references ...
|
|
12
|
+
*
|
|
13
|
+
* Printed to a TTY that erases the current line and moves the cursor up, so the
|
|
14
|
+
* BLOCK line WARDEN had just written is overwritten by text the attacker chose.
|
|
15
|
+
* The same trick with zero-width and bidi characters makes a name read as
|
|
16
|
+
* something it is not.
|
|
17
|
+
*
|
|
18
|
+
* So control characters and invisible characters are replaced with a VISIBLE
|
|
19
|
+
* escape rather than stripped. Stripping would hide the very thing worth seeing:
|
|
20
|
+
* a name with an embedded U+202E should look suspicious in the report, not clean.
|
|
21
|
+
* Length is capped, because a finding message is a line of output, not somewhere
|
|
22
|
+
* a hostile server gets to paste a megabyte.
|
|
23
|
+
*
|
|
24
|
+
* This is display hygiene, not a security boundary. `WardenFinding.tool` keeps
|
|
25
|
+
* the RAW advertised name, because that is the key a host uses to filter its own
|
|
26
|
+
* tool list and a sanitized key would match nothing.
|
|
27
|
+
*/
|
|
28
|
+
/** Default cap for an interpolated fragment: long enough to be useful in a log line. */
|
|
29
|
+
export declare const DEFAULT_DISPLAY_MAX = 200;
|
|
30
|
+
/**
|
|
31
|
+
* Render `value` as a single-line, control-character-free fragment, escaping what
|
|
32
|
+
* cannot be shown and truncating past `max`.
|
|
33
|
+
*
|
|
34
|
+
* Exported so a host can apply it to the raw names it gets back in
|
|
35
|
+
* `allowedTools` / `blockedTools`, which are deliberately not sanitized.
|
|
36
|
+
*/
|
|
37
|
+
export declare function displaySafe(value: unknown, max?: number): string;
|
|
38
|
+
//# sourceMappingURL=sanitize.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sanitize.d.ts","sourceRoot":"","sources":["../src/sanitize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAUH,wFAAwF;AACxF,eAAO,MAAM,mBAAmB,MAAM,CAAC;AAEvC;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,GAAE,MAA4B,GAAG,MAAM,CASrF"}
|
package/dist/sanitize.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Make untrusted text safe to *display*.
|
|
3
|
+
*
|
|
4
|
+
* Every finding WARDEN emits quotes something the other side chose: a tool name,
|
|
5
|
+
* a server id, a catalog name, the `reason` string out of a signed threat feed.
|
|
6
|
+
* Those strings are then printed to a terminal by the host CLI, rendered in a
|
|
7
|
+
* dashboard, and stored in receipts. Interpolating them raw makes the firewall
|
|
8
|
+
* itself the delivery vehicle for a terminal-control payload:
|
|
9
|
+
*
|
|
10
|
+
* tool name: <ESC>[2K<ESC>[1A + "overwritten ~/.ssh"
|
|
11
|
+
* finding: Tool "<ESC>[2K<ESC>[1Aoverwritten ~/.ssh": Server references ...
|
|
12
|
+
*
|
|
13
|
+
* Printed to a TTY that erases the current line and moves the cursor up, so the
|
|
14
|
+
* BLOCK line WARDEN had just written is overwritten by text the attacker chose.
|
|
15
|
+
* The same trick with zero-width and bidi characters makes a name read as
|
|
16
|
+
* something it is not.
|
|
17
|
+
*
|
|
18
|
+
* So control characters and invisible characters are replaced with a VISIBLE
|
|
19
|
+
* escape rather than stripped. Stripping would hide the very thing worth seeing:
|
|
20
|
+
* a name with an embedded U+202E should look suspicious in the report, not clean.
|
|
21
|
+
* Length is capped, because a finding message is a line of output, not somewhere
|
|
22
|
+
* a hostile server gets to paste a megabyte.
|
|
23
|
+
*
|
|
24
|
+
* This is display hygiene, not a security boundary. `WardenFinding.tool` keeps
|
|
25
|
+
* the RAW advertised name, because that is the key a host uses to filter its own
|
|
26
|
+
* tool list and a sanitized key would match nothing.
|
|
27
|
+
*/
|
|
28
|
+
/** Zero-width and bidi controls: invisible, therefore always escaped. */
|
|
29
|
+
const INVISIBLE = "\\u200B-\\u200F\\u202A-\\u202E\\u2060\\u2066-\\u2069\\uFEFF";
|
|
30
|
+
/** C0 controls, DEL, and C1 - the terminal-control range. */
|
|
31
|
+
const CONTROL = "\\u0000-\\u001F\\u007F-\\u009F";
|
|
32
|
+
const UNSAFE = new RegExp(`[${CONTROL}${INVISIBLE}]`, "g");
|
|
33
|
+
/** Default cap for an interpolated fragment: long enough to be useful in a log line. */
|
|
34
|
+
export const DEFAULT_DISPLAY_MAX = 200;
|
|
35
|
+
/**
|
|
36
|
+
* Render `value` as a single-line, control-character-free fragment, escaping what
|
|
37
|
+
* cannot be shown and truncating past `max`.
|
|
38
|
+
*
|
|
39
|
+
* Exported so a host can apply it to the raw names it gets back in
|
|
40
|
+
* `allowedTools` / `blockedTools`, which are deliberately not sanitized.
|
|
41
|
+
*/
|
|
42
|
+
export function displaySafe(value, max = DEFAULT_DISPLAY_MAX) {
|
|
43
|
+
const text = typeof value === "string" ? value : String(value ?? "");
|
|
44
|
+
const escaped = text.replace(UNSAFE, (ch) => {
|
|
45
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
46
|
+
return `\\u${code.toString(16).toUpperCase().padStart(4, "0")}`;
|
|
47
|
+
});
|
|
48
|
+
if (escaped.length <= max)
|
|
49
|
+
return escaped;
|
|
50
|
+
// Report the real length: "this name is 4 MB long" is itself worth knowing.
|
|
51
|
+
return `${escaped.slice(0, max)}... [truncated, ${text.length} chars]`;
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=sanitize.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sanitize.js","sourceRoot":"","sources":["../src/sanitize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,yEAAyE;AACzE,MAAM,SAAS,GAAG,6DAA6D,CAAC;AAEhF,6DAA6D;AAC7D,MAAM,OAAO,GAAG,gCAAgC,CAAC;AAEjD,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,OAAO,GAAG,SAAS,GAAG,EAAE,GAAG,CAAC,CAAC;AAE3D,wFAAwF;AACxF,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAAG,CAAC;AAEvC;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,KAAc,EAAE,MAAc,mBAAmB;IAC3E,MAAM,IAAI,GAAG,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC;IACrE,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,EAAE;QAC1C,MAAM,IAAI,GAAG,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QACpC,OAAO,MAAM,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC;IAClE,CAAC,CAAC,CAAC;IACH,IAAI,OAAO,CAAC,MAAM,IAAI,GAAG;QAAE,OAAO,OAAO,CAAC;IAC1C,4EAA4E;IAC5E,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,mBAAmB,IAAI,CAAC,MAAM,SAAS,CAAC;AACzE,CAAC"}
|
package/dist/static-scan.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RulesetRef, Severity, WardenGate, WardenGateInput, WardenGateResult } from "./types.js";
|
|
1
|
+
import type { RulesetRef, Severity, WardenGate, WardenGateInput, WardenGateResult, WardenLogger } from "./types.js";
|
|
2
2
|
/**
|
|
3
3
|
* Static tool-definition scanner.
|
|
4
4
|
*
|
|
@@ -22,6 +22,22 @@ import type { RulesetRef, Severity, WardenGate, WardenGateInput, WardenGateResul
|
|
|
22
22
|
* which is how a scanner gets switched off entirely.
|
|
23
23
|
*/
|
|
24
24
|
type Tier = "block" | "advise";
|
|
25
|
+
/**
|
|
26
|
+
* Which part of a tool definition a rule is meaningful against.
|
|
27
|
+
*
|
|
28
|
+
* `name` is an IDENTIFIER, not prose, and that difference decides the table
|
|
29
|
+
* below. A rule that keys on a noun - `api_key`, `private_key`, `.env` - matches
|
|
30
|
+
* ordinary identifiers (`sign_with_private_key` is a plausible wallet tool), so
|
|
31
|
+
* running it over names would refuse honest servers on their naming convention:
|
|
32
|
+
* exactly the ruleset v1 mistake, one surface over. A rule that keys on a PHRASE
|
|
33
|
+
* needs whitespace and cannot match `snake_case` at all, and the two hidden-payload
|
|
34
|
+
* rules are about characters that are never legitimate in a name.
|
|
35
|
+
*
|
|
36
|
+
* Names were scanned by nothing at all until ruleset v3, which meant zero-width
|
|
37
|
+
* characters and a base64 blob could sit in the one field that reaches the model
|
|
38
|
+
* first and WARDEN reported nothing.
|
|
39
|
+
*/
|
|
40
|
+
type Surface = "name" | "description" | "inputSchema";
|
|
25
41
|
/**
|
|
26
42
|
* Ruleset version. Bump on ANY change to the table above — a scan result is only
|
|
27
43
|
* comparable to another scan made under the same version and digest.
|
|
@@ -30,15 +46,36 @@ type Tier = "block" | "advise";
|
|
|
30
46
|
* 2 — block/advise tiers; exfil "send … to" anchored on an external destination;
|
|
31
47
|
* TOOL_DEF_SECRET_HARVEST added so demoting the credential nouns does not
|
|
32
48
|
* open a hole; advisory findings no longer affect the score.
|
|
49
|
+
* 3 — rules gained `surfaces`, and the tool NAME became a scanned surface. Until
|
|
50
|
+
* v3 the name was scanned by nothing, so zero-width characters, a base64 blob
|
|
51
|
+
* or an injection phrase in the one field that reaches the model first went
|
|
52
|
+
* entirely unreported. Noun-keyed rules stay off the name on purpose: they
|
|
53
|
+
* match ordinary identifiers, and refusing `sign_with_private_key` would be
|
|
54
|
+
* the v1 calibration error committed on a new surface.
|
|
55
|
+
* 4 — calibrated against 1 108 live public MCP servers (docs/mcp-survey.md). v3
|
|
56
|
+
* blocked 50 of them and only 4 held up on review, so v4 adds context
|
|
57
|
+
* GUARDS — polarity, quotation, URI form, payload length, entropy, script
|
|
58
|
+
* adjacency — and demotes four blocking rules the survey showed were
|
|
59
|
+
* selecting for honest servers: the bare `exfiltrat*` noun, `system prompt`,
|
|
60
|
+
* `do not tell the user`, and (in severity only) the credential nouns. A
|
|
61
|
+
* rule's guards are part of this table and therefore of the digest.
|
|
33
62
|
*/
|
|
34
|
-
export declare const STATIC_SCAN_RULESET_VERSION = "
|
|
63
|
+
export declare const STATIC_SCAN_RULESET_VERSION = "4";
|
|
35
64
|
export interface StaticScanRule {
|
|
36
65
|
code: string;
|
|
37
66
|
severity: Severity;
|
|
38
67
|
tier: Tier;
|
|
68
|
+
/** Which surfaces the rule is run against, so a re-run scans the same fields. */
|
|
69
|
+
surfaces: Surface[];
|
|
39
70
|
/** The regex source, so a third party can re-run the exact rule. */
|
|
40
71
|
source: string;
|
|
41
72
|
flags: string;
|
|
73
|
+
/**
|
|
74
|
+
* Which context guards this rule is subject to. Published because the regex
|
|
75
|
+
* alone no longer describes the rule: the same pattern with and without
|
|
76
|
+
* `polarity` reports different findings on the same text.
|
|
77
|
+
*/
|
|
78
|
+
guards: string[];
|
|
42
79
|
}
|
|
43
80
|
export interface StaticScanRuleset extends RulesetRef {
|
|
44
81
|
rules: StaticScanRule[];
|
|
@@ -60,6 +97,13 @@ export declare function staticScanRuleset(): StaticScanRuleset;
|
|
|
60
97
|
export declare function staticScanRulesetRef(): RulesetRef;
|
|
61
98
|
export declare class StaticScanGate implements WardenGate {
|
|
62
99
|
readonly name = "static-scan";
|
|
100
|
+
private readonly log;
|
|
101
|
+
/**
|
|
102
|
+
* @param log where dropped matches are reported. A guard silently discarding a
|
|
103
|
+
* finding is the one behaviour in this gate that cannot be seen in the
|
|
104
|
+
* verdict, so it is the one that most needs a debug line.
|
|
105
|
+
*/
|
|
106
|
+
constructor(log?: WardenLogger);
|
|
63
107
|
evaluate(input: WardenGateInput): Promise<WardenGateResult>;
|
|
64
108
|
}
|
|
65
109
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"static-scan.d.ts","sourceRoot":"","sources":["../src/static-scan.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"static-scan.d.ts","sourceRoot":"","sources":["../src/static-scan.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,UAAU,EACV,QAAQ,EAER,UAAU,EACV,eAAe,EACf,gBAAgB,EAChB,YAAY,EACb,MAAM,YAAY,CAAC;AAEpB;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,KAAK,IAAI,GAAG,OAAO,GAAG,QAAQ,CAAC;AAE/B;;;;;;;;;;;;;;GAcG;AACH,KAAK,OAAO,GAAG,MAAM,GAAG,aAAa,GAAG,aAAa,CAAC;AAuZtD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,2BAA2B,MAAM,CAAC;AAmB/C,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,QAAQ,CAAC;IACnB,IAAI,EAAE,IAAI,CAAC;IACX,iFAAiF;IACjF,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,oEAAoE;IACpE,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,iBAAkB,SAAQ,UAAU;IACnD,KAAK,EAAE,cAAc,EAAE,CAAC;CACzB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,IAAI,iBAAiB,CAerD;AAOD,uEAAuE;AACvE,wBAAgB,oBAAoB,IAAI,UAAU,CAGjD;AAED,qBAAa,cAAe,YAAW,UAAU;IAC/C,QAAQ,CAAC,IAAI,iBAAiB;IAE9B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IAEnC;;;;OAIG;gBACS,GAAG,GAAE,YAA6B;IAIxC,QAAQ,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,gBAAgB,CAAC;CAoDlE"}
|