@solongate/proxy 0.2.9 → 0.3.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/index.js +307 -513
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1338,118 +1338,6 @@ var init_create = __esm({
|
|
|
1338
1338
|
}
|
|
1339
1339
|
});
|
|
1340
1340
|
|
|
1341
|
-
// ../core/dist/index.js
|
|
1342
|
-
import { z } from "zod";
|
|
1343
|
-
var Permission = {
|
|
1344
|
-
READ: "READ",
|
|
1345
|
-
WRITE: "WRITE",
|
|
1346
|
-
EXECUTE: "EXECUTE"
|
|
1347
|
-
};
|
|
1348
|
-
var PermissionSchema = z.enum(["READ", "WRITE", "EXECUTE"]);
|
|
1349
|
-
var NO_PERMISSIONS = Object.freeze(
|
|
1350
|
-
/* @__PURE__ */ new Set()
|
|
1351
|
-
);
|
|
1352
|
-
var READ_ONLY = Object.freeze(
|
|
1353
|
-
/* @__PURE__ */ new Set([Permission.READ])
|
|
1354
|
-
);
|
|
1355
|
-
var PolicyRuleSchema = z.object({
|
|
1356
|
-
id: z.string().min(1).max(256),
|
|
1357
|
-
description: z.string().max(1024),
|
|
1358
|
-
effect: z.enum(["ALLOW", "DENY"]),
|
|
1359
|
-
priority: z.number().int().min(0).max(1e4).default(1e3),
|
|
1360
|
-
toolPattern: z.string().min(1).max(512),
|
|
1361
|
-
permission: z.enum(["READ", "WRITE", "EXECUTE"]),
|
|
1362
|
-
minimumTrustLevel: z.enum(["UNTRUSTED", "VERIFIED", "TRUSTED"]),
|
|
1363
|
-
argumentConstraints: z.record(z.unknown()).optional(),
|
|
1364
|
-
pathConstraints: z.object({
|
|
1365
|
-
allowed: z.array(z.string()).optional(),
|
|
1366
|
-
denied: z.array(z.string()).optional(),
|
|
1367
|
-
rootDirectory: z.string().optional(),
|
|
1368
|
-
allowSymlinks: z.boolean().optional()
|
|
1369
|
-
}).optional(),
|
|
1370
|
-
enabled: z.boolean().default(true),
|
|
1371
|
-
createdAt: z.string().datetime(),
|
|
1372
|
-
updatedAt: z.string().datetime()
|
|
1373
|
-
});
|
|
1374
|
-
var PolicySetSchema = z.object({
|
|
1375
|
-
id: z.string().min(1).max(256),
|
|
1376
|
-
name: z.string().min(1).max(256),
|
|
1377
|
-
description: z.string().max(2048),
|
|
1378
|
-
version: z.number().int().min(0),
|
|
1379
|
-
rules: z.array(PolicyRuleSchema),
|
|
1380
|
-
createdAt: z.string().datetime(),
|
|
1381
|
-
updatedAt: z.string().datetime()
|
|
1382
|
-
});
|
|
1383
|
-
var SECURITY_CONTEXT_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
1384
|
-
var DEFAULT_INPUT_GUARD_CONFIG = Object.freeze({
|
|
1385
|
-
pathTraversal: true,
|
|
1386
|
-
shellInjection: true,
|
|
1387
|
-
wildcardAbuse: true,
|
|
1388
|
-
lengthLimit: 4096,
|
|
1389
|
-
entropyLimit: true,
|
|
1390
|
-
ssrf: true,
|
|
1391
|
-
sqlInjection: true
|
|
1392
|
-
});
|
|
1393
|
-
var SSRF_PATTERNS = [
|
|
1394
|
-
/^https?:\/\/localhost\b/i,
|
|
1395
|
-
/^https?:\/\/127\.\d{1,3}\.\d{1,3}\.\d{1,3}/,
|
|
1396
|
-
/^https?:\/\/0\.0\.0\.0/,
|
|
1397
|
-
/^https?:\/\/\[::1\]/,
|
|
1398
|
-
// IPv6 loopback
|
|
1399
|
-
/^https?:\/\/10\.\d{1,3}\.\d{1,3}\.\d{1,3}/,
|
|
1400
|
-
// 10.x.x.x
|
|
1401
|
-
/^https?:\/\/172\.(1[6-9]|2\d|3[01])\./,
|
|
1402
|
-
// 172.16-31.x.x
|
|
1403
|
-
/^https?:\/\/192\.168\./,
|
|
1404
|
-
// 192.168.x.x
|
|
1405
|
-
/^https?:\/\/169\.254\./,
|
|
1406
|
-
// Link-local / AWS metadata
|
|
1407
|
-
/metadata\.google\.internal/i,
|
|
1408
|
-
// GCP metadata
|
|
1409
|
-
/^https?:\/\/metadata\b/i,
|
|
1410
|
-
// Generic metadata endpoint
|
|
1411
|
-
// IPv6 bypass patterns
|
|
1412
|
-
/^https?:\/\/\[fe80:/i,
|
|
1413
|
-
// IPv6 link-local
|
|
1414
|
-
/^https?:\/\/\[fc00:/i,
|
|
1415
|
-
// IPv6 unique local
|
|
1416
|
-
/^https?:\/\/\[fd[0-9a-f]{2}:/i,
|
|
1417
|
-
// IPv6 unique local (fd00::/8)
|
|
1418
|
-
/^https?:\/\/\[::ffff:127\./i,
|
|
1419
|
-
// IPv4-mapped IPv6 loopback
|
|
1420
|
-
/^https?:\/\/\[::ffff:10\./i,
|
|
1421
|
-
// IPv4-mapped IPv6 private
|
|
1422
|
-
/^https?:\/\/\[::ffff:172\.(1[6-9]|2\d|3[01])\./i,
|
|
1423
|
-
// IPv4-mapped IPv6 private
|
|
1424
|
-
/^https?:\/\/\[::ffff:192\.168\./i,
|
|
1425
|
-
// IPv4-mapped IPv6 private
|
|
1426
|
-
/^https?:\/\/\[::ffff:169\.254\./i,
|
|
1427
|
-
// IPv4-mapped IPv6 link-local
|
|
1428
|
-
// Hex IP bypass (e.g., 0x7f000001 = 127.0.0.1)
|
|
1429
|
-
/^https?:\/\/0x[0-9a-f]+\b/i,
|
|
1430
|
-
// Octal IP bypass (e.g., 0177.0.0.1 = 127.0.0.1)
|
|
1431
|
-
/^https?:\/\/0[0-7]{1,3}\./
|
|
1432
|
-
];
|
|
1433
|
-
function detectDecimalIP(value) {
|
|
1434
|
-
const match = value.match(/^https?:\/\/(\d{8,10})(?:[:/]|$)/);
|
|
1435
|
-
if (!match || !match[1]) return false;
|
|
1436
|
-
const decimal = parseInt(match[1], 10);
|
|
1437
|
-
if (isNaN(decimal) || decimal > 4294967295) return false;
|
|
1438
|
-
return decimal >= 2130706432 && decimal <= 2147483647 || // 127.0.0.0/8
|
|
1439
|
-
decimal >= 167772160 && decimal <= 184549375 || // 10.0.0.0/8
|
|
1440
|
-
decimal >= 2886729728 && decimal <= 2887778303 || // 172.16.0.0/12
|
|
1441
|
-
decimal >= 3232235520 && decimal <= 3232301055 || // 192.168.0.0/16
|
|
1442
|
-
decimal >= 2851995648 && decimal <= 2852061183 || // 169.254.0.0/16
|
|
1443
|
-
decimal === 0;
|
|
1444
|
-
}
|
|
1445
|
-
function detectSSRF(value) {
|
|
1446
|
-
for (const pattern of SSRF_PATTERNS) {
|
|
1447
|
-
if (pattern.test(value)) return true;
|
|
1448
|
-
}
|
|
1449
|
-
if (detectDecimalIP(value)) return true;
|
|
1450
|
-
return false;
|
|
1451
|
-
}
|
|
1452
|
-
|
|
1453
1341
|
// src/config.ts
|
|
1454
1342
|
import { readFileSync, existsSync } from "fs";
|
|
1455
1343
|
import { resolve } from "path";
|
|
@@ -1497,48 +1385,72 @@ var PRESETS = {
|
|
|
1497
1385
|
restricted: {
|
|
1498
1386
|
id: "restricted",
|
|
1499
1387
|
name: "Restricted",
|
|
1500
|
-
description: "Blocks dangerous
|
|
1388
|
+
description: "Blocks dangerous commands (rm -rf, curl|bash, dd, etc.), allows safe tools with path restrictions",
|
|
1501
1389
|
version: 1,
|
|
1502
1390
|
rules: [
|
|
1503
1391
|
{
|
|
1504
|
-
id: "deny-
|
|
1505
|
-
description: "Block shell
|
|
1506
|
-
effect: "DENY",
|
|
1507
|
-
priority: 100,
|
|
1508
|
-
toolPattern: "*shell*",
|
|
1509
|
-
permission: "EXECUTE",
|
|
1510
|
-
minimumTrustLevel: "UNTRUSTED",
|
|
1511
|
-
enabled: true,
|
|
1512
|
-
createdAt: "",
|
|
1513
|
-
updatedAt: ""
|
|
1514
|
-
},
|
|
1515
|
-
{
|
|
1516
|
-
id: "deny-exec",
|
|
1517
|
-
description: "Block command execution",
|
|
1392
|
+
id: "deny-dangerous-commands",
|
|
1393
|
+
description: "Block dangerous shell commands",
|
|
1518
1394
|
effect: "DENY",
|
|
1519
|
-
priority:
|
|
1520
|
-
toolPattern: "*
|
|
1395
|
+
priority: 50,
|
|
1396
|
+
toolPattern: "*",
|
|
1521
1397
|
permission: "EXECUTE",
|
|
1522
1398
|
minimumTrustLevel: "UNTRUSTED",
|
|
1523
1399
|
enabled: true,
|
|
1400
|
+
commandConstraints: {
|
|
1401
|
+
denied: [
|
|
1402
|
+
"rm -rf *",
|
|
1403
|
+
"rm -r /*",
|
|
1404
|
+
"mkfs*",
|
|
1405
|
+
"dd if=*",
|
|
1406
|
+
"curl*|*bash*",
|
|
1407
|
+
"curl*|*sh*",
|
|
1408
|
+
"wget*|*bash*",
|
|
1409
|
+
"wget*|*sh*",
|
|
1410
|
+
"shutdown*",
|
|
1411
|
+
"reboot*",
|
|
1412
|
+
"kill -9*",
|
|
1413
|
+
"chmod*777*",
|
|
1414
|
+
"iptables*",
|
|
1415
|
+
"passwd*",
|
|
1416
|
+
"useradd*",
|
|
1417
|
+
"userdel*"
|
|
1418
|
+
]
|
|
1419
|
+
},
|
|
1524
1420
|
createdAt: "",
|
|
1525
1421
|
updatedAt: ""
|
|
1526
1422
|
},
|
|
1527
1423
|
{
|
|
1528
|
-
id: "deny-
|
|
1529
|
-
description: "Block
|
|
1424
|
+
id: "deny-sensitive-paths",
|
|
1425
|
+
description: "Block access to sensitive files",
|
|
1530
1426
|
effect: "DENY",
|
|
1531
|
-
priority:
|
|
1532
|
-
toolPattern: "*
|
|
1427
|
+
priority: 51,
|
|
1428
|
+
toolPattern: "*",
|
|
1533
1429
|
permission: "EXECUTE",
|
|
1534
1430
|
minimumTrustLevel: "UNTRUSTED",
|
|
1535
1431
|
enabled: true,
|
|
1432
|
+
pathConstraints: {
|
|
1433
|
+
denied: [
|
|
1434
|
+
"**/.env*",
|
|
1435
|
+
"**/.ssh/**",
|
|
1436
|
+
"**/.aws/**",
|
|
1437
|
+
"**/.kube/**",
|
|
1438
|
+
"**/credentials*",
|
|
1439
|
+
"**/secrets*",
|
|
1440
|
+
"**/*.pem",
|
|
1441
|
+
"**/*.key",
|
|
1442
|
+
"/etc/passwd",
|
|
1443
|
+
"/etc/shadow",
|
|
1444
|
+
"/proc/**",
|
|
1445
|
+
"/dev/**"
|
|
1446
|
+
]
|
|
1447
|
+
},
|
|
1536
1448
|
createdAt: "",
|
|
1537
1449
|
updatedAt: ""
|
|
1538
1450
|
},
|
|
1539
1451
|
{
|
|
1540
1452
|
id: "allow-rest",
|
|
1541
|
-
description: "Allow all other
|
|
1453
|
+
description: "Allow all other tool calls",
|
|
1542
1454
|
effect: "ALLOW",
|
|
1543
1455
|
priority: 1e3,
|
|
1544
1456
|
toolPattern: "*",
|
|
@@ -1555,7 +1467,7 @@ var PRESETS = {
|
|
|
1555
1467
|
"read-only": {
|
|
1556
1468
|
id: "read-only",
|
|
1557
1469
|
name: "Read Only",
|
|
1558
|
-
description: "Only allows read
|
|
1470
|
+
description: "Only allows read/list/get/search/query tools",
|
|
1559
1471
|
version: 1,
|
|
1560
1472
|
rules: [
|
|
1561
1473
|
{
|
|
@@ -1622,6 +1534,110 @@ var PRESETS = {
|
|
|
1622
1534
|
createdAt: "",
|
|
1623
1535
|
updatedAt: ""
|
|
1624
1536
|
},
|
|
1537
|
+
sandbox: {
|
|
1538
|
+
id: "sandbox",
|
|
1539
|
+
name: "Sandbox",
|
|
1540
|
+
description: "Allows file ops within working directory only, blocks dangerous commands, allows safe shell commands",
|
|
1541
|
+
version: 1,
|
|
1542
|
+
rules: [
|
|
1543
|
+
{
|
|
1544
|
+
id: "deny-dangerous-commands",
|
|
1545
|
+
description: "Block dangerous shell commands",
|
|
1546
|
+
effect: "DENY",
|
|
1547
|
+
priority: 50,
|
|
1548
|
+
toolPattern: "*",
|
|
1549
|
+
permission: "EXECUTE",
|
|
1550
|
+
minimumTrustLevel: "UNTRUSTED",
|
|
1551
|
+
enabled: true,
|
|
1552
|
+
commandConstraints: {
|
|
1553
|
+
denied: [
|
|
1554
|
+
"rm -rf *",
|
|
1555
|
+
"rm -r /*",
|
|
1556
|
+
"mkfs*",
|
|
1557
|
+
"dd if=*",
|
|
1558
|
+
"curl*|*bash*",
|
|
1559
|
+
"wget*|*sh*",
|
|
1560
|
+
"shutdown*",
|
|
1561
|
+
"reboot*",
|
|
1562
|
+
"chmod*777*"
|
|
1563
|
+
]
|
|
1564
|
+
},
|
|
1565
|
+
createdAt: "",
|
|
1566
|
+
updatedAt: ""
|
|
1567
|
+
},
|
|
1568
|
+
{
|
|
1569
|
+
id: "allow-safe-commands",
|
|
1570
|
+
description: "Allow safe shell commands only",
|
|
1571
|
+
effect: "ALLOW",
|
|
1572
|
+
priority: 100,
|
|
1573
|
+
toolPattern: "*shell*",
|
|
1574
|
+
permission: "EXECUTE",
|
|
1575
|
+
minimumTrustLevel: "UNTRUSTED",
|
|
1576
|
+
enabled: true,
|
|
1577
|
+
commandConstraints: {
|
|
1578
|
+
allowed: [
|
|
1579
|
+
"ls*",
|
|
1580
|
+
"cat*",
|
|
1581
|
+
"head*",
|
|
1582
|
+
"tail*",
|
|
1583
|
+
"wc*",
|
|
1584
|
+
"grep*",
|
|
1585
|
+
"find*",
|
|
1586
|
+
"echo*",
|
|
1587
|
+
"pwd",
|
|
1588
|
+
"whoami",
|
|
1589
|
+
"date",
|
|
1590
|
+
"env",
|
|
1591
|
+
"git*",
|
|
1592
|
+
"npm*",
|
|
1593
|
+
"pnpm*",
|
|
1594
|
+
"yarn*",
|
|
1595
|
+
"node*",
|
|
1596
|
+
"python*",
|
|
1597
|
+
"pip*"
|
|
1598
|
+
]
|
|
1599
|
+
},
|
|
1600
|
+
createdAt: "",
|
|
1601
|
+
updatedAt: ""
|
|
1602
|
+
},
|
|
1603
|
+
{
|
|
1604
|
+
id: "deny-sensitive-paths",
|
|
1605
|
+
description: "Block access to sensitive files",
|
|
1606
|
+
effect: "DENY",
|
|
1607
|
+
priority: 51,
|
|
1608
|
+
toolPattern: "*",
|
|
1609
|
+
permission: "EXECUTE",
|
|
1610
|
+
minimumTrustLevel: "UNTRUSTED",
|
|
1611
|
+
enabled: true,
|
|
1612
|
+
pathConstraints: {
|
|
1613
|
+
denied: [
|
|
1614
|
+
"**/.env*",
|
|
1615
|
+
"**/.ssh/**",
|
|
1616
|
+
"**/.aws/**",
|
|
1617
|
+
"**/credentials*",
|
|
1618
|
+
"**/*.pem",
|
|
1619
|
+
"**/*.key"
|
|
1620
|
+
]
|
|
1621
|
+
},
|
|
1622
|
+
createdAt: "",
|
|
1623
|
+
updatedAt: ""
|
|
1624
|
+
},
|
|
1625
|
+
{
|
|
1626
|
+
id: "allow-rest",
|
|
1627
|
+
description: "Allow all other tools",
|
|
1628
|
+
effect: "ALLOW",
|
|
1629
|
+
priority: 1e3,
|
|
1630
|
+
toolPattern: "*",
|
|
1631
|
+
permission: "EXECUTE",
|
|
1632
|
+
minimumTrustLevel: "UNTRUSTED",
|
|
1633
|
+
enabled: true,
|
|
1634
|
+
createdAt: "",
|
|
1635
|
+
updatedAt: ""
|
|
1636
|
+
}
|
|
1637
|
+
],
|
|
1638
|
+
createdAt: "",
|
|
1639
|
+
updatedAt: ""
|
|
1640
|
+
},
|
|
1625
1641
|
permissive: {
|
|
1626
1642
|
id: "permissive",
|
|
1627
1643
|
name: "Permissive",
|
|
@@ -1671,7 +1687,6 @@ function parseArgs(argv) {
|
|
|
1671
1687
|
let policySource = "restricted";
|
|
1672
1688
|
let name = "solongate-proxy";
|
|
1673
1689
|
let verbose = false;
|
|
1674
|
-
let validateInput = true;
|
|
1675
1690
|
let rateLimitPerTool;
|
|
1676
1691
|
let globalRateLimit;
|
|
1677
1692
|
let configFile;
|
|
@@ -1694,9 +1709,6 @@ function parseArgs(argv) {
|
|
|
1694
1709
|
case "--verbose":
|
|
1695
1710
|
verbose = true;
|
|
1696
1711
|
break;
|
|
1697
|
-
case "--no-input-guard":
|
|
1698
|
-
validateInput = false;
|
|
1699
|
-
break;
|
|
1700
1712
|
case "--rate-limit":
|
|
1701
1713
|
rateLimitPerTool = parseInt(flags[++i], 10);
|
|
1702
1714
|
break;
|
|
@@ -1745,17 +1757,11 @@ function parseArgs(argv) {
|
|
|
1745
1757
|
if (!fileConfig.upstream) {
|
|
1746
1758
|
throw new Error('Config file must include "upstream" with at least "command" or "url"');
|
|
1747
1759
|
}
|
|
1748
|
-
if (fileConfig.upstream.url && detectSSRF(fileConfig.upstream.url)) {
|
|
1749
|
-
throw new Error(
|
|
1750
|
-
`Upstream URL blocked: "${fileConfig.upstream.url}" points to an internal or private network address.`
|
|
1751
|
-
);
|
|
1752
|
-
}
|
|
1753
1760
|
return {
|
|
1754
1761
|
upstream: fileConfig.upstream,
|
|
1755
1762
|
policy: loadPolicy(fileConfig.policy ?? policySource),
|
|
1756
1763
|
name: fileConfig.name ?? name,
|
|
1757
1764
|
verbose: fileConfig.verbose ?? verbose,
|
|
1758
|
-
validateInput: fileConfig.validateInput ?? validateInput,
|
|
1759
1765
|
rateLimitPerTool: fileConfig.rateLimitPerTool ?? rateLimitPerTool,
|
|
1760
1766
|
globalRateLimit: fileConfig.globalRateLimit ?? globalRateLimit,
|
|
1761
1767
|
apiKey: apiKey ?? fileConfig.apiKey,
|
|
@@ -1764,12 +1770,6 @@ function parseArgs(argv) {
|
|
|
1764
1770
|
};
|
|
1765
1771
|
}
|
|
1766
1772
|
if (upstreamUrl) {
|
|
1767
|
-
if (detectSSRF(upstreamUrl)) {
|
|
1768
|
-
throw new Error(
|
|
1769
|
-
`Upstream URL blocked: "${upstreamUrl}" points to an internal or private network address.
|
|
1770
|
-
SSRF protection prevents connecting to localhost, private IPs, or cloud metadata endpoints.`
|
|
1771
|
-
);
|
|
1772
|
-
}
|
|
1773
1773
|
const transport = upstreamTransport ?? (upstreamUrl.includes("/sse") ? "sse" : "http");
|
|
1774
1774
|
return {
|
|
1775
1775
|
upstream: {
|
|
@@ -1781,7 +1781,6 @@ SSRF protection prevents connecting to localhost, private IPs, or cloud metadata
|
|
|
1781
1781
|
policy: loadPolicy(policySource),
|
|
1782
1782
|
name,
|
|
1783
1783
|
verbose,
|
|
1784
|
-
validateInput,
|
|
1785
1784
|
rateLimitPerTool,
|
|
1786
1785
|
globalRateLimit,
|
|
1787
1786
|
apiKey,
|
|
@@ -1805,7 +1804,6 @@ SSRF protection prevents connecting to localhost, private IPs, or cloud metadata
|
|
|
1805
1804
|
policy: loadPolicy(policySource),
|
|
1806
1805
|
name,
|
|
1807
1806
|
verbose,
|
|
1808
|
-
validateInput,
|
|
1809
1807
|
rateLimitPerTool,
|
|
1810
1808
|
globalRateLimit,
|
|
1811
1809
|
apiKey,
|
|
@@ -1834,7 +1832,7 @@ import {
|
|
|
1834
1832
|
import { createServer as createHttpServer } from "http";
|
|
1835
1833
|
|
|
1836
1834
|
// ../sdk-ts/dist/index.js
|
|
1837
|
-
import { z
|
|
1835
|
+
import { z } from "zod";
|
|
1838
1836
|
import { createHash, randomUUID, createHmac } from "crypto";
|
|
1839
1837
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
1840
1838
|
var SolonGateError = class extends Error {
|
|
@@ -1873,16 +1871,6 @@ var PolicyDeniedError = class extends SolonGateError {
|
|
|
1873
1871
|
this.name = "PolicyDeniedError";
|
|
1874
1872
|
}
|
|
1875
1873
|
};
|
|
1876
|
-
var SchemaValidationError = class extends SolonGateError {
|
|
1877
|
-
constructor(toolName, validationErrors) {
|
|
1878
|
-
super(
|
|
1879
|
-
`Schema validation failed for tool "${toolName}": ${validationErrors.join("; ")}`,
|
|
1880
|
-
"SCHEMA_VALIDATION_FAILED",
|
|
1881
|
-
{ toolName, validationErrors }
|
|
1882
|
-
);
|
|
1883
|
-
this.name = "SchemaValidationError";
|
|
1884
|
-
}
|
|
1885
|
-
};
|
|
1886
1874
|
var RateLimitError = class extends SolonGateError {
|
|
1887
1875
|
constructor(toolName, limitPerMinute) {
|
|
1888
1876
|
super(
|
|
@@ -1898,49 +1886,53 @@ var TrustLevel = {
|
|
|
1898
1886
|
VERIFIED: "VERIFIED",
|
|
1899
1887
|
TRUSTED: "TRUSTED"
|
|
1900
1888
|
};
|
|
1901
|
-
var
|
|
1889
|
+
var Permission = {
|
|
1902
1890
|
READ: "READ",
|
|
1903
1891
|
WRITE: "WRITE",
|
|
1904
1892
|
EXECUTE: "EXECUTE"
|
|
1905
1893
|
};
|
|
1906
|
-
|
|
1894
|
+
z.enum(["READ", "WRITE", "EXECUTE"]);
|
|
1907
1895
|
Object.freeze(
|
|
1908
1896
|
/* @__PURE__ */ new Set()
|
|
1909
1897
|
);
|
|
1910
1898
|
Object.freeze(
|
|
1911
|
-
/* @__PURE__ */ new Set([
|
|
1899
|
+
/* @__PURE__ */ new Set([Permission.READ])
|
|
1912
1900
|
);
|
|
1913
1901
|
var PolicyEffect = {
|
|
1914
1902
|
ALLOW: "ALLOW",
|
|
1915
1903
|
DENY: "DENY"
|
|
1916
1904
|
};
|
|
1917
|
-
var
|
|
1918
|
-
id:
|
|
1919
|
-
description:
|
|
1920
|
-
effect:
|
|
1921
|
-
priority:
|
|
1922
|
-
toolPattern:
|
|
1923
|
-
permission:
|
|
1924
|
-
minimumTrustLevel:
|
|
1925
|
-
argumentConstraints:
|
|
1926
|
-
pathConstraints:
|
|
1927
|
-
allowed:
|
|
1928
|
-
denied:
|
|
1929
|
-
rootDirectory:
|
|
1930
|
-
allowSymlinks:
|
|
1905
|
+
var PolicyRuleSchema = z.object({
|
|
1906
|
+
id: z.string().min(1).max(256),
|
|
1907
|
+
description: z.string().max(1024),
|
|
1908
|
+
effect: z.enum(["ALLOW", "DENY"]),
|
|
1909
|
+
priority: z.number().int().min(0).max(1e4).default(1e3),
|
|
1910
|
+
toolPattern: z.string().min(1).max(512),
|
|
1911
|
+
permission: z.enum(["READ", "WRITE", "EXECUTE"]),
|
|
1912
|
+
minimumTrustLevel: z.enum(["UNTRUSTED", "VERIFIED", "TRUSTED"]),
|
|
1913
|
+
argumentConstraints: z.record(z.unknown()).optional(),
|
|
1914
|
+
pathConstraints: z.object({
|
|
1915
|
+
allowed: z.array(z.string()).optional(),
|
|
1916
|
+
denied: z.array(z.string()).optional(),
|
|
1917
|
+
rootDirectory: z.string().optional(),
|
|
1918
|
+
allowSymlinks: z.boolean().optional()
|
|
1919
|
+
}).optional(),
|
|
1920
|
+
commandConstraints: z.object({
|
|
1921
|
+
allowed: z.array(z.string()).optional(),
|
|
1922
|
+
denied: z.array(z.string()).optional()
|
|
1931
1923
|
}).optional(),
|
|
1932
|
-
enabled:
|
|
1933
|
-
createdAt:
|
|
1934
|
-
updatedAt:
|
|
1924
|
+
enabled: z.boolean().default(true),
|
|
1925
|
+
createdAt: z.string().datetime(),
|
|
1926
|
+
updatedAt: z.string().datetime()
|
|
1935
1927
|
});
|
|
1936
|
-
var
|
|
1937
|
-
id:
|
|
1938
|
-
name:
|
|
1939
|
-
description:
|
|
1940
|
-
version:
|
|
1941
|
-
rules:
|
|
1942
|
-
createdAt:
|
|
1943
|
-
updatedAt:
|
|
1928
|
+
var PolicySetSchema = z.object({
|
|
1929
|
+
id: z.string().min(1).max(256),
|
|
1930
|
+
name: z.string().min(1).max(256),
|
|
1931
|
+
description: z.string().max(2048),
|
|
1932
|
+
version: z.number().int().min(0),
|
|
1933
|
+
rules: z.array(PolicyRuleSchema),
|
|
1934
|
+
createdAt: z.string().datetime(),
|
|
1935
|
+
updatedAt: z.string().datetime()
|
|
1944
1936
|
});
|
|
1945
1937
|
function createSecurityContext(params) {
|
|
1946
1938
|
return {
|
|
@@ -1979,7 +1971,7 @@ function createDeniedToolResult(reason) {
|
|
|
1979
1971
|
isError: true
|
|
1980
1972
|
};
|
|
1981
1973
|
}
|
|
1982
|
-
var
|
|
1974
|
+
var DEFAULT_INPUT_GUARD_CONFIG = Object.freeze({
|
|
1983
1975
|
pathTraversal: true,
|
|
1984
1976
|
shellInjection: true,
|
|
1985
1977
|
wildcardAbuse: true,
|
|
@@ -1988,296 +1980,6 @@ var DEFAULT_INPUT_GUARD_CONFIG2 = Object.freeze({
|
|
|
1988
1980
|
ssrf: true,
|
|
1989
1981
|
sqlInjection: true
|
|
1990
1982
|
});
|
|
1991
|
-
var PATH_TRAVERSAL_PATTERNS = [
|
|
1992
|
-
/\.\.\//,
|
|
1993
|
-
// ../
|
|
1994
|
-
/\.\.\\/,
|
|
1995
|
-
// ..\
|
|
1996
|
-
/%2e%2e/i,
|
|
1997
|
-
// URL-encoded ..
|
|
1998
|
-
/%2e\./i,
|
|
1999
|
-
// partial URL-encoded
|
|
2000
|
-
/\.%2e/i,
|
|
2001
|
-
// partial URL-encoded
|
|
2002
|
-
/%252e%252e/i,
|
|
2003
|
-
// double URL-encoded
|
|
2004
|
-
/\.\.\0/
|
|
2005
|
-
// null byte variant
|
|
2006
|
-
];
|
|
2007
|
-
var SENSITIVE_PATHS = [
|
|
2008
|
-
/\/etc\/passwd/i,
|
|
2009
|
-
/\/etc\/shadow/i,
|
|
2010
|
-
/\/proc\//i,
|
|
2011
|
-
/\/dev\//i,
|
|
2012
|
-
/c:\\windows\\system32/i,
|
|
2013
|
-
/c:\\windows\\syswow64/i,
|
|
2014
|
-
/\/root\//i,
|
|
2015
|
-
/~\//,
|
|
2016
|
-
/\.env(\.|$)/i,
|
|
2017
|
-
// .env, .env.local, .env.production
|
|
2018
|
-
/\.aws\/credentials/i,
|
|
2019
|
-
// AWS credentials
|
|
2020
|
-
/\.ssh\/id_/i,
|
|
2021
|
-
// SSH keys
|
|
2022
|
-
/\.kube\/config/i,
|
|
2023
|
-
// Kubernetes config
|
|
2024
|
-
/wp-config\.php/i,
|
|
2025
|
-
// WordPress config
|
|
2026
|
-
/\.git\/config/i,
|
|
2027
|
-
// Git config
|
|
2028
|
-
/\.npmrc/i,
|
|
2029
|
-
// npm credentials
|
|
2030
|
-
/\.pypirc/i
|
|
2031
|
-
// PyPI credentials
|
|
2032
|
-
];
|
|
2033
|
-
function detectPathTraversal(value) {
|
|
2034
|
-
for (const pattern of PATH_TRAVERSAL_PATTERNS) {
|
|
2035
|
-
if (pattern.test(value)) return true;
|
|
2036
|
-
}
|
|
2037
|
-
for (const pattern of SENSITIVE_PATHS) {
|
|
2038
|
-
if (pattern.test(value)) return true;
|
|
2039
|
-
}
|
|
2040
|
-
return false;
|
|
2041
|
-
}
|
|
2042
|
-
var SHELL_INJECTION_PATTERNS = [
|
|
2043
|
-
/[;|&`]/,
|
|
2044
|
-
// Command separators and backtick execution
|
|
2045
|
-
/\$\(/,
|
|
2046
|
-
// Command substitution $(...)
|
|
2047
|
-
/\$\{/,
|
|
2048
|
-
// Variable expansion ${...}
|
|
2049
|
-
/>\s*/,
|
|
2050
|
-
// Output redirect
|
|
2051
|
-
/<\s*/,
|
|
2052
|
-
// Input redirect
|
|
2053
|
-
/&&/,
|
|
2054
|
-
// AND chaining
|
|
2055
|
-
/\|\|/,
|
|
2056
|
-
// OR chaining
|
|
2057
|
-
/\beval\b/i,
|
|
2058
|
-
// eval command
|
|
2059
|
-
/\bexec\b/i,
|
|
2060
|
-
// exec command
|
|
2061
|
-
/\bsystem\b/i,
|
|
2062
|
-
// system call
|
|
2063
|
-
/%0a/i,
|
|
2064
|
-
// URL-encoded newline
|
|
2065
|
-
/%0d/i,
|
|
2066
|
-
// URL-encoded carriage return
|
|
2067
|
-
/%09/i,
|
|
2068
|
-
// URL-encoded tab
|
|
2069
|
-
/\r\n/,
|
|
2070
|
-
// CRLF injection
|
|
2071
|
-
/\n/
|
|
2072
|
-
// Newline (command separator on Unix)
|
|
2073
|
-
];
|
|
2074
|
-
function detectShellInjection(value) {
|
|
2075
|
-
for (const pattern of SHELL_INJECTION_PATTERNS) {
|
|
2076
|
-
if (pattern.test(value)) return true;
|
|
2077
|
-
}
|
|
2078
|
-
return false;
|
|
2079
|
-
}
|
|
2080
|
-
var MAX_WILDCARDS_PER_VALUE = 3;
|
|
2081
|
-
function detectWildcardAbuse(value) {
|
|
2082
|
-
if (value.includes("**")) return true;
|
|
2083
|
-
const wildcardCount = (value.match(/\*/g) || []).length;
|
|
2084
|
-
if (wildcardCount > MAX_WILDCARDS_PER_VALUE) return true;
|
|
2085
|
-
return false;
|
|
2086
|
-
}
|
|
2087
|
-
var SSRF_PATTERNS2 = [
|
|
2088
|
-
/^https?:\/\/localhost\b/i,
|
|
2089
|
-
/^https?:\/\/127\.\d{1,3}\.\d{1,3}\.\d{1,3}/,
|
|
2090
|
-
/^https?:\/\/0\.0\.0\.0/,
|
|
2091
|
-
/^https?:\/\/\[::1\]/,
|
|
2092
|
-
// IPv6 loopback
|
|
2093
|
-
/^https?:\/\/10\.\d{1,3}\.\d{1,3}\.\d{1,3}/,
|
|
2094
|
-
// 10.x.x.x
|
|
2095
|
-
/^https?:\/\/172\.(1[6-9]|2\d|3[01])\./,
|
|
2096
|
-
// 172.16-31.x.x
|
|
2097
|
-
/^https?:\/\/192\.168\./,
|
|
2098
|
-
// 192.168.x.x
|
|
2099
|
-
/^https?:\/\/169\.254\./,
|
|
2100
|
-
// Link-local / AWS metadata
|
|
2101
|
-
/metadata\.google\.internal/i,
|
|
2102
|
-
// GCP metadata
|
|
2103
|
-
/^https?:\/\/metadata\b/i,
|
|
2104
|
-
// Generic metadata endpoint
|
|
2105
|
-
// IPv6 bypass patterns
|
|
2106
|
-
/^https?:\/\/\[fe80:/i,
|
|
2107
|
-
// IPv6 link-local
|
|
2108
|
-
/^https?:\/\/\[fc00:/i,
|
|
2109
|
-
// IPv6 unique local
|
|
2110
|
-
/^https?:\/\/\[fd[0-9a-f]{2}:/i,
|
|
2111
|
-
// IPv6 unique local (fd00::/8)
|
|
2112
|
-
/^https?:\/\/\[::ffff:127\./i,
|
|
2113
|
-
// IPv4-mapped IPv6 loopback
|
|
2114
|
-
/^https?:\/\/\[::ffff:10\./i,
|
|
2115
|
-
// IPv4-mapped IPv6 private
|
|
2116
|
-
/^https?:\/\/\[::ffff:172\.(1[6-9]|2\d|3[01])\./i,
|
|
2117
|
-
// IPv4-mapped IPv6 private
|
|
2118
|
-
/^https?:\/\/\[::ffff:192\.168\./i,
|
|
2119
|
-
// IPv4-mapped IPv6 private
|
|
2120
|
-
/^https?:\/\/\[::ffff:169\.254\./i,
|
|
2121
|
-
// IPv4-mapped IPv6 link-local
|
|
2122
|
-
// Hex IP bypass (e.g., 0x7f000001 = 127.0.0.1)
|
|
2123
|
-
/^https?:\/\/0x[0-9a-f]+\b/i,
|
|
2124
|
-
// Octal IP bypass (e.g., 0177.0.0.1 = 127.0.0.1)
|
|
2125
|
-
/^https?:\/\/0[0-7]{1,3}\./
|
|
2126
|
-
];
|
|
2127
|
-
function detectDecimalIP2(value) {
|
|
2128
|
-
const match = value.match(/^https?:\/\/(\d{8,10})(?:[:/]|$)/);
|
|
2129
|
-
if (!match || !match[1]) return false;
|
|
2130
|
-
const decimal = parseInt(match[1], 10);
|
|
2131
|
-
if (isNaN(decimal) || decimal > 4294967295) return false;
|
|
2132
|
-
return decimal >= 2130706432 && decimal <= 2147483647 || // 127.0.0.0/8
|
|
2133
|
-
decimal >= 167772160 && decimal <= 184549375 || // 10.0.0.0/8
|
|
2134
|
-
decimal >= 2886729728 && decimal <= 2887778303 || // 172.16.0.0/12
|
|
2135
|
-
decimal >= 3232235520 && decimal <= 3232301055 || // 192.168.0.0/16
|
|
2136
|
-
decimal >= 2851995648 && decimal <= 2852061183 || // 169.254.0.0/16
|
|
2137
|
-
decimal === 0;
|
|
2138
|
-
}
|
|
2139
|
-
function detectSSRF2(value) {
|
|
2140
|
-
for (const pattern of SSRF_PATTERNS2) {
|
|
2141
|
-
if (pattern.test(value)) return true;
|
|
2142
|
-
}
|
|
2143
|
-
if (detectDecimalIP2(value)) return true;
|
|
2144
|
-
return false;
|
|
2145
|
-
}
|
|
2146
|
-
var SQL_INJECTION_PATTERNS = [
|
|
2147
|
-
/'\s{0,20}(OR|AND)\s{0,20}'.{0,200}'/i,
|
|
2148
|
-
// ' OR '1'='1 — bounded to prevent ReDoS
|
|
2149
|
-
/'\s{0,10};\s{0,10}(DROP|DELETE|UPDATE|INSERT|ALTER|CREATE|EXEC)/i,
|
|
2150
|
-
// '; DROP TABLE
|
|
2151
|
-
/UNION\s+(ALL\s+)?SELECT/i,
|
|
2152
|
-
// UNION SELECT
|
|
2153
|
-
/--\s*$/m,
|
|
2154
|
-
// SQL comment at end of line
|
|
2155
|
-
/\/\*.{0,500}?\*\//,
|
|
2156
|
-
// SQL block comment — bounded + non-greedy
|
|
2157
|
-
/\bSLEEP\s*\(/i,
|
|
2158
|
-
// Time-based injection
|
|
2159
|
-
/\bBENCHMARK\s*\(/i,
|
|
2160
|
-
// MySQL benchmark
|
|
2161
|
-
/\bWAITFOR\s+DELAY/i,
|
|
2162
|
-
// MSSQL delay
|
|
2163
|
-
/\b(LOAD_FILE|INTO\s+OUTFILE|INTO\s+DUMPFILE)\b/i
|
|
2164
|
-
// File operations
|
|
2165
|
-
];
|
|
2166
|
-
function detectSQLInjection(value) {
|
|
2167
|
-
for (const pattern of SQL_INJECTION_PATTERNS) {
|
|
2168
|
-
if (pattern.test(value)) return true;
|
|
2169
|
-
}
|
|
2170
|
-
return false;
|
|
2171
|
-
}
|
|
2172
|
-
function checkLengthLimits(value, maxLength = 4096) {
|
|
2173
|
-
return value.length <= maxLength;
|
|
2174
|
-
}
|
|
2175
|
-
var ENTROPY_THRESHOLD = 4.5;
|
|
2176
|
-
var MIN_LENGTH_FOR_ENTROPY_CHECK = 32;
|
|
2177
|
-
function checkEntropyLimits(value) {
|
|
2178
|
-
if (value.length < MIN_LENGTH_FOR_ENTROPY_CHECK) return true;
|
|
2179
|
-
const entropy = calculateShannonEntropy(value);
|
|
2180
|
-
return entropy <= ENTROPY_THRESHOLD;
|
|
2181
|
-
}
|
|
2182
|
-
function calculateShannonEntropy(str) {
|
|
2183
|
-
const freq = /* @__PURE__ */ new Map();
|
|
2184
|
-
for (const char of str) {
|
|
2185
|
-
freq.set(char, (freq.get(char) ?? 0) + 1);
|
|
2186
|
-
}
|
|
2187
|
-
let entropy = 0;
|
|
2188
|
-
const len = str.length;
|
|
2189
|
-
for (const count of freq.values()) {
|
|
2190
|
-
const p = count / len;
|
|
2191
|
-
if (p > 0) {
|
|
2192
|
-
entropy -= p * Math.log2(p);
|
|
2193
|
-
}
|
|
2194
|
-
}
|
|
2195
|
-
return entropy;
|
|
2196
|
-
}
|
|
2197
|
-
function sanitizeInput(field, value, config = DEFAULT_INPUT_GUARD_CONFIG2) {
|
|
2198
|
-
const threats = [];
|
|
2199
|
-
if (typeof value !== "string") {
|
|
2200
|
-
if (typeof value === "object" && value !== null) {
|
|
2201
|
-
return sanitizeObject(field, value, config);
|
|
2202
|
-
}
|
|
2203
|
-
return { safe: true, threats: [] };
|
|
2204
|
-
}
|
|
2205
|
-
if (config.pathTraversal && detectPathTraversal(value)) {
|
|
2206
|
-
threats.push({
|
|
2207
|
-
type: "PATH_TRAVERSAL",
|
|
2208
|
-
field,
|
|
2209
|
-
value: truncate(value, 100),
|
|
2210
|
-
description: "Path traversal pattern detected"
|
|
2211
|
-
});
|
|
2212
|
-
}
|
|
2213
|
-
if (config.shellInjection && detectShellInjection(value)) {
|
|
2214
|
-
threats.push({
|
|
2215
|
-
type: "SHELL_INJECTION",
|
|
2216
|
-
field,
|
|
2217
|
-
value: truncate(value, 100),
|
|
2218
|
-
description: "Shell injection pattern detected"
|
|
2219
|
-
});
|
|
2220
|
-
}
|
|
2221
|
-
if (config.wildcardAbuse && detectWildcardAbuse(value)) {
|
|
2222
|
-
threats.push({
|
|
2223
|
-
type: "WILDCARD_ABUSE",
|
|
2224
|
-
field,
|
|
2225
|
-
value: truncate(value, 100),
|
|
2226
|
-
description: "Wildcard abuse pattern detected"
|
|
2227
|
-
});
|
|
2228
|
-
}
|
|
2229
|
-
if (!checkLengthLimits(value, config.lengthLimit)) {
|
|
2230
|
-
threats.push({
|
|
2231
|
-
type: "LENGTH_EXCEEDED",
|
|
2232
|
-
field,
|
|
2233
|
-
value: `[${value.length} chars]`,
|
|
2234
|
-
description: `Value exceeds maximum length of ${config.lengthLimit}`
|
|
2235
|
-
});
|
|
2236
|
-
}
|
|
2237
|
-
if (config.entropyLimit && !checkEntropyLimits(value)) {
|
|
2238
|
-
threats.push({
|
|
2239
|
-
type: "HIGH_ENTROPY",
|
|
2240
|
-
field,
|
|
2241
|
-
value: truncate(value, 100),
|
|
2242
|
-
description: "High entropy string detected - possible encoded payload"
|
|
2243
|
-
});
|
|
2244
|
-
}
|
|
2245
|
-
if (config.ssrf && detectSSRF2(value)) {
|
|
2246
|
-
threats.push({
|
|
2247
|
-
type: "SSRF",
|
|
2248
|
-
field,
|
|
2249
|
-
value: truncate(value, 100),
|
|
2250
|
-
description: "Server-side request forgery pattern detected \u2014 internal/metadata URL blocked"
|
|
2251
|
-
});
|
|
2252
|
-
}
|
|
2253
|
-
if (config.sqlInjection && detectSQLInjection(value)) {
|
|
2254
|
-
threats.push({
|
|
2255
|
-
type: "SQL_INJECTION",
|
|
2256
|
-
field,
|
|
2257
|
-
value: truncate(value, 100),
|
|
2258
|
-
description: "SQL injection pattern detected"
|
|
2259
|
-
});
|
|
2260
|
-
}
|
|
2261
|
-
return { safe: threats.length === 0, threats };
|
|
2262
|
-
}
|
|
2263
|
-
function sanitizeObject(basePath, obj, config) {
|
|
2264
|
-
const threats = [];
|
|
2265
|
-
if (Array.isArray(obj)) {
|
|
2266
|
-
for (let i = 0; i < obj.length; i++) {
|
|
2267
|
-
const result = sanitizeInput(`${basePath}[${i}]`, obj[i], config);
|
|
2268
|
-
threats.push(...result.threats);
|
|
2269
|
-
}
|
|
2270
|
-
} else {
|
|
2271
|
-
for (const [key, val] of Object.entries(obj)) {
|
|
2272
|
-
const result = sanitizeInput(`${basePath}.${key}`, val, config);
|
|
2273
|
-
threats.push(...result.threats);
|
|
2274
|
-
}
|
|
2275
|
-
}
|
|
2276
|
-
return { safe: threats.length === 0, threats };
|
|
2277
|
-
}
|
|
2278
|
-
function truncate(str, maxLen) {
|
|
2279
|
-
return str.length > maxLen ? str.slice(0, maxLen) + "..." : str;
|
|
2280
|
-
}
|
|
2281
1983
|
var DEFAULT_TOKEN_TTL_SECONDS = 30;
|
|
2282
1984
|
var TOKEN_ALGORITHM = "HS256";
|
|
2283
1985
|
var MIN_SECRET_LENGTH = 32;
|
|
@@ -2335,6 +2037,14 @@ function matchParts(pathParts, pi, patternParts, qi) {
|
|
|
2335
2037
|
qi++;
|
|
2336
2038
|
continue;
|
|
2337
2039
|
}
|
|
2040
|
+
if (pattern.includes("*")) {
|
|
2041
|
+
if (!matchSegmentGlob(pathParts[pi], pattern)) {
|
|
2042
|
+
return false;
|
|
2043
|
+
}
|
|
2044
|
+
pi++;
|
|
2045
|
+
qi++;
|
|
2046
|
+
continue;
|
|
2047
|
+
}
|
|
2338
2048
|
if (pattern !== pathParts[pi]) {
|
|
2339
2049
|
return false;
|
|
2340
2050
|
}
|
|
@@ -2371,6 +2081,31 @@ function isPathAllowed(path, constraints) {
|
|
|
2371
2081
|
}
|
|
2372
2082
|
return true;
|
|
2373
2083
|
}
|
|
2084
|
+
function matchSegmentGlob(segment, pattern) {
|
|
2085
|
+
const startsWithStar = pattern.startsWith("*");
|
|
2086
|
+
const endsWithStar = pattern.endsWith("*");
|
|
2087
|
+
if (pattern === "*") return true;
|
|
2088
|
+
if (startsWithStar && endsWithStar) {
|
|
2089
|
+
const infix = pattern.slice(1, -1);
|
|
2090
|
+
return segment.toLowerCase().includes(infix.toLowerCase());
|
|
2091
|
+
}
|
|
2092
|
+
if (startsWithStar) {
|
|
2093
|
+
const suffix = pattern.slice(1);
|
|
2094
|
+
return segment.toLowerCase().endsWith(suffix.toLowerCase());
|
|
2095
|
+
}
|
|
2096
|
+
if (endsWithStar) {
|
|
2097
|
+
const prefix = pattern.slice(0, -1);
|
|
2098
|
+
return segment.toLowerCase().startsWith(prefix.toLowerCase());
|
|
2099
|
+
}
|
|
2100
|
+
const starIdx = pattern.indexOf("*");
|
|
2101
|
+
if (starIdx !== -1) {
|
|
2102
|
+
const prefix = pattern.slice(0, starIdx);
|
|
2103
|
+
const suffix = pattern.slice(starIdx + 1);
|
|
2104
|
+
const seg = segment.toLowerCase();
|
|
2105
|
+
return seg.startsWith(prefix.toLowerCase()) && seg.endsWith(suffix.toLowerCase()) && seg.length >= prefix.length + suffix.length;
|
|
2106
|
+
}
|
|
2107
|
+
return segment === pattern;
|
|
2108
|
+
}
|
|
2374
2109
|
function extractPathArguments(args) {
|
|
2375
2110
|
const paths = [];
|
|
2376
2111
|
for (const value of Object.values(args)) {
|
|
@@ -2380,6 +2115,69 @@ function extractPathArguments(args) {
|
|
|
2380
2115
|
}
|
|
2381
2116
|
return paths;
|
|
2382
2117
|
}
|
|
2118
|
+
var COMMAND_FIELDS = /* @__PURE__ */ new Set([
|
|
2119
|
+
"command",
|
|
2120
|
+
"cmd",
|
|
2121
|
+
"query",
|
|
2122
|
+
"code",
|
|
2123
|
+
"script",
|
|
2124
|
+
"shell",
|
|
2125
|
+
"exec",
|
|
2126
|
+
"sql",
|
|
2127
|
+
"expression"
|
|
2128
|
+
]);
|
|
2129
|
+
function extractCommandArguments(args) {
|
|
2130
|
+
const commands = [];
|
|
2131
|
+
for (const [key, value] of Object.entries(args)) {
|
|
2132
|
+
if (typeof value !== "string") continue;
|
|
2133
|
+
if (COMMAND_FIELDS.has(key.toLowerCase())) {
|
|
2134
|
+
commands.push(value);
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
return commands;
|
|
2138
|
+
}
|
|
2139
|
+
function matchCommandPattern(command, pattern) {
|
|
2140
|
+
if (pattern === "*") return true;
|
|
2141
|
+
const normalizedCommand = command.trim().toLowerCase();
|
|
2142
|
+
const normalizedPattern = pattern.trim().toLowerCase();
|
|
2143
|
+
if (normalizedPattern === normalizedCommand) return true;
|
|
2144
|
+
const startsWithStar = normalizedPattern.startsWith("*");
|
|
2145
|
+
const endsWithStar = normalizedPattern.endsWith("*");
|
|
2146
|
+
if (startsWithStar && endsWithStar) {
|
|
2147
|
+
const infix = normalizedPattern.slice(1, -1);
|
|
2148
|
+
return infix.length > 0 && normalizedCommand.includes(infix);
|
|
2149
|
+
}
|
|
2150
|
+
if (endsWithStar) {
|
|
2151
|
+
const prefix = normalizedPattern.slice(0, -1);
|
|
2152
|
+
return normalizedCommand.startsWith(prefix);
|
|
2153
|
+
}
|
|
2154
|
+
if (startsWithStar) {
|
|
2155
|
+
const suffix = normalizedPattern.slice(1);
|
|
2156
|
+
return normalizedCommand.endsWith(suffix);
|
|
2157
|
+
}
|
|
2158
|
+
const commandName = normalizedCommand.split(/\s+/)[0] ?? "";
|
|
2159
|
+
return commandName === normalizedPattern;
|
|
2160
|
+
}
|
|
2161
|
+
function isCommandAllowed(command, constraints) {
|
|
2162
|
+
if (constraints.denied && constraints.denied.length > 0) {
|
|
2163
|
+
for (const pattern of constraints.denied) {
|
|
2164
|
+
if (matchCommandPattern(command, pattern)) {
|
|
2165
|
+
return false;
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
if (constraints.allowed && constraints.allowed.length > 0) {
|
|
2170
|
+
let matchesAllowed = false;
|
|
2171
|
+
for (const pattern of constraints.allowed) {
|
|
2172
|
+
if (matchCommandPattern(command, pattern)) {
|
|
2173
|
+
matchesAllowed = true;
|
|
2174
|
+
break;
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
if (!matchesAllowed) return false;
|
|
2178
|
+
}
|
|
2179
|
+
return true;
|
|
2180
|
+
}
|
|
2383
2181
|
function ruleMatchesRequest(rule, request) {
|
|
2384
2182
|
if (!rule.enabled) return false;
|
|
2385
2183
|
if (rule.permission !== request.requiredPermission) return false;
|
|
@@ -2393,8 +2191,19 @@ function ruleMatchesRequest(rule, request) {
|
|
|
2393
2191
|
}
|
|
2394
2192
|
}
|
|
2395
2193
|
if (rule.pathConstraints) {
|
|
2396
|
-
|
|
2397
|
-
|
|
2194
|
+
const satisfied = pathConstraintsMatch(rule.pathConstraints, request.arguments);
|
|
2195
|
+
if (rule.effect === "DENY") {
|
|
2196
|
+
if (satisfied) return false;
|
|
2197
|
+
} else {
|
|
2198
|
+
if (!satisfied) return false;
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
if (rule.commandConstraints) {
|
|
2202
|
+
const satisfied = commandConstraintsMatch(rule.commandConstraints, request.arguments);
|
|
2203
|
+
if (rule.effect === "DENY") {
|
|
2204
|
+
if (satisfied) return false;
|
|
2205
|
+
} else {
|
|
2206
|
+
if (!satisfied) return false;
|
|
2398
2207
|
}
|
|
2399
2208
|
}
|
|
2400
2209
|
return true;
|
|
@@ -2482,6 +2291,11 @@ function pathConstraintsMatch(constraints, args) {
|
|
|
2482
2291
|
if (paths.length === 0) return true;
|
|
2483
2292
|
return paths.every((path) => isPathAllowed(path, constraints));
|
|
2484
2293
|
}
|
|
2294
|
+
function commandConstraintsMatch(constraints, args) {
|
|
2295
|
+
const commands = extractCommandArguments(args);
|
|
2296
|
+
if (commands.length === 0) return true;
|
|
2297
|
+
return commands.every((cmd) => isCommandAllowed(cmd, constraints));
|
|
2298
|
+
}
|
|
2485
2299
|
function evaluatePolicy(policySet, request) {
|
|
2486
2300
|
const startTime = performance.now();
|
|
2487
2301
|
const sortedRules = [...policySet.rules].sort(
|
|
@@ -2519,7 +2333,7 @@ function evaluatePolicy(policySet, request) {
|
|
|
2519
2333
|
function validatePolicyRule(input) {
|
|
2520
2334
|
const errors = [];
|
|
2521
2335
|
const warnings = [];
|
|
2522
|
-
const result =
|
|
2336
|
+
const result = PolicyRuleSchema.safeParse(input);
|
|
2523
2337
|
if (!result.success) {
|
|
2524
2338
|
return {
|
|
2525
2339
|
valid: false,
|
|
@@ -2544,7 +2358,7 @@ function validatePolicyRule(input) {
|
|
|
2544
2358
|
function validatePolicySet(input) {
|
|
2545
2359
|
const errors = [];
|
|
2546
2360
|
const warnings = [];
|
|
2547
|
-
const result =
|
|
2361
|
+
const result = PolicySetSchema.safeParse(input);
|
|
2548
2362
|
if (!result.success) {
|
|
2549
2363
|
return {
|
|
2550
2364
|
valid: false,
|
|
@@ -2638,7 +2452,7 @@ function createDefaultDenyPolicySet() {
|
|
|
2638
2452
|
effect: PolicyEffect.DENY,
|
|
2639
2453
|
priority: 1e4,
|
|
2640
2454
|
toolPattern: "*",
|
|
2641
|
-
permission:
|
|
2455
|
+
permission: Permission.EXECUTE,
|
|
2642
2456
|
minimumTrustLevel: TrustLevel.UNTRUSTED,
|
|
2643
2457
|
enabled: true,
|
|
2644
2458
|
createdAt: now,
|
|
@@ -2650,7 +2464,7 @@ function createDefaultDenyPolicySet() {
|
|
|
2650
2464
|
effect: PolicyEffect.DENY,
|
|
2651
2465
|
priority: 1e4,
|
|
2652
2466
|
toolPattern: "*",
|
|
2653
|
-
permission:
|
|
2467
|
+
permission: Permission.WRITE,
|
|
2654
2468
|
minimumTrustLevel: TrustLevel.UNTRUSTED,
|
|
2655
2469
|
enabled: true,
|
|
2656
2470
|
createdAt: now,
|
|
@@ -2662,7 +2476,7 @@ function createDefaultDenyPolicySet() {
|
|
|
2662
2476
|
effect: PolicyEffect.DENY,
|
|
2663
2477
|
priority: 1e4,
|
|
2664
2478
|
toolPattern: "*",
|
|
2665
|
-
permission:
|
|
2479
|
+
permission: Permission.READ,
|
|
2666
2480
|
minimumTrustLevel: TrustLevel.UNTRUSTED,
|
|
2667
2481
|
enabled: true,
|
|
2668
2482
|
createdAt: now,
|
|
@@ -2841,7 +2655,6 @@ var DEFAULT_CONFIG = Object.freeze({
|
|
|
2841
2655
|
globalRateLimitPerMinute: 600,
|
|
2842
2656
|
rateLimitPerTool: 60,
|
|
2843
2657
|
tokenTtlSeconds: 30,
|
|
2844
|
-
inputGuardConfig: DEFAULT_INPUT_GUARD_CONFIG2,
|
|
2845
2658
|
enableVersionedPolicies: true
|
|
2846
2659
|
});
|
|
2847
2660
|
function resolveConfig(userConfig) {
|
|
@@ -2874,7 +2687,7 @@ async function interceptToolCall(params, upstreamCall, options) {
|
|
|
2874
2687
|
toolName: params.name,
|
|
2875
2688
|
serverName: "default",
|
|
2876
2689
|
arguments: params.arguments ?? {},
|
|
2877
|
-
requiredPermission:
|
|
2690
|
+
requiredPermission: Permission.EXECUTE,
|
|
2878
2691
|
timestamp
|
|
2879
2692
|
};
|
|
2880
2693
|
if (options.rateLimiter) {
|
|
@@ -2912,24 +2725,6 @@ async function interceptToolCall(params, upstreamCall, options) {
|
|
|
2912
2725
|
}
|
|
2913
2726
|
}
|
|
2914
2727
|
}
|
|
2915
|
-
if (options.validateSchemas && params.arguments) {
|
|
2916
|
-
const guardConfig = options.inputGuardConfig ?? DEFAULT_INPUT_GUARD_CONFIG2;
|
|
2917
|
-
const sanitization = sanitizeInput("arguments", params.arguments, guardConfig);
|
|
2918
|
-
if (!sanitization.safe) {
|
|
2919
|
-
const threatDescriptions = sanitization.threats.map(
|
|
2920
|
-
(t) => `${t.type}: ${t.description} (field: ${t.field})`
|
|
2921
|
-
);
|
|
2922
|
-
const result = {
|
|
2923
|
-
status: "ERROR",
|
|
2924
|
-
request,
|
|
2925
|
-
error: new SchemaValidationError(params.name, threatDescriptions),
|
|
2926
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2927
|
-
};
|
|
2928
|
-
options.onDecision?.(result);
|
|
2929
|
-
const reason = options.verboseErrors ? `Input validation failed: ${sanitization.threats.length} threat(s) detected` : "Input validation failed.";
|
|
2930
|
-
return createDeniedToolResult(reason);
|
|
2931
|
-
}
|
|
2932
|
-
}
|
|
2933
2728
|
const decision = options.policyEngine.evaluate(request);
|
|
2934
2729
|
if (decision.effect === "DENY") {
|
|
2935
2730
|
const result = {
|
|
@@ -2946,7 +2741,7 @@ async function interceptToolCall(params, upstreamCall, options) {
|
|
|
2946
2741
|
if (options.tokenIssuer) {
|
|
2947
2742
|
capabilityToken = options.tokenIssuer.issue(
|
|
2948
2743
|
requestId,
|
|
2949
|
-
[
|
|
2744
|
+
[Permission.EXECUTE],
|
|
2950
2745
|
[params.name]
|
|
2951
2746
|
);
|
|
2952
2747
|
}
|
|
@@ -3523,7 +3318,6 @@ var SolonGate = class {
|
|
|
3523
3318
|
tokenIssuer: this.tokenIssuer ?? void 0,
|
|
3524
3319
|
serverVerifier: this.serverVerifier ?? void 0,
|
|
3525
3320
|
rateLimiter: this.rateLimiter,
|
|
3526
|
-
inputGuardConfig: this.config.inputGuardConfig,
|
|
3527
3321
|
rateLimitPerTool: this.config.rateLimitPerTool,
|
|
3528
3322
|
globalRateLimitPerMinute: this.config.globalRateLimitPerMinute
|
|
3529
3323
|
});
|
|
@@ -3591,7 +3385,7 @@ var SolonGateProxy = class {
|
|
|
3591
3385
|
apiKey: "sg_test_proxy_internal_00000000",
|
|
3592
3386
|
policySet: config.policy,
|
|
3593
3387
|
config: {
|
|
3594
|
-
validateSchemas:
|
|
3388
|
+
validateSchemas: true,
|
|
3595
3389
|
verboseErrors: config.verbose ?? false,
|
|
3596
3390
|
rateLimitPerTool: config.rateLimitPerTool,
|
|
3597
3391
|
globalRateLimitPerMinute: config.globalRateLimit
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solongate/proxy",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "MCP security proxy
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "MCP security proxy — protect any MCP server with customizable policies, path/command constraints, rate limiting, and audit logging. Zero code changes required.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"solongate-proxy": "./dist/index.js",
|