policylayer 0.1.3 → 0.1.5
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/README.md +31 -23
- package/dist/index.js +1124 -5
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -5,51 +5,59 @@ Scan your MCP config. See what your AI agent can do. Get a shareable report.
|
|
|
5
5
|
## Quick start
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npx -y policylayer
|
|
8
|
+
npx -y policylayer
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
That's it. The CLI finds your MCP config,
|
|
11
|
+
That's it. The CLI finds your MCP config, live-scans each server, classifies every tool, and prints a report URL.
|
|
12
12
|
|
|
13
13
|
## What it does
|
|
14
14
|
|
|
15
15
|
1. **Finds your config** -- checks `.mcp.json`, `~/.claude.json`, Claude Desktop, Cursor, VS Code, Windsurf, and Codex configs automatically
|
|
16
|
-
2. **
|
|
17
|
-
3. **
|
|
18
|
-
4. **
|
|
16
|
+
2. **Live-scans servers** -- spawns each MCP server, performs the JSON-RPC handshake, and discovers every tool with its full schema
|
|
17
|
+
3. **Classifies tools** -- checks against the PolicyLayer database of 2,500+ classified tools. Unknown tools are classified locally using schema analysis, blast radius detection, and verb matching
|
|
18
|
+
4. **Generates a policy YAML** -- suggested default rules for every tool, ready to use with [Intercept](https://github.com/policylayer/intercept)
|
|
19
|
+
5. **Prints a report URL** -- permanent, shareable, no login required
|
|
20
|
+
6. **Contributes back** -- live-scanned tool data is contributed to the database so future scans are faster for everyone
|
|
19
21
|
|
|
20
22
|
## Commands
|
|
21
23
|
|
|
22
|
-
### `policylayer scan`
|
|
23
|
-
|
|
24
|
-
Scan your MCP configuration.
|
|
25
|
-
|
|
26
24
|
```bash
|
|
27
|
-
npx -y policylayer
|
|
25
|
+
npx -y policylayer
|
|
28
26
|
```
|
|
29
27
|
|
|
30
28
|
**Options:**
|
|
31
29
|
|
|
32
30
|
| Flag | Description |
|
|
33
31
|
|------|-------------|
|
|
34
|
-
|
|
|
35
|
-
|
|
|
32
|
+
| `-d, --dir <path>` | Directory to scan for config files (default: cwd) |
|
|
33
|
+
| `-o, --output <path>` | Output path for policy YAML (default: `policylayer.yaml`) |
|
|
34
|
+
| `--no-live` | Skip live scanning, classify from config only |
|
|
35
|
+
| `--no-report` | Skip submitting report to PolicyLayer |
|
|
36
|
+
| `--timeout <ms>` | Timeout per server in milliseconds (default: 30000) |
|
|
37
|
+
| `--json` | Output results as JSON |
|
|
36
38
|
|
|
37
39
|
### Examples
|
|
38
40
|
|
|
39
41
|
```bash
|
|
40
42
|
# Auto-detect config and scan
|
|
41
|
-
npx -y policylayer
|
|
43
|
+
npx -y policylayer
|
|
44
|
+
|
|
45
|
+
# Scan a specific directory
|
|
46
|
+
npx -y policylayer -d ~/projects/my-app
|
|
47
|
+
|
|
48
|
+
# Skip live scanning (faster, less detailed)
|
|
49
|
+
npx -y policylayer --no-live
|
|
42
50
|
|
|
43
|
-
#
|
|
44
|
-
npx -y policylayer
|
|
51
|
+
# Output as JSON for piping
|
|
52
|
+
npx -y policylayer --json
|
|
45
53
|
|
|
46
|
-
#
|
|
47
|
-
npx -y policylayer
|
|
54
|
+
# Custom policy output path
|
|
55
|
+
npx -y policylayer -o my-policy.yaml
|
|
48
56
|
```
|
|
49
57
|
|
|
50
58
|
## Config detection
|
|
51
59
|
|
|
52
|
-
The CLI searches these paths
|
|
60
|
+
The CLI searches these paths and uses all configs found:
|
|
53
61
|
|
|
54
62
|
| Client | Path |
|
|
55
63
|
|--------|------|
|
|
@@ -67,15 +75,15 @@ The CLI searches these paths in order and uses the first one found:
|
|
|
67
75
|
The CLI **never sends** your raw config. Before anything leaves your machine:
|
|
68
76
|
|
|
69
77
|
- Environment variables are stripped
|
|
70
|
-
- Auth tokens
|
|
71
|
-
-
|
|
72
|
-
-
|
|
73
|
-
- Only server names and npm package identifiers are sent
|
|
78
|
+
- Auth tokens are removed
|
|
79
|
+
- Only server names, package identifiers, and tool schemas are sent
|
|
80
|
+
- Live-scanned tool data (names, descriptions, schemas) is contributed to improve the database
|
|
74
81
|
|
|
75
|
-
Use `--
|
|
82
|
+
Use `--no-report` to skip sending anything.
|
|
76
83
|
|
|
77
84
|
## Links
|
|
78
85
|
|
|
79
86
|
- [Example report](https://policylayer.com/scan/report/65545482-5d1d-472f-9fca-472ff1181d0d)
|
|
80
87
|
- [Scan your config online](https://policylayer.com/scan)
|
|
88
|
+
- [Policy library](https://policylayer.com/policies)
|
|
81
89
|
- [Intercept](https://github.com/policylayer/intercept) -- enforce limits on every MCP tool call
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,1125 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
9
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
20
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
21
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
22
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
23
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
24
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
25
|
+
mod
|
|
26
|
+
));
|
|
27
|
+
|
|
28
|
+
// ../classifier/dist/verbs.js
|
|
29
|
+
var require_verbs = __commonJS({
|
|
30
|
+
"../classifier/dist/verbs.js"(exports) {
|
|
31
|
+
"use strict";
|
|
32
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
33
|
+
exports.VERB_MAP = void 0;
|
|
34
|
+
exports.splitName = splitName;
|
|
35
|
+
exports.findFirstVerb = findFirstVerb;
|
|
36
|
+
exports.firstVerbIsRead = firstVerbIsRead;
|
|
37
|
+
exports.VERB_MAP = {};
|
|
38
|
+
var READ_VERBS = [
|
|
39
|
+
"get",
|
|
40
|
+
"list",
|
|
41
|
+
"search",
|
|
42
|
+
"read",
|
|
43
|
+
"fetch",
|
|
44
|
+
"query",
|
|
45
|
+
"browse",
|
|
46
|
+
"view",
|
|
47
|
+
"show",
|
|
48
|
+
"describe",
|
|
49
|
+
"find",
|
|
50
|
+
"check",
|
|
51
|
+
"lookup",
|
|
52
|
+
"retrieve",
|
|
53
|
+
"locate",
|
|
54
|
+
"inspect",
|
|
55
|
+
"ask",
|
|
56
|
+
"count",
|
|
57
|
+
"poll",
|
|
58
|
+
"download",
|
|
59
|
+
"extract",
|
|
60
|
+
"screenshot",
|
|
61
|
+
"introspect",
|
|
62
|
+
"detect",
|
|
63
|
+
"analyze",
|
|
64
|
+
"analyse",
|
|
65
|
+
"scan",
|
|
66
|
+
"validate",
|
|
67
|
+
"verify",
|
|
68
|
+
"monitor",
|
|
69
|
+
"watch",
|
|
70
|
+
"observe",
|
|
71
|
+
"consume",
|
|
72
|
+
"load",
|
|
73
|
+
"data",
|
|
74
|
+
"docs",
|
|
75
|
+
"enrich",
|
|
76
|
+
"translate",
|
|
77
|
+
"cognify",
|
|
78
|
+
"take",
|
|
79
|
+
"test",
|
|
80
|
+
"snapshot",
|
|
81
|
+
"geocode",
|
|
82
|
+
"status",
|
|
83
|
+
"support",
|
|
84
|
+
"matrix",
|
|
85
|
+
"expert",
|
|
86
|
+
"is"
|
|
87
|
+
];
|
|
88
|
+
var DESTRUCTIVE_VERBS = [
|
|
89
|
+
"delete",
|
|
90
|
+
"remove",
|
|
91
|
+
"destroy",
|
|
92
|
+
"drop",
|
|
93
|
+
"truncate",
|
|
94
|
+
"purge",
|
|
95
|
+
"wipe",
|
|
96
|
+
"erase",
|
|
97
|
+
"revoke",
|
|
98
|
+
"cancel",
|
|
99
|
+
"uninstall",
|
|
100
|
+
"reset",
|
|
101
|
+
"clear",
|
|
102
|
+
"force",
|
|
103
|
+
"decompile",
|
|
104
|
+
"unpublish",
|
|
105
|
+
"discard"
|
|
106
|
+
];
|
|
107
|
+
var EXECUTE_VERBS = [
|
|
108
|
+
"run",
|
|
109
|
+
"execute",
|
|
110
|
+
"invoke",
|
|
111
|
+
"trigger",
|
|
112
|
+
"start",
|
|
113
|
+
"deploy",
|
|
114
|
+
"build",
|
|
115
|
+
"restart",
|
|
116
|
+
"scale",
|
|
117
|
+
"compile",
|
|
118
|
+
"launch",
|
|
119
|
+
"emulate",
|
|
120
|
+
"initialize",
|
|
121
|
+
"initialise",
|
|
122
|
+
"navigate",
|
|
123
|
+
"parse",
|
|
124
|
+
"rephrase",
|
|
125
|
+
"transform",
|
|
126
|
+
"stop",
|
|
127
|
+
"performance",
|
|
128
|
+
"wait",
|
|
129
|
+
"new",
|
|
130
|
+
"produce"
|
|
131
|
+
];
|
|
132
|
+
var WRITE_VERBS = [
|
|
133
|
+
"create",
|
|
134
|
+
"add",
|
|
135
|
+
"update",
|
|
136
|
+
"set",
|
|
137
|
+
"modify",
|
|
138
|
+
"put",
|
|
139
|
+
"patch",
|
|
140
|
+
"edit",
|
|
141
|
+
"configure",
|
|
142
|
+
"enable",
|
|
143
|
+
"disable",
|
|
144
|
+
"assign",
|
|
145
|
+
"upload",
|
|
146
|
+
"write",
|
|
147
|
+
"send",
|
|
148
|
+
"post",
|
|
149
|
+
"publish",
|
|
150
|
+
"manage",
|
|
151
|
+
"save",
|
|
152
|
+
"install",
|
|
153
|
+
"boot",
|
|
154
|
+
"setup",
|
|
155
|
+
"select",
|
|
156
|
+
"click",
|
|
157
|
+
"tap",
|
|
158
|
+
"press",
|
|
159
|
+
"scroll",
|
|
160
|
+
"swipe",
|
|
161
|
+
"drag",
|
|
162
|
+
"activate",
|
|
163
|
+
"handle",
|
|
164
|
+
"switch",
|
|
165
|
+
"generate",
|
|
166
|
+
"move",
|
|
167
|
+
"copy",
|
|
168
|
+
"import",
|
|
169
|
+
"export",
|
|
170
|
+
"connect",
|
|
171
|
+
"disconnect",
|
|
172
|
+
"close",
|
|
173
|
+
"open",
|
|
174
|
+
"attach",
|
|
175
|
+
"insert",
|
|
176
|
+
"apply",
|
|
177
|
+
"convert",
|
|
178
|
+
"merge",
|
|
179
|
+
"register",
|
|
180
|
+
"submit",
|
|
181
|
+
"approve",
|
|
182
|
+
"reject",
|
|
183
|
+
"mark",
|
|
184
|
+
"comment",
|
|
185
|
+
"annotate",
|
|
186
|
+
"pin",
|
|
187
|
+
"lock",
|
|
188
|
+
"unlock",
|
|
189
|
+
"archive",
|
|
190
|
+
"restore",
|
|
191
|
+
"suspend",
|
|
192
|
+
"resume",
|
|
193
|
+
"terminate",
|
|
194
|
+
"complete",
|
|
195
|
+
"uncomplete",
|
|
196
|
+
"rename",
|
|
197
|
+
"fill",
|
|
198
|
+
"invite",
|
|
199
|
+
"alter",
|
|
200
|
+
"vote",
|
|
201
|
+
"plan",
|
|
202
|
+
"planner",
|
|
203
|
+
"replace",
|
|
204
|
+
"upsert",
|
|
205
|
+
"type",
|
|
206
|
+
"resize",
|
|
207
|
+
"push",
|
|
208
|
+
"issue",
|
|
209
|
+
"autofix",
|
|
210
|
+
"login",
|
|
211
|
+
"checkout",
|
|
212
|
+
"commit",
|
|
213
|
+
"promote",
|
|
214
|
+
"resolve",
|
|
215
|
+
"unarchive",
|
|
216
|
+
"schedule",
|
|
217
|
+
"migrate"
|
|
218
|
+
];
|
|
219
|
+
for (const v of READ_VERBS)
|
|
220
|
+
exports.VERB_MAP[v] = "Read";
|
|
221
|
+
for (const v of DESTRUCTIVE_VERBS)
|
|
222
|
+
exports.VERB_MAP[v] = "Destructive";
|
|
223
|
+
for (const v of EXECUTE_VERBS)
|
|
224
|
+
exports.VERB_MAP[v] = "Execute";
|
|
225
|
+
for (const v of WRITE_VERBS)
|
|
226
|
+
exports.VERB_MAP[v] = "Write";
|
|
227
|
+
function splitName(name) {
|
|
228
|
+
const parts = name.split(/[-_]/);
|
|
229
|
+
const words = [];
|
|
230
|
+
for (const part of parts) {
|
|
231
|
+
const camelParts = part.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase().split("_");
|
|
232
|
+
words.push(...camelParts.filter(Boolean));
|
|
233
|
+
}
|
|
234
|
+
return words;
|
|
235
|
+
}
|
|
236
|
+
function findFirstVerb(words) {
|
|
237
|
+
for (const word of words) {
|
|
238
|
+
const cat = exports.VERB_MAP[word];
|
|
239
|
+
if (cat)
|
|
240
|
+
return cat;
|
|
241
|
+
}
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
function firstVerbIsRead(words) {
|
|
245
|
+
for (const word of words) {
|
|
246
|
+
if (exports.VERB_MAP[word])
|
|
247
|
+
return exports.VERB_MAP[word] === "Read";
|
|
248
|
+
}
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// ../classifier/dist/schema.js
|
|
255
|
+
var require_schema = __commonJS({
|
|
256
|
+
"../classifier/dist/schema.js"(exports) {
|
|
257
|
+
"use strict";
|
|
258
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
259
|
+
exports.analyseSchema = analyseSchema;
|
|
260
|
+
var CODE_EXECUTION_PROPS = /* @__PURE__ */ new Set([
|
|
261
|
+
"sql",
|
|
262
|
+
"query",
|
|
263
|
+
"raw_sql",
|
|
264
|
+
"raw_query",
|
|
265
|
+
"statement",
|
|
266
|
+
"code",
|
|
267
|
+
"script",
|
|
268
|
+
"command",
|
|
269
|
+
"shell",
|
|
270
|
+
"shell_command",
|
|
271
|
+
"cmd",
|
|
272
|
+
"bash",
|
|
273
|
+
"exec",
|
|
274
|
+
"expression",
|
|
275
|
+
"eval",
|
|
276
|
+
"python",
|
|
277
|
+
"javascript",
|
|
278
|
+
"js",
|
|
279
|
+
"lua",
|
|
280
|
+
"ruby"
|
|
281
|
+
]);
|
|
282
|
+
var FILESYSTEM_PROPS = /* @__PURE__ */ new Set([
|
|
283
|
+
"path",
|
|
284
|
+
"file",
|
|
285
|
+
"file_path",
|
|
286
|
+
"filepath",
|
|
287
|
+
"filename",
|
|
288
|
+
"directory",
|
|
289
|
+
"dir",
|
|
290
|
+
"folder",
|
|
291
|
+
"source_path",
|
|
292
|
+
"dest_path",
|
|
293
|
+
"destination",
|
|
294
|
+
"target_path",
|
|
295
|
+
"working_directory",
|
|
296
|
+
"cwd"
|
|
297
|
+
]);
|
|
298
|
+
var NETWORK_PROPS = /* @__PURE__ */ new Set([
|
|
299
|
+
"url",
|
|
300
|
+
"uri",
|
|
301
|
+
"endpoint",
|
|
302
|
+
"host",
|
|
303
|
+
"hostname",
|
|
304
|
+
"webhook",
|
|
305
|
+
"webhook_url",
|
|
306
|
+
"callback_url",
|
|
307
|
+
"redirect_url",
|
|
308
|
+
"api_url",
|
|
309
|
+
"base_url"
|
|
310
|
+
]);
|
|
311
|
+
var CREDENTIAL_PROPS = /* @__PURE__ */ new Set([
|
|
312
|
+
"password",
|
|
313
|
+
"secret",
|
|
314
|
+
"token",
|
|
315
|
+
"api_key",
|
|
316
|
+
"apikey",
|
|
317
|
+
"private_key",
|
|
318
|
+
"access_token",
|
|
319
|
+
"auth",
|
|
320
|
+
"credentials",
|
|
321
|
+
"connection_string"
|
|
322
|
+
]);
|
|
323
|
+
var INJECTION_PROPS = /* @__PURE__ */ new Set([
|
|
324
|
+
"html",
|
|
325
|
+
"body",
|
|
326
|
+
"template",
|
|
327
|
+
"content",
|
|
328
|
+
"raw_body",
|
|
329
|
+
"raw_html",
|
|
330
|
+
"markup",
|
|
331
|
+
"payload"
|
|
332
|
+
]);
|
|
333
|
+
function extractProperties(schema, prefix = "") {
|
|
334
|
+
const props = [];
|
|
335
|
+
const properties = schema.properties;
|
|
336
|
+
if (!properties || typeof properties !== "object")
|
|
337
|
+
return props;
|
|
338
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
339
|
+
if (!value || typeof value !== "object")
|
|
340
|
+
continue;
|
|
341
|
+
const fullName = prefix ? `${prefix}.${key}` : key;
|
|
342
|
+
props.push({
|
|
343
|
+
name: fullName,
|
|
344
|
+
type: value.type,
|
|
345
|
+
description: value.description
|
|
346
|
+
});
|
|
347
|
+
if (value.type === "object" && value.properties) {
|
|
348
|
+
props.push(...extractProperties(value, fullName));
|
|
349
|
+
}
|
|
350
|
+
if (value.type === "array" && value.items && typeof value.items === "object") {
|
|
351
|
+
const items = value.items;
|
|
352
|
+
if (items.type === "object" && items.properties) {
|
|
353
|
+
props.push(...extractProperties(items, `${fullName}[]`));
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return props;
|
|
358
|
+
}
|
|
359
|
+
function normalisePropName(name) {
|
|
360
|
+
const leaf = name.includes(".") ? name.split(".").pop() : name;
|
|
361
|
+
return leaf.toLowerCase().replace(/[-_\s]/g, "_");
|
|
362
|
+
}
|
|
363
|
+
function analyseSchema(schema) {
|
|
364
|
+
const signals = [];
|
|
365
|
+
const properties = extractProperties(schema);
|
|
366
|
+
if (properties.length === 0)
|
|
367
|
+
return signals;
|
|
368
|
+
const seen = /* @__PURE__ */ new Set();
|
|
369
|
+
for (const prop of properties) {
|
|
370
|
+
const norm = normalisePropName(prop.name);
|
|
371
|
+
if (CODE_EXECUTION_PROPS.has(norm) && !seen.has("code_exec")) {
|
|
372
|
+
seen.add("code_exec");
|
|
373
|
+
signals.push({
|
|
374
|
+
signal: `Accepts freeform code/query input (${prop.name})`,
|
|
375
|
+
weight: 0.15
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
if (FILESYSTEM_PROPS.has(norm) && !seen.has("filesystem")) {
|
|
379
|
+
seen.add("filesystem");
|
|
380
|
+
signals.push({
|
|
381
|
+
signal: `Accepts file system path (${prop.name})`,
|
|
382
|
+
weight: 0.1
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
if (NETWORK_PROPS.has(norm) && !seen.has("network")) {
|
|
386
|
+
seen.add("network");
|
|
387
|
+
signals.push({
|
|
388
|
+
signal: `Accepts URL/endpoint input (${prop.name})`,
|
|
389
|
+
weight: 0.08
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
if (CREDENTIAL_PROPS.has(norm) && !seen.has("credential")) {
|
|
393
|
+
seen.add("credential");
|
|
394
|
+
signals.push({
|
|
395
|
+
signal: `Handles credentials or secrets (${prop.name})`,
|
|
396
|
+
weight: 0.12
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
if (INJECTION_PROPS.has(norm) && !seen.has("injection")) {
|
|
400
|
+
seen.add("injection");
|
|
401
|
+
signals.push({
|
|
402
|
+
signal: `Accepts raw HTML/template content (${prop.name})`,
|
|
403
|
+
weight: 0.05
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
if (properties.length >= 10) {
|
|
408
|
+
signals.push({
|
|
409
|
+
signal: `High parameter count (${properties.length} properties)`,
|
|
410
|
+
weight: 0.05
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
return signals;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
// ../classifier/dist/blast-radius.js
|
|
419
|
+
var require_blast_radius = __commonJS({
|
|
420
|
+
"../classifier/dist/blast-radius.js"(exports) {
|
|
421
|
+
"use strict";
|
|
422
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
423
|
+
exports.analyseBlastRadius = analyseBlastRadius;
|
|
424
|
+
var BULK_NAME_PATTERNS = [
|
|
425
|
+
/\ball[_-]?/i,
|
|
426
|
+
/\bbulk[_-]?/i,
|
|
427
|
+
/\bbatch[_-]?/i,
|
|
428
|
+
/\bmass[_-]?/i,
|
|
429
|
+
/\bglobal[_-]?/i,
|
|
430
|
+
/[_-]all$/i
|
|
431
|
+
];
|
|
432
|
+
var SINGLE_NAME_PATTERNS = [
|
|
433
|
+
/by[_-]?id$/i,
|
|
434
|
+
/by[_-]?name$/i,
|
|
435
|
+
/by[_-]?slug$/i,
|
|
436
|
+
/by[_-]?key$/i,
|
|
437
|
+
/by[_-]?email$/i,
|
|
438
|
+
/[_-]one$/i,
|
|
439
|
+
/[_-]single$/i
|
|
440
|
+
];
|
|
441
|
+
var BULK_DESC = /\b(all\s+(?:records?|items?|users?|entries|data|files?|messages?|documents?)|every\s+|entire\s+|everything|in\s+bulk|batch\s+(?:delete|remove|update)|mass\s+(?:delete|remove|update)|wipe\s+(?:all|everything)|clear\s+(?:all|everything))\b/i;
|
|
442
|
+
var SINGLE_DESC = /\b(a\s+(?:single|specific|particular|given)|(?:one|individual)\s+(?:record|item|user|entry|file|message|document)|by\s+(?:its?\s+)?(?:ID|identifier|key|name))\b/i;
|
|
443
|
+
var ADMIN_NAME = /^(?:admin|root|system|sudo|superuser|master)[_-]/i;
|
|
444
|
+
var ADMIN_DESC = /\b(admin(?:istrat(?:or|ive))?|root|system[_-]?level|superuser|privileged|elevated\s+permissions?)\b/i;
|
|
445
|
+
function analyseBlastRadius(name, description) {
|
|
446
|
+
const signals = [];
|
|
447
|
+
const isBulk = BULK_NAME_PATTERNS.some((p) => p.test(name)) || BULK_DESC.test(description);
|
|
448
|
+
const isSingle = SINGLE_NAME_PATTERNS.some((p) => p.test(name)) || SINGLE_DESC.test(description);
|
|
449
|
+
if (isBulk && !isSingle) {
|
|
450
|
+
signals.push({
|
|
451
|
+
signal: "Bulk/mass operation \u2014 affects multiple targets",
|
|
452
|
+
weight: 0.15
|
|
453
|
+
});
|
|
454
|
+
} else if (isSingle && !isBulk) {
|
|
455
|
+
signals.push({
|
|
456
|
+
signal: "Single-target operation",
|
|
457
|
+
weight: -0.05
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
if (ADMIN_NAME.test(name) || ADMIN_DESC.test(description)) {
|
|
461
|
+
signals.push({
|
|
462
|
+
signal: "Admin/system-level operation",
|
|
463
|
+
weight: 0.1
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
return signals;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
// ../classifier/dist/classify.js
|
|
472
|
+
var require_classify = __commonJS({
|
|
473
|
+
"../classifier/dist/classify.js"(exports) {
|
|
474
|
+
"use strict";
|
|
475
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
476
|
+
exports.classifyTool = classifyTool2;
|
|
477
|
+
var verbs_1 = require_verbs();
|
|
478
|
+
var schema_1 = require_schema();
|
|
479
|
+
var blast_radius_1 = require_blast_radius();
|
|
480
|
+
var FINANCIAL_COMPOUND = /(?:^|[_-])(?:create[_-]?payment|create[_-]?charge|create[_-]?refund|create[_-]?payment[_-]?link|process[_-]?payment|finalize[_-]?invoice|finalise[_-]?invoice|send[_-]?invoice)(?:[_A-Z-]|$)/i;
|
|
481
|
+
var FINANCIAL_VERBS_SEGMENT = /^(pay|charge|refund|transfer|withdraw|deposit|finalize|finalise)$/i;
|
|
482
|
+
var DESTRUCTIVE_DESC = /\b(permanently|irreversible|cannot be undone|destroy|clear,?\s*(copy,?\s*)?delete|wipe|purge)\b/i;
|
|
483
|
+
var FINANCIAL_DESC = /\b(process\s+payment|initiate\s+transfer|send\s+money|charge\s+customer|collect\s+payment)\b/i;
|
|
484
|
+
var READ_DESC = /\b(lists?|retrieves?|gets?|fetche?s?|search(?:es)?|shows?|views?|browses?|quer(?:y|ies)|reads?)\b/i;
|
|
485
|
+
var WRITE_DESC = /\b(create|add|update|modify|edit|configure|set|save|write|send|upload|install|manage|assign)\b/i;
|
|
486
|
+
var EXECUTE_DESC = /\b(run|execute|invoke|trigger|start|deploy|build|restart|scale)\b/i;
|
|
487
|
+
var DESTRUCTIVE_DESC_FALLBACK = /\b(delete|remove|destroy|drop|purge|wipe|erase|revoke|cancel|uninstall|reset|clear)\b/i;
|
|
488
|
+
var SEVERITY_MAP = {
|
|
489
|
+
Read: "Low",
|
|
490
|
+
Write: "Medium",
|
|
491
|
+
Execute: "Medium",
|
|
492
|
+
Destructive: "High",
|
|
493
|
+
Financial: "Critical",
|
|
494
|
+
Other: "Low"
|
|
495
|
+
};
|
|
496
|
+
var BASE_WEIGHT = {
|
|
497
|
+
Read: 0.1,
|
|
498
|
+
Other: 0.15,
|
|
499
|
+
Write: 0.3,
|
|
500
|
+
Execute: 0.5,
|
|
501
|
+
Destructive: 0.7,
|
|
502
|
+
Financial: 0.85
|
|
503
|
+
};
|
|
504
|
+
function classifyTool2(input) {
|
|
505
|
+
const { name, description, inputSchema, serverContext } = input;
|
|
506
|
+
const signals = [];
|
|
507
|
+
const { category, confidence } = categorise(name, description, serverContext);
|
|
508
|
+
const schemaSignals = inputSchema ? (0, schema_1.analyseSchema)(inputSchema) : [];
|
|
509
|
+
signals.push(...schemaSignals);
|
|
510
|
+
const blastSignals = (0, blast_radius_1.analyseBlastRadius)(name, description);
|
|
511
|
+
signals.push(...blastSignals);
|
|
512
|
+
let riskWeight = BASE_WEIGHT[category];
|
|
513
|
+
const schemaModifier = Math.min(0.3, schemaSignals.reduce((sum, s) => sum + s.weight, 0));
|
|
514
|
+
if (schemaModifier > 0)
|
|
515
|
+
riskWeight += schemaModifier;
|
|
516
|
+
const blastModifier = blastSignals.reduce((sum, s) => sum + s.weight, 0);
|
|
517
|
+
riskWeight += blastModifier;
|
|
518
|
+
if (confidence === "low") {
|
|
519
|
+
riskWeight *= 0.8;
|
|
520
|
+
}
|
|
521
|
+
riskWeight = Math.max(0, Math.min(1, riskWeight));
|
|
522
|
+
riskWeight = Math.round(riskWeight * 100) / 100;
|
|
523
|
+
const severity = deriveSeverity(category, riskWeight);
|
|
524
|
+
return { category, severity, confidence, riskWeight, signals };
|
|
525
|
+
}
|
|
526
|
+
function categorise(name, description, serverContext) {
|
|
527
|
+
const words = (0, verbs_1.splitName)(name);
|
|
528
|
+
if (FINANCIAL_COMPOUND.test(name)) {
|
|
529
|
+
return { category: "Financial", confidence: "high" };
|
|
530
|
+
}
|
|
531
|
+
for (const word of words) {
|
|
532
|
+
if (FINANCIAL_VERBS_SEGMENT.test(word)) {
|
|
533
|
+
if (!(0, verbs_1.firstVerbIsRead)(words)) {
|
|
534
|
+
return { category: "Financial", confidence: "high" };
|
|
535
|
+
}
|
|
536
|
+
break;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
if (DESTRUCTIVE_DESC.test(description)) {
|
|
540
|
+
return { category: "Destructive", confidence: "medium" };
|
|
541
|
+
}
|
|
542
|
+
if (FINANCIAL_DESC.test(description)) {
|
|
543
|
+
return { category: "Financial", confidence: "medium" };
|
|
544
|
+
}
|
|
545
|
+
const verbCat = (0, verbs_1.findFirstVerb)(words);
|
|
546
|
+
if (verbCat) {
|
|
547
|
+
return { category: verbCat, confidence: "high" };
|
|
548
|
+
}
|
|
549
|
+
if (DESTRUCTIVE_DESC_FALLBACK.test(description)) {
|
|
550
|
+
return { category: "Destructive", confidence: "medium" };
|
|
551
|
+
}
|
|
552
|
+
if (EXECUTE_DESC.test(description)) {
|
|
553
|
+
return { category: "Execute", confidence: "medium" };
|
|
554
|
+
}
|
|
555
|
+
if (WRITE_DESC.test(description)) {
|
|
556
|
+
return { category: "Write", confidence: "medium" };
|
|
557
|
+
}
|
|
558
|
+
if (READ_DESC.test(description)) {
|
|
559
|
+
return { category: "Read", confidence: "medium" };
|
|
560
|
+
}
|
|
561
|
+
if (serverContext && serverContext.toolCount >= 3) {
|
|
562
|
+
const safe = serverContext.dominantCategory !== "Financial" && serverContext.dominantCategory !== "Destructive";
|
|
563
|
+
if (safe) {
|
|
564
|
+
return { category: serverContext.dominantCategory, confidence: "low" };
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
return { category: "Other", confidence: "low" };
|
|
568
|
+
}
|
|
569
|
+
function deriveSeverity(category, riskWeight) {
|
|
570
|
+
if (riskWeight >= 0.85)
|
|
571
|
+
return "Critical";
|
|
572
|
+
if (riskWeight >= 0.6)
|
|
573
|
+
return "High";
|
|
574
|
+
if (riskWeight >= 0.35)
|
|
575
|
+
return "Medium";
|
|
576
|
+
return SEVERITY_MAP[category];
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
// ../classifier/dist/index.js
|
|
582
|
+
var require_dist = __commonJS({
|
|
583
|
+
"../classifier/dist/index.js"(exports) {
|
|
584
|
+
"use strict";
|
|
585
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
586
|
+
exports.classifyTool = void 0;
|
|
587
|
+
var classify_1 = require_classify();
|
|
588
|
+
Object.defineProperty(exports, "classifyTool", { enumerable: true, get: function() {
|
|
589
|
+
return classify_1.classifyTool;
|
|
590
|
+
} });
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
// src/index.ts
|
|
595
|
+
import { Command } from "commander";
|
|
596
|
+
import { join as join2 } from "path";
|
|
597
|
+
|
|
598
|
+
// src/discover.ts
|
|
599
|
+
import { existsSync, readFileSync } from "fs";
|
|
600
|
+
import { join } from "path";
|
|
601
|
+
import { homedir } from "os";
|
|
602
|
+
import { parse as parseToml } from "smol-toml";
|
|
603
|
+
function buildDescriptors(cwd) {
|
|
604
|
+
return [
|
|
605
|
+
{ path: join(cwd, ".mcp.json"), source: ".mcp.json (project)", type: "json-mcp" },
|
|
606
|
+
{ path: join(cwd, ".cursor/mcp.json"), source: ".cursor/mcp.json (project)", type: "json-mcp" },
|
|
607
|
+
{ path: join(cwd, ".vscode/settings.json"), source: "VS Code (project)", type: "json-vscode" },
|
|
608
|
+
{ path: join(cwd, ".codex/config.toml"), source: ".codex/config.toml (project)", type: "toml-codex" },
|
|
609
|
+
{ path: join(homedir(), ".claude.json"), source: "~/.claude.json (user)", type: "json-mcp" },
|
|
610
|
+
{ path: join(homedir(), "Library/Application Support/Claude/claude_desktop_config.json"), source: "Claude Desktop (user)", type: "json-mcp" },
|
|
611
|
+
{ path: join(homedir(), ".codeium/windsurf/mcp_config.json"), source: "Windsurf (user)", type: "json-mcp" },
|
|
612
|
+
{ path: join(homedir(), ".codex/config.toml"), source: "~/.codex/config.toml (user)", type: "toml-codex" }
|
|
613
|
+
];
|
|
614
|
+
}
|
|
615
|
+
function discoverConfigs(cwd) {
|
|
616
|
+
const results = [];
|
|
617
|
+
for (const { path, source, type } of buildDescriptors(cwd)) {
|
|
618
|
+
if (!existsSync(path)) continue;
|
|
619
|
+
try {
|
|
620
|
+
const content = readFileSync(path, "utf-8");
|
|
621
|
+
let servers = null;
|
|
622
|
+
if (type === "json-mcp") {
|
|
623
|
+
const raw = JSON.parse(content);
|
|
624
|
+
servers = raw.mcpServers || {};
|
|
625
|
+
} else if (type === "json-vscode") {
|
|
626
|
+
const raw = JSON.parse(content);
|
|
627
|
+
if (!raw.mcp) continue;
|
|
628
|
+
servers = raw.mcp.servers || {};
|
|
629
|
+
} else if (type === "toml-codex") {
|
|
630
|
+
const raw = parseToml(content);
|
|
631
|
+
servers = raw.mcp_servers || {};
|
|
632
|
+
}
|
|
633
|
+
if (!servers || Object.keys(servers).length === 0) continue;
|
|
634
|
+
results.push({ source, path, servers });
|
|
635
|
+
} catch {
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return results;
|
|
639
|
+
}
|
|
640
|
+
function mergeConfigs(results) {
|
|
641
|
+
const merged = {};
|
|
642
|
+
const userConfigs = results.filter((r) => r.source.includes("user"));
|
|
643
|
+
const projectConfigs = results.filter((r) => !r.source.includes("user"));
|
|
644
|
+
for (const c of userConfigs) Object.assign(merged, c.servers);
|
|
645
|
+
for (const c of projectConfigs) Object.assign(merged, c.servers);
|
|
646
|
+
return merged;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// src/live-scan.ts
|
|
650
|
+
import { spawn } from "child_process";
|
|
651
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
652
|
+
async function liveScan(command, args, timeoutMs = DEFAULT_TIMEOUT_MS, env) {
|
|
653
|
+
return new Promise((resolve, reject) => {
|
|
654
|
+
const child = spawn(command, args, {
|
|
655
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
656
|
+
env: {
|
|
657
|
+
...process.env,
|
|
658
|
+
...env,
|
|
659
|
+
NODE_NO_WARNINGS: "1",
|
|
660
|
+
BROWSER: "none",
|
|
661
|
+
NO_BROWSER: "1"
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
let nextId = 1;
|
|
665
|
+
const pending = /* @__PURE__ */ new Map();
|
|
666
|
+
let buffer = "";
|
|
667
|
+
let settled = false;
|
|
668
|
+
const timer = setTimeout(() => {
|
|
669
|
+
if (!settled) {
|
|
670
|
+
settled = true;
|
|
671
|
+
cleanup();
|
|
672
|
+
reject(new Error(`Timed out after ${timeoutMs}ms`));
|
|
673
|
+
}
|
|
674
|
+
}, timeoutMs);
|
|
675
|
+
function cleanup() {
|
|
676
|
+
clearTimeout(timer);
|
|
677
|
+
try {
|
|
678
|
+
child.stdin.end();
|
|
679
|
+
} catch {
|
|
680
|
+
}
|
|
681
|
+
try {
|
|
682
|
+
child.kill("SIGKILL");
|
|
683
|
+
} catch {
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
function settle(fn) {
|
|
687
|
+
if (!settled) {
|
|
688
|
+
settled = true;
|
|
689
|
+
cleanup();
|
|
690
|
+
fn();
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
child.stdout.on("data", (chunk) => {
|
|
694
|
+
buffer += chunk.toString();
|
|
695
|
+
const lines = buffer.split("\n");
|
|
696
|
+
buffer = lines.pop() || "";
|
|
697
|
+
for (const line of lines) {
|
|
698
|
+
const trimmed = line.trim();
|
|
699
|
+
if (!trimmed || !trimmed.startsWith("{")) continue;
|
|
700
|
+
try {
|
|
701
|
+
const resp = JSON.parse(trimmed);
|
|
702
|
+
if (resp.id && pending.has(resp.id)) {
|
|
703
|
+
const p = pending.get(resp.id);
|
|
704
|
+
pending.delete(resp.id);
|
|
705
|
+
p.resolve(resp);
|
|
706
|
+
}
|
|
707
|
+
} catch {
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
});
|
|
711
|
+
child.on("error", (err) => {
|
|
712
|
+
settle(() => reject(new Error(`Failed to spawn ${command}: ${err.message}`)));
|
|
713
|
+
});
|
|
714
|
+
child.on("exit", (code) => {
|
|
715
|
+
settle(() => reject(new Error(`${command} exited with code ${code} before discovery completed`)));
|
|
716
|
+
});
|
|
717
|
+
function sendRequest(method, params) {
|
|
718
|
+
return new Promise((res, rej) => {
|
|
719
|
+
const id = nextId++;
|
|
720
|
+
pending.set(id, { resolve: res, reject: rej });
|
|
721
|
+
const msg = JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n";
|
|
722
|
+
child.stdin.write(msg);
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
function sendNotification(method) {
|
|
726
|
+
const msg = JSON.stringify({ jsonrpc: "2.0", method }) + "\n";
|
|
727
|
+
child.stdin.write(msg);
|
|
728
|
+
}
|
|
729
|
+
(async () => {
|
|
730
|
+
const initResp = await sendRequest("initialize", {
|
|
731
|
+
protocolVersion: "2024-11-05",
|
|
732
|
+
capabilities: {},
|
|
733
|
+
clientInfo: { name: "policylayer-scan", version: "0.1.0" }
|
|
734
|
+
});
|
|
735
|
+
if (initResp.error) {
|
|
736
|
+
throw new Error(`Initialize failed: ${initResp.error.message}`);
|
|
737
|
+
}
|
|
738
|
+
sendNotification("notifications/initialized");
|
|
739
|
+
const allTools = [];
|
|
740
|
+
let cursor;
|
|
741
|
+
let pages = 0;
|
|
742
|
+
do {
|
|
743
|
+
const params = {};
|
|
744
|
+
if (cursor) params.cursor = cursor;
|
|
745
|
+
const resp = await sendRequest("tools/list", params);
|
|
746
|
+
if (resp.error) {
|
|
747
|
+
throw new Error(`tools/list failed: ${resp.error.message}`);
|
|
748
|
+
}
|
|
749
|
+
const result = resp.result;
|
|
750
|
+
if (result.tools) allTools.push(...result.tools);
|
|
751
|
+
cursor = result.nextCursor || void 0;
|
|
752
|
+
pages++;
|
|
753
|
+
} while (cursor && pages < 100);
|
|
754
|
+
settle(() => resolve(allTools));
|
|
755
|
+
})().catch((err) => {
|
|
756
|
+
settle(() => reject(err));
|
|
757
|
+
});
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// src/classify.ts
|
|
762
|
+
var import_classifier = __toESM(require_dist(), 1);
|
|
763
|
+
var API_BASE = process.env.POLICYLAYER_API_URL || "https://api.policylayer.com";
|
|
764
|
+
async function classifyViaApi(packages) {
|
|
765
|
+
try {
|
|
766
|
+
const res = await fetch(`${API_BASE}/api/scan/lookup`, {
|
|
767
|
+
method: "POST",
|
|
768
|
+
headers: { "Content-Type": "application/json" },
|
|
769
|
+
body: JSON.stringify({ packages }),
|
|
770
|
+
signal: AbortSignal.timeout(1e4)
|
|
771
|
+
});
|
|
772
|
+
if (!res.ok) return null;
|
|
773
|
+
return await res.json();
|
|
774
|
+
} catch {
|
|
775
|
+
return null;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
function classifyLocally(serverName, pkg, tools) {
|
|
779
|
+
return {
|
|
780
|
+
name: serverName,
|
|
781
|
+
package: pkg,
|
|
782
|
+
source: "local",
|
|
783
|
+
tools: tools.map((t) => {
|
|
784
|
+
const result = (0, import_classifier.classifyTool)({
|
|
785
|
+
name: t.name,
|
|
786
|
+
description: t.description || "",
|
|
787
|
+
inputSchema: t.inputSchema
|
|
788
|
+
});
|
|
789
|
+
const signalsSummary = result.signals.length > 0 ? result.signals.map((s) => s.signal).join("; ") : null;
|
|
790
|
+
return {
|
|
791
|
+
name: t.name,
|
|
792
|
+
description: t.description || "",
|
|
793
|
+
category: result.category,
|
|
794
|
+
severity: result.severity,
|
|
795
|
+
riskNote: signalsSummary,
|
|
796
|
+
riskWeight: result.riskWeight,
|
|
797
|
+
riskType: null,
|
|
798
|
+
confidence: result.confidence,
|
|
799
|
+
inputSchema: t.inputSchema
|
|
800
|
+
};
|
|
801
|
+
})
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// src/policy.ts
|
|
806
|
+
import { writeFileSync } from "fs";
|
|
807
|
+
function generatePolicyYaml(servers) {
|
|
808
|
+
let yaml = 'version: "1"\ndefault: deny\n\ntools:\n';
|
|
809
|
+
for (const server of servers) {
|
|
810
|
+
yaml += ` # ${server.name} (${server.tools.length} tools)
|
|
811
|
+
`;
|
|
812
|
+
for (const tool of server.tools) {
|
|
813
|
+
switch (tool.category) {
|
|
814
|
+
case "Read":
|
|
815
|
+
yaml += ` ${tool.name}:
|
|
816
|
+
rules:
|
|
817
|
+
- action: allow
|
|
818
|
+
rate_limit: 60/minute
|
|
819
|
+
|
|
820
|
+
`;
|
|
821
|
+
break;
|
|
822
|
+
case "Write":
|
|
823
|
+
case "Execute":
|
|
824
|
+
yaml += ` ${tool.name}:
|
|
825
|
+
rules:
|
|
826
|
+
- action: allow
|
|
827
|
+
rate_limit: 10/hour
|
|
828
|
+
|
|
829
|
+
`;
|
|
830
|
+
break;
|
|
831
|
+
case "Financial":
|
|
832
|
+
yaml += ` ${tool.name}:
|
|
833
|
+
rules:
|
|
834
|
+
- action: deny
|
|
835
|
+
on_deny: "Financial operation blocked by policy"
|
|
836
|
+
|
|
837
|
+
`;
|
|
838
|
+
break;
|
|
839
|
+
case "Destructive":
|
|
840
|
+
yaml += ` ${tool.name}:
|
|
841
|
+
rules:
|
|
842
|
+
- action: deny
|
|
843
|
+
on_deny: "Destructive operation blocked by policy"
|
|
844
|
+
|
|
845
|
+
`;
|
|
846
|
+
break;
|
|
847
|
+
default:
|
|
848
|
+
yaml += ` ${tool.name}:
|
|
849
|
+
rules:
|
|
850
|
+
- action: allow
|
|
851
|
+
rate_limit: 30/minute
|
|
852
|
+
|
|
853
|
+
`;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
return yaml.trim();
|
|
858
|
+
}
|
|
859
|
+
function writePolicyFile(path, servers) {
|
|
860
|
+
const yaml = generatePolicyYaml(servers);
|
|
861
|
+
writeFileSync(path, yaml + "\n");
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// src/report.ts
|
|
865
|
+
var API_BASE2 = process.env.POLICYLAYER_API_URL || "https://api.policylayer.com";
|
|
866
|
+
function buildSummary(servers) {
|
|
867
|
+
const categories = {};
|
|
868
|
+
const severities = {};
|
|
869
|
+
let totalTools = 0;
|
|
870
|
+
for (const s of servers) {
|
|
871
|
+
for (const t of s.tools) {
|
|
872
|
+
totalTools++;
|
|
873
|
+
categories[t.category] = (categories[t.category] || 0) + 1;
|
|
874
|
+
severities[t.severity] = (severities[t.severity] || 0) + 1;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
return { totalServers: servers.length, totalTools, categories, severities };
|
|
878
|
+
}
|
|
879
|
+
async function submitReport(servers, policy) {
|
|
880
|
+
const summary = buildSummary(servers);
|
|
881
|
+
const unknownTools = servers.filter((s) => s.source === "local").flatMap(
|
|
882
|
+
(s) => s.tools.map((t) => ({
|
|
883
|
+
serverName: s.name,
|
|
884
|
+
serverPackage: s.package,
|
|
885
|
+
toolName: t.name,
|
|
886
|
+
toolDescription: t.description,
|
|
887
|
+
inputSchema: t.inputSchema || null,
|
|
888
|
+
suggestedCategory: t.category
|
|
889
|
+
}))
|
|
890
|
+
);
|
|
891
|
+
try {
|
|
892
|
+
const res = await fetch(`${API_BASE2}/api/scan/submit`, {
|
|
893
|
+
method: "POST",
|
|
894
|
+
headers: { "Content-Type": "application/json" },
|
|
895
|
+
body: JSON.stringify({
|
|
896
|
+
source: "cli",
|
|
897
|
+
summary,
|
|
898
|
+
servers: servers.map((s) => ({
|
|
899
|
+
name: s.name,
|
|
900
|
+
package: s.package,
|
|
901
|
+
toolCount: s.tools.length,
|
|
902
|
+
tools: s.tools
|
|
903
|
+
})),
|
|
904
|
+
policy,
|
|
905
|
+
unknownTools: unknownTools.length > 0 ? unknownTools : void 0
|
|
906
|
+
}),
|
|
907
|
+
signal: AbortSignal.timeout(1e4)
|
|
908
|
+
});
|
|
909
|
+
if (!res.ok) return null;
|
|
910
|
+
const liveServers = servers.filter((s) => s.source === "local" && s.tools.length > 0);
|
|
911
|
+
if (liveServers.length > 0) {
|
|
912
|
+
contributeTools(liveServers).catch(() => {
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
return await res.json();
|
|
916
|
+
} catch {
|
|
917
|
+
return null;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
async function contributeTools(servers) {
|
|
921
|
+
await fetch(`${API_BASE2}/api/scan/contribute`, {
|
|
922
|
+
method: "POST",
|
|
923
|
+
headers: { "Content-Type": "application/json" },
|
|
924
|
+
body: JSON.stringify({
|
|
925
|
+
servers: servers.map((s) => ({
|
|
926
|
+
package: s.package,
|
|
927
|
+
tools: s.tools.map((t) => ({
|
|
928
|
+
name: t.name,
|
|
929
|
+
description: t.description,
|
|
930
|
+
inputSchema: t.inputSchema
|
|
931
|
+
}))
|
|
932
|
+
}))
|
|
933
|
+
}),
|
|
934
|
+
signal: AbortSignal.timeout(1e4)
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// src/index.ts
|
|
939
|
+
var program = new Command();
|
|
940
|
+
program.name("policylayer").description("Scan your MCP servers for security risks").version("0.1.0").option("-d, --dir <path>", "directory to scan for config files", process.cwd()).option("-o, --output <path>", "output path for policy YAML", "policylayer.yaml").option("--no-live", "skip live scanning, classify from config only").option("--no-report", "skip submitting report to PolicyLayer").option("--timeout <ms>", "timeout per server in milliseconds", "30000").option("--json", "output results as JSON").action(run);
|
|
941
|
+
async function run(opts) {
|
|
942
|
+
const cwd = opts.dir;
|
|
943
|
+
const timeoutMs = parseInt(opts.timeout, 10);
|
|
944
|
+
if (!opts.json) console.log("Discovering MCP config files...");
|
|
945
|
+
const configs = discoverConfigs(cwd);
|
|
946
|
+
if (configs.length === 0) {
|
|
947
|
+
if (opts.json) {
|
|
948
|
+
console.log(JSON.stringify({ error: "No MCP config files found", configs: [] }));
|
|
949
|
+
} else {
|
|
950
|
+
console.log("No MCP config files found. Checked:");
|
|
951
|
+
console.log(" .mcp.json, .cursor/mcp.json, .vscode/settings.json, .codex/config.toml");
|
|
952
|
+
console.log(" ~/.claude.json, Claude Desktop, Windsurf, ~/.codex/config.toml");
|
|
953
|
+
}
|
|
954
|
+
process.exit(0);
|
|
955
|
+
}
|
|
956
|
+
if (!opts.json) {
|
|
957
|
+
console.log(`Found ${configs.length} config file(s):`);
|
|
958
|
+
for (const c of configs) {
|
|
959
|
+
console.log(` ${c.source} \u2014 ${Object.keys(c.servers).length} server(s)`);
|
|
960
|
+
}
|
|
961
|
+
console.log();
|
|
962
|
+
}
|
|
963
|
+
const merged = mergeConfigs(configs);
|
|
964
|
+
const serverNames = Object.keys(merged);
|
|
965
|
+
if (!opts.json) {
|
|
966
|
+
console.log(`${serverNames.length} unique server(s) across all configs`);
|
|
967
|
+
console.log();
|
|
968
|
+
}
|
|
969
|
+
const liveResults = /* @__PURE__ */ new Map();
|
|
970
|
+
if (opts.live) {
|
|
971
|
+
if (!opts.json) console.log("Live scanning servers...");
|
|
972
|
+
for (const [name, entry] of Object.entries(merged)) {
|
|
973
|
+
const { command, args } = resolveCommand(entry);
|
|
974
|
+
if (!command) {
|
|
975
|
+
if (!opts.json) console.log(` ${name}: skipped (no command/url)`);
|
|
976
|
+
continue;
|
|
977
|
+
}
|
|
978
|
+
try {
|
|
979
|
+
if (!opts.json) process.stdout.write(` ${name}: scanning...`);
|
|
980
|
+
const tools = await liveScan(command, args, timeoutMs, entry.env);
|
|
981
|
+
liveResults.set(name, tools);
|
|
982
|
+
if (!opts.json) console.log(`\r ${name}: ${tools.length} tool(s) discovered`);
|
|
983
|
+
} catch (err) {
|
|
984
|
+
if (!opts.json) console.log(`\r ${name}: failed (${err.message})`);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
if (!opts.json) console.log();
|
|
988
|
+
}
|
|
989
|
+
const classified = [];
|
|
990
|
+
const lookupKeys = [];
|
|
991
|
+
const keyToName = /* @__PURE__ */ new Map();
|
|
992
|
+
for (const name of serverNames) {
|
|
993
|
+
const keys = buildLookupKeys(name, merged[name]);
|
|
994
|
+
for (const key of keys) {
|
|
995
|
+
lookupKeys.push(key);
|
|
996
|
+
keyToName.set(key, name);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
const uniqueKeys = [...new Set(lookupKeys)];
|
|
1000
|
+
const apiResult = uniqueKeys.length > 0 ? await classifyViaApi(uniqueKeys) : null;
|
|
1001
|
+
const matchedByPkg = /* @__PURE__ */ new Map();
|
|
1002
|
+
if (apiResult) {
|
|
1003
|
+
for (const m of apiResult.matched) {
|
|
1004
|
+
matchedByPkg.set(m.slug, m);
|
|
1005
|
+
matchedByPkg.set(m.name, m);
|
|
1006
|
+
for (const pkg of m.packages) {
|
|
1007
|
+
matchedByPkg.set(pkg, m);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
for (const [name, entry] of Object.entries(merged)) {
|
|
1012
|
+
const pkg = resolvePackage(name, entry) || name;
|
|
1013
|
+
const liveTools = liveResults.get(name);
|
|
1014
|
+
const apiMatch = matchedByPkg.get(pkg) || matchedByPkg.get(name);
|
|
1015
|
+
if (apiMatch && !liveTools) {
|
|
1016
|
+
classified.push({
|
|
1017
|
+
name: apiMatch.name,
|
|
1018
|
+
package: pkg,
|
|
1019
|
+
source: "api",
|
|
1020
|
+
tools: apiMatch.tools.map((t) => ({
|
|
1021
|
+
name: t.name,
|
|
1022
|
+
description: t.description || "",
|
|
1023
|
+
category: t.category,
|
|
1024
|
+
severity: t.severity,
|
|
1025
|
+
riskNote: t.riskNote,
|
|
1026
|
+
riskWeight: t.riskWeight,
|
|
1027
|
+
riskType: t.riskType,
|
|
1028
|
+
confidence: t.confidence
|
|
1029
|
+
}))
|
|
1030
|
+
});
|
|
1031
|
+
} else if (liveTools && liveTools.length > 0) {
|
|
1032
|
+
classified.push(classifyLocally(name, pkg, liveTools));
|
|
1033
|
+
} else {
|
|
1034
|
+
if (!opts.json) console.log(` ${name}: no tools found, skipping`);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
if (classified.length === 0) {
|
|
1038
|
+
if (opts.json) {
|
|
1039
|
+
console.log(JSON.stringify({ error: "No tools discovered", servers: 0 }));
|
|
1040
|
+
} else {
|
|
1041
|
+
console.log("No tools discovered from any server.");
|
|
1042
|
+
}
|
|
1043
|
+
process.exit(0);
|
|
1044
|
+
}
|
|
1045
|
+
const policyYaml = generatePolicyYaml(classified);
|
|
1046
|
+
const outputPath = join2(cwd, opts.output);
|
|
1047
|
+
writePolicyFile(outputPath, classified);
|
|
1048
|
+
let reportResult = null;
|
|
1049
|
+
if (opts.report) {
|
|
1050
|
+
reportResult = await submitReport(classified, policyYaml);
|
|
1051
|
+
}
|
|
1052
|
+
const totalTools = classified.reduce((sum, s) => sum + s.tools.length, 0);
|
|
1053
|
+
const categories = {};
|
|
1054
|
+
for (const s of classified) {
|
|
1055
|
+
for (const t of s.tools) {
|
|
1056
|
+
categories[t.category] = (categories[t.category] || 0) + 1;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
if (opts.json) {
|
|
1060
|
+
console.log(JSON.stringify({
|
|
1061
|
+
servers: classified.length,
|
|
1062
|
+
tools: totalTools,
|
|
1063
|
+
categories,
|
|
1064
|
+
policyFile: outputPath,
|
|
1065
|
+
reportUrl: reportResult?.url || null,
|
|
1066
|
+
reportId: reportResult?.id || null,
|
|
1067
|
+
results: classified
|
|
1068
|
+
}, null, 2));
|
|
1069
|
+
} else {
|
|
1070
|
+
console.log("Scan complete.");
|
|
1071
|
+
console.log();
|
|
1072
|
+
console.log(` Servers: ${classified.length}`);
|
|
1073
|
+
console.log(` Tools: ${totalTools}`);
|
|
1074
|
+
console.log(` Categories: ${Object.entries(categories).map(([k, v]) => `${k}(${v})`).join(" ")}`);
|
|
1075
|
+
console.log();
|
|
1076
|
+
console.log(` Policy: ${outputPath}`);
|
|
1077
|
+
if (reportResult) {
|
|
1078
|
+
console.log(` Report: ${reportResult.url}`);
|
|
1079
|
+
}
|
|
1080
|
+
console.log();
|
|
1081
|
+
const highRisk = classified.flatMap(
|
|
1082
|
+
(s) => s.tools.filter((t) => t.category === "Financial" || t.category === "Destructive")
|
|
1083
|
+
);
|
|
1084
|
+
if (highRisk.length > 0) {
|
|
1085
|
+
console.log(` ${highRisk.length} high-risk tool(s) detected:`);
|
|
1086
|
+
for (const t of highRisk.slice(0, 10)) {
|
|
1087
|
+
console.log(` - ${t.name} (${t.category})`);
|
|
1088
|
+
}
|
|
1089
|
+
if (highRisk.length > 10) {
|
|
1090
|
+
console.log(` ... and ${highRisk.length - 10} more`);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
function resolveCommand(entry) {
|
|
1096
|
+
if (entry.command) {
|
|
1097
|
+
return { command: entry.command, args: (entry.args || []).map(String) };
|
|
1098
|
+
}
|
|
1099
|
+
return { command: "", args: [] };
|
|
1100
|
+
}
|
|
1101
|
+
function resolvePackage(name, entry) {
|
|
1102
|
+
if (entry.command === "npx") {
|
|
1103
|
+
const args = (entry.args || []).map(String);
|
|
1104
|
+
for (const arg of args) {
|
|
1105
|
+
if (!arg.startsWith("-")) return arg;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
if (entry.args) {
|
|
1109
|
+
for (const arg of entry.args) {
|
|
1110
|
+
const s = String(arg);
|
|
1111
|
+
if (s.startsWith("@") && s.includes("/")) return s;
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
return null;
|
|
1115
|
+
}
|
|
1116
|
+
function buildLookupKeys(name, entry) {
|
|
1117
|
+
const keys = [];
|
|
1118
|
+
const pkg = resolvePackage(name, entry);
|
|
1119
|
+
if (pkg) keys.push(pkg);
|
|
1120
|
+
keys.push(name);
|
|
1121
|
+
const stripped = name.replace(/[-_]mcp[-_]server$/i, "").replace(/[-_]mcp$/i, "").replace(/[-_]server$/i, "");
|
|
1122
|
+
if (stripped !== name) keys.push(stripped);
|
|
1123
|
+
return [...new Set(keys)];
|
|
1124
|
+
}
|
|
1125
|
+
program.parse();
|
package/package.json
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "policylayer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Scan your MCP
|
|
5
|
+
"description": "Scan your MCP servers for security risks — live tool discovery + classification + shareable report",
|
|
6
6
|
"bin": {
|
|
7
7
|
"policylayer": "./dist/index.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
|
-
"dist"
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md"
|
|
11
12
|
],
|
|
12
13
|
"engines": {
|
|
13
14
|
"node": ">=20"
|
|
14
15
|
},
|
|
15
16
|
"scripts": {
|
|
16
17
|
"build": "tsup",
|
|
17
|
-
"test": "vitest run",
|
|
18
18
|
"prepublishOnly": "npm run build"
|
|
19
19
|
},
|
|
20
20
|
"keywords": [
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
"security",
|
|
23
23
|
"scan",
|
|
24
24
|
"claude",
|
|
25
|
-
"ai-agents"
|
|
25
|
+
"ai-agents",
|
|
26
|
+
"policylayer"
|
|
26
27
|
],
|
|
27
28
|
"license": "MIT",
|
|
28
29
|
"dependencies": {
|
|
@@ -30,9 +31,8 @@
|
|
|
30
31
|
"smol-toml": "^1.6.1"
|
|
31
32
|
},
|
|
32
33
|
"devDependencies": {
|
|
33
|
-
"@types/node": "^
|
|
34
|
+
"@types/node": "^22.0.0",
|
|
34
35
|
"tsup": "^8.5.1",
|
|
35
|
-
"typescript": "^5.9.3"
|
|
36
|
-
"vitest": "^4.1.0"
|
|
36
|
+
"typescript": "^5.9.3"
|
|
37
37
|
}
|
|
38
38
|
}
|