@solongate/proxy 0.6.8 → 0.8.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/dist/create.js +3 -3
- package/dist/index.js +973 -372
- package/dist/init.js +9 -14
- package/dist/pull-push.js +180 -36
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -65,19 +65,16 @@ async function sendAuditLog(apiKey, apiUrl, entry) {
|
|
|
65
65
|
}
|
|
66
66
|
function loadPolicy(source) {
|
|
67
67
|
if (typeof source === "object") return source;
|
|
68
|
-
if (PRESETS[source]) return PRESETS[source];
|
|
69
68
|
const filePath = resolve(source);
|
|
70
69
|
if (existsSync(filePath)) {
|
|
71
70
|
const content = readFileSync(filePath, "utf-8");
|
|
72
71
|
return JSON.parse(content);
|
|
73
72
|
}
|
|
74
|
-
|
|
75
|
-
`Unknown policy "${source}". Use a preset (${Object.keys(PRESETS).join(", ")}), a JSON file path, or a PolicySet object.`
|
|
76
|
-
);
|
|
73
|
+
return DEFAULT_POLICY;
|
|
77
74
|
}
|
|
78
75
|
function parseArgs(argv) {
|
|
79
76
|
const args = argv.slice(2);
|
|
80
|
-
let policySource
|
|
77
|
+
let policySource;
|
|
81
78
|
let name = "solongate-proxy";
|
|
82
79
|
let verbose = false;
|
|
83
80
|
let rateLimitPerTool;
|
|
@@ -159,7 +156,7 @@ function parseArgs(argv) {
|
|
|
159
156
|
"Invalid API key format. Keys must start with 'sg_live_' or 'sg_test_'.\nGet your API key at https://solongate.com\n"
|
|
160
157
|
);
|
|
161
158
|
}
|
|
162
|
-
const resolvedPolicyPath = resolvePolicyPath(policySource);
|
|
159
|
+
const resolvedPolicyPath = policySource ? resolvePolicyPath(policySource) : null;
|
|
163
160
|
if (configFile) {
|
|
164
161
|
const filePath = resolve(configFile);
|
|
165
162
|
const content = readFileSync(filePath, "utf-8");
|
|
@@ -167,7 +164,7 @@ function parseArgs(argv) {
|
|
|
167
164
|
if (!fileConfig.upstream) {
|
|
168
165
|
throw new Error('Config file must include "upstream" with at least "command" or "url"');
|
|
169
166
|
}
|
|
170
|
-
const cfgPolicySource = fileConfig.policy ?? policySource;
|
|
167
|
+
const cfgPolicySource = fileConfig.policy ?? policySource ?? "policy.json";
|
|
171
168
|
return {
|
|
172
169
|
upstream: fileConfig.upstream,
|
|
173
170
|
policy: loadPolicy(cfgPolicySource),
|
|
@@ -191,7 +188,7 @@ function parseArgs(argv) {
|
|
|
191
188
|
// not used for URL-based transports
|
|
192
189
|
url: upstreamUrl
|
|
193
190
|
},
|
|
194
|
-
policy: loadPolicy(policySource),
|
|
191
|
+
policy: loadPolicy(policySource ?? "policy.json"),
|
|
195
192
|
name,
|
|
196
193
|
verbose,
|
|
197
194
|
rateLimitPerTool,
|
|
@@ -205,7 +202,7 @@ function parseArgs(argv) {
|
|
|
205
202
|
}
|
|
206
203
|
if (upstreamArgs.length === 0) {
|
|
207
204
|
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
|
|
205
|
+
"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
206
|
);
|
|
210
207
|
}
|
|
211
208
|
const [command, ...commandArgs] = upstreamArgs;
|
|
@@ -216,7 +213,7 @@ function parseArgs(argv) {
|
|
|
216
213
|
args: commandArgs,
|
|
217
214
|
env: { ...process.env }
|
|
218
215
|
},
|
|
219
|
-
policy: loadPolicy(policySource),
|
|
216
|
+
policy: loadPolicy(policySource ?? "policy.json"),
|
|
220
217
|
name,
|
|
221
218
|
verbose,
|
|
222
219
|
rateLimitPerTool,
|
|
@@ -229,303 +226,22 @@ function parseArgs(argv) {
|
|
|
229
226
|
};
|
|
230
227
|
}
|
|
231
228
|
function resolvePolicyPath(source) {
|
|
232
|
-
if (PRESETS[source]) return null;
|
|
233
229
|
const filePath = resolve(source);
|
|
234
230
|
if (existsSync(filePath)) return filePath;
|
|
235
231
|
return null;
|
|
236
232
|
}
|
|
237
|
-
var
|
|
233
|
+
var DEFAULT_POLICY;
|
|
238
234
|
var init_config = __esm({
|
|
239
235
|
"src/config.ts"() {
|
|
240
236
|
"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
|
-
}
|
|
237
|
+
DEFAULT_POLICY = {
|
|
238
|
+
id: "default",
|
|
239
|
+
name: "Default (Deny All)",
|
|
240
|
+
description: "Default-deny policy. Create rules in the dashboard or push a policy.json.",
|
|
241
|
+
version: 1,
|
|
242
|
+
rules: [],
|
|
243
|
+
createdAt: "",
|
|
244
|
+
updatedAt: ""
|
|
529
245
|
};
|
|
530
246
|
}
|
|
531
247
|
});
|
|
@@ -591,18 +307,14 @@ function isAlreadyProtected(server) {
|
|
|
591
307
|
function wrapServer(server, policy) {
|
|
592
308
|
const env = { ...server.env ?? {} };
|
|
593
309
|
env.SOLONGATE_API_KEY = "${SOLONGATE_API_KEY}";
|
|
310
|
+
const proxyArgs = ["-y", "@solongate/proxy"];
|
|
311
|
+
if (policy) {
|
|
312
|
+
proxyArgs.push("--policy", policy);
|
|
313
|
+
}
|
|
314
|
+
proxyArgs.push("--verbose", "--", server.command, ...server.args ?? []);
|
|
594
315
|
return {
|
|
595
316
|
command: "npx",
|
|
596
|
-
args:
|
|
597
|
-
"-y",
|
|
598
|
-
"@solongate/proxy",
|
|
599
|
-
"--policy",
|
|
600
|
-
policy,
|
|
601
|
-
"--verbose",
|
|
602
|
-
"--",
|
|
603
|
-
server.command,
|
|
604
|
-
...server.args ?? []
|
|
605
|
-
],
|
|
317
|
+
args: proxyArgs,
|
|
606
318
|
env
|
|
607
319
|
};
|
|
608
320
|
}
|
|
@@ -651,8 +363,7 @@ USAGE
|
|
|
651
363
|
|
|
652
364
|
OPTIONS
|
|
653
365
|
--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
|
|
366
|
+
--policy <file> Custom policy JSON file (auto-detects policy.json)
|
|
656
367
|
--api-key <key> SolonGate API key (sg_live_... or sg_test_...)
|
|
657
368
|
--all Protect all servers without prompting
|
|
658
369
|
-h, --help Show this help message
|
|
@@ -870,7 +581,7 @@ async function main() {
|
|
|
870
581
|
console.log(" Invalid API key format. Must start with sg_live_ or sg_test_");
|
|
871
582
|
process.exit(1);
|
|
872
583
|
}
|
|
873
|
-
let policyValue
|
|
584
|
+
let policyValue;
|
|
874
585
|
if (options.policy) {
|
|
875
586
|
const policyPath = resolve2(options.policy);
|
|
876
587
|
if (existsSync3(policyPath)) {
|
|
@@ -886,7 +597,7 @@ async function main() {
|
|
|
886
597
|
policyValue = "./policy.json";
|
|
887
598
|
console.log(` Policy: ${defaultPolicy} (auto-detected)`);
|
|
888
599
|
} else {
|
|
889
|
-
console.log(` Policy:
|
|
600
|
+
console.log(` Policy: cloud-managed (fetched via API key)`);
|
|
890
601
|
}
|
|
891
602
|
}
|
|
892
603
|
await sleep(300);
|
|
@@ -1726,7 +1437,7 @@ function parseCreateArgs(argv) {
|
|
|
1726
1437
|
const args = argv.slice(2);
|
|
1727
1438
|
const opts = {
|
|
1728
1439
|
name: "",
|
|
1729
|
-
policy: "
|
|
1440
|
+
policy: "",
|
|
1730
1441
|
noInstall: false
|
|
1731
1442
|
};
|
|
1732
1443
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -1769,13 +1480,13 @@ USAGE
|
|
|
1769
1480
|
npx @solongate/proxy create <name> [options]
|
|
1770
1481
|
|
|
1771
1482
|
OPTIONS
|
|
1772
|
-
--policy <
|
|
1483
|
+
--policy <file> Policy JSON file (default: cloud-managed)
|
|
1773
1484
|
--no-install Skip dependency installation
|
|
1774
1485
|
-h, --help Show this help message
|
|
1775
1486
|
|
|
1776
1487
|
EXAMPLES
|
|
1777
1488
|
npx @solongate/proxy create my-server
|
|
1778
|
-
npx @solongate/proxy create db-tools --policy
|
|
1489
|
+
npx @solongate/proxy create db-tools --policy ./policy.json
|
|
1779
1490
|
`);
|
|
1780
1491
|
}
|
|
1781
1492
|
function createProject(dir, name, _policy) {
|
|
@@ -2061,7 +1772,7 @@ function parseCliArgs() {
|
|
|
2061
1772
|
}
|
|
2062
1773
|
}
|
|
2063
1774
|
if (!apiKey) {
|
|
2064
|
-
log5("ERROR: API key not found.");
|
|
1775
|
+
log5(red("ERROR: API key not found."));
|
|
2065
1776
|
log5("");
|
|
2066
1777
|
log5("Set it in .env file:");
|
|
2067
1778
|
log5(" SOLONGATE_API_KEY=sg_live_...");
|
|
@@ -2071,53 +1782,169 @@ function parseCliArgs() {
|
|
|
2071
1782
|
process.exit(1);
|
|
2072
1783
|
}
|
|
2073
1784
|
if (!apiKey.startsWith("sg_live_")) {
|
|
2074
|
-
log5("ERROR: Pull/push requires a live API key (sg_live_...).");
|
|
1785
|
+
log5(red("ERROR: Pull/push/list requires a live API key (sg_live_...)."));
|
|
2075
1786
|
process.exit(1);
|
|
2076
1787
|
}
|
|
2077
1788
|
return { command, apiKey, file: resolve5(file), policyId };
|
|
2078
1789
|
}
|
|
2079
1790
|
async function listPolicies(apiKey) {
|
|
2080
|
-
const res = await fetch(
|
|
1791
|
+
const res = await fetch(`${API_URL}/api/v1/policies`, {
|
|
2081
1792
|
headers: { "Authorization": `Bearer ${apiKey}` }
|
|
2082
1793
|
});
|
|
2083
1794
|
if (!res.ok) throw new Error(`Failed to list policies (${res.status})`);
|
|
2084
1795
|
const data = await res.json();
|
|
2085
1796
|
return data.policies ?? [];
|
|
2086
1797
|
}
|
|
1798
|
+
async function list(apiKey, policyId) {
|
|
1799
|
+
const policies = await listPolicies(apiKey);
|
|
1800
|
+
if (policies.length === 0) {
|
|
1801
|
+
log5(yellow("No policies found. Create one in the dashboard first."));
|
|
1802
|
+
log5(dim(" https://dashboard.solongate.com/policies"));
|
|
1803
|
+
return;
|
|
1804
|
+
}
|
|
1805
|
+
if (policyId) {
|
|
1806
|
+
const match = policies.find((p) => p.id === policyId);
|
|
1807
|
+
if (!match) {
|
|
1808
|
+
log5(red(`Policy not found: ${policyId}`));
|
|
1809
|
+
log5("");
|
|
1810
|
+
log5("Available policies:");
|
|
1811
|
+
for (const p of policies) {
|
|
1812
|
+
log5(` ${dim("\u2022")} ${p.id}`);
|
|
1813
|
+
}
|
|
1814
|
+
process.exit(1);
|
|
1815
|
+
}
|
|
1816
|
+
const full = await fetchCloudPolicy(apiKey, API_URL, policyId);
|
|
1817
|
+
printPolicyDetail(full);
|
|
1818
|
+
return;
|
|
1819
|
+
}
|
|
1820
|
+
log5("");
|
|
1821
|
+
log5(bold(` Policies (${policies.length})`));
|
|
1822
|
+
log5(dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1823
|
+
log5("");
|
|
1824
|
+
for (const p of policies) {
|
|
1825
|
+
try {
|
|
1826
|
+
const full = await fetchCloudPolicy(apiKey, API_URL, p.id);
|
|
1827
|
+
printPolicySummary(p, full.rules);
|
|
1828
|
+
} catch {
|
|
1829
|
+
printPolicySummary(p, []);
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
log5(dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1833
|
+
log5("");
|
|
1834
|
+
log5(` ${dim("View details:")} solongate-proxy list --policy-id <ID>`);
|
|
1835
|
+
log5(` ${dim("Pull policy:")} solongate-proxy pull --policy-id <ID>`);
|
|
1836
|
+
log5(` ${dim("Push policy:")} solongate-proxy push --policy-id <ID>`);
|
|
1837
|
+
log5("");
|
|
1838
|
+
}
|
|
1839
|
+
function printPolicySummary(p, rules) {
|
|
1840
|
+
const ruleCount = rules.length;
|
|
1841
|
+
const allowCount = rules.filter((r) => r.effect === "ALLOW").length;
|
|
1842
|
+
const denyCount = rules.filter((r) => r.effect === "DENY").length;
|
|
1843
|
+
log5(` ${cyan(p.id)}`);
|
|
1844
|
+
log5(` ${bold(p.name)} ${dim(`v${p.version ?? "?"}`)}`);
|
|
1845
|
+
log5(` ${dim("Rules:")} ${ruleCount} ${green(`${allowCount} ALLOW`)} ${red(`${denyCount} DENY`)}`);
|
|
1846
|
+
if (p.created_at) {
|
|
1847
|
+
log5(` ${dim("Updated:")} ${new Date(p.created_at).toLocaleString()}`);
|
|
1848
|
+
}
|
|
1849
|
+
log5("");
|
|
1850
|
+
}
|
|
1851
|
+
function printPolicyDetail(policy) {
|
|
1852
|
+
log5("");
|
|
1853
|
+
log5(bold(` ${policy.name}`));
|
|
1854
|
+
log5(` ${dim("ID:")} ${cyan(policy.id)} ${dim("Version:")} ${policy.version} ${dim("Rules:")} ${policy.rules.length}`);
|
|
1855
|
+
log5("");
|
|
1856
|
+
if (policy.rules.length === 0) {
|
|
1857
|
+
log5(yellow(" No rules defined."));
|
|
1858
|
+
log5("");
|
|
1859
|
+
return;
|
|
1860
|
+
}
|
|
1861
|
+
log5(dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1862
|
+
for (const rule of policy.rules) {
|
|
1863
|
+
const effectColor = rule.effect === "ALLOW" ? green : red;
|
|
1864
|
+
log5("");
|
|
1865
|
+
log5(` ${effectColor(rule.effect.padEnd(5))} ${bold(rule.toolPattern)} ${dim(`P:${rule.priority}`)}`);
|
|
1866
|
+
if (rule.description) {
|
|
1867
|
+
log5(` ${dim(rule.description)}`);
|
|
1868
|
+
}
|
|
1869
|
+
log5(` ${dim(`${rule.permission} trust:${rule.minimumTrustLevel || "UNTRUSTED"}`)}`);
|
|
1870
|
+
if (rule.pathConstraints) {
|
|
1871
|
+
const pc = rule.pathConstraints;
|
|
1872
|
+
if (pc.rootDirectory) log5(` ${magenta("ROOT")} ${pc.rootDirectory}`);
|
|
1873
|
+
if (pc.allowed?.length) log5(` ${green("PATHS")} ${pc.allowed.join(", ")}`);
|
|
1874
|
+
if (pc.denied?.length) log5(` ${red("DENY")} ${pc.denied.join(", ")}`);
|
|
1875
|
+
}
|
|
1876
|
+
if (rule.commandConstraints) {
|
|
1877
|
+
const cc = rule.commandConstraints;
|
|
1878
|
+
if (cc.allowed?.length) log5(` ${green("CMDS")} ${cc.allowed.join(", ")}`);
|
|
1879
|
+
if (cc.denied?.length) log5(` ${red("DENY")} ${cc.denied.join(", ")}`);
|
|
1880
|
+
}
|
|
1881
|
+
if (rule.filenameConstraints) {
|
|
1882
|
+
const fc = rule.filenameConstraints;
|
|
1883
|
+
if (fc.allowed?.length) log5(` ${green("FILES")} ${fc.allowed.join(", ")}`);
|
|
1884
|
+
if (fc.denied?.length) log5(` ${red("DENY")} ${fc.denied.join(", ")}`);
|
|
1885
|
+
}
|
|
1886
|
+
if (rule.urlConstraints) {
|
|
1887
|
+
const uc = rule.urlConstraints;
|
|
1888
|
+
if (uc.allowed?.length) log5(` ${green("URLS")} ${uc.allowed.join(", ")}`);
|
|
1889
|
+
if (uc.denied?.length) log5(` ${red("DENY")} ${uc.denied.join(", ")}`);
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
log5("");
|
|
1893
|
+
log5(dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1894
|
+
log5("");
|
|
1895
|
+
}
|
|
2087
1896
|
async function pull(apiKey, file, policyId) {
|
|
2088
|
-
const apiUrl = "https://api.solongate.com";
|
|
2089
1897
|
if (!policyId) {
|
|
2090
1898
|
const policies = await listPolicies(apiKey);
|
|
2091
1899
|
if (policies.length === 0) {
|
|
2092
|
-
log5("No policies found. Create one in the dashboard first.");
|
|
1900
|
+
log5(red("No policies found. Create one in the dashboard first."));
|
|
2093
1901
|
process.exit(1);
|
|
2094
1902
|
}
|
|
2095
|
-
if (policies.length
|
|
2096
|
-
|
|
2097
|
-
`);
|
|
1903
|
+
if (policies.length === 1) {
|
|
1904
|
+
policyId = policies[0].id;
|
|
1905
|
+
log5(dim(`Auto-selecting only policy: ${policyId}`));
|
|
1906
|
+
} else {
|
|
1907
|
+
log5(yellow(`Found ${policies.length} policies:`));
|
|
1908
|
+
log5("");
|
|
2098
1909
|
for (const p of policies) {
|
|
2099
|
-
log5(` ${p.id} ${p.name} (${
|
|
1910
|
+
log5(` ${cyan(p.id)} ${p.name} ${dim(`v${p.version ?? "?"}`)}`);
|
|
2100
1911
|
}
|
|
2101
1912
|
log5("");
|
|
2102
1913
|
log5("Use --policy-id <ID> to specify which one to pull.");
|
|
2103
1914
|
process.exit(1);
|
|
2104
1915
|
}
|
|
2105
1916
|
}
|
|
2106
|
-
log5(`Pulling
|
|
2107
|
-
const policy = await fetchCloudPolicy(apiKey,
|
|
2108
|
-
const
|
|
1917
|
+
log5(`Pulling ${cyan(policyId)} from dashboard...`);
|
|
1918
|
+
const policy = await fetchCloudPolicy(apiKey, API_URL, policyId);
|
|
1919
|
+
const { id: _id, ...policyWithoutId } = policy;
|
|
1920
|
+
const json = JSON.stringify(policyWithoutId, null, 2) + "\n";
|
|
2109
1921
|
writeFileSync5(file, json, "utf-8");
|
|
2110
|
-
log5(`Saved: ${file}`);
|
|
2111
|
-
log5(` Name: ${policy.name}`);
|
|
2112
|
-
log5(` Version: ${policy.version}`);
|
|
2113
|
-
log5(` Rules: ${policy.rules.length}`);
|
|
2114
1922
|
log5("");
|
|
2115
|
-
log5("
|
|
1923
|
+
log5(green(" Saved to: ") + file);
|
|
1924
|
+
log5(` ${dim("Name:")} ${policy.name}`);
|
|
1925
|
+
log5(` ${dim("Version:")} ${policy.version}`);
|
|
1926
|
+
log5(` ${dim("Rules:")} ${policy.rules.length}`);
|
|
1927
|
+
log5("");
|
|
1928
|
+
log5(dim("The policy file does not contain an ID."));
|
|
1929
|
+
log5(dim("Use --policy-id to specify the target when pushing/pulling."));
|
|
1930
|
+
log5("");
|
|
2116
1931
|
}
|
|
2117
|
-
async function push(apiKey, file) {
|
|
2118
|
-
const apiUrl = "https://api.solongate.com";
|
|
1932
|
+
async function push(apiKey, file, policyId) {
|
|
2119
1933
|
if (!existsSync6(file)) {
|
|
2120
|
-
log5(`ERROR: File not found: ${file}`);
|
|
1934
|
+
log5(red(`ERROR: File not found: ${file}`));
|
|
1935
|
+
process.exit(1);
|
|
1936
|
+
}
|
|
1937
|
+
if (!policyId) {
|
|
1938
|
+
log5(red("ERROR: --policy-id is required for push."));
|
|
1939
|
+
log5("");
|
|
1940
|
+
log5("This determines which cloud policy to update.");
|
|
1941
|
+
log5("");
|
|
1942
|
+
log5("Usage:");
|
|
1943
|
+
log5(" solongate-proxy push --policy-id my-policy");
|
|
1944
|
+
log5(" solongate-proxy push --policy-id my-policy --file custom.json");
|
|
1945
|
+
log5("");
|
|
1946
|
+
log5("List your policies:");
|
|
1947
|
+
log5(" solongate-proxy list");
|
|
2121
1948
|
process.exit(1);
|
|
2122
1949
|
}
|
|
2123
1950
|
const content = readFileSync5(file, "utf-8");
|
|
@@ -2125,21 +1952,26 @@ async function push(apiKey, file) {
|
|
|
2125
1952
|
try {
|
|
2126
1953
|
policy = JSON.parse(content);
|
|
2127
1954
|
} catch {
|
|
2128
|
-
log5(`ERROR: Invalid JSON in ${file}`);
|
|
1955
|
+
log5(red(`ERROR: Invalid JSON in ${file}`));
|
|
2129
1956
|
process.exit(1);
|
|
2130
1957
|
}
|
|
2131
|
-
log5(`Pushing
|
|
2132
|
-
log5(` File:
|
|
2133
|
-
log5(` Name:
|
|
2134
|
-
log5(` Rules: ${(policy.rules || []).length}`);
|
|
2135
|
-
const
|
|
2136
|
-
|
|
1958
|
+
log5(`Pushing to ${cyan(policyId)}...`);
|
|
1959
|
+
log5(` ${dim("File:")} ${file}`);
|
|
1960
|
+
log5(` ${dim("Name:")} ${policy.name || "Unnamed"}`);
|
|
1961
|
+
log5(` ${dim("Rules:")} ${(policy.rules || []).length}`);
|
|
1962
|
+
const checkRes = await fetch(`${API_URL}/api/v1/policies/${policyId}`, {
|
|
1963
|
+
headers: { "Authorization": `Bearer ${apiKey}` }
|
|
1964
|
+
});
|
|
1965
|
+
const method = checkRes.ok ? "PUT" : "POST";
|
|
1966
|
+
const url = checkRes.ok ? `${API_URL}/api/v1/policies/${policyId}` : `${API_URL}/api/v1/policies`;
|
|
1967
|
+
const res = await fetch(url, {
|
|
1968
|
+
method,
|
|
2137
1969
|
headers: {
|
|
2138
1970
|
"Authorization": `Bearer ${apiKey}`,
|
|
2139
1971
|
"Content-Type": "application/json"
|
|
2140
1972
|
},
|
|
2141
1973
|
body: JSON.stringify({
|
|
2142
|
-
id:
|
|
1974
|
+
id: policyId,
|
|
2143
1975
|
name: policy.name || "Local Policy",
|
|
2144
1976
|
description: policy.description || "Pushed from local file",
|
|
2145
1977
|
version: policy.version || 1,
|
|
@@ -2148,13 +1980,15 @@ async function push(apiKey, file) {
|
|
|
2148
1980
|
});
|
|
2149
1981
|
if (!res.ok) {
|
|
2150
1982
|
const body = await res.text().catch(() => "");
|
|
2151
|
-
log5(`ERROR: Push failed (${res.status}): ${body}`);
|
|
1983
|
+
log5(red(`ERROR: Push failed (${res.status}): ${body}`));
|
|
2152
1984
|
process.exit(1);
|
|
2153
1985
|
}
|
|
2154
1986
|
const data = await res.json();
|
|
2155
|
-
log5(` Cloud version: ${data._version ?? "created"}`);
|
|
2156
1987
|
log5("");
|
|
2157
|
-
log5(
|
|
1988
|
+
log5(green(` Pushed to cloud: v${data._version ?? "created"}`));
|
|
1989
|
+
log5(` ${dim("Policy ID:")} ${policyId}`);
|
|
1990
|
+
log5(` ${dim("Method:")} ${method === "PUT" ? "Updated existing" : "Created new"}`);
|
|
1991
|
+
log5("");
|
|
2158
1992
|
}
|
|
2159
1993
|
async function main4() {
|
|
2160
1994
|
const { command, apiKey, file, policyId } = parseCliArgs();
|
|
@@ -2162,24 +1996,45 @@ async function main4() {
|
|
|
2162
1996
|
if (command === "pull") {
|
|
2163
1997
|
await pull(apiKey, file, policyId);
|
|
2164
1998
|
} else if (command === "push") {
|
|
2165
|
-
await push(apiKey, file);
|
|
1999
|
+
await push(apiKey, file, policyId);
|
|
2000
|
+
} else if (command === "list" || command === "ls") {
|
|
2001
|
+
await list(apiKey, policyId);
|
|
2166
2002
|
} else {
|
|
2167
|
-
log5(`Unknown command: ${command}`);
|
|
2168
|
-
log5("
|
|
2003
|
+
log5(red(`Unknown command: ${command}`));
|
|
2004
|
+
log5("");
|
|
2005
|
+
log5(bold("Usage:"));
|
|
2006
|
+
log5(" solongate-proxy list List all policies");
|
|
2007
|
+
log5(" solongate-proxy list --policy-id <ID> Show policy details");
|
|
2008
|
+
log5(" solongate-proxy pull --policy-id <ID> Pull policy to local file");
|
|
2009
|
+
log5(" solongate-proxy push --policy-id <ID> Push local file to cloud");
|
|
2010
|
+
log5("");
|
|
2011
|
+
log5(bold("Flags:"));
|
|
2012
|
+
log5(" --policy-id, --id <ID> Cloud policy ID (required for push)");
|
|
2013
|
+
log5(" --file, -f <path> Local file path (default: policy.json)");
|
|
2014
|
+
log5(" --api-key <key> API key (or set SOLONGATE_API_KEY)");
|
|
2015
|
+
log5("");
|
|
2169
2016
|
process.exit(1);
|
|
2170
2017
|
}
|
|
2171
2018
|
} catch (err) {
|
|
2172
|
-
log5(`ERROR: ${err instanceof Error ? err.message : String(err)}`);
|
|
2019
|
+
log5(red(`ERROR: ${err instanceof Error ? err.message : String(err)}`));
|
|
2173
2020
|
process.exit(1);
|
|
2174
2021
|
}
|
|
2175
2022
|
}
|
|
2176
|
-
var log5;
|
|
2023
|
+
var log5, dim, bold, green, red, yellow, cyan, magenta, API_URL;
|
|
2177
2024
|
var init_pull_push = __esm({
|
|
2178
2025
|
"src/pull-push.ts"() {
|
|
2179
2026
|
"use strict";
|
|
2180
2027
|
init_config();
|
|
2181
|
-
log5 = (...args) => process.stderr.write(
|
|
2028
|
+
log5 = (...args) => process.stderr.write(`${args.map(String).join(" ")}
|
|
2182
2029
|
`);
|
|
2030
|
+
dim = (s) => `\x1B[2m${s}\x1B[0m`;
|
|
2031
|
+
bold = (s) => `\x1B[1m${s}\x1B[0m`;
|
|
2032
|
+
green = (s) => `\x1B[32m${s}\x1B[0m`;
|
|
2033
|
+
red = (s) => `\x1B[31m${s}\x1B[0m`;
|
|
2034
|
+
yellow = (s) => `\x1B[33m${s}\x1B[0m`;
|
|
2035
|
+
cyan = (s) => `\x1B[36m${s}\x1B[0m`;
|
|
2036
|
+
magenta = (s) => `\x1B[35m${s}\x1B[0m`;
|
|
2037
|
+
API_URL = "https://api.solongate.com";
|
|
2183
2038
|
main4();
|
|
2184
2039
|
}
|
|
2185
2040
|
});
|
|
@@ -2296,6 +2151,14 @@ var PolicyRuleSchema = z.object({
|
|
|
2296
2151
|
allowed: z.array(z.string()).optional(),
|
|
2297
2152
|
denied: z.array(z.string()).optional()
|
|
2298
2153
|
}).optional(),
|
|
2154
|
+
filenameConstraints: z.object({
|
|
2155
|
+
allowed: z.array(z.string()).optional(),
|
|
2156
|
+
denied: z.array(z.string()).optional()
|
|
2157
|
+
}).optional(),
|
|
2158
|
+
urlConstraints: z.object({
|
|
2159
|
+
allowed: z.array(z.string()).optional(),
|
|
2160
|
+
denied: z.array(z.string()).optional()
|
|
2161
|
+
}).optional(),
|
|
2299
2162
|
enabled: z.boolean().default(true),
|
|
2300
2163
|
createdAt: z.string().datetime(),
|
|
2301
2164
|
updatedAt: z.string().datetime()
|
|
@@ -2481,11 +2344,48 @@ function matchSegmentGlob(segment, pattern) {
|
|
|
2481
2344
|
}
|
|
2482
2345
|
return segment === pattern;
|
|
2483
2346
|
}
|
|
2347
|
+
var PATH_FIELDS = /* @__PURE__ */ new Set([
|
|
2348
|
+
"path",
|
|
2349
|
+
"file",
|
|
2350
|
+
"file_path",
|
|
2351
|
+
"filepath",
|
|
2352
|
+
"filename",
|
|
2353
|
+
"directory",
|
|
2354
|
+
"dir",
|
|
2355
|
+
"folder",
|
|
2356
|
+
"source",
|
|
2357
|
+
"destination",
|
|
2358
|
+
"dest",
|
|
2359
|
+
"target",
|
|
2360
|
+
"input",
|
|
2361
|
+
"output",
|
|
2362
|
+
"cwd",
|
|
2363
|
+
"root",
|
|
2364
|
+
"notebook_path"
|
|
2365
|
+
]);
|
|
2484
2366
|
function extractPathArguments(args) {
|
|
2485
2367
|
const paths = [];
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2368
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2369
|
+
function addPath(value) {
|
|
2370
|
+
const trimmed = value.trim();
|
|
2371
|
+
if (trimmed && !seen.has(trimmed)) {
|
|
2372
|
+
seen.add(trimmed);
|
|
2373
|
+
paths.push(trimmed);
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
for (const [key, value] of Object.entries(args)) {
|
|
2377
|
+
if (typeof value !== "string") continue;
|
|
2378
|
+
if (PATH_FIELDS.has(key.toLowerCase())) {
|
|
2379
|
+
addPath(value);
|
|
2380
|
+
continue;
|
|
2381
|
+
}
|
|
2382
|
+
if (value.includes("/") || value.includes("\\")) {
|
|
2383
|
+
addPath(value);
|
|
2384
|
+
continue;
|
|
2385
|
+
}
|
|
2386
|
+
if (value.startsWith(".")) {
|
|
2387
|
+
addPath(value);
|
|
2388
|
+
continue;
|
|
2489
2389
|
}
|
|
2490
2390
|
}
|
|
2491
2391
|
return paths;
|
|
@@ -2499,15 +2399,62 @@ var COMMAND_FIELDS = /* @__PURE__ */ new Set([
|
|
|
2499
2399
|
"shell",
|
|
2500
2400
|
"exec",
|
|
2501
2401
|
"sql",
|
|
2502
|
-
"expression"
|
|
2402
|
+
"expression",
|
|
2403
|
+
"function"
|
|
2503
2404
|
]);
|
|
2405
|
+
var COMMAND_HEURISTICS = [
|
|
2406
|
+
/^(sh|bash|cmd|powershell|zsh|fish)\s+-c\s+/i,
|
|
2407
|
+
// shell -c "..."
|
|
2408
|
+
/^(sudo|doas)\s+/i,
|
|
2409
|
+
// privilege escalation
|
|
2410
|
+
/^\w+\s+&&\s+/,
|
|
2411
|
+
// cmd1 && cmd2
|
|
2412
|
+
/^\w+\s*\|\s*\w+/,
|
|
2413
|
+
// cmd1 | cmd2
|
|
2414
|
+
/^\w+\s*;\s*\w+/,
|
|
2415
|
+
// cmd1; cmd2
|
|
2416
|
+
/^(curl|wget|nc|ncat)\s+/i,
|
|
2417
|
+
// network commands
|
|
2418
|
+
/^(rm|del|rmdir)\s+/i,
|
|
2419
|
+
// destructive commands
|
|
2420
|
+
/^(cat|type|more|less)\s+.*[/\\]/i
|
|
2421
|
+
// file read commands with paths
|
|
2422
|
+
];
|
|
2504
2423
|
function extractCommandArguments(args) {
|
|
2505
2424
|
const commands = [];
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2425
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2426
|
+
function addCommand(value) {
|
|
2427
|
+
const trimmed = value.trim();
|
|
2428
|
+
if (trimmed && !seen.has(trimmed)) {
|
|
2429
|
+
seen.add(trimmed);
|
|
2430
|
+
commands.push(trimmed);
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
function scanValue(key, value) {
|
|
2434
|
+
if (typeof value === "string") {
|
|
2435
|
+
if (COMMAND_FIELDS.has(key.toLowerCase())) {
|
|
2436
|
+
addCommand(value);
|
|
2437
|
+
return;
|
|
2438
|
+
}
|
|
2439
|
+
for (const pattern of COMMAND_HEURISTICS) {
|
|
2440
|
+
if (pattern.test(value)) {
|
|
2441
|
+
addCommand(value);
|
|
2442
|
+
return;
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2510
2445
|
}
|
|
2446
|
+
if (Array.isArray(value)) {
|
|
2447
|
+
for (const item of value) {
|
|
2448
|
+
scanValue(key, item);
|
|
2449
|
+
}
|
|
2450
|
+
} else if (typeof value === "object" && value !== null) {
|
|
2451
|
+
for (const [k, v] of Object.entries(value)) {
|
|
2452
|
+
scanValue(k, v);
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
for (const [key, value] of Object.entries(args)) {
|
|
2457
|
+
scanValue(key, value);
|
|
2511
2458
|
}
|
|
2512
2459
|
return commands;
|
|
2513
2460
|
}
|
|
@@ -2553,6 +2500,224 @@ function isCommandAllowed(command, constraints) {
|
|
|
2553
2500
|
}
|
|
2554
2501
|
return true;
|
|
2555
2502
|
}
|
|
2503
|
+
function extractFilenames(args) {
|
|
2504
|
+
const filenames = [];
|
|
2505
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2506
|
+
function addFilename(name) {
|
|
2507
|
+
const trimmed = name.trim();
|
|
2508
|
+
if (trimmed && !seen.has(trimmed)) {
|
|
2509
|
+
seen.add(trimmed);
|
|
2510
|
+
filenames.push(trimmed);
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
function scanValue(value) {
|
|
2514
|
+
if (typeof value === "string") {
|
|
2515
|
+
const trimmed = value.trim();
|
|
2516
|
+
if (!trimmed) return;
|
|
2517
|
+
if (/^https?:\/\//i.test(trimmed)) return;
|
|
2518
|
+
if (trimmed.includes("/") || trimmed.includes("\\")) {
|
|
2519
|
+
const normalized = trimmed.replace(/\\/g, "/");
|
|
2520
|
+
const parts = normalized.split("/");
|
|
2521
|
+
const basename = parts[parts.length - 1];
|
|
2522
|
+
if (basename && basename.length > 0) {
|
|
2523
|
+
addFilename(basename);
|
|
2524
|
+
}
|
|
2525
|
+
return;
|
|
2526
|
+
}
|
|
2527
|
+
if (trimmed.includes(" ")) {
|
|
2528
|
+
for (const token of trimmed.split(/\s+/)) {
|
|
2529
|
+
if (token.includes("/") || token.includes("\\")) {
|
|
2530
|
+
const parts = token.replace(/\\/g, "/").split("/");
|
|
2531
|
+
const basename = parts[parts.length - 1];
|
|
2532
|
+
if (basename && looksLikeFilename(basename)) addFilename(basename);
|
|
2533
|
+
} else if (looksLikeFilename(token)) {
|
|
2534
|
+
addFilename(token);
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
return;
|
|
2538
|
+
}
|
|
2539
|
+
if (looksLikeFilename(trimmed)) {
|
|
2540
|
+
addFilename(trimmed);
|
|
2541
|
+
}
|
|
2542
|
+
return;
|
|
2543
|
+
}
|
|
2544
|
+
if (Array.isArray(value)) {
|
|
2545
|
+
for (const item of value) {
|
|
2546
|
+
scanValue(item);
|
|
2547
|
+
}
|
|
2548
|
+
} else if (typeof value === "object" && value !== null) {
|
|
2549
|
+
for (const v of Object.values(value)) {
|
|
2550
|
+
scanValue(v);
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
for (const value of Object.values(args)) {
|
|
2555
|
+
scanValue(value);
|
|
2556
|
+
}
|
|
2557
|
+
return filenames;
|
|
2558
|
+
}
|
|
2559
|
+
function looksLikeFilename(s) {
|
|
2560
|
+
if (s.startsWith(".")) return true;
|
|
2561
|
+
if (/\.\w+$/.test(s)) return true;
|
|
2562
|
+
const knownFiles = /* @__PURE__ */ new Set([
|
|
2563
|
+
"id_rsa",
|
|
2564
|
+
"id_dsa",
|
|
2565
|
+
"id_ecdsa",
|
|
2566
|
+
"id_ed25519",
|
|
2567
|
+
"authorized_keys",
|
|
2568
|
+
"known_hosts",
|
|
2569
|
+
"makefile",
|
|
2570
|
+
"dockerfile",
|
|
2571
|
+
"vagrantfile",
|
|
2572
|
+
"gemfile",
|
|
2573
|
+
"rakefile",
|
|
2574
|
+
"procfile"
|
|
2575
|
+
]);
|
|
2576
|
+
if (knownFiles.has(s.toLowerCase())) return true;
|
|
2577
|
+
return false;
|
|
2578
|
+
}
|
|
2579
|
+
function matchFilenamePattern(filename, pattern) {
|
|
2580
|
+
if (pattern === "*") return true;
|
|
2581
|
+
const normalizedFilename = filename.toLowerCase();
|
|
2582
|
+
const normalizedPattern = pattern.toLowerCase();
|
|
2583
|
+
if (normalizedFilename === normalizedPattern) return true;
|
|
2584
|
+
const startsWithStar = normalizedPattern.startsWith("*");
|
|
2585
|
+
const endsWithStar = normalizedPattern.endsWith("*");
|
|
2586
|
+
if (startsWithStar && endsWithStar) {
|
|
2587
|
+
const infix = normalizedPattern.slice(1, -1);
|
|
2588
|
+
return infix.length > 0 && normalizedFilename.includes(infix);
|
|
2589
|
+
}
|
|
2590
|
+
if (startsWithStar) {
|
|
2591
|
+
const suffix = normalizedPattern.slice(1);
|
|
2592
|
+
return normalizedFilename.endsWith(suffix);
|
|
2593
|
+
}
|
|
2594
|
+
if (endsWithStar) {
|
|
2595
|
+
const prefix = normalizedPattern.slice(0, -1);
|
|
2596
|
+
return normalizedFilename.startsWith(prefix);
|
|
2597
|
+
}
|
|
2598
|
+
const starIdx = normalizedPattern.indexOf("*");
|
|
2599
|
+
if (starIdx !== -1) {
|
|
2600
|
+
const prefix = normalizedPattern.slice(0, starIdx);
|
|
2601
|
+
const suffix = normalizedPattern.slice(starIdx + 1);
|
|
2602
|
+
return normalizedFilename.startsWith(prefix) && normalizedFilename.endsWith(suffix) && normalizedFilename.length >= prefix.length + suffix.length;
|
|
2603
|
+
}
|
|
2604
|
+
return false;
|
|
2605
|
+
}
|
|
2606
|
+
function isFilenameAllowed(filename, constraints) {
|
|
2607
|
+
if (constraints.denied && constraints.denied.length > 0) {
|
|
2608
|
+
for (const pattern of constraints.denied) {
|
|
2609
|
+
if (matchFilenamePattern(filename, pattern)) {
|
|
2610
|
+
return false;
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
if (constraints.allowed && constraints.allowed.length > 0) {
|
|
2615
|
+
let matchesAllowed = false;
|
|
2616
|
+
for (const pattern of constraints.allowed) {
|
|
2617
|
+
if (matchFilenamePattern(filename, pattern)) {
|
|
2618
|
+
matchesAllowed = true;
|
|
2619
|
+
break;
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
if (!matchesAllowed) return false;
|
|
2623
|
+
}
|
|
2624
|
+
return true;
|
|
2625
|
+
}
|
|
2626
|
+
var URL_FIELDS = /* @__PURE__ */ new Set([
|
|
2627
|
+
"url",
|
|
2628
|
+
"href",
|
|
2629
|
+
"uri",
|
|
2630
|
+
"endpoint",
|
|
2631
|
+
"link",
|
|
2632
|
+
"src",
|
|
2633
|
+
"source",
|
|
2634
|
+
"target",
|
|
2635
|
+
"redirect",
|
|
2636
|
+
"callback",
|
|
2637
|
+
"webhook"
|
|
2638
|
+
]);
|
|
2639
|
+
function extractUrlArguments(args) {
|
|
2640
|
+
const urls = [];
|
|
2641
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2642
|
+
function addUrl(value) {
|
|
2643
|
+
const trimmed = value.trim();
|
|
2644
|
+
if (trimmed && !seen.has(trimmed)) {
|
|
2645
|
+
seen.add(trimmed);
|
|
2646
|
+
urls.push(trimmed);
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
function scanValue(key, value) {
|
|
2650
|
+
if (typeof value === "string") {
|
|
2651
|
+
const lower = key.toLowerCase();
|
|
2652
|
+
if (URL_FIELDS.has(lower)) {
|
|
2653
|
+
addUrl(value);
|
|
2654
|
+
return;
|
|
2655
|
+
}
|
|
2656
|
+
if (/https?:\/\//i.test(value)) {
|
|
2657
|
+
addUrl(value);
|
|
2658
|
+
return;
|
|
2659
|
+
}
|
|
2660
|
+
if (/^[a-zA-Z0-9]([a-zA-Z0-9-]*\.)+[a-zA-Z]{2,}(\/.*)?$/.test(value)) {
|
|
2661
|
+
addUrl(value);
|
|
2662
|
+
return;
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
if (Array.isArray(value)) {
|
|
2666
|
+
for (const item of value) {
|
|
2667
|
+
scanValue(key, item);
|
|
2668
|
+
}
|
|
2669
|
+
} else if (typeof value === "object" && value !== null) {
|
|
2670
|
+
for (const [k, v] of Object.entries(value)) {
|
|
2671
|
+
scanValue(k, v);
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
for (const [key, value] of Object.entries(args)) {
|
|
2676
|
+
scanValue(key, value);
|
|
2677
|
+
}
|
|
2678
|
+
return urls;
|
|
2679
|
+
}
|
|
2680
|
+
function matchUrlPattern(url, pattern) {
|
|
2681
|
+
if (pattern === "*") return true;
|
|
2682
|
+
const normalizedUrl = url.trim().toLowerCase();
|
|
2683
|
+
const normalizedPattern = pattern.trim().toLowerCase();
|
|
2684
|
+
if (normalizedPattern === normalizedUrl) return true;
|
|
2685
|
+
const startsWithStar = normalizedPattern.startsWith("*");
|
|
2686
|
+
const endsWithStar = normalizedPattern.endsWith("*");
|
|
2687
|
+
if (startsWithStar && endsWithStar) {
|
|
2688
|
+
const infix = normalizedPattern.slice(1, -1);
|
|
2689
|
+
return infix.length > 0 && normalizedUrl.includes(infix);
|
|
2690
|
+
}
|
|
2691
|
+
if (endsWithStar) {
|
|
2692
|
+
const prefix = normalizedPattern.slice(0, -1);
|
|
2693
|
+
return normalizedUrl.startsWith(prefix);
|
|
2694
|
+
}
|
|
2695
|
+
if (startsWithStar) {
|
|
2696
|
+
const suffix = normalizedPattern.slice(1);
|
|
2697
|
+
return normalizedUrl.endsWith(suffix);
|
|
2698
|
+
}
|
|
2699
|
+
return false;
|
|
2700
|
+
}
|
|
2701
|
+
function isUrlAllowed(url, constraints) {
|
|
2702
|
+
if (constraints.denied && constraints.denied.length > 0) {
|
|
2703
|
+
for (const pattern of constraints.denied) {
|
|
2704
|
+
if (matchUrlPattern(url, pattern)) {
|
|
2705
|
+
return false;
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
if (constraints.allowed && constraints.allowed.length > 0) {
|
|
2710
|
+
let matchesAllowed = false;
|
|
2711
|
+
for (const pattern of constraints.allowed) {
|
|
2712
|
+
if (matchUrlPattern(url, pattern)) {
|
|
2713
|
+
matchesAllowed = true;
|
|
2714
|
+
break;
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
if (!matchesAllowed) return false;
|
|
2718
|
+
}
|
|
2719
|
+
return true;
|
|
2720
|
+
}
|
|
2556
2721
|
function ruleMatchesRequest(rule, request) {
|
|
2557
2722
|
if (!rule.enabled) return false;
|
|
2558
2723
|
if (rule.permission !== request.requiredPermission) return false;
|
|
@@ -2581,6 +2746,22 @@ function ruleMatchesRequest(rule, request) {
|
|
|
2581
2746
|
if (!satisfied) return false;
|
|
2582
2747
|
}
|
|
2583
2748
|
}
|
|
2749
|
+
if (rule.filenameConstraints) {
|
|
2750
|
+
const satisfied = filenameConstraintsMatch(rule.filenameConstraints, request.arguments);
|
|
2751
|
+
if (rule.effect === "DENY") {
|
|
2752
|
+
if (satisfied) return false;
|
|
2753
|
+
} else {
|
|
2754
|
+
if (!satisfied) return false;
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
if (rule.urlConstraints) {
|
|
2758
|
+
const satisfied = urlConstraintsMatch(rule.urlConstraints, request.arguments);
|
|
2759
|
+
if (rule.effect === "DENY") {
|
|
2760
|
+
if (satisfied) return false;
|
|
2761
|
+
} else {
|
|
2762
|
+
if (!satisfied) return false;
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2584
2765
|
return true;
|
|
2585
2766
|
}
|
|
2586
2767
|
function toolPatternMatches(pattern, toolName) {
|
|
@@ -2671,6 +2852,16 @@ function commandConstraintsMatch(constraints, args) {
|
|
|
2671
2852
|
if (commands.length === 0) return true;
|
|
2672
2853
|
return commands.every((cmd) => isCommandAllowed(cmd, constraints));
|
|
2673
2854
|
}
|
|
2855
|
+
function filenameConstraintsMatch(constraints, args) {
|
|
2856
|
+
const filenames = extractFilenames(args);
|
|
2857
|
+
if (filenames.length === 0) return true;
|
|
2858
|
+
return filenames.every((name) => isFilenameAllowed(name, constraints));
|
|
2859
|
+
}
|
|
2860
|
+
function urlConstraintsMatch(constraints, args) {
|
|
2861
|
+
const urls = extractUrlArguments(args);
|
|
2862
|
+
if (urls.length === 0) return true;
|
|
2863
|
+
return urls.every((url) => isUrlAllowed(url, constraints));
|
|
2864
|
+
}
|
|
2674
2865
|
function evaluatePolicy(policySet, request) {
|
|
2675
2866
|
const startTime = performance.now();
|
|
2676
2867
|
const sortedRules = [...policySet.rules].sort(
|
|
@@ -3051,6 +3242,11 @@ function resolveConfig(userConfig) {
|
|
|
3051
3242
|
"Token secret is shorter than 32 characters. Use a longer secret for production."
|
|
3052
3243
|
);
|
|
3053
3244
|
}
|
|
3245
|
+
if (config.apiUrl && config.apiUrl.startsWith("http://") && !config.apiUrl.startsWith("http://localhost") && !config.apiUrl.startsWith("http://127.0.0.1")) {
|
|
3246
|
+
warnings.push(
|
|
3247
|
+
"API URL uses plaintext HTTP. API keys will be sent unencrypted. Use HTTPS in production."
|
|
3248
|
+
);
|
|
3249
|
+
}
|
|
3054
3250
|
return { config, warnings };
|
|
3055
3251
|
}
|
|
3056
3252
|
async function interceptToolCall(params, upstreamCall, options) {
|
|
@@ -3556,6 +3752,12 @@ var SolonGate = class {
|
|
|
3556
3752
|
async validateLicense() {
|
|
3557
3753
|
if (this.licenseValidated) return;
|
|
3558
3754
|
if (this.apiKey.startsWith("sg_test_")) {
|
|
3755
|
+
const nodeEnv = typeof process !== "undefined" ? process.env.NODE_ENV : "";
|
|
3756
|
+
if (nodeEnv === "production") {
|
|
3757
|
+
throw new LicenseError(
|
|
3758
|
+
"Test API keys (sg_test_) cannot be used in production. Use a sg_live_ key instead."
|
|
3759
|
+
);
|
|
3760
|
+
}
|
|
3559
3761
|
this.licenseValidated = true;
|
|
3560
3762
|
return;
|
|
3561
3763
|
}
|
|
@@ -3722,6 +3924,361 @@ var SolonGate = class {
|
|
|
3722
3924
|
}
|
|
3723
3925
|
};
|
|
3724
3926
|
|
|
3927
|
+
// ../core/dist/index.js
|
|
3928
|
+
import { z as z2 } from "zod";
|
|
3929
|
+
var Permission2 = {
|
|
3930
|
+
READ: "READ",
|
|
3931
|
+
WRITE: "WRITE",
|
|
3932
|
+
EXECUTE: "EXECUTE"
|
|
3933
|
+
};
|
|
3934
|
+
var PermissionSchema = z2.enum(["READ", "WRITE", "EXECUTE"]);
|
|
3935
|
+
var NO_PERMISSIONS = Object.freeze(
|
|
3936
|
+
/* @__PURE__ */ new Set()
|
|
3937
|
+
);
|
|
3938
|
+
var READ_ONLY = Object.freeze(
|
|
3939
|
+
/* @__PURE__ */ new Set([Permission2.READ])
|
|
3940
|
+
);
|
|
3941
|
+
var PolicyRuleSchema2 = z2.object({
|
|
3942
|
+
id: z2.string().min(1).max(256),
|
|
3943
|
+
description: z2.string().max(1024),
|
|
3944
|
+
effect: z2.enum(["ALLOW", "DENY"]),
|
|
3945
|
+
priority: z2.number().int().min(0).max(1e4).default(1e3),
|
|
3946
|
+
toolPattern: z2.string().min(1).max(512),
|
|
3947
|
+
permission: z2.enum(["READ", "WRITE", "EXECUTE"]),
|
|
3948
|
+
minimumTrustLevel: z2.enum(["UNTRUSTED", "VERIFIED", "TRUSTED"]),
|
|
3949
|
+
argumentConstraints: z2.record(z2.unknown()).optional(),
|
|
3950
|
+
pathConstraints: z2.object({
|
|
3951
|
+
allowed: z2.array(z2.string()).optional(),
|
|
3952
|
+
denied: z2.array(z2.string()).optional(),
|
|
3953
|
+
rootDirectory: z2.string().optional(),
|
|
3954
|
+
allowSymlinks: z2.boolean().optional()
|
|
3955
|
+
}).optional(),
|
|
3956
|
+
commandConstraints: z2.object({
|
|
3957
|
+
allowed: z2.array(z2.string()).optional(),
|
|
3958
|
+
denied: z2.array(z2.string()).optional()
|
|
3959
|
+
}).optional(),
|
|
3960
|
+
filenameConstraints: z2.object({
|
|
3961
|
+
allowed: z2.array(z2.string()).optional(),
|
|
3962
|
+
denied: z2.array(z2.string()).optional()
|
|
3963
|
+
}).optional(),
|
|
3964
|
+
urlConstraints: z2.object({
|
|
3965
|
+
allowed: z2.array(z2.string()).optional(),
|
|
3966
|
+
denied: z2.array(z2.string()).optional()
|
|
3967
|
+
}).optional(),
|
|
3968
|
+
enabled: z2.boolean().default(true),
|
|
3969
|
+
createdAt: z2.string().datetime(),
|
|
3970
|
+
updatedAt: z2.string().datetime()
|
|
3971
|
+
});
|
|
3972
|
+
var PolicySetSchema2 = z2.object({
|
|
3973
|
+
id: z2.string().min(1).max(256),
|
|
3974
|
+
name: z2.string().min(1).max(256),
|
|
3975
|
+
description: z2.string().max(2048),
|
|
3976
|
+
version: z2.number().int().min(0),
|
|
3977
|
+
rules: z2.array(PolicyRuleSchema2),
|
|
3978
|
+
createdAt: z2.string().datetime(),
|
|
3979
|
+
updatedAt: z2.string().datetime()
|
|
3980
|
+
});
|
|
3981
|
+
var SECURITY_CONTEXT_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
3982
|
+
var DEFAULT_INPUT_GUARD_CONFIG2 = Object.freeze({
|
|
3983
|
+
pathTraversal: true,
|
|
3984
|
+
shellInjection: true,
|
|
3985
|
+
wildcardAbuse: true,
|
|
3986
|
+
lengthLimit: 4096,
|
|
3987
|
+
entropyLimit: true,
|
|
3988
|
+
ssrf: true,
|
|
3989
|
+
sqlInjection: true
|
|
3990
|
+
});
|
|
3991
|
+
var PATH_TRAVERSAL_PATTERNS = [
|
|
3992
|
+
/\.\.\//,
|
|
3993
|
+
// ../
|
|
3994
|
+
/\.\.\\/,
|
|
3995
|
+
// ..\
|
|
3996
|
+
/%2e%2e/i,
|
|
3997
|
+
// URL-encoded ..
|
|
3998
|
+
/%2e\./i,
|
|
3999
|
+
// partial URL-encoded
|
|
4000
|
+
/\.%2e/i,
|
|
4001
|
+
// partial URL-encoded
|
|
4002
|
+
/%252e%252e/i,
|
|
4003
|
+
// double URL-encoded
|
|
4004
|
+
/\.\.\0/
|
|
4005
|
+
// null byte variant
|
|
4006
|
+
];
|
|
4007
|
+
var SENSITIVE_PATHS = [
|
|
4008
|
+
/\/etc\/passwd/i,
|
|
4009
|
+
/\/etc\/shadow/i,
|
|
4010
|
+
/\/proc\//i,
|
|
4011
|
+
/\/dev\//i,
|
|
4012
|
+
/c:\\windows\\system32/i,
|
|
4013
|
+
/c:\\windows\\syswow64/i,
|
|
4014
|
+
/\/root\//i,
|
|
4015
|
+
/~\//,
|
|
4016
|
+
/\.env(\.|$)/i,
|
|
4017
|
+
// .env, .env.local, .env.production
|
|
4018
|
+
/\.aws\/credentials/i,
|
|
4019
|
+
// AWS credentials
|
|
4020
|
+
/\.ssh\/id_/i,
|
|
4021
|
+
// SSH keys
|
|
4022
|
+
/\.kube\/config/i,
|
|
4023
|
+
// Kubernetes config
|
|
4024
|
+
/wp-config\.php/i,
|
|
4025
|
+
// WordPress config
|
|
4026
|
+
/\.git\/config/i,
|
|
4027
|
+
// Git config
|
|
4028
|
+
/\.npmrc/i,
|
|
4029
|
+
// npm credentials
|
|
4030
|
+
/\.pypirc/i
|
|
4031
|
+
// PyPI credentials
|
|
4032
|
+
];
|
|
4033
|
+
function detectPathTraversal(value) {
|
|
4034
|
+
for (const pattern of PATH_TRAVERSAL_PATTERNS) {
|
|
4035
|
+
if (pattern.test(value)) return true;
|
|
4036
|
+
}
|
|
4037
|
+
for (const pattern of SENSITIVE_PATHS) {
|
|
4038
|
+
if (pattern.test(value)) return true;
|
|
4039
|
+
}
|
|
4040
|
+
return false;
|
|
4041
|
+
}
|
|
4042
|
+
var SHELL_INJECTION_PATTERNS = [
|
|
4043
|
+
/[;|&`]/,
|
|
4044
|
+
// Command separators and backtick execution
|
|
4045
|
+
/\$\(/,
|
|
4046
|
+
// Command substitution $(...)
|
|
4047
|
+
/\$\{/,
|
|
4048
|
+
// Variable expansion ${...}
|
|
4049
|
+
/>\s*/,
|
|
4050
|
+
// Output redirect
|
|
4051
|
+
/<\s*/,
|
|
4052
|
+
// Input redirect
|
|
4053
|
+
/&&/,
|
|
4054
|
+
// AND chaining
|
|
4055
|
+
/\|\|/,
|
|
4056
|
+
// OR chaining
|
|
4057
|
+
/\beval\b/i,
|
|
4058
|
+
// eval command
|
|
4059
|
+
/\bexec\b/i,
|
|
4060
|
+
// exec command
|
|
4061
|
+
/\bsystem\b/i,
|
|
4062
|
+
// system call
|
|
4063
|
+
/%0a/i,
|
|
4064
|
+
// URL-encoded newline
|
|
4065
|
+
/%0d/i,
|
|
4066
|
+
// URL-encoded carriage return
|
|
4067
|
+
/%09/i,
|
|
4068
|
+
// URL-encoded tab
|
|
4069
|
+
/\r\n/,
|
|
4070
|
+
// CRLF injection
|
|
4071
|
+
/\n/
|
|
4072
|
+
// Newline (command separator on Unix)
|
|
4073
|
+
];
|
|
4074
|
+
function detectShellInjection(value) {
|
|
4075
|
+
for (const pattern of SHELL_INJECTION_PATTERNS) {
|
|
4076
|
+
if (pattern.test(value)) return true;
|
|
4077
|
+
}
|
|
4078
|
+
return false;
|
|
4079
|
+
}
|
|
4080
|
+
var MAX_WILDCARDS_PER_VALUE = 3;
|
|
4081
|
+
function detectWildcardAbuse(value) {
|
|
4082
|
+
if (value.includes("**")) return true;
|
|
4083
|
+
const wildcardCount = (value.match(/\*/g) || []).length;
|
|
4084
|
+
if (wildcardCount > MAX_WILDCARDS_PER_VALUE) return true;
|
|
4085
|
+
return false;
|
|
4086
|
+
}
|
|
4087
|
+
var SSRF_PATTERNS = [
|
|
4088
|
+
/^https?:\/\/localhost\b/i,
|
|
4089
|
+
/^https?:\/\/127\.\d{1,3}\.\d{1,3}\.\d{1,3}/,
|
|
4090
|
+
/^https?:\/\/0\.0\.0\.0/,
|
|
4091
|
+
/^https?:\/\/\[::1\]/,
|
|
4092
|
+
// IPv6 loopback
|
|
4093
|
+
/^https?:\/\/10\.\d{1,3}\.\d{1,3}\.\d{1,3}/,
|
|
4094
|
+
// 10.x.x.x
|
|
4095
|
+
/^https?:\/\/172\.(1[6-9]|2\d|3[01])\./,
|
|
4096
|
+
// 172.16-31.x.x
|
|
4097
|
+
/^https?:\/\/192\.168\./,
|
|
4098
|
+
// 192.168.x.x
|
|
4099
|
+
/^https?:\/\/169\.254\./,
|
|
4100
|
+
// Link-local / AWS metadata
|
|
4101
|
+
/metadata\.google\.internal/i,
|
|
4102
|
+
// GCP metadata
|
|
4103
|
+
/^https?:\/\/metadata\b/i,
|
|
4104
|
+
// Generic metadata endpoint
|
|
4105
|
+
// IPv6 bypass patterns
|
|
4106
|
+
/^https?:\/\/\[fe80:/i,
|
|
4107
|
+
// IPv6 link-local
|
|
4108
|
+
/^https?:\/\/\[fc00:/i,
|
|
4109
|
+
// IPv6 unique local
|
|
4110
|
+
/^https?:\/\/\[fd[0-9a-f]{2}:/i,
|
|
4111
|
+
// IPv6 unique local (fd00::/8)
|
|
4112
|
+
/^https?:\/\/\[::ffff:127\./i,
|
|
4113
|
+
// IPv4-mapped IPv6 loopback
|
|
4114
|
+
/^https?:\/\/\[::ffff:10\./i,
|
|
4115
|
+
// IPv4-mapped IPv6 private
|
|
4116
|
+
/^https?:\/\/\[::ffff:172\.(1[6-9]|2\d|3[01])\./i,
|
|
4117
|
+
// IPv4-mapped IPv6 private
|
|
4118
|
+
/^https?:\/\/\[::ffff:192\.168\./i,
|
|
4119
|
+
// IPv4-mapped IPv6 private
|
|
4120
|
+
/^https?:\/\/\[::ffff:169\.254\./i,
|
|
4121
|
+
// IPv4-mapped IPv6 link-local
|
|
4122
|
+
// Hex IP bypass (e.g., 0x7f000001 = 127.0.0.1)
|
|
4123
|
+
/^https?:\/\/0x[0-9a-f]+\b/i,
|
|
4124
|
+
// Octal IP bypass (e.g., 0177.0.0.1 = 127.0.0.1)
|
|
4125
|
+
/^https?:\/\/0[0-7]{1,3}\./
|
|
4126
|
+
];
|
|
4127
|
+
function detectDecimalIP(value) {
|
|
4128
|
+
const match = value.match(/^https?:\/\/(\d{8,10})(?:[:/]|$)/);
|
|
4129
|
+
if (!match || !match[1]) return false;
|
|
4130
|
+
const decimal = parseInt(match[1], 10);
|
|
4131
|
+
if (isNaN(decimal) || decimal > 4294967295) return false;
|
|
4132
|
+
return decimal >= 2130706432 && decimal <= 2147483647 || // 127.0.0.0/8
|
|
4133
|
+
decimal >= 167772160 && decimal <= 184549375 || // 10.0.0.0/8
|
|
4134
|
+
decimal >= 2886729728 && decimal <= 2887778303 || // 172.16.0.0/12
|
|
4135
|
+
decimal >= 3232235520 && decimal <= 3232301055 || // 192.168.0.0/16
|
|
4136
|
+
decimal >= 2851995648 && decimal <= 2852061183 || // 169.254.0.0/16
|
|
4137
|
+
decimal === 0;
|
|
4138
|
+
}
|
|
4139
|
+
function detectSSRF(value) {
|
|
4140
|
+
for (const pattern of SSRF_PATTERNS) {
|
|
4141
|
+
if (pattern.test(value)) return true;
|
|
4142
|
+
}
|
|
4143
|
+
if (detectDecimalIP(value)) return true;
|
|
4144
|
+
return false;
|
|
4145
|
+
}
|
|
4146
|
+
var SQL_INJECTION_PATTERNS = [
|
|
4147
|
+
/'\s{0,20}(OR|AND)\s{0,20}'.{0,200}'/i,
|
|
4148
|
+
// ' OR '1'='1 — bounded to prevent ReDoS
|
|
4149
|
+
/'\s{0,10};\s{0,10}(DROP|DELETE|UPDATE|INSERT|ALTER|CREATE|EXEC)/i,
|
|
4150
|
+
// '; DROP TABLE
|
|
4151
|
+
/UNION\s+(ALL\s+)?SELECT/i,
|
|
4152
|
+
// UNION SELECT
|
|
4153
|
+
/--\s*$/m,
|
|
4154
|
+
// SQL comment at end of line
|
|
4155
|
+
/\/\*.{0,500}?\*\//,
|
|
4156
|
+
// SQL block comment — bounded + non-greedy
|
|
4157
|
+
/\bSLEEP\s*\(/i,
|
|
4158
|
+
// Time-based injection
|
|
4159
|
+
/\bBENCHMARK\s*\(/i,
|
|
4160
|
+
// MySQL benchmark
|
|
4161
|
+
/\bWAITFOR\s+DELAY/i,
|
|
4162
|
+
// MSSQL delay
|
|
4163
|
+
/\b(LOAD_FILE|INTO\s+OUTFILE|INTO\s+DUMPFILE)\b/i
|
|
4164
|
+
// File operations
|
|
4165
|
+
];
|
|
4166
|
+
function detectSQLInjection(value) {
|
|
4167
|
+
for (const pattern of SQL_INJECTION_PATTERNS) {
|
|
4168
|
+
if (pattern.test(value)) return true;
|
|
4169
|
+
}
|
|
4170
|
+
return false;
|
|
4171
|
+
}
|
|
4172
|
+
function checkLengthLimits(value, maxLength = 4096) {
|
|
4173
|
+
return value.length <= maxLength;
|
|
4174
|
+
}
|
|
4175
|
+
var ENTROPY_THRESHOLD = 4.5;
|
|
4176
|
+
var MIN_LENGTH_FOR_ENTROPY_CHECK = 32;
|
|
4177
|
+
function checkEntropyLimits(value) {
|
|
4178
|
+
if (value.length < MIN_LENGTH_FOR_ENTROPY_CHECK) return true;
|
|
4179
|
+
const entropy = calculateShannonEntropy(value);
|
|
4180
|
+
return entropy <= ENTROPY_THRESHOLD;
|
|
4181
|
+
}
|
|
4182
|
+
function calculateShannonEntropy(str) {
|
|
4183
|
+
const freq = /* @__PURE__ */ new Map();
|
|
4184
|
+
for (const char of str) {
|
|
4185
|
+
freq.set(char, (freq.get(char) ?? 0) + 1);
|
|
4186
|
+
}
|
|
4187
|
+
let entropy = 0;
|
|
4188
|
+
const len = str.length;
|
|
4189
|
+
for (const count of freq.values()) {
|
|
4190
|
+
const p = count / len;
|
|
4191
|
+
if (p > 0) {
|
|
4192
|
+
entropy -= p * Math.log2(p);
|
|
4193
|
+
}
|
|
4194
|
+
}
|
|
4195
|
+
return entropy;
|
|
4196
|
+
}
|
|
4197
|
+
function sanitizeInput(field, value, config = DEFAULT_INPUT_GUARD_CONFIG2) {
|
|
4198
|
+
const threats = [];
|
|
4199
|
+
if (typeof value !== "string") {
|
|
4200
|
+
if (typeof value === "object" && value !== null) {
|
|
4201
|
+
return sanitizeObject(field, value, config);
|
|
4202
|
+
}
|
|
4203
|
+
return { safe: true, threats: [] };
|
|
4204
|
+
}
|
|
4205
|
+
if (config.pathTraversal && detectPathTraversal(value)) {
|
|
4206
|
+
threats.push({
|
|
4207
|
+
type: "PATH_TRAVERSAL",
|
|
4208
|
+
field,
|
|
4209
|
+
value: truncate(value, 100),
|
|
4210
|
+
description: "Path traversal pattern detected"
|
|
4211
|
+
});
|
|
4212
|
+
}
|
|
4213
|
+
if (config.shellInjection && detectShellInjection(value)) {
|
|
4214
|
+
threats.push({
|
|
4215
|
+
type: "SHELL_INJECTION",
|
|
4216
|
+
field,
|
|
4217
|
+
value: truncate(value, 100),
|
|
4218
|
+
description: "Shell injection pattern detected"
|
|
4219
|
+
});
|
|
4220
|
+
}
|
|
4221
|
+
if (config.wildcardAbuse && detectWildcardAbuse(value)) {
|
|
4222
|
+
threats.push({
|
|
4223
|
+
type: "WILDCARD_ABUSE",
|
|
4224
|
+
field,
|
|
4225
|
+
value: truncate(value, 100),
|
|
4226
|
+
description: "Wildcard abuse pattern detected"
|
|
4227
|
+
});
|
|
4228
|
+
}
|
|
4229
|
+
if (!checkLengthLimits(value, config.lengthLimit)) {
|
|
4230
|
+
threats.push({
|
|
4231
|
+
type: "LENGTH_EXCEEDED",
|
|
4232
|
+
field,
|
|
4233
|
+
value: `[${value.length} chars]`,
|
|
4234
|
+
description: `Value exceeds maximum length of ${config.lengthLimit}`
|
|
4235
|
+
});
|
|
4236
|
+
}
|
|
4237
|
+
if (config.entropyLimit && !checkEntropyLimits(value)) {
|
|
4238
|
+
threats.push({
|
|
4239
|
+
type: "HIGH_ENTROPY",
|
|
4240
|
+
field,
|
|
4241
|
+
value: truncate(value, 100),
|
|
4242
|
+
description: "High entropy string detected - possible encoded payload"
|
|
4243
|
+
});
|
|
4244
|
+
}
|
|
4245
|
+
if (config.ssrf && detectSSRF(value)) {
|
|
4246
|
+
threats.push({
|
|
4247
|
+
type: "SSRF",
|
|
4248
|
+
field,
|
|
4249
|
+
value: truncate(value, 100),
|
|
4250
|
+
description: "Server-side request forgery pattern detected \u2014 internal/metadata URL blocked"
|
|
4251
|
+
});
|
|
4252
|
+
}
|
|
4253
|
+
if (config.sqlInjection && detectSQLInjection(value)) {
|
|
4254
|
+
threats.push({
|
|
4255
|
+
type: "SQL_INJECTION",
|
|
4256
|
+
field,
|
|
4257
|
+
value: truncate(value, 100),
|
|
4258
|
+
description: "SQL injection pattern detected"
|
|
4259
|
+
});
|
|
4260
|
+
}
|
|
4261
|
+
return { safe: threats.length === 0, threats };
|
|
4262
|
+
}
|
|
4263
|
+
function sanitizeObject(basePath, obj, config) {
|
|
4264
|
+
const threats = [];
|
|
4265
|
+
if (Array.isArray(obj)) {
|
|
4266
|
+
for (let i = 0; i < obj.length; i++) {
|
|
4267
|
+
const result = sanitizeInput(`${basePath}[${i}]`, obj[i], config);
|
|
4268
|
+
threats.push(...result.threats);
|
|
4269
|
+
}
|
|
4270
|
+
} else {
|
|
4271
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
4272
|
+
const result = sanitizeInput(`${basePath}.${key}`, val, config);
|
|
4273
|
+
threats.push(...result.threats);
|
|
4274
|
+
}
|
|
4275
|
+
}
|
|
4276
|
+
return { safe: threats.length === 0, threats };
|
|
4277
|
+
}
|
|
4278
|
+
function truncate(str, maxLen) {
|
|
4279
|
+
return str.length > maxLen ? str.slice(0, maxLen) + "..." : str;
|
|
4280
|
+
}
|
|
4281
|
+
|
|
3725
4282
|
// src/proxy.ts
|
|
3726
4283
|
init_config();
|
|
3727
4284
|
|
|
@@ -3744,6 +4301,7 @@ var PolicySyncManager = class {
|
|
|
3744
4301
|
pollTimer = null;
|
|
3745
4302
|
watcher = null;
|
|
3746
4303
|
isLiveKey;
|
|
4304
|
+
/** The cloud policy ID from --policy-id flag. This is the ONLY source of truth for which cloud policy to use. */
|
|
3747
4305
|
policyId;
|
|
3748
4306
|
constructor(opts) {
|
|
3749
4307
|
this.localPath = opts.localPath;
|
|
@@ -3872,17 +4430,24 @@ var PolicySyncManager = class {
|
|
|
3872
4430
|
}
|
|
3873
4431
|
/**
|
|
3874
4432
|
* Push policy to cloud API.
|
|
4433
|
+
* Uses this.policyId (from --policy-id CLI flag) as the cloud policy ID.
|
|
4434
|
+
* Falls back to policy.id from local file only if --policy-id was not set.
|
|
3875
4435
|
*/
|
|
3876
4436
|
async pushToCloud(policy) {
|
|
3877
|
-
const
|
|
4437
|
+
const cloudId = this.policyId || policy.id || "default";
|
|
4438
|
+
const existingRes = await fetch(`${this.apiUrl}/api/v1/policies/${cloudId}`, {
|
|
4439
|
+
headers: { "Authorization": `Bearer ${this.apiKey}` }
|
|
4440
|
+
});
|
|
4441
|
+
const method = existingRes.ok ? "PUT" : "POST";
|
|
4442
|
+
const url = existingRes.ok ? `${this.apiUrl}/api/v1/policies/${cloudId}` : `${this.apiUrl}/api/v1/policies`;
|
|
3878
4443
|
const res = await fetch(url, {
|
|
3879
|
-
method
|
|
4444
|
+
method,
|
|
3880
4445
|
headers: {
|
|
3881
4446
|
"Authorization": `Bearer ${this.apiKey}`,
|
|
3882
4447
|
"Content-Type": "application/json"
|
|
3883
4448
|
},
|
|
3884
4449
|
body: JSON.stringify({
|
|
3885
|
-
id:
|
|
4450
|
+
id: cloudId,
|
|
3886
4451
|
name: policy.name || "Default Policy",
|
|
3887
4452
|
description: policy.description || "Synced from proxy",
|
|
3888
4453
|
version: policy.version || 1,
|
|
@@ -3898,12 +4463,14 @@ var PolicySyncManager = class {
|
|
|
3898
4463
|
}
|
|
3899
4464
|
/**
|
|
3900
4465
|
* Write policy to local file (with loop prevention).
|
|
4466
|
+
* Does NOT write the 'id' field — cloud ID is managed by --policy-id flag.
|
|
3901
4467
|
*/
|
|
3902
4468
|
writeToFile(policy) {
|
|
3903
4469
|
if (!this.localPath) return;
|
|
3904
4470
|
this.skipNextWatch = true;
|
|
3905
4471
|
try {
|
|
3906
|
-
const
|
|
4472
|
+
const { id: _id, ...rest } = policy;
|
|
4473
|
+
const json = JSON.stringify(rest, null, 2) + "\n";
|
|
3907
4474
|
writeFileSync(this.localPath, json, "utf-8");
|
|
3908
4475
|
} catch (err) {
|
|
3909
4476
|
log(`File write error: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -3911,10 +4478,10 @@ var PolicySyncManager = class {
|
|
|
3911
4478
|
}
|
|
3912
4479
|
}
|
|
3913
4480
|
/**
|
|
3914
|
-
* Compare two policies by rules content (ignoring timestamps).
|
|
4481
|
+
* Compare two policies by rules content (ignoring timestamps and id).
|
|
3915
4482
|
*/
|
|
3916
4483
|
policiesEqual(a, b) {
|
|
3917
|
-
if (a.
|
|
4484
|
+
if (a.name !== b.name || a.rules.length !== b.rules.length) return false;
|
|
3918
4485
|
return JSON.stringify(a.rules) === JSON.stringify(b.rules);
|
|
3919
4486
|
}
|
|
3920
4487
|
};
|
|
@@ -3977,7 +4544,12 @@ var SolonGateProxy = class {
|
|
|
3977
4544
|
const apiUrl = this.config.apiUrl ?? "https://api.solongate.com";
|
|
3978
4545
|
if (this.config.apiKey) {
|
|
3979
4546
|
if (this.config.apiKey.startsWith("sg_test_")) {
|
|
3980
|
-
|
|
4547
|
+
const nodeEnv = process.env.NODE_ENV ?? "";
|
|
4548
|
+
if (nodeEnv === "production") {
|
|
4549
|
+
log2("ERROR: Test API keys (sg_test_) cannot be used in production. Use a sg_live_ key.");
|
|
4550
|
+
process.exit(1);
|
|
4551
|
+
}
|
|
4552
|
+
log2("Using test API key \u2014 skipping online validation (non-production mode).");
|
|
3981
4553
|
} else {
|
|
3982
4554
|
log2(`Validating license with ${apiUrl}...`);
|
|
3983
4555
|
try {
|
|
@@ -4178,7 +4750,26 @@ var SolonGateProxy = class {
|
|
|
4178
4750
|
});
|
|
4179
4751
|
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
4180
4752
|
if (!this.client) throw new Error("Upstream client disconnected");
|
|
4181
|
-
|
|
4753
|
+
const uri = request.params.uri;
|
|
4754
|
+
const uriCheck = sanitizeInput("resource.uri", uri);
|
|
4755
|
+
if (!uriCheck.safe) {
|
|
4756
|
+
const threats = uriCheck.threats.map((t) => `${t.type}: ${t.description}`).join("; ");
|
|
4757
|
+
log2(`DENY resource read: ${uri} \u2014 ${threats}`);
|
|
4758
|
+
throw new Error(`Resource URI blocked by security policy: ${threats}`);
|
|
4759
|
+
}
|
|
4760
|
+
if (/^file:\/\//i.test(uri)) {
|
|
4761
|
+
const path = uri.replace(/^file:\/\/\/?/i, "/");
|
|
4762
|
+
if (detectPathTraversal(path)) {
|
|
4763
|
+
log2(`DENY resource read: ${uri} \u2014 path traversal in file URI`);
|
|
4764
|
+
throw new Error("Resource URI blocked: path traversal detected");
|
|
4765
|
+
}
|
|
4766
|
+
}
|
|
4767
|
+
if (/^https?:\/\//i.test(uri) && detectSSRF(uri)) {
|
|
4768
|
+
log2(`DENY resource read: ${uri} \u2014 SSRF pattern`);
|
|
4769
|
+
throw new Error("Resource URI blocked: internal/metadata URL not allowed");
|
|
4770
|
+
}
|
|
4771
|
+
log2(`Resource read: ${uri}`);
|
|
4772
|
+
return await this.client.readResource({ uri });
|
|
4182
4773
|
});
|
|
4183
4774
|
this.server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
|
|
4184
4775
|
if (!this.client) return { resourceTemplates: [] };
|
|
@@ -4198,9 +4789,19 @@ var SolonGateProxy = class {
|
|
|
4198
4789
|
});
|
|
4199
4790
|
this.server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
4200
4791
|
if (!this.client) throw new Error("Upstream client disconnected");
|
|
4792
|
+
const args = request.params.arguments;
|
|
4793
|
+
if (args && typeof args === "object") {
|
|
4794
|
+
const argsCheck = sanitizeInput("prompt.arguments", args);
|
|
4795
|
+
if (!argsCheck.safe) {
|
|
4796
|
+
const threats = argsCheck.threats.map((t) => `${t.type}: ${t.description}`).join("; ");
|
|
4797
|
+
log2(`DENY prompt get: ${request.params.name} \u2014 ${threats}`);
|
|
4798
|
+
throw new Error(`Prompt arguments blocked by security policy: ${threats}`);
|
|
4799
|
+
}
|
|
4800
|
+
}
|
|
4801
|
+
log2(`Prompt get: ${request.params.name}`);
|
|
4201
4802
|
return await this.client.getPrompt({
|
|
4202
4803
|
name: request.params.name,
|
|
4203
|
-
arguments:
|
|
4804
|
+
arguments: args
|
|
4204
4805
|
});
|
|
4205
4806
|
});
|
|
4206
4807
|
}
|
|
@@ -4396,7 +4997,7 @@ async function main5() {
|
|
|
4396
4997
|
await Promise.resolve().then(() => (init_create(), create_exports));
|
|
4397
4998
|
return;
|
|
4398
4999
|
}
|
|
4399
|
-
if (subcommand === "pull" || subcommand === "push") {
|
|
5000
|
+
if (subcommand === "pull" || subcommand === "push" || subcommand === "list" || subcommand === "ls") {
|
|
4400
5001
|
await Promise.resolve().then(() => (init_pull_push(), pull_push_exports));
|
|
4401
5002
|
return;
|
|
4402
5003
|
}
|