@solongate/proxy 0.26.4 → 0.27.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 +141 -25
- package/dist/init.js +3 -2
- package/dist/lib.js +102 -19
- package/hooks/guard.mjs +45 -8
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -47,7 +47,11 @@ async function fetchCloudPolicy(apiKey, apiUrl, policyId) {
|
|
|
47
47
|
}
|
|
48
48
|
async function sendAuditLog(apiKey, apiUrl, entry) {
|
|
49
49
|
const url = `${apiUrl}/api/v1/audit-logs`;
|
|
50
|
-
const body = JSON.stringify(
|
|
50
|
+
const body = JSON.stringify({
|
|
51
|
+
...entry,
|
|
52
|
+
agent_id: entry.agent_id,
|
|
53
|
+
agent_name: entry.agent_name
|
|
54
|
+
});
|
|
51
55
|
for (let attempt = 0; attempt < AUDIT_MAX_RETRIES; attempt++) {
|
|
52
56
|
try {
|
|
53
57
|
const res = await fetch(url, {
|
|
@@ -165,10 +169,36 @@ function parseArgs(argv) {
|
|
|
165
169
|
let aiJudgeEndpoint = "https://api.groq.com/openai";
|
|
166
170
|
let aiJudgeApiKey;
|
|
167
171
|
let aiJudgeTimeout = 5e3;
|
|
172
|
+
let agentName;
|
|
168
173
|
let separatorIndex = args.indexOf("--");
|
|
169
174
|
const flags = separatorIndex >= 0 ? args.slice(0, separatorIndex) : args;
|
|
170
|
-
|
|
175
|
+
let upstreamArgs = separatorIndex >= 0 ? args.slice(separatorIndex + 1) : [];
|
|
176
|
+
const flagsWithValue = /* @__PURE__ */ new Set([
|
|
177
|
+
"--policy",
|
|
178
|
+
"--name",
|
|
179
|
+
"--rate-limit",
|
|
180
|
+
"--global-rate-limit",
|
|
181
|
+
"--config",
|
|
182
|
+
"--api-key",
|
|
183
|
+
"--api-url",
|
|
184
|
+
"--upstream-url",
|
|
185
|
+
"--upstream-transport",
|
|
186
|
+
"--port",
|
|
187
|
+
"--policy-id",
|
|
188
|
+
"--id",
|
|
189
|
+
"--ai-judge-model",
|
|
190
|
+
"--ai-judge-endpoint",
|
|
191
|
+
"--ai-judge-api-key",
|
|
192
|
+
"--ai-judge-timeout",
|
|
193
|
+
"--agent-name"
|
|
194
|
+
]);
|
|
171
195
|
for (let i = 0; i < flags.length; i++) {
|
|
196
|
+
if (!flags[i].startsWith("--")) {
|
|
197
|
+
if (upstreamArgs.length === 0) {
|
|
198
|
+
upstreamArgs.push(...flags.slice(i));
|
|
199
|
+
}
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
172
202
|
switch (flags[i]) {
|
|
173
203
|
case "--policy":
|
|
174
204
|
policySource = flags[++i];
|
|
@@ -228,6 +258,9 @@ function parseArgs(argv) {
|
|
|
228
258
|
case "--ai-judge-timeout":
|
|
229
259
|
aiJudgeTimeout = parseInt(flags[++i], 10);
|
|
230
260
|
break;
|
|
261
|
+
case "--agent-name":
|
|
262
|
+
agentName = flags[++i];
|
|
263
|
+
break;
|
|
231
264
|
}
|
|
232
265
|
}
|
|
233
266
|
if (!aiJudgeApiKey) {
|
|
@@ -296,7 +329,8 @@ function parseArgs(argv) {
|
|
|
296
329
|
policyPath: resolvePolicyPath(cfgPolicySource) ?? void 0,
|
|
297
330
|
policyId: policyId ?? fileConfig.policyId,
|
|
298
331
|
advancedDetection: advancedDetection ? { enabled: true } : void 0,
|
|
299
|
-
aiJudge
|
|
332
|
+
aiJudge,
|
|
333
|
+
agentName
|
|
300
334
|
};
|
|
301
335
|
}
|
|
302
336
|
if (upstreamUrl) {
|
|
@@ -319,7 +353,8 @@ function parseArgs(argv) {
|
|
|
319
353
|
policyPath: resolvedPolicyPath ?? void 0,
|
|
320
354
|
policyId,
|
|
321
355
|
advancedDetection: advancedDetection ? { enabled: true } : void 0,
|
|
322
|
-
aiJudge
|
|
356
|
+
aiJudge,
|
|
357
|
+
agentName
|
|
323
358
|
};
|
|
324
359
|
}
|
|
325
360
|
if (upstreamArgs.length === 0) {
|
|
@@ -346,7 +381,8 @@ function parseArgs(argv) {
|
|
|
346
381
|
policyPath: resolvedPolicyPath ?? void 0,
|
|
347
382
|
policyId,
|
|
348
383
|
advancedDetection: advancedDetection ? { enabled: true } : void 0,
|
|
349
|
-
aiJudge
|
|
384
|
+
aiJudge,
|
|
385
|
+
agentName
|
|
350
386
|
};
|
|
351
387
|
}
|
|
352
388
|
function resolvePolicyPath(source) {
|
|
@@ -481,10 +517,11 @@ function isAlreadyProtected(server) {
|
|
|
481
517
|
}
|
|
482
518
|
return false;
|
|
483
519
|
}
|
|
484
|
-
function wrapServer(server, policy) {
|
|
520
|
+
function wrapServer(serverName, server, policy) {
|
|
485
521
|
const env = { ...server.env ?? {} };
|
|
486
522
|
env.SOLONGATE_API_KEY = "${SOLONGATE_API_KEY}";
|
|
487
523
|
const proxyArgs = ["-y", "@solongate/proxy@latest"];
|
|
524
|
+
proxyArgs.push("--agent-name", serverName);
|
|
488
525
|
if (policy) {
|
|
489
526
|
proxyArgs.push("--policy", policy);
|
|
490
527
|
}
|
|
@@ -885,7 +922,7 @@ async function main() {
|
|
|
885
922
|
const newConfig = { mcpServers: {} };
|
|
886
923
|
for (const name of serverNames) {
|
|
887
924
|
if (toProtect.includes(name)) {
|
|
888
|
-
newConfig.mcpServers[name] = wrapServer(config.mcpServers[name], policyValue);
|
|
925
|
+
newConfig.mcpServers[name] = wrapServer(name, config.mcpServers[name], policyValue);
|
|
889
926
|
} else {
|
|
890
927
|
newConfig.mcpServers[name] = config.mcpServers[name];
|
|
891
928
|
}
|
|
@@ -2889,25 +2926,25 @@ function checkEntropyLimits(value) {
|
|
|
2889
2926
|
}
|
|
2890
2927
|
function calculateShannonEntropy(str) {
|
|
2891
2928
|
const freq = new Uint32Array(128);
|
|
2892
|
-
|
|
2929
|
+
const nonAsciiFreq = /* @__PURE__ */ new Map();
|
|
2893
2930
|
for (let i = 0; i < str.length; i++) {
|
|
2894
2931
|
const code = str.charCodeAt(i);
|
|
2895
2932
|
if (code < 128) {
|
|
2896
|
-
freq[code] =
|
|
2933
|
+
freq[code] = freq[code] + 1;
|
|
2897
2934
|
} else {
|
|
2898
|
-
|
|
2935
|
+
nonAsciiFreq.set(code, (nonAsciiFreq.get(code) || 0) + 1);
|
|
2899
2936
|
}
|
|
2900
2937
|
}
|
|
2901
2938
|
let entropy = 0;
|
|
2902
2939
|
const len = str.length;
|
|
2903
2940
|
for (let i = 0; i < 128; i++) {
|
|
2904
|
-
if (
|
|
2905
|
-
const p =
|
|
2941
|
+
if (freq[i] > 0) {
|
|
2942
|
+
const p = freq[i] / len;
|
|
2906
2943
|
entropy -= p * Math.log2(p);
|
|
2907
2944
|
}
|
|
2908
2945
|
}
|
|
2909
|
-
|
|
2910
|
-
const p =
|
|
2946
|
+
for (const count of nonAsciiFreq.values()) {
|
|
2947
|
+
const p = count / len;
|
|
2911
2948
|
entropy -= p * Math.log2(p);
|
|
2912
2949
|
}
|
|
2913
2950
|
return entropy;
|
|
@@ -3978,7 +4015,7 @@ function urlConstraintsMatch(constraints, args) {
|
|
|
3978
4015
|
}
|
|
3979
4016
|
function evaluatePolicy(policySet, request) {
|
|
3980
4017
|
const startTime = performance.now();
|
|
3981
|
-
const sortedRules = policySet.rules;
|
|
4018
|
+
const sortedRules = [...policySet.rules].sort((a, b) => a.priority - b.priority);
|
|
3982
4019
|
for (const rule of sortedRules) {
|
|
3983
4020
|
if (ruleMatchesRequest(rule, request)) {
|
|
3984
4021
|
const endTime2 = performance.now();
|
|
@@ -5457,9 +5494,6 @@ var PolicySyncManager = class {
|
|
|
5457
5494
|
*/
|
|
5458
5495
|
policiesEqual(a, b) {
|
|
5459
5496
|
if (a.name !== b.name || a.rules.length !== b.rules.length) return false;
|
|
5460
|
-
if (a.version !== void 0 && b.version !== void 0 && a.version === b.version && a.id === b.id) {
|
|
5461
|
-
return true;
|
|
5462
|
-
}
|
|
5463
5497
|
return JSON.stringify(a.rules) === JSON.stringify(b.rules);
|
|
5464
5498
|
}
|
|
5465
5499
|
};
|
|
@@ -5499,12 +5533,17 @@ CRITICAL: Only DENY access to files EXPLICITLY in the protected_files list. "cat
|
|
|
5499
5533
|
|
|
5500
5534
|
Respond with ONLY valid JSON, no markdown, no explanation outside the JSON:
|
|
5501
5535
|
{"decision": "ALLOW" or "DENY", "reason": "brief one-line explanation", "confidence": 0.0 to 1.0}`;
|
|
5502
|
-
var AiJudge = class {
|
|
5536
|
+
var AiJudge = class _AiJudge {
|
|
5503
5537
|
config;
|
|
5504
5538
|
protectedFiles;
|
|
5505
5539
|
protectedPaths;
|
|
5506
5540
|
deniedActions;
|
|
5507
5541
|
isOllamaEndpoint;
|
|
5542
|
+
// Circuit breaker: after 3 consecutive failures in 60s, skip AI Judge
|
|
5543
|
+
consecutiveFailures = 0;
|
|
5544
|
+
lastFailureTime = 0;
|
|
5545
|
+
static CIRCUIT_BREAKER_THRESHOLD = 3;
|
|
5546
|
+
static CIRCUIT_BREAKER_RESET_MS = 6e4;
|
|
5508
5547
|
constructor(config, protectedFiles, protectedPaths, deniedActions = [
|
|
5509
5548
|
"file deletion",
|
|
5510
5549
|
"data exfiltration",
|
|
@@ -5523,6 +5562,17 @@ var AiJudge = class {
|
|
|
5523
5562
|
* Fail-closed: any error (timeout, parse failure, connection refused) → DENY.
|
|
5524
5563
|
*/
|
|
5525
5564
|
async evaluate(toolName, args) {
|
|
5565
|
+
if (this.consecutiveFailures >= _AiJudge.CIRCUIT_BREAKER_THRESHOLD) {
|
|
5566
|
+
const elapsed = Date.now() - this.lastFailureTime;
|
|
5567
|
+
if (elapsed < _AiJudge.CIRCUIT_BREAKER_RESET_MS) {
|
|
5568
|
+
return {
|
|
5569
|
+
decision: "ALLOW",
|
|
5570
|
+
reason: `AI Judge circuit breaker open (${this.consecutiveFailures} consecutive failures) \u2014 falling back to policy-only`,
|
|
5571
|
+
confidence: 0.5
|
|
5572
|
+
};
|
|
5573
|
+
}
|
|
5574
|
+
this.consecutiveFailures = 0;
|
|
5575
|
+
}
|
|
5526
5576
|
const sanitizedArgs = this.sanitizeArgs(args);
|
|
5527
5577
|
const userMessage = JSON.stringify({
|
|
5528
5578
|
tool: toolName,
|
|
@@ -5533,12 +5583,15 @@ var AiJudge = class {
|
|
|
5533
5583
|
});
|
|
5534
5584
|
try {
|
|
5535
5585
|
const response = await this.callLLM(userMessage);
|
|
5586
|
+
this.consecutiveFailures = 0;
|
|
5536
5587
|
return this.parseVerdict(response);
|
|
5537
5588
|
} catch (err) {
|
|
5589
|
+
this.consecutiveFailures++;
|
|
5590
|
+
this.lastFailureTime = Date.now();
|
|
5538
5591
|
const message = err instanceof Error ? err.message : String(err);
|
|
5539
5592
|
return {
|
|
5540
5593
|
decision: "DENY",
|
|
5541
|
-
reason: `AI Judge error (fail-closed): ${message}`,
|
|
5594
|
+
reason: `AI Judge error (fail-closed): ${message.slice(0, 100)}`,
|
|
5542
5595
|
confidence: 1
|
|
5543
5596
|
};
|
|
5544
5597
|
}
|
|
@@ -5551,8 +5604,15 @@ var AiJudge = class {
|
|
|
5551
5604
|
const sanitize = (val, depth = 0) => {
|
|
5552
5605
|
if (depth > 10) return "[nested]";
|
|
5553
5606
|
if (typeof val === "string") {
|
|
5554
|
-
|
|
5555
|
-
|
|
5607
|
+
let s = val.length > maxStringLen ? val.slice(0, maxStringLen) + "...[truncated]" : val;
|
|
5608
|
+
s = s.replace(/\{[^{}]*"decision"\s*:/gi, "{[redacted]:");
|
|
5609
|
+
s = s.replace(/\{[^{}]*"ALLOW"[^{}]*\}/gi, "[redacted-json]");
|
|
5610
|
+
s = s.replace(/\{[^{}]*"DENY"[^{}]*\}/gi, "[redacted-json]");
|
|
5611
|
+
s = s.replace(/respond\s+with\s*:/gi, "[redacted]");
|
|
5612
|
+
s = s.replace(/ignore\s+(previous|above|all)\s+(instructions?|prompts?)/gi, "[redacted]");
|
|
5613
|
+
s = s.replace(/you\s+are\s+now\s+/gi, "[redacted] ");
|
|
5614
|
+
s = s.replace(/system\s*:\s*/gi, "[redacted]: ");
|
|
5615
|
+
return s;
|
|
5556
5616
|
}
|
|
5557
5617
|
if (Array.isArray(val)) return val.slice(0, 50).map((v) => sanitize(v, depth + 1));
|
|
5558
5618
|
if (val && typeof val === "object") {
|
|
@@ -5595,7 +5655,8 @@ var AiJudge = class {
|
|
|
5595
5655
|
{ role: "user", content: userMessage }
|
|
5596
5656
|
],
|
|
5597
5657
|
temperature: 0,
|
|
5598
|
-
max_tokens: 200
|
|
5658
|
+
max_tokens: 200,
|
|
5659
|
+
response_format: { type: "json_object" }
|
|
5599
5660
|
});
|
|
5600
5661
|
if (this.config.apiKey) {
|
|
5601
5662
|
headers["Authorization"] = `Bearer ${this.config.apiKey}`;
|
|
@@ -5721,9 +5782,20 @@ var SolonGateProxy = class {
|
|
|
5721
5782
|
aiJudge = null;
|
|
5722
5783
|
upstreamTools = [];
|
|
5723
5784
|
guardConfig;
|
|
5785
|
+
/** Agent identity for trust map — resolved from CLI flag, HTTP headers, or MCP clientInfo */
|
|
5786
|
+
agentId = null;
|
|
5787
|
+
agentName = null;
|
|
5788
|
+
/** Per-session agent info for HTTP mode (keyed by session ID) */
|
|
5789
|
+
httpAgentInfo = /* @__PURE__ */ new Map();
|
|
5790
|
+
/** Per-request sub-agent info from HTTP headers (transient, overwritten per request) */
|
|
5791
|
+
httpSubAgent = null;
|
|
5724
5792
|
constructor(config) {
|
|
5725
5793
|
this.config = config;
|
|
5726
5794
|
this.guardConfig = config.advancedDetection ? { ...DEFAULT_INPUT_GUARD_CONFIG, advancedDetection: config.advancedDetection } : DEFAULT_INPUT_GUARD_CONFIG;
|
|
5795
|
+
if (config.agentName) {
|
|
5796
|
+
this.agentName = config.agentName;
|
|
5797
|
+
this.agentId = config.agentName.toLowerCase().replace(/\s+/g, "-");
|
|
5798
|
+
}
|
|
5727
5799
|
this.gate = new SolonGate({
|
|
5728
5800
|
name: config.name ?? "solongate-proxy",
|
|
5729
5801
|
apiKey: "sg_test_proxy_internal_00000000",
|
|
@@ -5740,6 +5812,19 @@ var SolonGateProxy = class {
|
|
|
5740
5812
|
log2("WARNING:", w);
|
|
5741
5813
|
}
|
|
5742
5814
|
}
|
|
5815
|
+
/** Extract sub-agent identity from MCP _meta field */
|
|
5816
|
+
extractSubAgent(request) {
|
|
5817
|
+
const meta = request?.params?._meta;
|
|
5818
|
+
if (meta && typeof meta === "object") {
|
|
5819
|
+
const solonMeta = meta["io.solongate/agent"];
|
|
5820
|
+
if (solonMeta && typeof solonMeta === "object") {
|
|
5821
|
+
const agent = solonMeta;
|
|
5822
|
+
const id = agent.id ? String(agent.id) : null;
|
|
5823
|
+
if (id) return { subAgentId: id, subAgentName: agent.name ? String(agent.name) : id };
|
|
5824
|
+
}
|
|
5825
|
+
}
|
|
5826
|
+
return null;
|
|
5827
|
+
}
|
|
5743
5828
|
/**
|
|
5744
5829
|
* Start the proxy: connect to upstream, then serve downstream.
|
|
5745
5830
|
*/
|
|
@@ -5920,6 +6005,16 @@ var SolonGateProxy = class {
|
|
|
5920
6005
|
}
|
|
5921
6006
|
}
|
|
5922
6007
|
);
|
|
6008
|
+
this.server.oninitialized = () => {
|
|
6009
|
+
if (!this.agentId && this.server) {
|
|
6010
|
+
const clientVersion = this.server.getClientVersion();
|
|
6011
|
+
if (clientVersion?.name) {
|
|
6012
|
+
this.agentId = clientVersion.version ? `${clientVersion.name}/${clientVersion.version}` : clientVersion.name;
|
|
6013
|
+
this.agentName = clientVersion.name;
|
|
6014
|
+
log2(`Agent identified from MCP clientInfo: ${this.agentName} (${this.agentId})`);
|
|
6015
|
+
}
|
|
6016
|
+
}
|
|
6017
|
+
};
|
|
5923
6018
|
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
5924
6019
|
return { tools: this.upstreamTools };
|
|
5925
6020
|
});
|
|
@@ -5927,6 +6022,7 @@ var SolonGateProxy = class {
|
|
|
5927
6022
|
const MUTEX_TIMEOUT_MS = 3e4;
|
|
5928
6023
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
5929
6024
|
const { name, arguments: args } = request.params;
|
|
6025
|
+
const subAgent = this.extractSubAgent(request) || this.httpSubAgent;
|
|
5930
6026
|
const argsSize = TEXT_ENCODER.encode(JSON.stringify(args ?? {})).length;
|
|
5931
6027
|
if (argsSize > MAX_ARGUMENT_SIZE) {
|
|
5932
6028
|
log2(`DENY: ${name} \u2014 payload size ${argsSize} exceeds limit ${MAX_ARGUMENT_SIZE}`);
|
|
@@ -5964,7 +6060,11 @@ var SolonGateProxy = class {
|
|
|
5964
6060
|
decision: "DENY",
|
|
5965
6061
|
reason: `Prompt injection detected: ${threats}`,
|
|
5966
6062
|
evaluationTimeMs: 0,
|
|
5967
|
-
promptInjection: piResult
|
|
6063
|
+
promptInjection: piResult,
|
|
6064
|
+
agent_id: this.agentId ?? void 0,
|
|
6065
|
+
agent_name: this.agentName ?? void 0,
|
|
6066
|
+
sub_agent_id: subAgent?.subAgentId,
|
|
6067
|
+
sub_agent_name: subAgent?.subAgentName
|
|
5968
6068
|
});
|
|
5969
6069
|
}
|
|
5970
6070
|
return {
|
|
@@ -6056,7 +6156,11 @@ var SolonGateProxy = class {
|
|
|
6056
6156
|
reason,
|
|
6057
6157
|
matchedRule,
|
|
6058
6158
|
evaluationTimeMs,
|
|
6059
|
-
promptInjection: piResult
|
|
6159
|
+
promptInjection: piResult,
|
|
6160
|
+
agent_id: this.agentId ?? void 0,
|
|
6161
|
+
agent_name: this.agentName ?? void 0,
|
|
6162
|
+
sub_agent_id: subAgent?.subAgentId,
|
|
6163
|
+
sub_agent_name: subAgent?.subAgentName
|
|
6060
6164
|
});
|
|
6061
6165
|
} else {
|
|
6062
6166
|
log2(`Skipping audit log (apiKey: ${this.config.apiKey ? "test key" : "not set"})`);
|
|
@@ -6341,6 +6445,18 @@ ${msg.content.text}`;
|
|
|
6341
6445
|
await this.server.connect(httpTransport);
|
|
6342
6446
|
const httpServer = createHttpServer(async (req, res) => {
|
|
6343
6447
|
if (req.url === "/mcp" || req.url?.startsWith("/mcp?")) {
|
|
6448
|
+
if (!this.agentId) {
|
|
6449
|
+
const headerAgentId = req.headers["x-agent-id"];
|
|
6450
|
+
const headerAgentName = req.headers["x-agent-name"];
|
|
6451
|
+
if (headerAgentId) {
|
|
6452
|
+
this.agentId = headerAgentId;
|
|
6453
|
+
this.agentName = headerAgentName || headerAgentId;
|
|
6454
|
+
log2(`Agent identified from HTTP headers: ${this.agentName} (${this.agentId})`);
|
|
6455
|
+
}
|
|
6456
|
+
}
|
|
6457
|
+
const subAgentId = req.headers["x-sub-agent-id"];
|
|
6458
|
+
const subAgentName = req.headers["x-sub-agent-name"];
|
|
6459
|
+
this.httpSubAgent = subAgentId ? { subAgentId, subAgentName: subAgentName || subAgentId } : null;
|
|
6344
6460
|
await httpTransport.handleRequest(req, res);
|
|
6345
6461
|
} else if (req.url === "/health") {
|
|
6346
6462
|
res.writeHead(200, { "Content-Type": "application/json" });
|
package/dist/init.js
CHANGED
|
@@ -98,10 +98,11 @@ function isAlreadyProtected(server) {
|
|
|
98
98
|
}
|
|
99
99
|
return false;
|
|
100
100
|
}
|
|
101
|
-
function wrapServer(server, policy) {
|
|
101
|
+
function wrapServer(serverName, server, policy) {
|
|
102
102
|
const env = { ...server.env ?? {} };
|
|
103
103
|
env.SOLONGATE_API_KEY = "${SOLONGATE_API_KEY}";
|
|
104
104
|
const proxyArgs = ["-y", "@solongate/proxy@latest"];
|
|
105
|
+
proxyArgs.push("--agent-name", serverName);
|
|
105
106
|
if (policy) {
|
|
106
107
|
proxyArgs.push("--policy", policy);
|
|
107
108
|
}
|
|
@@ -504,7 +505,7 @@ async function main() {
|
|
|
504
505
|
const newConfig = { mcpServers: {} };
|
|
505
506
|
for (const name of serverNames) {
|
|
506
507
|
if (toProtect.includes(name)) {
|
|
507
|
-
newConfig.mcpServers[name] = wrapServer(config.mcpServers[name], policyValue);
|
|
508
|
+
newConfig.mcpServers[name] = wrapServer(name, config.mcpServers[name], policyValue);
|
|
508
509
|
} else {
|
|
509
510
|
newConfig.mcpServers[name] = config.mcpServers[name];
|
|
510
511
|
}
|
package/dist/lib.js
CHANGED
|
@@ -1064,25 +1064,25 @@ function checkEntropyLimits(value) {
|
|
|
1064
1064
|
}
|
|
1065
1065
|
function calculateShannonEntropy(str) {
|
|
1066
1066
|
const freq = new Uint32Array(128);
|
|
1067
|
-
|
|
1067
|
+
const nonAsciiFreq = /* @__PURE__ */ new Map();
|
|
1068
1068
|
for (let i = 0; i < str.length; i++) {
|
|
1069
1069
|
const code = str.charCodeAt(i);
|
|
1070
1070
|
if (code < 128) {
|
|
1071
|
-
freq[code] =
|
|
1071
|
+
freq[code] = freq[code] + 1;
|
|
1072
1072
|
} else {
|
|
1073
|
-
|
|
1073
|
+
nonAsciiFreq.set(code, (nonAsciiFreq.get(code) || 0) + 1);
|
|
1074
1074
|
}
|
|
1075
1075
|
}
|
|
1076
1076
|
let entropy = 0;
|
|
1077
1077
|
const len = str.length;
|
|
1078
1078
|
for (let i = 0; i < 128; i++) {
|
|
1079
|
-
if (
|
|
1080
|
-
const p =
|
|
1079
|
+
if (freq[i] > 0) {
|
|
1080
|
+
const p = freq[i] / len;
|
|
1081
1081
|
entropy -= p * Math.log2(p);
|
|
1082
1082
|
}
|
|
1083
1083
|
}
|
|
1084
|
-
|
|
1085
|
-
const p =
|
|
1084
|
+
for (const count of nonAsciiFreq.values()) {
|
|
1085
|
+
const p = count / len;
|
|
1086
1086
|
entropy -= p * Math.log2(p);
|
|
1087
1087
|
}
|
|
1088
1088
|
return entropy;
|
|
@@ -2178,7 +2178,7 @@ function urlConstraintsMatch(constraints, args) {
|
|
|
2178
2178
|
}
|
|
2179
2179
|
function evaluatePolicy(policySet, request) {
|
|
2180
2180
|
const startTime = performance.now();
|
|
2181
|
-
const sortedRules = policySet.rules;
|
|
2181
|
+
const sortedRules = [...policySet.rules].sort((a, b) => a.priority - b.priority);
|
|
2182
2182
|
for (const rule of sortedRules) {
|
|
2183
2183
|
if (ruleMatchesRequest(rule, request)) {
|
|
2184
2184
|
const endTime2 = performance.now();
|
|
@@ -3661,7 +3661,11 @@ var AUDIT_MAX_RETRIES = 3;
|
|
|
3661
3661
|
var AUDIT_LOG_BACKUP_PATH = resolve(".solongate-audit-backup.jsonl");
|
|
3662
3662
|
async function sendAuditLog(apiKey, apiUrl, entry) {
|
|
3663
3663
|
const url = `${apiUrl}/api/v1/audit-logs`;
|
|
3664
|
-
const body = JSON.stringify(
|
|
3664
|
+
const body = JSON.stringify({
|
|
3665
|
+
...entry,
|
|
3666
|
+
agent_id: entry.agent_id,
|
|
3667
|
+
agent_name: entry.agent_name
|
|
3668
|
+
});
|
|
3665
3669
|
for (let attempt = 0; attempt < AUDIT_MAX_RETRIES; attempt++) {
|
|
3666
3670
|
try {
|
|
3667
3671
|
const res = await fetch(url, {
|
|
@@ -3930,9 +3934,6 @@ var PolicySyncManager = class {
|
|
|
3930
3934
|
*/
|
|
3931
3935
|
policiesEqual(a, b) {
|
|
3932
3936
|
if (a.name !== b.name || a.rules.length !== b.rules.length) return false;
|
|
3933
|
-
if (a.version !== void 0 && b.version !== void 0 && a.version === b.version && a.id === b.id) {
|
|
3934
|
-
return true;
|
|
3935
|
-
}
|
|
3936
3937
|
return JSON.stringify(a.rules) === JSON.stringify(b.rules);
|
|
3937
3938
|
}
|
|
3938
3939
|
};
|
|
@@ -3972,12 +3973,17 @@ CRITICAL: Only DENY access to files EXPLICITLY in the protected_files list. "cat
|
|
|
3972
3973
|
|
|
3973
3974
|
Respond with ONLY valid JSON, no markdown, no explanation outside the JSON:
|
|
3974
3975
|
{"decision": "ALLOW" or "DENY", "reason": "brief one-line explanation", "confidence": 0.0 to 1.0}`;
|
|
3975
|
-
var AiJudge = class {
|
|
3976
|
+
var AiJudge = class _AiJudge {
|
|
3976
3977
|
config;
|
|
3977
3978
|
protectedFiles;
|
|
3978
3979
|
protectedPaths;
|
|
3979
3980
|
deniedActions;
|
|
3980
3981
|
isOllamaEndpoint;
|
|
3982
|
+
// Circuit breaker: after 3 consecutive failures in 60s, skip AI Judge
|
|
3983
|
+
consecutiveFailures = 0;
|
|
3984
|
+
lastFailureTime = 0;
|
|
3985
|
+
static CIRCUIT_BREAKER_THRESHOLD = 3;
|
|
3986
|
+
static CIRCUIT_BREAKER_RESET_MS = 6e4;
|
|
3981
3987
|
constructor(config, protectedFiles, protectedPaths, deniedActions = [
|
|
3982
3988
|
"file deletion",
|
|
3983
3989
|
"data exfiltration",
|
|
@@ -3996,6 +4002,17 @@ var AiJudge = class {
|
|
|
3996
4002
|
* Fail-closed: any error (timeout, parse failure, connection refused) → DENY.
|
|
3997
4003
|
*/
|
|
3998
4004
|
async evaluate(toolName, args) {
|
|
4005
|
+
if (this.consecutiveFailures >= _AiJudge.CIRCUIT_BREAKER_THRESHOLD) {
|
|
4006
|
+
const elapsed = Date.now() - this.lastFailureTime;
|
|
4007
|
+
if (elapsed < _AiJudge.CIRCUIT_BREAKER_RESET_MS) {
|
|
4008
|
+
return {
|
|
4009
|
+
decision: "ALLOW",
|
|
4010
|
+
reason: `AI Judge circuit breaker open (${this.consecutiveFailures} consecutive failures) \u2014 falling back to policy-only`,
|
|
4011
|
+
confidence: 0.5
|
|
4012
|
+
};
|
|
4013
|
+
}
|
|
4014
|
+
this.consecutiveFailures = 0;
|
|
4015
|
+
}
|
|
3999
4016
|
const sanitizedArgs = this.sanitizeArgs(args);
|
|
4000
4017
|
const userMessage = JSON.stringify({
|
|
4001
4018
|
tool: toolName,
|
|
@@ -4006,12 +4023,15 @@ var AiJudge = class {
|
|
|
4006
4023
|
});
|
|
4007
4024
|
try {
|
|
4008
4025
|
const response = await this.callLLM(userMessage);
|
|
4026
|
+
this.consecutiveFailures = 0;
|
|
4009
4027
|
return this.parseVerdict(response);
|
|
4010
4028
|
} catch (err) {
|
|
4029
|
+
this.consecutiveFailures++;
|
|
4030
|
+
this.lastFailureTime = Date.now();
|
|
4011
4031
|
const message = err instanceof Error ? err.message : String(err);
|
|
4012
4032
|
return {
|
|
4013
4033
|
decision: "DENY",
|
|
4014
|
-
reason: `AI Judge error (fail-closed): ${message}`,
|
|
4034
|
+
reason: `AI Judge error (fail-closed): ${message.slice(0, 100)}`,
|
|
4015
4035
|
confidence: 1
|
|
4016
4036
|
};
|
|
4017
4037
|
}
|
|
@@ -4024,8 +4044,15 @@ var AiJudge = class {
|
|
|
4024
4044
|
const sanitize = (val, depth = 0) => {
|
|
4025
4045
|
if (depth > 10) return "[nested]";
|
|
4026
4046
|
if (typeof val === "string") {
|
|
4027
|
-
|
|
4028
|
-
|
|
4047
|
+
let s = val.length > maxStringLen ? val.slice(0, maxStringLen) + "...[truncated]" : val;
|
|
4048
|
+
s = s.replace(/\{[^{}]*"decision"\s*:/gi, "{[redacted]:");
|
|
4049
|
+
s = s.replace(/\{[^{}]*"ALLOW"[^{}]*\}/gi, "[redacted-json]");
|
|
4050
|
+
s = s.replace(/\{[^{}]*"DENY"[^{}]*\}/gi, "[redacted-json]");
|
|
4051
|
+
s = s.replace(/respond\s+with\s*:/gi, "[redacted]");
|
|
4052
|
+
s = s.replace(/ignore\s+(previous|above|all)\s+(instructions?|prompts?)/gi, "[redacted]");
|
|
4053
|
+
s = s.replace(/you\s+are\s+now\s+/gi, "[redacted] ");
|
|
4054
|
+
s = s.replace(/system\s*:\s*/gi, "[redacted]: ");
|
|
4055
|
+
return s;
|
|
4029
4056
|
}
|
|
4030
4057
|
if (Array.isArray(val)) return val.slice(0, 50).map((v) => sanitize(v, depth + 1));
|
|
4031
4058
|
if (val && typeof val === "object") {
|
|
@@ -4068,7 +4095,8 @@ var AiJudge = class {
|
|
|
4068
4095
|
{ role: "user", content: userMessage }
|
|
4069
4096
|
],
|
|
4070
4097
|
temperature: 0,
|
|
4071
|
-
max_tokens: 200
|
|
4098
|
+
max_tokens: 200,
|
|
4099
|
+
response_format: { type: "json_object" }
|
|
4072
4100
|
});
|
|
4073
4101
|
if (this.config.apiKey) {
|
|
4074
4102
|
headers["Authorization"] = `Bearer ${this.config.apiKey}`;
|
|
@@ -4194,9 +4222,20 @@ var SolonGateProxy = class {
|
|
|
4194
4222
|
aiJudge = null;
|
|
4195
4223
|
upstreamTools = [];
|
|
4196
4224
|
guardConfig;
|
|
4225
|
+
/** Agent identity for trust map — resolved from CLI flag, HTTP headers, or MCP clientInfo */
|
|
4226
|
+
agentId = null;
|
|
4227
|
+
agentName = null;
|
|
4228
|
+
/** Per-session agent info for HTTP mode (keyed by session ID) */
|
|
4229
|
+
httpAgentInfo = /* @__PURE__ */ new Map();
|
|
4230
|
+
/** Per-request sub-agent info from HTTP headers (transient, overwritten per request) */
|
|
4231
|
+
httpSubAgent = null;
|
|
4197
4232
|
constructor(config) {
|
|
4198
4233
|
this.config = config;
|
|
4199
4234
|
this.guardConfig = config.advancedDetection ? { ...DEFAULT_INPUT_GUARD_CONFIG, advancedDetection: config.advancedDetection } : DEFAULT_INPUT_GUARD_CONFIG;
|
|
4235
|
+
if (config.agentName) {
|
|
4236
|
+
this.agentName = config.agentName;
|
|
4237
|
+
this.agentId = config.agentName.toLowerCase().replace(/\s+/g, "-");
|
|
4238
|
+
}
|
|
4200
4239
|
this.gate = new SolonGate({
|
|
4201
4240
|
name: config.name ?? "solongate-proxy",
|
|
4202
4241
|
apiKey: "sg_test_proxy_internal_00000000",
|
|
@@ -4213,6 +4252,19 @@ var SolonGateProxy = class {
|
|
|
4213
4252
|
log2("WARNING:", w);
|
|
4214
4253
|
}
|
|
4215
4254
|
}
|
|
4255
|
+
/** Extract sub-agent identity from MCP _meta field */
|
|
4256
|
+
extractSubAgent(request) {
|
|
4257
|
+
const meta = request?.params?._meta;
|
|
4258
|
+
if (meta && typeof meta === "object") {
|
|
4259
|
+
const solonMeta = meta["io.solongate/agent"];
|
|
4260
|
+
if (solonMeta && typeof solonMeta === "object") {
|
|
4261
|
+
const agent = solonMeta;
|
|
4262
|
+
const id = agent.id ? String(agent.id) : null;
|
|
4263
|
+
if (id) return { subAgentId: id, subAgentName: agent.name ? String(agent.name) : id };
|
|
4264
|
+
}
|
|
4265
|
+
}
|
|
4266
|
+
return null;
|
|
4267
|
+
}
|
|
4216
4268
|
/**
|
|
4217
4269
|
* Start the proxy: connect to upstream, then serve downstream.
|
|
4218
4270
|
*/
|
|
@@ -4393,6 +4445,16 @@ var SolonGateProxy = class {
|
|
|
4393
4445
|
}
|
|
4394
4446
|
}
|
|
4395
4447
|
);
|
|
4448
|
+
this.server.oninitialized = () => {
|
|
4449
|
+
if (!this.agentId && this.server) {
|
|
4450
|
+
const clientVersion = this.server.getClientVersion();
|
|
4451
|
+
if (clientVersion?.name) {
|
|
4452
|
+
this.agentId = clientVersion.version ? `${clientVersion.name}/${clientVersion.version}` : clientVersion.name;
|
|
4453
|
+
this.agentName = clientVersion.name;
|
|
4454
|
+
log2(`Agent identified from MCP clientInfo: ${this.agentName} (${this.agentId})`);
|
|
4455
|
+
}
|
|
4456
|
+
}
|
|
4457
|
+
};
|
|
4396
4458
|
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
4397
4459
|
return { tools: this.upstreamTools };
|
|
4398
4460
|
});
|
|
@@ -4400,6 +4462,7 @@ var SolonGateProxy = class {
|
|
|
4400
4462
|
const MUTEX_TIMEOUT_MS = 3e4;
|
|
4401
4463
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
4402
4464
|
const { name, arguments: args } = request.params;
|
|
4465
|
+
const subAgent = this.extractSubAgent(request) || this.httpSubAgent;
|
|
4403
4466
|
const argsSize = TEXT_ENCODER.encode(JSON.stringify(args ?? {})).length;
|
|
4404
4467
|
if (argsSize > MAX_ARGUMENT_SIZE) {
|
|
4405
4468
|
log2(`DENY: ${name} \u2014 payload size ${argsSize} exceeds limit ${MAX_ARGUMENT_SIZE}`);
|
|
@@ -4437,7 +4500,11 @@ var SolonGateProxy = class {
|
|
|
4437
4500
|
decision: "DENY",
|
|
4438
4501
|
reason: `Prompt injection detected: ${threats}`,
|
|
4439
4502
|
evaluationTimeMs: 0,
|
|
4440
|
-
promptInjection: piResult
|
|
4503
|
+
promptInjection: piResult,
|
|
4504
|
+
agent_id: this.agentId ?? void 0,
|
|
4505
|
+
agent_name: this.agentName ?? void 0,
|
|
4506
|
+
sub_agent_id: subAgent?.subAgentId,
|
|
4507
|
+
sub_agent_name: subAgent?.subAgentName
|
|
4441
4508
|
});
|
|
4442
4509
|
}
|
|
4443
4510
|
return {
|
|
@@ -4529,7 +4596,11 @@ var SolonGateProxy = class {
|
|
|
4529
4596
|
reason,
|
|
4530
4597
|
matchedRule,
|
|
4531
4598
|
evaluationTimeMs,
|
|
4532
|
-
promptInjection: piResult
|
|
4599
|
+
promptInjection: piResult,
|
|
4600
|
+
agent_id: this.agentId ?? void 0,
|
|
4601
|
+
agent_name: this.agentName ?? void 0,
|
|
4602
|
+
sub_agent_id: subAgent?.subAgentId,
|
|
4603
|
+
sub_agent_name: subAgent?.subAgentName
|
|
4533
4604
|
});
|
|
4534
4605
|
} else {
|
|
4535
4606
|
log2(`Skipping audit log (apiKey: ${this.config.apiKey ? "test key" : "not set"})`);
|
|
@@ -4814,6 +4885,18 @@ ${msg.content.text}`;
|
|
|
4814
4885
|
await this.server.connect(httpTransport);
|
|
4815
4886
|
const httpServer = createHttpServer(async (req, res) => {
|
|
4816
4887
|
if (req.url === "/mcp" || req.url?.startsWith("/mcp?")) {
|
|
4888
|
+
if (!this.agentId) {
|
|
4889
|
+
const headerAgentId = req.headers["x-agent-id"];
|
|
4890
|
+
const headerAgentName = req.headers["x-agent-name"];
|
|
4891
|
+
if (headerAgentId) {
|
|
4892
|
+
this.agentId = headerAgentId;
|
|
4893
|
+
this.agentName = headerAgentName || headerAgentId;
|
|
4894
|
+
log2(`Agent identified from HTTP headers: ${this.agentName} (${this.agentId})`);
|
|
4895
|
+
}
|
|
4896
|
+
}
|
|
4897
|
+
const subAgentId = req.headers["x-sub-agent-id"];
|
|
4898
|
+
const subAgentName = req.headers["x-sub-agent-name"];
|
|
4899
|
+
this.httpSubAgent = subAgentId ? { subAgentId, subAgentName: subAgentName || subAgentId } : null;
|
|
4817
4900
|
await httpTransport.handleRequest(req, res);
|
|
4818
4901
|
} else if (req.url === "/health") {
|
|
4819
4902
|
res.writeHead(200, { "Content-Type": "application/json" });
|
package/hooks/guard.mjs
CHANGED
|
@@ -7,9 +7,19 @@
|
|
|
7
7
|
* Logs ALL decisions (ALLOW + DENY) to SolonGate Cloud.
|
|
8
8
|
* Auto-installed by: npx @solongate/proxy init
|
|
9
9
|
*/
|
|
10
|
-
import { readFileSync, existsSync } from 'node:fs';
|
|
10
|
+
import { readFileSync, existsSync, statSync } from 'node:fs';
|
|
11
11
|
import { resolve } from 'node:path';
|
|
12
12
|
|
|
13
|
+
// Safe file read with size limit (1MB max) to prevent DoS via large files
|
|
14
|
+
const MAX_FILE_READ = 1024 * 1024; // 1MB
|
|
15
|
+
function safeReadFileSync(filePath, encoding = 'utf-8') {
|
|
16
|
+
try {
|
|
17
|
+
const stat = statSync(filePath);
|
|
18
|
+
if (stat.size > MAX_FILE_READ) return '';
|
|
19
|
+
return readFileSync(filePath, encoding);
|
|
20
|
+
} catch { return ''; }
|
|
21
|
+
}
|
|
22
|
+
|
|
13
23
|
// ── Load API key from .env file (Claude Code doesn't load .env into process.env) ──
|
|
14
24
|
function loadEnvKey(dir) {
|
|
15
25
|
try {
|
|
@@ -157,6 +167,32 @@ function matchPathGlob(path, pattern) {
|
|
|
157
167
|
return matchGlob(p, g);
|
|
158
168
|
}
|
|
159
169
|
|
|
170
|
+
// ── Safe Webhook URL Validation (prevent SSRF) ──
|
|
171
|
+
function isSafeWebhookUrl(urlStr) {
|
|
172
|
+
try {
|
|
173
|
+
const u = new URL(urlStr);
|
|
174
|
+
if (u.protocol !== 'https:') return false;
|
|
175
|
+
const host = u.hostname.toLowerCase();
|
|
176
|
+
// Block private/reserved IPs and metadata endpoints
|
|
177
|
+
if (host === 'localhost' || host === '127.0.0.1' || host === '0.0.0.0' || host === '::1') return false;
|
|
178
|
+
if (host.startsWith('10.') || host.startsWith('192.168.') || host.startsWith('172.')) return false;
|
|
179
|
+
if (host === '169.254.169.254' || host === 'metadata.google.internal') return false;
|
|
180
|
+
if (host.endsWith('.internal') || host.endsWith('.local')) return false;
|
|
181
|
+
return true;
|
|
182
|
+
} catch { return false; }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ── Safe Regex Validation (prevent ReDoS from cloud-supplied patterns) ──
|
|
186
|
+
function isSafeRegex(pattern) {
|
|
187
|
+
if (typeof pattern !== 'string' || pattern.length > 512) return false;
|
|
188
|
+
// Block nested quantifiers: (a+)+, (a*)+, (a{1,})+, etc.
|
|
189
|
+
if (/(\+|\*|\{[^}]+\})\s*(\+|\*|\{[^}]+\})/.test(pattern)) return false;
|
|
190
|
+
if (/\([^)]*(\+|\*|\{[^}]+\})[^)]*\)\s*(\+|\*|\{[^}]+\})/.test(pattern)) return false;
|
|
191
|
+
// Block excessive alternation groups (>10 alternatives)
|
|
192
|
+
if ((pattern.match(/\|/g) || []).length > 10) return false;
|
|
193
|
+
try { new RegExp(pattern); return true; } catch { return false; }
|
|
194
|
+
}
|
|
195
|
+
|
|
160
196
|
// ── Extract Functions (deep scan all string values) ──
|
|
161
197
|
function scanStrings(obj) {
|
|
162
198
|
const strings = [];
|
|
@@ -643,7 +679,7 @@ process.stdin.on('end', async () => {
|
|
|
643
679
|
try {
|
|
644
680
|
const targetPath = resolve(hookCwdForNpm, scriptFileMatch[1]);
|
|
645
681
|
if (existsSync(targetPath)) {
|
|
646
|
-
const targetContent =
|
|
682
|
+
const targetContent = safeReadFileSync(targetPath).toLowerCase();
|
|
647
683
|
// Check target file for protected paths
|
|
648
684
|
for (const pp of [...protectedPaths, ...writeProtectedPaths]) {
|
|
649
685
|
if (targetContent.includes(pp)) {
|
|
@@ -670,7 +706,7 @@ process.stdin.on('end', async () => {
|
|
|
670
706
|
const candidates = [impAbs, impAbs + '.mjs', impAbs + '.js', impAbs + '.cjs'];
|
|
671
707
|
for (const c of candidates) {
|
|
672
708
|
if (existsSync(c)) {
|
|
673
|
-
const impContent =
|
|
709
|
+
const impContent = safeReadFileSync(c).toLowerCase();
|
|
674
710
|
const iDisc = /\breaddirsync\b|\breaddir\b|\bos\.listdir\b|\bglob\b/i.test(impContent);
|
|
675
711
|
const iDest = /\brmsync\b|\bunlinksync\b|\bfs\.\s*(?:rm|unlink|rmdir)/i.test(impContent);
|
|
676
712
|
if ((iDisc && tDest) || (tDisc && iDest)) {
|
|
@@ -756,7 +792,7 @@ process.stdin.on('end', async () => {
|
|
|
756
792
|
? scriptPath
|
|
757
793
|
: resolve(hookCwdForScript, scriptPath);
|
|
758
794
|
if (existsSync(absPath)) {
|
|
759
|
-
const scriptContent =
|
|
795
|
+
const scriptContent = safeReadFileSync(absPath).toLowerCase();
|
|
760
796
|
// Check for discovery+destruction combo
|
|
761
797
|
const hasDiscovery = /\breaddirsync\b|\breaddir\b|\bos\.listdir\b|\bscandir\b|\bglob(?:sync)?\b|\bls\s+-[adl]|\bls\s+\.\b|\bopendir\b|\bdir\.entries\b|\bwalkdir\b|\bls\b.*\.\[/.test(scriptContent);
|
|
762
798
|
const hasDestruction = /\brmsync\b|\brm\s+-rf\b|\bunlinksync\b|\brmdirsync\b|\bunlink\b|\brimraf\b|\bremovesync\b|\bremove_tree\b|\bshutil\.rmtree\b|\bwritefilesync\b|\bexecsync\b.*\brm\b|\bchild_process\b|\bfs\.\s*(?:rm|unlink|rmdir|write)/.test(scriptContent);
|
|
@@ -778,7 +814,7 @@ process.stdin.on('end', async () => {
|
|
|
778
814
|
const candidates = [importAbs, importAbs + '.mjs', importAbs + '.js', importAbs + '.cjs'];
|
|
779
815
|
for (const candidate of candidates) {
|
|
780
816
|
if (existsSync(candidate)) {
|
|
781
|
-
const importContent =
|
|
817
|
+
const importContent = safeReadFileSync(candidate).toLowerCase();
|
|
782
818
|
// Cross-module: check if imported module has discovery/destruction/string construction
|
|
783
819
|
const importDisc = /\breaddirsync\b|\breaddir\b|\bos\.listdir\b|\bscandir\b|\bglob(?:sync)?\b/i.test(importContent);
|
|
784
820
|
const importDest = /\brmsync\b|\bunlinksync\b|\brmdirsync\b|\bunlink\b|\brimraf\b|\bfs\.\s*(?:rm|unlink|rmdir)/i.test(importContent);
|
|
@@ -890,6 +926,7 @@ process.stdin.on('end', async () => {
|
|
|
890
926
|
if (piCfg.piEnabled !== false && Array.isArray(piCfg.piWhitelist) && piCfg.piWhitelist.length > 0) {
|
|
891
927
|
for (const wlPattern of piCfg.piWhitelist) {
|
|
892
928
|
try {
|
|
929
|
+
if (!isSafeRegex(wlPattern)) continue;
|
|
893
930
|
if (new RegExp(wlPattern, 'i').test(allText)) {
|
|
894
931
|
whitelisted = true;
|
|
895
932
|
break;
|
|
@@ -902,7 +939,7 @@ process.stdin.on('end', async () => {
|
|
|
902
939
|
const customCategories = [];
|
|
903
940
|
if (piCfg.piEnabled !== false && Array.isArray(piCfg.piCustomPatterns)) {
|
|
904
941
|
for (const cp of piCfg.piCustomPatterns) {
|
|
905
|
-
if (cp && cp.pattern) {
|
|
942
|
+
if (cp && cp.pattern && isSafeRegex(cp.pattern)) {
|
|
906
943
|
try {
|
|
907
944
|
customCategories.push({
|
|
908
945
|
name: cp.name || 'custom_pattern',
|
|
@@ -950,8 +987,8 @@ process.stdin.on('end', async () => {
|
|
|
950
987
|
});
|
|
951
988
|
} catch {}
|
|
952
989
|
|
|
953
|
-
// Webhook notification
|
|
954
|
-
if (piCfg.piWebhookUrl) {
|
|
990
|
+
// Webhook notification (SSRF-safe: HTTPS only, no private IPs)
|
|
991
|
+
if (piCfg.piWebhookUrl && isSafeWebhookUrl(piCfg.piWebhookUrl)) {
|
|
955
992
|
try {
|
|
956
993
|
await fetch(piCfg.piWebhookUrl, {
|
|
957
994
|
method: 'POST',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solongate/proxy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.1",
|
|
4
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": {
|