perimetercli 0.1.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/LICENSE +21 -0
- package/README.md +203 -0
- package/bin/perimeter.js +5 -0
- package/package.json +55 -0
- package/src/audit.js +164 -0
- package/src/catalog.js +335 -0
- package/src/cli.js +469 -0
- package/src/discover.js +262 -0
- package/src/guard.js +159 -0
- package/src/index.js +9 -0
- package/src/report.js +333 -0
- package/src/rules.js +357 -0
- package/src/serve.js +64 -0
- package/src/server.js +194 -0
- package/src/sessions.js +161 -0
- package/src/tokens.js +99 -0
- package/src/util.js +137 -0
- package/src/version.js +1 -0
package/src/catalog.js
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small, offline knowledge base of well-known MCP servers. Matching a package
|
|
3
|
+
* name against this catalog lets Perimeter report concrete capabilities and
|
|
4
|
+
* risk without talking to the network.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const servers = [
|
|
8
|
+
{
|
|
9
|
+
match: ["@modelcontextprotocol/server-filesystem", "server-filesystem"],
|
|
10
|
+
name: "Filesystem",
|
|
11
|
+
capabilities: ["filesystem-read", "filesystem-write"],
|
|
12
|
+
risk: "medium",
|
|
13
|
+
tools: 6,
|
|
14
|
+
tokens: 2600,
|
|
15
|
+
note: "Reads and writes files. Write access is worth reviewing; watch for paths outside the project.",
|
|
16
|
+
owasp: "AGENT-04",
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
match: ["@modelcontextprotocol/server-github", "server-github"],
|
|
20
|
+
name: "GitHub",
|
|
21
|
+
capabilities: ["network-in", "repo-write"],
|
|
22
|
+
risk: "medium",
|
|
23
|
+
tools: 12,
|
|
24
|
+
tokens: 5200,
|
|
25
|
+
note: "Can read and write issues, PRs and repo contents. Credentials are usually in env — ensure scoped tokens.",
|
|
26
|
+
owasp: "AGENT-04",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
match: ["@modelcontextprotocol/server-sqlite", "server-sqlite"],
|
|
30
|
+
name: "SQLite",
|
|
31
|
+
capabilities: ["db-write", "sql"],
|
|
32
|
+
risk: "medium",
|
|
33
|
+
tools: 3,
|
|
34
|
+
tokens: 3900,
|
|
35
|
+
note: "Executes SQL against a database. Ensure the agent only touches an allowed database file.",
|
|
36
|
+
owasp: "AGENT-05",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
match: ["@modelcontextprotocol/server-postgres", "server-postgres"],
|
|
40
|
+
name: "Postgres",
|
|
41
|
+
capabilities: ["db-write", "sql", "network-in"],
|
|
42
|
+
risk: "high",
|
|
43
|
+
tools: 8,
|
|
44
|
+
tokens: 4800,
|
|
45
|
+
note: "Remote database write capability. Highest-risk database class; enforce read-only or a dedicated schema.",
|
|
46
|
+
owasp: "AGENT-05",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
match: ["@modelcontextprotocol/server-brave-search", "server-brave-search"],
|
|
50
|
+
name: "Brave Search",
|
|
51
|
+
capabilities: ["network-in"],
|
|
52
|
+
risk: "low",
|
|
53
|
+
tools: 1,
|
|
54
|
+
tokens: 2100,
|
|
55
|
+
note: "Performs web searches. Network read only.",
|
|
56
|
+
owasp: "AGENT-02",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
match: ["@modelcontextprotocol/server-puppeteer", "server-puppeteer"],
|
|
60
|
+
name: "Puppeteer",
|
|
61
|
+
capabilities: ["browser", "network-in"],
|
|
62
|
+
risk: "low",
|
|
63
|
+
tools: 4,
|
|
64
|
+
tokens: 3400,
|
|
65
|
+
note: "Automates a browser. Can render pages and click; not a file or shell primitive.",
|
|
66
|
+
owasp: "AGENT-02",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
match: ["@modelcontextprotocol/server-memory", "server-memory"],
|
|
70
|
+
name: "Memory",
|
|
71
|
+
capabilities: ["memory"],
|
|
72
|
+
risk: "low",
|
|
73
|
+
tools: 3,
|
|
74
|
+
tokens: 1100,
|
|
75
|
+
note: "Stores and retrieves memory entries.",
|
|
76
|
+
owasp: "AGENT-03",
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
match: ["@modelcontextprotocol/server-everything", "server-everything"],
|
|
80
|
+
name: "Everything (demo)",
|
|
81
|
+
capabilities: ["filesystem-write", "code-exec", "shell"],
|
|
82
|
+
risk: "high",
|
|
83
|
+
tools: 17,
|
|
84
|
+
tokens: 8200,
|
|
85
|
+
note: "A demo server with broad capabilities. Treat as high risk; it purposely exercises many surfaces.",
|
|
86
|
+
owasp: "AGENT-05",
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
match: ["@modelcontextprotocol/server-sequential-thinking", "server-sequential-thinking"],
|
|
90
|
+
name: "Sequential Thinking",
|
|
91
|
+
capabilities: ["thinking"],
|
|
92
|
+
risk: "low",
|
|
93
|
+
tools: 1,
|
|
94
|
+
tokens: 1600,
|
|
95
|
+
note: "A structured reasoning tool. No privileged capability.",
|
|
96
|
+
owasp: "AGENT-03",
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
match: ["@modelcontextprotocol/server-aws", "server-aws", "@aws-sdk/mcp"],
|
|
100
|
+
name: "AWS",
|
|
101
|
+
capabilities: ["cloud-write", "network-out"],
|
|
102
|
+
risk: "high",
|
|
103
|
+
tools: 15,
|
|
104
|
+
tokens: 6000,
|
|
105
|
+
note: "Cloud resource manipulation. Require role-scoped credentials and a dry-run policy.",
|
|
106
|
+
owasp: "AGENT-05",
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
match: ["@modelcontextprotocol/server-slack", "server-slack"],
|
|
110
|
+
name: "Slack",
|
|
111
|
+
capabilities: ["network-out", "message-write"],
|
|
112
|
+
risk: "medium",
|
|
113
|
+
tools: 9,
|
|
114
|
+
tokens: 3600,
|
|
115
|
+
note: "Posts and reads messages. Can be used for outbound phishing or credential-adjacent content.",
|
|
116
|
+
owasp: "AGENT-02",
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
match: ["@modelcontextprotocol/server-gitlab", "server-gitlab"],
|
|
120
|
+
name: "GitLab",
|
|
121
|
+
capabilities: ["network-in", "repo-write"],
|
|
122
|
+
risk: "medium",
|
|
123
|
+
tools: 10,
|
|
124
|
+
tokens: 4800,
|
|
125
|
+
note: "Can read and write merge requests and repository contents. Scope the token.",
|
|
126
|
+
owasp: "AGENT-04",
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
match: ["@modelcontextprotocol/server-redis", "server-redis"],
|
|
130
|
+
name: "Redis",
|
|
131
|
+
capabilities: ["db-write"],
|
|
132
|
+
risk: "medium",
|
|
133
|
+
tools: 5,
|
|
134
|
+
tokens: 2200,
|
|
135
|
+
note: "Executes commands against a Redis server.",
|
|
136
|
+
owasp: "AGENT-05",
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
match: ["@modelcontextprotocol/server-mysql", "server-mysql"],
|
|
140
|
+
name: "MySQL",
|
|
141
|
+
capabilities: ["db-write", "sql", "network-in"],
|
|
142
|
+
risk: "high",
|
|
143
|
+
tools: 7,
|
|
144
|
+
tokens: 4400,
|
|
145
|
+
note: "Remote database write capability.",
|
|
146
|
+
owasp: "AGENT-05",
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
match: ["@modelcontextprotocol/server-kubernetes", "server-kubernetes", "k8s"],
|
|
150
|
+
name: "Kubernetes",
|
|
151
|
+
capabilities: ["cloud-write", "network-out"],
|
|
152
|
+
risk: "high",
|
|
153
|
+
tools: 12,
|
|
154
|
+
tokens: 5400,
|
|
155
|
+
note: "Can mutate cluster resources. Require a read-only or namespace-scoped role.",
|
|
156
|
+
owasp: "AGENT-05",
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
match: ["@modelcontextprotocol/server-fetch", "server-fetch", "@henrymemory/mcp-fetch"],
|
|
160
|
+
name: "Fetch",
|
|
161
|
+
capabilities: ["network-in"],
|
|
162
|
+
risk: "low",
|
|
163
|
+
tools: 1,
|
|
164
|
+
tokens: 1300,
|
|
165
|
+
note: "Fetches URLs. Network read only.",
|
|
166
|
+
owasp: "AGENT-02",
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
match: ["@modelcontextprotocol/server-jira", "server-jira"],
|
|
170
|
+
name: "Jira",
|
|
171
|
+
capabilities: ["network-in", "repo-write"],
|
|
172
|
+
risk: "medium",
|
|
173
|
+
tools: 8,
|
|
174
|
+
tokens: 4200,
|
|
175
|
+
note: "Reads and writes tickets. Scope the API token.",
|
|
176
|
+
owasp: "AGENT-04",
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
match: ["@modelcontextprotocol/server-linear", "server-linear"],
|
|
180
|
+
name: "Linear",
|
|
181
|
+
capabilities: ["network-in", "repo-write"],
|
|
182
|
+
risk: "medium",
|
|
183
|
+
tools: 6,
|
|
184
|
+
tokens: 3400,
|
|
185
|
+
note: "Reads and writes issues. Scope the API token.",
|
|
186
|
+
owasp: "AGENT-04",
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
match: ["@modelcontextprotocol/server-notion", "server-notion"],
|
|
190
|
+
name: "Notion",
|
|
191
|
+
capabilities: ["network-in", "filesystem-write"],
|
|
192
|
+
risk: "medium",
|
|
193
|
+
tools: 6,
|
|
194
|
+
tokens: 3500,
|
|
195
|
+
note: "Reads and writes Notion pages. Scope the integration token.",
|
|
196
|
+
owasp: "AGENT-04",
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
match: ["@modelcontextprotocol/server-sentry", "server-sentry"],
|
|
200
|
+
name: "Sentry",
|
|
201
|
+
capabilities: ["network-in"],
|
|
202
|
+
risk: "low",
|
|
203
|
+
tools: 5,
|
|
204
|
+
tokens: 2900,
|
|
205
|
+
note: "Reads issue/event data. Network read only.",
|
|
206
|
+
owasp: "AGENT-02",
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
match: ["@modelcontextprotocol/server-google-drive", "server-google-drive"],
|
|
210
|
+
name: "Google Drive",
|
|
211
|
+
capabilities: ["filesystem-write", "network-in"],
|
|
212
|
+
risk: "medium",
|
|
213
|
+
tools: 7,
|
|
214
|
+
tokens: 3800,
|
|
215
|
+
note: "Reads and writes Drive files. Scope to a specific folder.",
|
|
216
|
+
owasp: "AGENT-04",
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
match: ["@modelcontextprotocol/server-mongodb", "server-mongodb"],
|
|
220
|
+
name: "MongoDB",
|
|
221
|
+
capabilities: ["db-write", "network-in"],
|
|
222
|
+
risk: "high",
|
|
223
|
+
tools: 6,
|
|
224
|
+
tokens: 4100,
|
|
225
|
+
note: "Executes queries against MongoDB. Writes can mutate data.",
|
|
226
|
+
owasp: "AGENT-05",
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
match: ["@modelcontextprotocol/server-supabase", "server-supabase"],
|
|
230
|
+
name: "Supabase",
|
|
231
|
+
capabilities: ["db-write", "network-in"],
|
|
232
|
+
risk: "high",
|
|
233
|
+
tools: 8,
|
|
234
|
+
tokens: 4600,
|
|
235
|
+
note: "Database + storage writes. Scope to a read-only role.",
|
|
236
|
+
owasp: "AGENT-05",
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
match: ["@modelcontextprotocol/server-stripe", "server-stripe", "@stripe/mcp"],
|
|
240
|
+
name: "Stripe",
|
|
241
|
+
capabilities: ["payment", "network-in"],
|
|
242
|
+
risk: "high",
|
|
243
|
+
tools: 9,
|
|
244
|
+
tokens: 5000,
|
|
245
|
+
note: "Payment operation. Financial impact — require an approval gate.",
|
|
246
|
+
owasp: "AGENT-05",
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
match: ["@modelcontextprotocol/server-elasticsearch", "server-elasticsearch"],
|
|
250
|
+
name: "Elasticsearch",
|
|
251
|
+
capabilities: ["network-in", "db-write"],
|
|
252
|
+
risk: "medium",
|
|
253
|
+
tools: 5,
|
|
254
|
+
tokens: 3300,
|
|
255
|
+
note: "Queries and can index documents. Network + write.",
|
|
256
|
+
owasp: "AGENT-05",
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
match: ["@modelcontextprotocol/server-bitbucket", "server-bitbucket"],
|
|
260
|
+
name: "Bitbucket",
|
|
261
|
+
capabilities: ["network-in", "repo-write"],
|
|
262
|
+
risk: "medium",
|
|
263
|
+
tools: 9,
|
|
264
|
+
tokens: 4500,
|
|
265
|
+
note: "Reads and writes repositories. Scope the token.",
|
|
266
|
+
owasp: "AGENT-04",
|
|
267
|
+
},
|
|
268
|
+
];
|
|
269
|
+
|
|
270
|
+
export const KNOWN_SERVERS = servers;
|
|
271
|
+
|
|
272
|
+
export function catalogFor(packageOrName) {
|
|
273
|
+
const key = String(packageOrName || "").toLowerCase();
|
|
274
|
+
for (const s of servers) {
|
|
275
|
+
for (const m of s.match) {
|
|
276
|
+
if (key.includes(m.toLowerCase()) || m.toLowerCase().includes(key)) {
|
|
277
|
+
return s;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** A default token/risk profile for an unknown server. */
|
|
285
|
+
export function unknownServerProfile(name) {
|
|
286
|
+
return {
|
|
287
|
+
name,
|
|
288
|
+
capabilities: [],
|
|
289
|
+
risk: "unknown",
|
|
290
|
+
tools: null,
|
|
291
|
+
tokens: 1500,
|
|
292
|
+
note: "Unrecognised server. Capabilities could not be verified offline.",
|
|
293
|
+
owasp: "AGENT-06",
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** A lightweight typosquat check against catalog names. */
|
|
298
|
+
export function looksTyposquatted(name) {
|
|
299
|
+
const key = String(name || "").toLowerCase();
|
|
300
|
+
const normalized = key
|
|
301
|
+
.replace(/^@/, "")
|
|
302
|
+
.replace(/^modelcontextprotocol\//, "")
|
|
303
|
+
.replace(/^server-/, "");
|
|
304
|
+
for (const s of servers) {
|
|
305
|
+
for (const m of s.match) {
|
|
306
|
+
const theirs = m
|
|
307
|
+
.replace(/^@/, "")
|
|
308
|
+
.replace(/^modelcontextprotocol\//, "")
|
|
309
|
+
.replace(/^server-/, "");
|
|
310
|
+
if (theirs === normalized) continue;
|
|
311
|
+
if (similarEnough(theirs, normalized)) return true;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function similarEnough(a, b) {
|
|
318
|
+
if (a.length !== b.length && Math.abs(a.length - b.length) > 1) return false;
|
|
319
|
+
// Levenshtein distance <= 1
|
|
320
|
+
let edits = 0;
|
|
321
|
+
const n = a.length;
|
|
322
|
+
const m = b.length;
|
|
323
|
+
const dp = Array.from({ length: n + 1 }, (_, i) =>
|
|
324
|
+
Array.from({ length: m + 1 }, (_, j) => (j === 0 ? i : i === 0 ? j : 0))
|
|
325
|
+
);
|
|
326
|
+
for (let i = 0; i <= n; i++) dp[i][0] = i;
|
|
327
|
+
for (let j = 0; j <= m; j++) dp[0][j] = j;
|
|
328
|
+
for (let i = 1; i <= n; i++) {
|
|
329
|
+
for (let j = 1; j <= m; j++) {
|
|
330
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
331
|
+
dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return dp[n][m] <= 1;
|
|
335
|
+
}
|