@solongate/proxy 0.7.0 → 0.8.1
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/dist/create.js +3 -3
- package/dist/index.js +810 -333
- package/dist/init.js +9 -14
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -63,21 +63,48 @@ async function sendAuditLog(apiKey, apiUrl, entry) {
|
|
|
63
63
|
`);
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
+
function ensureCatchAllAllow(policy) {
|
|
67
|
+
const hasCatchAllAllow = policy.rules.some(
|
|
68
|
+
(r) => r.effect === "ALLOW" && r.toolPattern === "*" && r.enabled !== false
|
|
69
|
+
);
|
|
70
|
+
if (hasCatchAllAllow) return policy;
|
|
71
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
72
|
+
return {
|
|
73
|
+
...policy,
|
|
74
|
+
rules: [
|
|
75
|
+
...policy.rules,
|
|
76
|
+
{
|
|
77
|
+
id: "_solongate-catch-all-allow",
|
|
78
|
+
description: "Auto-added: allow everything not explicitly denied",
|
|
79
|
+
effect: "ALLOW",
|
|
80
|
+
priority: 9999,
|
|
81
|
+
toolPattern: "*",
|
|
82
|
+
minimumTrustLevel: "UNTRUSTED",
|
|
83
|
+
enabled: true,
|
|
84
|
+
createdAt: now,
|
|
85
|
+
updatedAt: now
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
};
|
|
89
|
+
}
|
|
66
90
|
function loadPolicy(source) {
|
|
67
|
-
|
|
68
|
-
if (
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
91
|
+
let policy;
|
|
92
|
+
if (typeof source === "object") {
|
|
93
|
+
policy = source;
|
|
94
|
+
} else {
|
|
95
|
+
const filePath = resolve(source);
|
|
96
|
+
if (existsSync(filePath)) {
|
|
97
|
+
const content = readFileSync(filePath, "utf-8");
|
|
98
|
+
policy = JSON.parse(content);
|
|
99
|
+
} else {
|
|
100
|
+
return DEFAULT_POLICY;
|
|
101
|
+
}
|
|
73
102
|
}
|
|
74
|
-
|
|
75
|
-
`Unknown policy "${source}". Use a preset (${Object.keys(PRESETS).join(", ")}), a JSON file path, or a PolicySet object.`
|
|
76
|
-
);
|
|
103
|
+
return ensureCatchAllAllow(policy);
|
|
77
104
|
}
|
|
78
105
|
function parseArgs(argv) {
|
|
79
106
|
const args = argv.slice(2);
|
|
80
|
-
let policySource
|
|
107
|
+
let policySource;
|
|
81
108
|
let name = "solongate-proxy";
|
|
82
109
|
let verbose = false;
|
|
83
110
|
let rateLimitPerTool;
|
|
@@ -159,7 +186,7 @@ function parseArgs(argv) {
|
|
|
159
186
|
"Invalid API key format. Keys must start with 'sg_live_' or 'sg_test_'.\nGet your API key at https://solongate.com\n"
|
|
160
187
|
);
|
|
161
188
|
}
|
|
162
|
-
const resolvedPolicyPath = resolvePolicyPath(policySource);
|
|
189
|
+
const resolvedPolicyPath = policySource ? resolvePolicyPath(policySource) : null;
|
|
163
190
|
if (configFile) {
|
|
164
191
|
const filePath = resolve(configFile);
|
|
165
192
|
const content = readFileSync(filePath, "utf-8");
|
|
@@ -167,7 +194,7 @@ function parseArgs(argv) {
|
|
|
167
194
|
if (!fileConfig.upstream) {
|
|
168
195
|
throw new Error('Config file must include "upstream" with at least "command" or "url"');
|
|
169
196
|
}
|
|
170
|
-
const cfgPolicySource = fileConfig.policy ?? policySource;
|
|
197
|
+
const cfgPolicySource = fileConfig.policy ?? policySource ?? "policy.json";
|
|
171
198
|
return {
|
|
172
199
|
upstream: fileConfig.upstream,
|
|
173
200
|
policy: loadPolicy(cfgPolicySource),
|
|
@@ -191,7 +218,7 @@ function parseArgs(argv) {
|
|
|
191
218
|
// not used for URL-based transports
|
|
192
219
|
url: upstreamUrl
|
|
193
220
|
},
|
|
194
|
-
policy: loadPolicy(policySource),
|
|
221
|
+
policy: loadPolicy(policySource ?? "policy.json"),
|
|
195
222
|
name,
|
|
196
223
|
verbose,
|
|
197
224
|
rateLimitPerTool,
|
|
@@ -205,7 +232,7 @@ function parseArgs(argv) {
|
|
|
205
232
|
}
|
|
206
233
|
if (upstreamArgs.length === 0) {
|
|
207
234
|
throw new Error(
|
|
208
|
-
"No upstream server command provided.\n\nUsage: solongate-proxy [options] -- <command> [args...]\n\nExamples:\n solongate-proxy -- node my-server.js\n solongate-proxy --policy
|
|
235
|
+
"No upstream server command provided.\n\nUsage: solongate-proxy [options] -- <command> [args...]\n\nExamples:\n solongate-proxy -- node my-server.js\n solongate-proxy --policy ./policy.json -- npx @playwright/mcp@latest\n solongate-proxy --upstream-url http://localhost:3001/mcp\n solongate-proxy --config solongate.json\n"
|
|
209
236
|
);
|
|
210
237
|
}
|
|
211
238
|
const [command, ...commandArgs] = upstreamArgs;
|
|
@@ -216,7 +243,7 @@ function parseArgs(argv) {
|
|
|
216
243
|
args: commandArgs,
|
|
217
244
|
env: { ...process.env }
|
|
218
245
|
},
|
|
219
|
-
policy: loadPolicy(policySource),
|
|
246
|
+
policy: loadPolicy(policySource ?? "policy.json"),
|
|
220
247
|
name,
|
|
221
248
|
verbose,
|
|
222
249
|
rateLimitPerTool,
|
|
@@ -229,303 +256,22 @@ function parseArgs(argv) {
|
|
|
229
256
|
};
|
|
230
257
|
}
|
|
231
258
|
function resolvePolicyPath(source) {
|
|
232
|
-
if (PRESETS[source]) return null;
|
|
233
259
|
const filePath = resolve(source);
|
|
234
260
|
if (existsSync(filePath)) return filePath;
|
|
235
261
|
return null;
|
|
236
262
|
}
|
|
237
|
-
var
|
|
263
|
+
var DEFAULT_POLICY;
|
|
238
264
|
var init_config = __esm({
|
|
239
265
|
"src/config.ts"() {
|
|
240
266
|
"use strict";
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
id: "deny-dangerous-commands",
|
|
250
|
-
description: "Block dangerous shell commands",
|
|
251
|
-
effect: "DENY",
|
|
252
|
-
priority: 50,
|
|
253
|
-
toolPattern: "*",
|
|
254
|
-
permission: "EXECUTE",
|
|
255
|
-
minimumTrustLevel: "UNTRUSTED",
|
|
256
|
-
enabled: true,
|
|
257
|
-
commandConstraints: {
|
|
258
|
-
denied: [
|
|
259
|
-
"rm -rf *",
|
|
260
|
-
"rm -r /*",
|
|
261
|
-
"mkfs*",
|
|
262
|
-
"dd if=*",
|
|
263
|
-
"curl*|*bash*",
|
|
264
|
-
"curl*|*sh*",
|
|
265
|
-
"wget*|*bash*",
|
|
266
|
-
"wget*|*sh*",
|
|
267
|
-
"shutdown*",
|
|
268
|
-
"reboot*",
|
|
269
|
-
"kill -9*",
|
|
270
|
-
"chmod*777*",
|
|
271
|
-
"iptables*",
|
|
272
|
-
"passwd*",
|
|
273
|
-
"useradd*",
|
|
274
|
-
"userdel*"
|
|
275
|
-
]
|
|
276
|
-
},
|
|
277
|
-
createdAt: "",
|
|
278
|
-
updatedAt: ""
|
|
279
|
-
},
|
|
280
|
-
{
|
|
281
|
-
id: "deny-sensitive-paths",
|
|
282
|
-
description: "Block access to sensitive files",
|
|
283
|
-
effect: "DENY",
|
|
284
|
-
priority: 51,
|
|
285
|
-
toolPattern: "*",
|
|
286
|
-
permission: "EXECUTE",
|
|
287
|
-
minimumTrustLevel: "UNTRUSTED",
|
|
288
|
-
enabled: true,
|
|
289
|
-
pathConstraints: {
|
|
290
|
-
denied: [
|
|
291
|
-
"**/.env*",
|
|
292
|
-
"**/.ssh/**",
|
|
293
|
-
"**/.aws/**",
|
|
294
|
-
"**/.kube/**",
|
|
295
|
-
"**/credentials*",
|
|
296
|
-
"**/secrets*",
|
|
297
|
-
"**/*.pem",
|
|
298
|
-
"**/*.key",
|
|
299
|
-
"/etc/passwd",
|
|
300
|
-
"/etc/shadow",
|
|
301
|
-
"/proc/**",
|
|
302
|
-
"/dev/**"
|
|
303
|
-
]
|
|
304
|
-
},
|
|
305
|
-
createdAt: "",
|
|
306
|
-
updatedAt: ""
|
|
307
|
-
},
|
|
308
|
-
{
|
|
309
|
-
id: "allow-rest",
|
|
310
|
-
description: "Allow all other tool calls",
|
|
311
|
-
effect: "ALLOW",
|
|
312
|
-
priority: 1e3,
|
|
313
|
-
toolPattern: "*",
|
|
314
|
-
permission: "EXECUTE",
|
|
315
|
-
minimumTrustLevel: "UNTRUSTED",
|
|
316
|
-
enabled: true,
|
|
317
|
-
createdAt: "",
|
|
318
|
-
updatedAt: ""
|
|
319
|
-
}
|
|
320
|
-
],
|
|
321
|
-
createdAt: "",
|
|
322
|
-
updatedAt: ""
|
|
323
|
-
},
|
|
324
|
-
"read-only": {
|
|
325
|
-
id: "read-only",
|
|
326
|
-
name: "Read Only",
|
|
327
|
-
description: "Only allows read/list/get/search/query tools",
|
|
328
|
-
version: 1,
|
|
329
|
-
rules: [
|
|
330
|
-
{
|
|
331
|
-
id: "allow-read",
|
|
332
|
-
description: "Allow read tools",
|
|
333
|
-
effect: "ALLOW",
|
|
334
|
-
priority: 100,
|
|
335
|
-
toolPattern: "*read*",
|
|
336
|
-
permission: "EXECUTE",
|
|
337
|
-
minimumTrustLevel: "UNTRUSTED",
|
|
338
|
-
enabled: true,
|
|
339
|
-
createdAt: "",
|
|
340
|
-
updatedAt: ""
|
|
341
|
-
},
|
|
342
|
-
{
|
|
343
|
-
id: "allow-list",
|
|
344
|
-
description: "Allow list tools",
|
|
345
|
-
effect: "ALLOW",
|
|
346
|
-
priority: 101,
|
|
347
|
-
toolPattern: "*list*",
|
|
348
|
-
permission: "EXECUTE",
|
|
349
|
-
minimumTrustLevel: "UNTRUSTED",
|
|
350
|
-
enabled: true,
|
|
351
|
-
createdAt: "",
|
|
352
|
-
updatedAt: ""
|
|
353
|
-
},
|
|
354
|
-
{
|
|
355
|
-
id: "allow-get",
|
|
356
|
-
description: "Allow get tools",
|
|
357
|
-
effect: "ALLOW",
|
|
358
|
-
priority: 102,
|
|
359
|
-
toolPattern: "*get*",
|
|
360
|
-
permission: "EXECUTE",
|
|
361
|
-
minimumTrustLevel: "UNTRUSTED",
|
|
362
|
-
enabled: true,
|
|
363
|
-
createdAt: "",
|
|
364
|
-
updatedAt: ""
|
|
365
|
-
},
|
|
366
|
-
{
|
|
367
|
-
id: "allow-search",
|
|
368
|
-
description: "Allow search tools",
|
|
369
|
-
effect: "ALLOW",
|
|
370
|
-
priority: 103,
|
|
371
|
-
toolPattern: "*search*",
|
|
372
|
-
permission: "EXECUTE",
|
|
373
|
-
minimumTrustLevel: "UNTRUSTED",
|
|
374
|
-
enabled: true,
|
|
375
|
-
createdAt: "",
|
|
376
|
-
updatedAt: ""
|
|
377
|
-
},
|
|
378
|
-
{
|
|
379
|
-
id: "allow-query",
|
|
380
|
-
description: "Allow query tools",
|
|
381
|
-
effect: "ALLOW",
|
|
382
|
-
priority: 104,
|
|
383
|
-
toolPattern: "*query*",
|
|
384
|
-
permission: "EXECUTE",
|
|
385
|
-
minimumTrustLevel: "UNTRUSTED",
|
|
386
|
-
enabled: true,
|
|
387
|
-
createdAt: "",
|
|
388
|
-
updatedAt: ""
|
|
389
|
-
}
|
|
390
|
-
],
|
|
391
|
-
createdAt: "",
|
|
392
|
-
updatedAt: ""
|
|
393
|
-
},
|
|
394
|
-
sandbox: {
|
|
395
|
-
id: "sandbox",
|
|
396
|
-
name: "Sandbox",
|
|
397
|
-
description: "Allows file ops within working directory only, blocks dangerous commands, allows safe shell commands",
|
|
398
|
-
version: 1,
|
|
399
|
-
rules: [
|
|
400
|
-
{
|
|
401
|
-
id: "deny-dangerous-commands",
|
|
402
|
-
description: "Block dangerous shell commands",
|
|
403
|
-
effect: "DENY",
|
|
404
|
-
priority: 50,
|
|
405
|
-
toolPattern: "*",
|
|
406
|
-
permission: "EXECUTE",
|
|
407
|
-
minimumTrustLevel: "UNTRUSTED",
|
|
408
|
-
enabled: true,
|
|
409
|
-
commandConstraints: {
|
|
410
|
-
denied: [
|
|
411
|
-
"rm -rf *",
|
|
412
|
-
"rm -r /*",
|
|
413
|
-
"mkfs*",
|
|
414
|
-
"dd if=*",
|
|
415
|
-
"curl*|*bash*",
|
|
416
|
-
"wget*|*sh*",
|
|
417
|
-
"shutdown*",
|
|
418
|
-
"reboot*",
|
|
419
|
-
"chmod*777*"
|
|
420
|
-
]
|
|
421
|
-
},
|
|
422
|
-
createdAt: "",
|
|
423
|
-
updatedAt: ""
|
|
424
|
-
},
|
|
425
|
-
{
|
|
426
|
-
id: "allow-safe-commands",
|
|
427
|
-
description: "Allow safe shell commands only",
|
|
428
|
-
effect: "ALLOW",
|
|
429
|
-
priority: 100,
|
|
430
|
-
toolPattern: "*shell*",
|
|
431
|
-
permission: "EXECUTE",
|
|
432
|
-
minimumTrustLevel: "UNTRUSTED",
|
|
433
|
-
enabled: true,
|
|
434
|
-
commandConstraints: {
|
|
435
|
-
allowed: [
|
|
436
|
-
"ls*",
|
|
437
|
-
"cat*",
|
|
438
|
-
"head*",
|
|
439
|
-
"tail*",
|
|
440
|
-
"wc*",
|
|
441
|
-
"grep*",
|
|
442
|
-
"find*",
|
|
443
|
-
"echo*",
|
|
444
|
-
"pwd",
|
|
445
|
-
"whoami",
|
|
446
|
-
"date",
|
|
447
|
-
"env",
|
|
448
|
-
"git*",
|
|
449
|
-
"npm*",
|
|
450
|
-
"pnpm*",
|
|
451
|
-
"yarn*",
|
|
452
|
-
"node*",
|
|
453
|
-
"python*",
|
|
454
|
-
"pip*"
|
|
455
|
-
]
|
|
456
|
-
},
|
|
457
|
-
createdAt: "",
|
|
458
|
-
updatedAt: ""
|
|
459
|
-
},
|
|
460
|
-
{
|
|
461
|
-
id: "deny-sensitive-paths",
|
|
462
|
-
description: "Block access to sensitive files",
|
|
463
|
-
effect: "DENY",
|
|
464
|
-
priority: 51,
|
|
465
|
-
toolPattern: "*",
|
|
466
|
-
permission: "EXECUTE",
|
|
467
|
-
minimumTrustLevel: "UNTRUSTED",
|
|
468
|
-
enabled: true,
|
|
469
|
-
pathConstraints: {
|
|
470
|
-
denied: [
|
|
471
|
-
"**/.env*",
|
|
472
|
-
"**/.ssh/**",
|
|
473
|
-
"**/.aws/**",
|
|
474
|
-
"**/credentials*",
|
|
475
|
-
"**/*.pem",
|
|
476
|
-
"**/*.key"
|
|
477
|
-
]
|
|
478
|
-
},
|
|
479
|
-
createdAt: "",
|
|
480
|
-
updatedAt: ""
|
|
481
|
-
},
|
|
482
|
-
{
|
|
483
|
-
id: "allow-rest",
|
|
484
|
-
description: "Allow all other tools",
|
|
485
|
-
effect: "ALLOW",
|
|
486
|
-
priority: 1e3,
|
|
487
|
-
toolPattern: "*",
|
|
488
|
-
permission: "EXECUTE",
|
|
489
|
-
minimumTrustLevel: "UNTRUSTED",
|
|
490
|
-
enabled: true,
|
|
491
|
-
createdAt: "",
|
|
492
|
-
updatedAt: ""
|
|
493
|
-
}
|
|
494
|
-
],
|
|
495
|
-
createdAt: "",
|
|
496
|
-
updatedAt: ""
|
|
497
|
-
},
|
|
498
|
-
permissive: {
|
|
499
|
-
id: "permissive",
|
|
500
|
-
name: "Permissive",
|
|
501
|
-
description: "Allows all tool calls (monitoring only)",
|
|
502
|
-
version: 1,
|
|
503
|
-
rules: [
|
|
504
|
-
{
|
|
505
|
-
id: "allow-all",
|
|
506
|
-
description: "Allow all",
|
|
507
|
-
effect: "ALLOW",
|
|
508
|
-
priority: 1e3,
|
|
509
|
-
toolPattern: "*",
|
|
510
|
-
permission: "EXECUTE",
|
|
511
|
-
minimumTrustLevel: "UNTRUSTED",
|
|
512
|
-
enabled: true,
|
|
513
|
-
createdAt: "",
|
|
514
|
-
updatedAt: ""
|
|
515
|
-
}
|
|
516
|
-
],
|
|
517
|
-
createdAt: "",
|
|
518
|
-
updatedAt: ""
|
|
519
|
-
},
|
|
520
|
-
"deny-all": {
|
|
521
|
-
id: "deny-all",
|
|
522
|
-
name: "Deny All",
|
|
523
|
-
description: "Blocks all tool calls",
|
|
524
|
-
version: 1,
|
|
525
|
-
rules: [],
|
|
526
|
-
createdAt: "",
|
|
527
|
-
updatedAt: ""
|
|
528
|
-
}
|
|
267
|
+
DEFAULT_POLICY = {
|
|
268
|
+
id: "default",
|
|
269
|
+
name: "Default (Deny All)",
|
|
270
|
+
description: "Default-deny policy. Create rules in the dashboard or push a policy.json.",
|
|
271
|
+
version: 1,
|
|
272
|
+
rules: [],
|
|
273
|
+
createdAt: "",
|
|
274
|
+
updatedAt: ""
|
|
529
275
|
};
|
|
530
276
|
}
|
|
531
277
|
});
|
|
@@ -591,18 +337,14 @@ function isAlreadyProtected(server) {
|
|
|
591
337
|
function wrapServer(server, policy) {
|
|
592
338
|
const env = { ...server.env ?? {} };
|
|
593
339
|
env.SOLONGATE_API_KEY = "${SOLONGATE_API_KEY}";
|
|
340
|
+
const proxyArgs = ["-y", "@solongate/proxy"];
|
|
341
|
+
if (policy) {
|
|
342
|
+
proxyArgs.push("--policy", policy);
|
|
343
|
+
}
|
|
344
|
+
proxyArgs.push("--verbose", "--", server.command, ...server.args ?? []);
|
|
594
345
|
return {
|
|
595
346
|
command: "npx",
|
|
596
|
-
args:
|
|
597
|
-
"-y",
|
|
598
|
-
"@solongate/proxy",
|
|
599
|
-
"--policy",
|
|
600
|
-
policy,
|
|
601
|
-
"--verbose",
|
|
602
|
-
"--",
|
|
603
|
-
server.command,
|
|
604
|
-
...server.args ?? []
|
|
605
|
-
],
|
|
347
|
+
args: proxyArgs,
|
|
606
348
|
env
|
|
607
349
|
};
|
|
608
350
|
}
|
|
@@ -651,8 +393,7 @@ USAGE
|
|
|
651
393
|
|
|
652
394
|
OPTIONS
|
|
653
395
|
--config <path> Path to MCP config file (default: auto-detect)
|
|
654
|
-
--policy <file> Custom policy JSON file (
|
|
655
|
-
Auto-detects policy.json in current directory
|
|
396
|
+
--policy <file> Custom policy JSON file (auto-detects policy.json)
|
|
656
397
|
--api-key <key> SolonGate API key (sg_live_... or sg_test_...)
|
|
657
398
|
--all Protect all servers without prompting
|
|
658
399
|
-h, --help Show this help message
|
|
@@ -870,7 +611,7 @@ async function main() {
|
|
|
870
611
|
console.log(" Invalid API key format. Must start with sg_live_ or sg_test_");
|
|
871
612
|
process.exit(1);
|
|
872
613
|
}
|
|
873
|
-
let policyValue
|
|
614
|
+
let policyValue;
|
|
874
615
|
if (options.policy) {
|
|
875
616
|
const policyPath = resolve2(options.policy);
|
|
876
617
|
if (existsSync3(policyPath)) {
|
|
@@ -886,7 +627,7 @@ async function main() {
|
|
|
886
627
|
policyValue = "./policy.json";
|
|
887
628
|
console.log(` Policy: ${defaultPolicy} (auto-detected)`);
|
|
888
629
|
} else {
|
|
889
|
-
console.log(` Policy:
|
|
630
|
+
console.log(` Policy: cloud-managed (fetched via API key)`);
|
|
890
631
|
}
|
|
891
632
|
}
|
|
892
633
|
await sleep(300);
|
|
@@ -1726,7 +1467,7 @@ function parseCreateArgs(argv) {
|
|
|
1726
1467
|
const args = argv.slice(2);
|
|
1727
1468
|
const opts = {
|
|
1728
1469
|
name: "",
|
|
1729
|
-
policy: "
|
|
1470
|
+
policy: "",
|
|
1730
1471
|
noInstall: false
|
|
1731
1472
|
};
|
|
1732
1473
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -1769,13 +1510,13 @@ USAGE
|
|
|
1769
1510
|
npx @solongate/proxy create <name> [options]
|
|
1770
1511
|
|
|
1771
1512
|
OPTIONS
|
|
1772
|
-
--policy <
|
|
1513
|
+
--policy <file> Policy JSON file (default: cloud-managed)
|
|
1773
1514
|
--no-install Skip dependency installation
|
|
1774
1515
|
-h, --help Show this help message
|
|
1775
1516
|
|
|
1776
1517
|
EXAMPLES
|
|
1777
1518
|
npx @solongate/proxy create my-server
|
|
1778
|
-
npx @solongate/proxy create db-tools --policy
|
|
1519
|
+
npx @solongate/proxy create db-tools --policy ./policy.json
|
|
1779
1520
|
`);
|
|
1780
1521
|
}
|
|
1781
1522
|
function createProject(dir, name, _policy) {
|
|
@@ -2440,6 +2181,14 @@ var PolicyRuleSchema = z.object({
|
|
|
2440
2181
|
allowed: z.array(z.string()).optional(),
|
|
2441
2182
|
denied: z.array(z.string()).optional()
|
|
2442
2183
|
}).optional(),
|
|
2184
|
+
filenameConstraints: z.object({
|
|
2185
|
+
allowed: z.array(z.string()).optional(),
|
|
2186
|
+
denied: z.array(z.string()).optional()
|
|
2187
|
+
}).optional(),
|
|
2188
|
+
urlConstraints: z.object({
|
|
2189
|
+
allowed: z.array(z.string()).optional(),
|
|
2190
|
+
denied: z.array(z.string()).optional()
|
|
2191
|
+
}).optional(),
|
|
2443
2192
|
enabled: z.boolean().default(true),
|
|
2444
2193
|
createdAt: z.string().datetime(),
|
|
2445
2194
|
updatedAt: z.string().datetime()
|
|
@@ -2625,11 +2374,48 @@ function matchSegmentGlob(segment, pattern) {
|
|
|
2625
2374
|
}
|
|
2626
2375
|
return segment === pattern;
|
|
2627
2376
|
}
|
|
2377
|
+
var PATH_FIELDS = /* @__PURE__ */ new Set([
|
|
2378
|
+
"path",
|
|
2379
|
+
"file",
|
|
2380
|
+
"file_path",
|
|
2381
|
+
"filepath",
|
|
2382
|
+
"filename",
|
|
2383
|
+
"directory",
|
|
2384
|
+
"dir",
|
|
2385
|
+
"folder",
|
|
2386
|
+
"source",
|
|
2387
|
+
"destination",
|
|
2388
|
+
"dest",
|
|
2389
|
+
"target",
|
|
2390
|
+
"input",
|
|
2391
|
+
"output",
|
|
2392
|
+
"cwd",
|
|
2393
|
+
"root",
|
|
2394
|
+
"notebook_path"
|
|
2395
|
+
]);
|
|
2628
2396
|
function extractPathArguments(args) {
|
|
2629
2397
|
const paths = [];
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2398
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2399
|
+
function addPath(value) {
|
|
2400
|
+
const trimmed = value.trim();
|
|
2401
|
+
if (trimmed && !seen.has(trimmed)) {
|
|
2402
|
+
seen.add(trimmed);
|
|
2403
|
+
paths.push(trimmed);
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
for (const [key, value] of Object.entries(args)) {
|
|
2407
|
+
if (typeof value !== "string") continue;
|
|
2408
|
+
if (PATH_FIELDS.has(key.toLowerCase())) {
|
|
2409
|
+
addPath(value);
|
|
2410
|
+
continue;
|
|
2411
|
+
}
|
|
2412
|
+
if (value.includes("/") || value.includes("\\")) {
|
|
2413
|
+
addPath(value);
|
|
2414
|
+
continue;
|
|
2415
|
+
}
|
|
2416
|
+
if (value.startsWith(".")) {
|
|
2417
|
+
addPath(value);
|
|
2418
|
+
continue;
|
|
2633
2419
|
}
|
|
2634
2420
|
}
|
|
2635
2421
|
return paths;
|
|
@@ -2643,15 +2429,62 @@ var COMMAND_FIELDS = /* @__PURE__ */ new Set([
|
|
|
2643
2429
|
"shell",
|
|
2644
2430
|
"exec",
|
|
2645
2431
|
"sql",
|
|
2646
|
-
"expression"
|
|
2432
|
+
"expression",
|
|
2433
|
+
"function"
|
|
2647
2434
|
]);
|
|
2435
|
+
var COMMAND_HEURISTICS = [
|
|
2436
|
+
/^(sh|bash|cmd|powershell|zsh|fish)\s+-c\s+/i,
|
|
2437
|
+
// shell -c "..."
|
|
2438
|
+
/^(sudo|doas)\s+/i,
|
|
2439
|
+
// privilege escalation
|
|
2440
|
+
/^\w+\s+&&\s+/,
|
|
2441
|
+
// cmd1 && cmd2
|
|
2442
|
+
/^\w+\s*\|\s*\w+/,
|
|
2443
|
+
// cmd1 | cmd2
|
|
2444
|
+
/^\w+\s*;\s*\w+/,
|
|
2445
|
+
// cmd1; cmd2
|
|
2446
|
+
/^(curl|wget|nc|ncat)\s+/i,
|
|
2447
|
+
// network commands
|
|
2448
|
+
/^(rm|del|rmdir)\s+/i,
|
|
2449
|
+
// destructive commands
|
|
2450
|
+
/^(cat|type|more|less)\s+.*[/\\]/i
|
|
2451
|
+
// file read commands with paths
|
|
2452
|
+
];
|
|
2648
2453
|
function extractCommandArguments(args) {
|
|
2649
2454
|
const commands = [];
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2455
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2456
|
+
function addCommand(value) {
|
|
2457
|
+
const trimmed = value.trim();
|
|
2458
|
+
if (trimmed && !seen.has(trimmed)) {
|
|
2459
|
+
seen.add(trimmed);
|
|
2460
|
+
commands.push(trimmed);
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
function scanValue(key, value) {
|
|
2464
|
+
if (typeof value === "string") {
|
|
2465
|
+
if (COMMAND_FIELDS.has(key.toLowerCase())) {
|
|
2466
|
+
addCommand(value);
|
|
2467
|
+
return;
|
|
2468
|
+
}
|
|
2469
|
+
for (const pattern of COMMAND_HEURISTICS) {
|
|
2470
|
+
if (pattern.test(value)) {
|
|
2471
|
+
addCommand(value);
|
|
2472
|
+
return;
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2654
2475
|
}
|
|
2476
|
+
if (Array.isArray(value)) {
|
|
2477
|
+
for (const item of value) {
|
|
2478
|
+
scanValue(key, item);
|
|
2479
|
+
}
|
|
2480
|
+
} else if (typeof value === "object" && value !== null) {
|
|
2481
|
+
for (const [k, v] of Object.entries(value)) {
|
|
2482
|
+
scanValue(k, v);
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
for (const [key, value] of Object.entries(args)) {
|
|
2487
|
+
scanValue(key, value);
|
|
2655
2488
|
}
|
|
2656
2489
|
return commands;
|
|
2657
2490
|
}
|
|
@@ -2697,6 +2530,224 @@ function isCommandAllowed(command, constraints) {
|
|
|
2697
2530
|
}
|
|
2698
2531
|
return true;
|
|
2699
2532
|
}
|
|
2533
|
+
function extractFilenames(args) {
|
|
2534
|
+
const filenames = [];
|
|
2535
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2536
|
+
function addFilename(name) {
|
|
2537
|
+
const trimmed = name.trim();
|
|
2538
|
+
if (trimmed && !seen.has(trimmed)) {
|
|
2539
|
+
seen.add(trimmed);
|
|
2540
|
+
filenames.push(trimmed);
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
function scanValue(value) {
|
|
2544
|
+
if (typeof value === "string") {
|
|
2545
|
+
const trimmed = value.trim();
|
|
2546
|
+
if (!trimmed) return;
|
|
2547
|
+
if (/^https?:\/\//i.test(trimmed)) return;
|
|
2548
|
+
if (trimmed.includes("/") || trimmed.includes("\\")) {
|
|
2549
|
+
const normalized = trimmed.replace(/\\/g, "/");
|
|
2550
|
+
const parts = normalized.split("/");
|
|
2551
|
+
const basename = parts[parts.length - 1];
|
|
2552
|
+
if (basename && basename.length > 0) {
|
|
2553
|
+
addFilename(basename);
|
|
2554
|
+
}
|
|
2555
|
+
return;
|
|
2556
|
+
}
|
|
2557
|
+
if (trimmed.includes(" ")) {
|
|
2558
|
+
for (const token of trimmed.split(/\s+/)) {
|
|
2559
|
+
if (token.includes("/") || token.includes("\\")) {
|
|
2560
|
+
const parts = token.replace(/\\/g, "/").split("/");
|
|
2561
|
+
const basename = parts[parts.length - 1];
|
|
2562
|
+
if (basename && looksLikeFilename(basename)) addFilename(basename);
|
|
2563
|
+
} else if (looksLikeFilename(token)) {
|
|
2564
|
+
addFilename(token);
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
return;
|
|
2568
|
+
}
|
|
2569
|
+
if (looksLikeFilename(trimmed)) {
|
|
2570
|
+
addFilename(trimmed);
|
|
2571
|
+
}
|
|
2572
|
+
return;
|
|
2573
|
+
}
|
|
2574
|
+
if (Array.isArray(value)) {
|
|
2575
|
+
for (const item of value) {
|
|
2576
|
+
scanValue(item);
|
|
2577
|
+
}
|
|
2578
|
+
} else if (typeof value === "object" && value !== null) {
|
|
2579
|
+
for (const v of Object.values(value)) {
|
|
2580
|
+
scanValue(v);
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
for (const value of Object.values(args)) {
|
|
2585
|
+
scanValue(value);
|
|
2586
|
+
}
|
|
2587
|
+
return filenames;
|
|
2588
|
+
}
|
|
2589
|
+
function looksLikeFilename(s) {
|
|
2590
|
+
if (s.startsWith(".")) return true;
|
|
2591
|
+
if (/\.\w+$/.test(s)) return true;
|
|
2592
|
+
const knownFiles = /* @__PURE__ */ new Set([
|
|
2593
|
+
"id_rsa",
|
|
2594
|
+
"id_dsa",
|
|
2595
|
+
"id_ecdsa",
|
|
2596
|
+
"id_ed25519",
|
|
2597
|
+
"authorized_keys",
|
|
2598
|
+
"known_hosts",
|
|
2599
|
+
"makefile",
|
|
2600
|
+
"dockerfile",
|
|
2601
|
+
"vagrantfile",
|
|
2602
|
+
"gemfile",
|
|
2603
|
+
"rakefile",
|
|
2604
|
+
"procfile"
|
|
2605
|
+
]);
|
|
2606
|
+
if (knownFiles.has(s.toLowerCase())) return true;
|
|
2607
|
+
return false;
|
|
2608
|
+
}
|
|
2609
|
+
function matchFilenamePattern(filename, pattern) {
|
|
2610
|
+
if (pattern === "*") return true;
|
|
2611
|
+
const normalizedFilename = filename.toLowerCase();
|
|
2612
|
+
const normalizedPattern = pattern.toLowerCase();
|
|
2613
|
+
if (normalizedFilename === normalizedPattern) return true;
|
|
2614
|
+
const startsWithStar = normalizedPattern.startsWith("*");
|
|
2615
|
+
const endsWithStar = normalizedPattern.endsWith("*");
|
|
2616
|
+
if (startsWithStar && endsWithStar) {
|
|
2617
|
+
const infix = normalizedPattern.slice(1, -1);
|
|
2618
|
+
return infix.length > 0 && normalizedFilename.includes(infix);
|
|
2619
|
+
}
|
|
2620
|
+
if (startsWithStar) {
|
|
2621
|
+
const suffix = normalizedPattern.slice(1);
|
|
2622
|
+
return normalizedFilename.endsWith(suffix);
|
|
2623
|
+
}
|
|
2624
|
+
if (endsWithStar) {
|
|
2625
|
+
const prefix = normalizedPattern.slice(0, -1);
|
|
2626
|
+
return normalizedFilename.startsWith(prefix);
|
|
2627
|
+
}
|
|
2628
|
+
const starIdx = normalizedPattern.indexOf("*");
|
|
2629
|
+
if (starIdx !== -1) {
|
|
2630
|
+
const prefix = normalizedPattern.slice(0, starIdx);
|
|
2631
|
+
const suffix = normalizedPattern.slice(starIdx + 1);
|
|
2632
|
+
return normalizedFilename.startsWith(prefix) && normalizedFilename.endsWith(suffix) && normalizedFilename.length >= prefix.length + suffix.length;
|
|
2633
|
+
}
|
|
2634
|
+
return false;
|
|
2635
|
+
}
|
|
2636
|
+
function isFilenameAllowed(filename, constraints) {
|
|
2637
|
+
if (constraints.denied && constraints.denied.length > 0) {
|
|
2638
|
+
for (const pattern of constraints.denied) {
|
|
2639
|
+
if (matchFilenamePattern(filename, pattern)) {
|
|
2640
|
+
return false;
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
if (constraints.allowed && constraints.allowed.length > 0) {
|
|
2645
|
+
let matchesAllowed = false;
|
|
2646
|
+
for (const pattern of constraints.allowed) {
|
|
2647
|
+
if (matchFilenamePattern(filename, pattern)) {
|
|
2648
|
+
matchesAllowed = true;
|
|
2649
|
+
break;
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
if (!matchesAllowed) return false;
|
|
2653
|
+
}
|
|
2654
|
+
return true;
|
|
2655
|
+
}
|
|
2656
|
+
var URL_FIELDS = /* @__PURE__ */ new Set([
|
|
2657
|
+
"url",
|
|
2658
|
+
"href",
|
|
2659
|
+
"uri",
|
|
2660
|
+
"endpoint",
|
|
2661
|
+
"link",
|
|
2662
|
+
"src",
|
|
2663
|
+
"source",
|
|
2664
|
+
"target",
|
|
2665
|
+
"redirect",
|
|
2666
|
+
"callback",
|
|
2667
|
+
"webhook"
|
|
2668
|
+
]);
|
|
2669
|
+
function extractUrlArguments(args) {
|
|
2670
|
+
const urls = [];
|
|
2671
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2672
|
+
function addUrl(value) {
|
|
2673
|
+
const trimmed = value.trim();
|
|
2674
|
+
if (trimmed && !seen.has(trimmed)) {
|
|
2675
|
+
seen.add(trimmed);
|
|
2676
|
+
urls.push(trimmed);
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
function scanValue(key, value) {
|
|
2680
|
+
if (typeof value === "string") {
|
|
2681
|
+
const lower = key.toLowerCase();
|
|
2682
|
+
if (URL_FIELDS.has(lower)) {
|
|
2683
|
+
addUrl(value);
|
|
2684
|
+
return;
|
|
2685
|
+
}
|
|
2686
|
+
if (/https?:\/\//i.test(value)) {
|
|
2687
|
+
addUrl(value);
|
|
2688
|
+
return;
|
|
2689
|
+
}
|
|
2690
|
+
if (/^[a-zA-Z0-9]([a-zA-Z0-9-]*\.)+[a-zA-Z]{2,}(\/.*)?$/.test(value)) {
|
|
2691
|
+
addUrl(value);
|
|
2692
|
+
return;
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
if (Array.isArray(value)) {
|
|
2696
|
+
for (const item of value) {
|
|
2697
|
+
scanValue(key, item);
|
|
2698
|
+
}
|
|
2699
|
+
} else if (typeof value === "object" && value !== null) {
|
|
2700
|
+
for (const [k, v] of Object.entries(value)) {
|
|
2701
|
+
scanValue(k, v);
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2705
|
+
for (const [key, value] of Object.entries(args)) {
|
|
2706
|
+
scanValue(key, value);
|
|
2707
|
+
}
|
|
2708
|
+
return urls;
|
|
2709
|
+
}
|
|
2710
|
+
function matchUrlPattern(url, pattern) {
|
|
2711
|
+
if (pattern === "*") return true;
|
|
2712
|
+
const normalizedUrl = url.trim().toLowerCase();
|
|
2713
|
+
const normalizedPattern = pattern.trim().toLowerCase();
|
|
2714
|
+
if (normalizedPattern === normalizedUrl) return true;
|
|
2715
|
+
const startsWithStar = normalizedPattern.startsWith("*");
|
|
2716
|
+
const endsWithStar = normalizedPattern.endsWith("*");
|
|
2717
|
+
if (startsWithStar && endsWithStar) {
|
|
2718
|
+
const infix = normalizedPattern.slice(1, -1);
|
|
2719
|
+
return infix.length > 0 && normalizedUrl.includes(infix);
|
|
2720
|
+
}
|
|
2721
|
+
if (endsWithStar) {
|
|
2722
|
+
const prefix = normalizedPattern.slice(0, -1);
|
|
2723
|
+
return normalizedUrl.startsWith(prefix);
|
|
2724
|
+
}
|
|
2725
|
+
if (startsWithStar) {
|
|
2726
|
+
const suffix = normalizedPattern.slice(1);
|
|
2727
|
+
return normalizedUrl.endsWith(suffix);
|
|
2728
|
+
}
|
|
2729
|
+
return false;
|
|
2730
|
+
}
|
|
2731
|
+
function isUrlAllowed(url, constraints) {
|
|
2732
|
+
if (constraints.denied && constraints.denied.length > 0) {
|
|
2733
|
+
for (const pattern of constraints.denied) {
|
|
2734
|
+
if (matchUrlPattern(url, pattern)) {
|
|
2735
|
+
return false;
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
if (constraints.allowed && constraints.allowed.length > 0) {
|
|
2740
|
+
let matchesAllowed = false;
|
|
2741
|
+
for (const pattern of constraints.allowed) {
|
|
2742
|
+
if (matchUrlPattern(url, pattern)) {
|
|
2743
|
+
matchesAllowed = true;
|
|
2744
|
+
break;
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
if (!matchesAllowed) return false;
|
|
2748
|
+
}
|
|
2749
|
+
return true;
|
|
2750
|
+
}
|
|
2700
2751
|
function ruleMatchesRequest(rule, request) {
|
|
2701
2752
|
if (!rule.enabled) return false;
|
|
2702
2753
|
if (rule.permission !== request.requiredPermission) return false;
|
|
@@ -2725,6 +2776,22 @@ function ruleMatchesRequest(rule, request) {
|
|
|
2725
2776
|
if (!satisfied) return false;
|
|
2726
2777
|
}
|
|
2727
2778
|
}
|
|
2779
|
+
if (rule.filenameConstraints) {
|
|
2780
|
+
const satisfied = filenameConstraintsMatch(rule.filenameConstraints, request.arguments);
|
|
2781
|
+
if (rule.effect === "DENY") {
|
|
2782
|
+
if (satisfied) return false;
|
|
2783
|
+
} else {
|
|
2784
|
+
if (!satisfied) return false;
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
if (rule.urlConstraints) {
|
|
2788
|
+
const satisfied = urlConstraintsMatch(rule.urlConstraints, request.arguments);
|
|
2789
|
+
if (rule.effect === "DENY") {
|
|
2790
|
+
if (satisfied) return false;
|
|
2791
|
+
} else {
|
|
2792
|
+
if (!satisfied) return false;
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2728
2795
|
return true;
|
|
2729
2796
|
}
|
|
2730
2797
|
function toolPatternMatches(pattern, toolName) {
|
|
@@ -2815,6 +2882,16 @@ function commandConstraintsMatch(constraints, args) {
|
|
|
2815
2882
|
if (commands.length === 0) return true;
|
|
2816
2883
|
return commands.every((cmd) => isCommandAllowed(cmd, constraints));
|
|
2817
2884
|
}
|
|
2885
|
+
function filenameConstraintsMatch(constraints, args) {
|
|
2886
|
+
const filenames = extractFilenames(args);
|
|
2887
|
+
if (filenames.length === 0) return true;
|
|
2888
|
+
return filenames.every((name) => isFilenameAllowed(name, constraints));
|
|
2889
|
+
}
|
|
2890
|
+
function urlConstraintsMatch(constraints, args) {
|
|
2891
|
+
const urls = extractUrlArguments(args);
|
|
2892
|
+
if (urls.length === 0) return true;
|
|
2893
|
+
return urls.every((url) => isUrlAllowed(url, constraints));
|
|
2894
|
+
}
|
|
2818
2895
|
function evaluatePolicy(policySet, request) {
|
|
2819
2896
|
const startTime = performance.now();
|
|
2820
2897
|
const sortedRules = [...policySet.rules].sort(
|
|
@@ -3195,6 +3272,11 @@ function resolveConfig(userConfig) {
|
|
|
3195
3272
|
"Token secret is shorter than 32 characters. Use a longer secret for production."
|
|
3196
3273
|
);
|
|
3197
3274
|
}
|
|
3275
|
+
if (config.apiUrl && config.apiUrl.startsWith("http://") && !config.apiUrl.startsWith("http://localhost") && !config.apiUrl.startsWith("http://127.0.0.1")) {
|
|
3276
|
+
warnings.push(
|
|
3277
|
+
"API URL uses plaintext HTTP. API keys will be sent unencrypted. Use HTTPS in production."
|
|
3278
|
+
);
|
|
3279
|
+
}
|
|
3198
3280
|
return { config, warnings };
|
|
3199
3281
|
}
|
|
3200
3282
|
async function interceptToolCall(params, upstreamCall, options) {
|
|
@@ -3700,6 +3782,12 @@ var SolonGate = class {
|
|
|
3700
3782
|
async validateLicense() {
|
|
3701
3783
|
if (this.licenseValidated) return;
|
|
3702
3784
|
if (this.apiKey.startsWith("sg_test_")) {
|
|
3785
|
+
const nodeEnv = typeof process !== "undefined" ? process.env.NODE_ENV : "";
|
|
3786
|
+
if (nodeEnv === "production") {
|
|
3787
|
+
throw new LicenseError(
|
|
3788
|
+
"Test API keys (sg_test_) cannot be used in production. Use a sg_live_ key instead."
|
|
3789
|
+
);
|
|
3790
|
+
}
|
|
3703
3791
|
this.licenseValidated = true;
|
|
3704
3792
|
return;
|
|
3705
3793
|
}
|
|
@@ -3866,6 +3954,361 @@ var SolonGate = class {
|
|
|
3866
3954
|
}
|
|
3867
3955
|
};
|
|
3868
3956
|
|
|
3957
|
+
// ../core/dist/index.js
|
|
3958
|
+
import { z as z2 } from "zod";
|
|
3959
|
+
var Permission2 = {
|
|
3960
|
+
READ: "READ",
|
|
3961
|
+
WRITE: "WRITE",
|
|
3962
|
+
EXECUTE: "EXECUTE"
|
|
3963
|
+
};
|
|
3964
|
+
var PermissionSchema = z2.enum(["READ", "WRITE", "EXECUTE"]);
|
|
3965
|
+
var NO_PERMISSIONS = Object.freeze(
|
|
3966
|
+
/* @__PURE__ */ new Set()
|
|
3967
|
+
);
|
|
3968
|
+
var READ_ONLY = Object.freeze(
|
|
3969
|
+
/* @__PURE__ */ new Set([Permission2.READ])
|
|
3970
|
+
);
|
|
3971
|
+
var PolicyRuleSchema2 = z2.object({
|
|
3972
|
+
id: z2.string().min(1).max(256),
|
|
3973
|
+
description: z2.string().max(1024),
|
|
3974
|
+
effect: z2.enum(["ALLOW", "DENY"]),
|
|
3975
|
+
priority: z2.number().int().min(0).max(1e4).default(1e3),
|
|
3976
|
+
toolPattern: z2.string().min(1).max(512),
|
|
3977
|
+
permission: z2.enum(["READ", "WRITE", "EXECUTE"]).optional(),
|
|
3978
|
+
minimumTrustLevel: z2.enum(["UNTRUSTED", "VERIFIED", "TRUSTED"]),
|
|
3979
|
+
argumentConstraints: z2.record(z2.unknown()).optional(),
|
|
3980
|
+
pathConstraints: z2.object({
|
|
3981
|
+
allowed: z2.array(z2.string()).optional(),
|
|
3982
|
+
denied: z2.array(z2.string()).optional(),
|
|
3983
|
+
rootDirectory: z2.string().optional(),
|
|
3984
|
+
allowSymlinks: z2.boolean().optional()
|
|
3985
|
+
}).optional(),
|
|
3986
|
+
commandConstraints: z2.object({
|
|
3987
|
+
allowed: z2.array(z2.string()).optional(),
|
|
3988
|
+
denied: z2.array(z2.string()).optional()
|
|
3989
|
+
}).optional(),
|
|
3990
|
+
filenameConstraints: z2.object({
|
|
3991
|
+
allowed: z2.array(z2.string()).optional(),
|
|
3992
|
+
denied: z2.array(z2.string()).optional()
|
|
3993
|
+
}).optional(),
|
|
3994
|
+
urlConstraints: z2.object({
|
|
3995
|
+
allowed: z2.array(z2.string()).optional(),
|
|
3996
|
+
denied: z2.array(z2.string()).optional()
|
|
3997
|
+
}).optional(),
|
|
3998
|
+
enabled: z2.boolean().default(true),
|
|
3999
|
+
createdAt: z2.string().datetime(),
|
|
4000
|
+
updatedAt: z2.string().datetime()
|
|
4001
|
+
});
|
|
4002
|
+
var PolicySetSchema2 = z2.object({
|
|
4003
|
+
id: z2.string().min(1).max(256),
|
|
4004
|
+
name: z2.string().min(1).max(256),
|
|
4005
|
+
description: z2.string().max(2048),
|
|
4006
|
+
version: z2.number().int().min(0),
|
|
4007
|
+
rules: z2.array(PolicyRuleSchema2),
|
|
4008
|
+
createdAt: z2.string().datetime(),
|
|
4009
|
+
updatedAt: z2.string().datetime()
|
|
4010
|
+
});
|
|
4011
|
+
var SECURITY_CONTEXT_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
4012
|
+
var DEFAULT_INPUT_GUARD_CONFIG2 = Object.freeze({
|
|
4013
|
+
pathTraversal: true,
|
|
4014
|
+
shellInjection: true,
|
|
4015
|
+
wildcardAbuse: true,
|
|
4016
|
+
lengthLimit: 4096,
|
|
4017
|
+
entropyLimit: true,
|
|
4018
|
+
ssrf: true,
|
|
4019
|
+
sqlInjection: true
|
|
4020
|
+
});
|
|
4021
|
+
var PATH_TRAVERSAL_PATTERNS = [
|
|
4022
|
+
/\.\.\//,
|
|
4023
|
+
// ../
|
|
4024
|
+
/\.\.\\/,
|
|
4025
|
+
// ..\
|
|
4026
|
+
/%2e%2e/i,
|
|
4027
|
+
// URL-encoded ..
|
|
4028
|
+
/%2e\./i,
|
|
4029
|
+
// partial URL-encoded
|
|
4030
|
+
/\.%2e/i,
|
|
4031
|
+
// partial URL-encoded
|
|
4032
|
+
/%252e%252e/i,
|
|
4033
|
+
// double URL-encoded
|
|
4034
|
+
/\.\.\0/
|
|
4035
|
+
// null byte variant
|
|
4036
|
+
];
|
|
4037
|
+
var SENSITIVE_PATHS = [
|
|
4038
|
+
/\/etc\/passwd/i,
|
|
4039
|
+
/\/etc\/shadow/i,
|
|
4040
|
+
/\/proc\//i,
|
|
4041
|
+
/\/dev\//i,
|
|
4042
|
+
/c:\\windows\\system32/i,
|
|
4043
|
+
/c:\\windows\\syswow64/i,
|
|
4044
|
+
/\/root\//i,
|
|
4045
|
+
/~\//,
|
|
4046
|
+
/\.env(\.|$)/i,
|
|
4047
|
+
// .env, .env.local, .env.production
|
|
4048
|
+
/\.aws\/credentials/i,
|
|
4049
|
+
// AWS credentials
|
|
4050
|
+
/\.ssh\/id_/i,
|
|
4051
|
+
// SSH keys
|
|
4052
|
+
/\.kube\/config/i,
|
|
4053
|
+
// Kubernetes config
|
|
4054
|
+
/wp-config\.php/i,
|
|
4055
|
+
// WordPress config
|
|
4056
|
+
/\.git\/config/i,
|
|
4057
|
+
// Git config
|
|
4058
|
+
/\.npmrc/i,
|
|
4059
|
+
// npm credentials
|
|
4060
|
+
/\.pypirc/i
|
|
4061
|
+
// PyPI credentials
|
|
4062
|
+
];
|
|
4063
|
+
function detectPathTraversal(value) {
|
|
4064
|
+
for (const pattern of PATH_TRAVERSAL_PATTERNS) {
|
|
4065
|
+
if (pattern.test(value)) return true;
|
|
4066
|
+
}
|
|
4067
|
+
for (const pattern of SENSITIVE_PATHS) {
|
|
4068
|
+
if (pattern.test(value)) return true;
|
|
4069
|
+
}
|
|
4070
|
+
return false;
|
|
4071
|
+
}
|
|
4072
|
+
var SHELL_INJECTION_PATTERNS = [
|
|
4073
|
+
/[;|&`]/,
|
|
4074
|
+
// Command separators and backtick execution
|
|
4075
|
+
/\$\(/,
|
|
4076
|
+
// Command substitution $(...)
|
|
4077
|
+
/\$\{/,
|
|
4078
|
+
// Variable expansion ${...}
|
|
4079
|
+
/>\s*/,
|
|
4080
|
+
// Output redirect
|
|
4081
|
+
/<\s*/,
|
|
4082
|
+
// Input redirect
|
|
4083
|
+
/&&/,
|
|
4084
|
+
// AND chaining
|
|
4085
|
+
/\|\|/,
|
|
4086
|
+
// OR chaining
|
|
4087
|
+
/\beval\b/i,
|
|
4088
|
+
// eval command
|
|
4089
|
+
/\bexec\b/i,
|
|
4090
|
+
// exec command
|
|
4091
|
+
/\bsystem\b/i,
|
|
4092
|
+
// system call
|
|
4093
|
+
/%0a/i,
|
|
4094
|
+
// URL-encoded newline
|
|
4095
|
+
/%0d/i,
|
|
4096
|
+
// URL-encoded carriage return
|
|
4097
|
+
/%09/i,
|
|
4098
|
+
// URL-encoded tab
|
|
4099
|
+
/\r\n/,
|
|
4100
|
+
// CRLF injection
|
|
4101
|
+
/\n/
|
|
4102
|
+
// Newline (command separator on Unix)
|
|
4103
|
+
];
|
|
4104
|
+
function detectShellInjection(value) {
|
|
4105
|
+
for (const pattern of SHELL_INJECTION_PATTERNS) {
|
|
4106
|
+
if (pattern.test(value)) return true;
|
|
4107
|
+
}
|
|
4108
|
+
return false;
|
|
4109
|
+
}
|
|
4110
|
+
var MAX_WILDCARDS_PER_VALUE = 3;
|
|
4111
|
+
function detectWildcardAbuse(value) {
|
|
4112
|
+
if (value.includes("**")) return true;
|
|
4113
|
+
const wildcardCount = (value.match(/\*/g) || []).length;
|
|
4114
|
+
if (wildcardCount > MAX_WILDCARDS_PER_VALUE) return true;
|
|
4115
|
+
return false;
|
|
4116
|
+
}
|
|
4117
|
+
var SSRF_PATTERNS = [
|
|
4118
|
+
/^https?:\/\/localhost\b/i,
|
|
4119
|
+
/^https?:\/\/127\.\d{1,3}\.\d{1,3}\.\d{1,3}/,
|
|
4120
|
+
/^https?:\/\/0\.0\.0\.0/,
|
|
4121
|
+
/^https?:\/\/\[::1\]/,
|
|
4122
|
+
// IPv6 loopback
|
|
4123
|
+
/^https?:\/\/10\.\d{1,3}\.\d{1,3}\.\d{1,3}/,
|
|
4124
|
+
// 10.x.x.x
|
|
4125
|
+
/^https?:\/\/172\.(1[6-9]|2\d|3[01])\./,
|
|
4126
|
+
// 172.16-31.x.x
|
|
4127
|
+
/^https?:\/\/192\.168\./,
|
|
4128
|
+
// 192.168.x.x
|
|
4129
|
+
/^https?:\/\/169\.254\./,
|
|
4130
|
+
// Link-local / AWS metadata
|
|
4131
|
+
/metadata\.google\.internal/i,
|
|
4132
|
+
// GCP metadata
|
|
4133
|
+
/^https?:\/\/metadata\b/i,
|
|
4134
|
+
// Generic metadata endpoint
|
|
4135
|
+
// IPv6 bypass patterns
|
|
4136
|
+
/^https?:\/\/\[fe80:/i,
|
|
4137
|
+
// IPv6 link-local
|
|
4138
|
+
/^https?:\/\/\[fc00:/i,
|
|
4139
|
+
// IPv6 unique local
|
|
4140
|
+
/^https?:\/\/\[fd[0-9a-f]{2}:/i,
|
|
4141
|
+
// IPv6 unique local (fd00::/8)
|
|
4142
|
+
/^https?:\/\/\[::ffff:127\./i,
|
|
4143
|
+
// IPv4-mapped IPv6 loopback
|
|
4144
|
+
/^https?:\/\/\[::ffff:10\./i,
|
|
4145
|
+
// IPv4-mapped IPv6 private
|
|
4146
|
+
/^https?:\/\/\[::ffff:172\.(1[6-9]|2\d|3[01])\./i,
|
|
4147
|
+
// IPv4-mapped IPv6 private
|
|
4148
|
+
/^https?:\/\/\[::ffff:192\.168\./i,
|
|
4149
|
+
// IPv4-mapped IPv6 private
|
|
4150
|
+
/^https?:\/\/\[::ffff:169\.254\./i,
|
|
4151
|
+
// IPv4-mapped IPv6 link-local
|
|
4152
|
+
// Hex IP bypass (e.g., 0x7f000001 = 127.0.0.1)
|
|
4153
|
+
/^https?:\/\/0x[0-9a-f]+\b/i,
|
|
4154
|
+
// Octal IP bypass (e.g., 0177.0.0.1 = 127.0.0.1)
|
|
4155
|
+
/^https?:\/\/0[0-7]{1,3}\./
|
|
4156
|
+
];
|
|
4157
|
+
function detectDecimalIP(value) {
|
|
4158
|
+
const match = value.match(/^https?:\/\/(\d{8,10})(?:[:/]|$)/);
|
|
4159
|
+
if (!match || !match[1]) return false;
|
|
4160
|
+
const decimal = parseInt(match[1], 10);
|
|
4161
|
+
if (isNaN(decimal) || decimal > 4294967295) return false;
|
|
4162
|
+
return decimal >= 2130706432 && decimal <= 2147483647 || // 127.0.0.0/8
|
|
4163
|
+
decimal >= 167772160 && decimal <= 184549375 || // 10.0.0.0/8
|
|
4164
|
+
decimal >= 2886729728 && decimal <= 2887778303 || // 172.16.0.0/12
|
|
4165
|
+
decimal >= 3232235520 && decimal <= 3232301055 || // 192.168.0.0/16
|
|
4166
|
+
decimal >= 2851995648 && decimal <= 2852061183 || // 169.254.0.0/16
|
|
4167
|
+
decimal === 0;
|
|
4168
|
+
}
|
|
4169
|
+
function detectSSRF(value) {
|
|
4170
|
+
for (const pattern of SSRF_PATTERNS) {
|
|
4171
|
+
if (pattern.test(value)) return true;
|
|
4172
|
+
}
|
|
4173
|
+
if (detectDecimalIP(value)) return true;
|
|
4174
|
+
return false;
|
|
4175
|
+
}
|
|
4176
|
+
var SQL_INJECTION_PATTERNS = [
|
|
4177
|
+
/'\s{0,20}(OR|AND)\s{0,20}'.{0,200}'/i,
|
|
4178
|
+
// ' OR '1'='1 — bounded to prevent ReDoS
|
|
4179
|
+
/'\s{0,10};\s{0,10}(DROP|DELETE|UPDATE|INSERT|ALTER|CREATE|EXEC)/i,
|
|
4180
|
+
// '; DROP TABLE
|
|
4181
|
+
/UNION\s+(ALL\s+)?SELECT/i,
|
|
4182
|
+
// UNION SELECT
|
|
4183
|
+
/--\s*$/m,
|
|
4184
|
+
// SQL comment at end of line
|
|
4185
|
+
/\/\*.{0,500}?\*\//,
|
|
4186
|
+
// SQL block comment — bounded + non-greedy
|
|
4187
|
+
/\bSLEEP\s*\(/i,
|
|
4188
|
+
// Time-based injection
|
|
4189
|
+
/\bBENCHMARK\s*\(/i,
|
|
4190
|
+
// MySQL benchmark
|
|
4191
|
+
/\bWAITFOR\s+DELAY/i,
|
|
4192
|
+
// MSSQL delay
|
|
4193
|
+
/\b(LOAD_FILE|INTO\s+OUTFILE|INTO\s+DUMPFILE)\b/i
|
|
4194
|
+
// File operations
|
|
4195
|
+
];
|
|
4196
|
+
function detectSQLInjection(value) {
|
|
4197
|
+
for (const pattern of SQL_INJECTION_PATTERNS) {
|
|
4198
|
+
if (pattern.test(value)) return true;
|
|
4199
|
+
}
|
|
4200
|
+
return false;
|
|
4201
|
+
}
|
|
4202
|
+
function checkLengthLimits(value, maxLength = 4096) {
|
|
4203
|
+
return value.length <= maxLength;
|
|
4204
|
+
}
|
|
4205
|
+
var ENTROPY_THRESHOLD = 4.5;
|
|
4206
|
+
var MIN_LENGTH_FOR_ENTROPY_CHECK = 32;
|
|
4207
|
+
function checkEntropyLimits(value) {
|
|
4208
|
+
if (value.length < MIN_LENGTH_FOR_ENTROPY_CHECK) return true;
|
|
4209
|
+
const entropy = calculateShannonEntropy(value);
|
|
4210
|
+
return entropy <= ENTROPY_THRESHOLD;
|
|
4211
|
+
}
|
|
4212
|
+
function calculateShannonEntropy(str) {
|
|
4213
|
+
const freq = /* @__PURE__ */ new Map();
|
|
4214
|
+
for (const char of str) {
|
|
4215
|
+
freq.set(char, (freq.get(char) ?? 0) + 1);
|
|
4216
|
+
}
|
|
4217
|
+
let entropy = 0;
|
|
4218
|
+
const len = str.length;
|
|
4219
|
+
for (const count of freq.values()) {
|
|
4220
|
+
const p = count / len;
|
|
4221
|
+
if (p > 0) {
|
|
4222
|
+
entropy -= p * Math.log2(p);
|
|
4223
|
+
}
|
|
4224
|
+
}
|
|
4225
|
+
return entropy;
|
|
4226
|
+
}
|
|
4227
|
+
function sanitizeInput(field, value, config = DEFAULT_INPUT_GUARD_CONFIG2) {
|
|
4228
|
+
const threats = [];
|
|
4229
|
+
if (typeof value !== "string") {
|
|
4230
|
+
if (typeof value === "object" && value !== null) {
|
|
4231
|
+
return sanitizeObject(field, value, config);
|
|
4232
|
+
}
|
|
4233
|
+
return { safe: true, threats: [] };
|
|
4234
|
+
}
|
|
4235
|
+
if (config.pathTraversal && detectPathTraversal(value)) {
|
|
4236
|
+
threats.push({
|
|
4237
|
+
type: "PATH_TRAVERSAL",
|
|
4238
|
+
field,
|
|
4239
|
+
value: truncate(value, 100),
|
|
4240
|
+
description: "Path traversal pattern detected"
|
|
4241
|
+
});
|
|
4242
|
+
}
|
|
4243
|
+
if (config.shellInjection && detectShellInjection(value)) {
|
|
4244
|
+
threats.push({
|
|
4245
|
+
type: "SHELL_INJECTION",
|
|
4246
|
+
field,
|
|
4247
|
+
value: truncate(value, 100),
|
|
4248
|
+
description: "Shell injection pattern detected"
|
|
4249
|
+
});
|
|
4250
|
+
}
|
|
4251
|
+
if (config.wildcardAbuse && detectWildcardAbuse(value)) {
|
|
4252
|
+
threats.push({
|
|
4253
|
+
type: "WILDCARD_ABUSE",
|
|
4254
|
+
field,
|
|
4255
|
+
value: truncate(value, 100),
|
|
4256
|
+
description: "Wildcard abuse pattern detected"
|
|
4257
|
+
});
|
|
4258
|
+
}
|
|
4259
|
+
if (!checkLengthLimits(value, config.lengthLimit)) {
|
|
4260
|
+
threats.push({
|
|
4261
|
+
type: "LENGTH_EXCEEDED",
|
|
4262
|
+
field,
|
|
4263
|
+
value: `[${value.length} chars]`,
|
|
4264
|
+
description: `Value exceeds maximum length of ${config.lengthLimit}`
|
|
4265
|
+
});
|
|
4266
|
+
}
|
|
4267
|
+
if (config.entropyLimit && !checkEntropyLimits(value)) {
|
|
4268
|
+
threats.push({
|
|
4269
|
+
type: "HIGH_ENTROPY",
|
|
4270
|
+
field,
|
|
4271
|
+
value: truncate(value, 100),
|
|
4272
|
+
description: "High entropy string detected - possible encoded payload"
|
|
4273
|
+
});
|
|
4274
|
+
}
|
|
4275
|
+
if (config.ssrf && detectSSRF(value)) {
|
|
4276
|
+
threats.push({
|
|
4277
|
+
type: "SSRF",
|
|
4278
|
+
field,
|
|
4279
|
+
value: truncate(value, 100),
|
|
4280
|
+
description: "Server-side request forgery pattern detected \u2014 internal/metadata URL blocked"
|
|
4281
|
+
});
|
|
4282
|
+
}
|
|
4283
|
+
if (config.sqlInjection && detectSQLInjection(value)) {
|
|
4284
|
+
threats.push({
|
|
4285
|
+
type: "SQL_INJECTION",
|
|
4286
|
+
field,
|
|
4287
|
+
value: truncate(value, 100),
|
|
4288
|
+
description: "SQL injection pattern detected"
|
|
4289
|
+
});
|
|
4290
|
+
}
|
|
4291
|
+
return { safe: threats.length === 0, threats };
|
|
4292
|
+
}
|
|
4293
|
+
function sanitizeObject(basePath, obj, config) {
|
|
4294
|
+
const threats = [];
|
|
4295
|
+
if (Array.isArray(obj)) {
|
|
4296
|
+
for (let i = 0; i < obj.length; i++) {
|
|
4297
|
+
const result = sanitizeInput(`${basePath}[${i}]`, obj[i], config);
|
|
4298
|
+
threats.push(...result.threats);
|
|
4299
|
+
}
|
|
4300
|
+
} else {
|
|
4301
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
4302
|
+
const result = sanitizeInput(`${basePath}.${key}`, val, config);
|
|
4303
|
+
threats.push(...result.threats);
|
|
4304
|
+
}
|
|
4305
|
+
}
|
|
4306
|
+
return { safe: threats.length === 0, threats };
|
|
4307
|
+
}
|
|
4308
|
+
function truncate(str, maxLen) {
|
|
4309
|
+
return str.length > maxLen ? str.slice(0, maxLen) + "..." : str;
|
|
4310
|
+
}
|
|
4311
|
+
|
|
3869
4312
|
// src/proxy.ts
|
|
3870
4313
|
init_config();
|
|
3871
4314
|
|
|
@@ -4131,7 +4574,12 @@ var SolonGateProxy = class {
|
|
|
4131
4574
|
const apiUrl = this.config.apiUrl ?? "https://api.solongate.com";
|
|
4132
4575
|
if (this.config.apiKey) {
|
|
4133
4576
|
if (this.config.apiKey.startsWith("sg_test_")) {
|
|
4134
|
-
|
|
4577
|
+
const nodeEnv = process.env.NODE_ENV ?? "";
|
|
4578
|
+
if (nodeEnv === "production") {
|
|
4579
|
+
log2("ERROR: Test API keys (sg_test_) cannot be used in production. Use a sg_live_ key.");
|
|
4580
|
+
process.exit(1);
|
|
4581
|
+
}
|
|
4582
|
+
log2("Using test API key \u2014 skipping online validation (non-production mode).");
|
|
4135
4583
|
} else {
|
|
4136
4584
|
log2(`Validating license with ${apiUrl}...`);
|
|
4137
4585
|
try {
|
|
@@ -4332,7 +4780,26 @@ var SolonGateProxy = class {
|
|
|
4332
4780
|
});
|
|
4333
4781
|
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
4334
4782
|
if (!this.client) throw new Error("Upstream client disconnected");
|
|
4335
|
-
|
|
4783
|
+
const uri = request.params.uri;
|
|
4784
|
+
const uriCheck = sanitizeInput("resource.uri", uri);
|
|
4785
|
+
if (!uriCheck.safe) {
|
|
4786
|
+
const threats = uriCheck.threats.map((t) => `${t.type}: ${t.description}`).join("; ");
|
|
4787
|
+
log2(`DENY resource read: ${uri} \u2014 ${threats}`);
|
|
4788
|
+
throw new Error(`Resource URI blocked by security policy: ${threats}`);
|
|
4789
|
+
}
|
|
4790
|
+
if (/^file:\/\//i.test(uri)) {
|
|
4791
|
+
const path = uri.replace(/^file:\/\/\/?/i, "/");
|
|
4792
|
+
if (detectPathTraversal(path)) {
|
|
4793
|
+
log2(`DENY resource read: ${uri} \u2014 path traversal in file URI`);
|
|
4794
|
+
throw new Error("Resource URI blocked: path traversal detected");
|
|
4795
|
+
}
|
|
4796
|
+
}
|
|
4797
|
+
if (/^https?:\/\//i.test(uri) && detectSSRF(uri)) {
|
|
4798
|
+
log2(`DENY resource read: ${uri} \u2014 SSRF pattern`);
|
|
4799
|
+
throw new Error("Resource URI blocked: internal/metadata URL not allowed");
|
|
4800
|
+
}
|
|
4801
|
+
log2(`Resource read: ${uri}`);
|
|
4802
|
+
return await this.client.readResource({ uri });
|
|
4336
4803
|
});
|
|
4337
4804
|
this.server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
|
|
4338
4805
|
if (!this.client) return { resourceTemplates: [] };
|
|
@@ -4352,9 +4819,19 @@ var SolonGateProxy = class {
|
|
|
4352
4819
|
});
|
|
4353
4820
|
this.server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
4354
4821
|
if (!this.client) throw new Error("Upstream client disconnected");
|
|
4822
|
+
const args = request.params.arguments;
|
|
4823
|
+
if (args && typeof args === "object") {
|
|
4824
|
+
const argsCheck = sanitizeInput("prompt.arguments", args);
|
|
4825
|
+
if (!argsCheck.safe) {
|
|
4826
|
+
const threats = argsCheck.threats.map((t) => `${t.type}: ${t.description}`).join("; ");
|
|
4827
|
+
log2(`DENY prompt get: ${request.params.name} \u2014 ${threats}`);
|
|
4828
|
+
throw new Error(`Prompt arguments blocked by security policy: ${threats}`);
|
|
4829
|
+
}
|
|
4830
|
+
}
|
|
4831
|
+
log2(`Prompt get: ${request.params.name}`);
|
|
4355
4832
|
return await this.client.getPrompt({
|
|
4356
4833
|
name: request.params.name,
|
|
4357
|
-
arguments:
|
|
4834
|
+
arguments: args
|
|
4358
4835
|
});
|
|
4359
4836
|
});
|
|
4360
4837
|
}
|