@solongate/sdk 0.1.1 → 0.1.2
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 +1127 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -3
package/dist/index.js
CHANGED
|
@@ -1,11 +1,1127 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import { PolicyStore, PolicyEngine } from '@solongate/policy-engine';
|
|
4
|
-
export { PolicyEngine, PolicyStore, createDefaultDenyPolicySet, createReadOnlyPolicySet } from '@solongate/policy-engine';
|
|
5
|
-
import { randomUUID, createHmac } from 'crypto';
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { createHash, randomUUID, createHmac } from 'crypto';
|
|
6
3
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
7
4
|
|
|
8
|
-
//
|
|
5
|
+
// ../core/dist/index.js
|
|
6
|
+
var SolonGateError = class extends Error {
|
|
7
|
+
code;
|
|
8
|
+
timestamp;
|
|
9
|
+
details;
|
|
10
|
+
constructor(message, code, details = {}) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "SolonGateError";
|
|
13
|
+
this.code = code;
|
|
14
|
+
this.timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
15
|
+
this.details = Object.freeze({ ...details });
|
|
16
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Serializable representation for logging and API responses.
|
|
20
|
+
* Never includes stack traces (information leakage prevention).
|
|
21
|
+
*/
|
|
22
|
+
toJSON() {
|
|
23
|
+
return {
|
|
24
|
+
name: this.name,
|
|
25
|
+
code: this.code,
|
|
26
|
+
message: this.message,
|
|
27
|
+
timestamp: this.timestamp,
|
|
28
|
+
details: this.details
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
var PolicyDeniedError = class extends SolonGateError {
|
|
33
|
+
constructor(toolName, reason, details = {}) {
|
|
34
|
+
super(
|
|
35
|
+
`Policy denied execution of tool "${toolName}": ${reason}`,
|
|
36
|
+
"POLICY_DENIED",
|
|
37
|
+
{ toolName, reason, ...details }
|
|
38
|
+
);
|
|
39
|
+
this.name = "PolicyDeniedError";
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
var SchemaValidationError = class extends SolonGateError {
|
|
43
|
+
constructor(toolName, validationErrors) {
|
|
44
|
+
super(
|
|
45
|
+
`Schema validation failed for tool "${toolName}": ${validationErrors.join("; ")}`,
|
|
46
|
+
"SCHEMA_VALIDATION_FAILED",
|
|
47
|
+
{ toolName, validationErrors }
|
|
48
|
+
);
|
|
49
|
+
this.name = "SchemaValidationError";
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
var RateLimitError = class extends SolonGateError {
|
|
53
|
+
constructor(toolName, limitPerMinute) {
|
|
54
|
+
super(
|
|
55
|
+
`Rate limit exceeded for tool "${toolName}": max ${limitPerMinute}/min`,
|
|
56
|
+
"RATE_LIMIT_EXCEEDED",
|
|
57
|
+
{ toolName, limitPerMinute }
|
|
58
|
+
);
|
|
59
|
+
this.name = "RateLimitError";
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
var InputGuardError = class extends SolonGateError {
|
|
63
|
+
constructor(toolName, threats) {
|
|
64
|
+
super(
|
|
65
|
+
`Input guard blocked tool "${toolName}": ${threats.map((t) => t.description).join("; ")}`,
|
|
66
|
+
"INPUT_GUARD_BLOCKED",
|
|
67
|
+
{ toolName, threatCount: threats.length, threats }
|
|
68
|
+
);
|
|
69
|
+
this.name = "InputGuardError";
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
var NetworkError = class extends SolonGateError {
|
|
73
|
+
constructor(operation, statusCode, details = {}) {
|
|
74
|
+
super(
|
|
75
|
+
`Network error during ${operation}${statusCode ? ` (HTTP ${statusCode})` : ""}`,
|
|
76
|
+
"NETWORK_ERROR",
|
|
77
|
+
{ operation, statusCode, ...details }
|
|
78
|
+
);
|
|
79
|
+
this.name = "NetworkError";
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
var TrustLevel = {
|
|
83
|
+
UNTRUSTED: "UNTRUSTED",
|
|
84
|
+
VERIFIED: "VERIFIED",
|
|
85
|
+
TRUSTED: "TRUSTED"
|
|
86
|
+
};
|
|
87
|
+
var Permission = {
|
|
88
|
+
READ: "READ",
|
|
89
|
+
WRITE: "WRITE",
|
|
90
|
+
EXECUTE: "EXECUTE"
|
|
91
|
+
};
|
|
92
|
+
z.enum(["READ", "WRITE", "EXECUTE"]);
|
|
93
|
+
Object.freeze(
|
|
94
|
+
/* @__PURE__ */ new Set()
|
|
95
|
+
);
|
|
96
|
+
Object.freeze(
|
|
97
|
+
/* @__PURE__ */ new Set([Permission.READ])
|
|
98
|
+
);
|
|
99
|
+
var PolicyEffect = {
|
|
100
|
+
ALLOW: "ALLOW",
|
|
101
|
+
DENY: "DENY"
|
|
102
|
+
};
|
|
103
|
+
var PolicyRuleSchema = z.object({
|
|
104
|
+
id: z.string().min(1).max(256),
|
|
105
|
+
description: z.string().max(1024),
|
|
106
|
+
effect: z.enum(["ALLOW", "DENY"]),
|
|
107
|
+
priority: z.number().int().min(0).max(1e4).default(1e3),
|
|
108
|
+
toolPattern: z.string().min(1).max(512),
|
|
109
|
+
permission: z.enum(["READ", "WRITE", "EXECUTE"]),
|
|
110
|
+
minimumTrustLevel: z.enum(["UNTRUSTED", "VERIFIED", "TRUSTED"]),
|
|
111
|
+
argumentConstraints: z.record(z.unknown()).optional(),
|
|
112
|
+
pathConstraints: z.object({
|
|
113
|
+
allowed: z.array(z.string()).optional(),
|
|
114
|
+
denied: z.array(z.string()).optional(),
|
|
115
|
+
rootDirectory: z.string().optional(),
|
|
116
|
+
allowSymlinks: z.boolean().optional()
|
|
117
|
+
}).optional(),
|
|
118
|
+
enabled: z.boolean().default(true),
|
|
119
|
+
createdAt: z.string().datetime(),
|
|
120
|
+
updatedAt: z.string().datetime()
|
|
121
|
+
});
|
|
122
|
+
var PolicySetSchema = z.object({
|
|
123
|
+
id: z.string().min(1).max(256),
|
|
124
|
+
name: z.string().min(1).max(256),
|
|
125
|
+
description: z.string().max(2048),
|
|
126
|
+
version: z.number().int().min(0),
|
|
127
|
+
rules: z.array(PolicyRuleSchema),
|
|
128
|
+
createdAt: z.string().datetime(),
|
|
129
|
+
updatedAt: z.string().datetime()
|
|
130
|
+
});
|
|
131
|
+
function createSecurityContext(params) {
|
|
132
|
+
return {
|
|
133
|
+
trustLevel: "UNTRUSTED",
|
|
134
|
+
grantedPermissions: /* @__PURE__ */ new Set(),
|
|
135
|
+
sessionId: null,
|
|
136
|
+
metadata: {},
|
|
137
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
138
|
+
...params
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
var DEFAULT_POLICY_EFFECT = "DENY";
|
|
142
|
+
var MAX_RULES_PER_POLICY_SET = 1e3;
|
|
143
|
+
var MAX_ARGUMENT_DEPTH = 10;
|
|
144
|
+
var MAX_ARGUMENTS_SIZE_BYTES = 1048576;
|
|
145
|
+
var POLICY_EVALUATION_TIMEOUT_MS = 100;
|
|
146
|
+
var RATE_LIMIT_WINDOW_MS = 6e4;
|
|
147
|
+
var RATE_LIMIT_MAX_ENTRIES = 1e4;
|
|
148
|
+
var UNSAFE_CONFIGURATION_WARNINGS = {
|
|
149
|
+
WILDCARD_ALLOW: "Wildcard ALLOW rules grant permission to ALL tools. This bypasses the default-deny model.",
|
|
150
|
+
TRUSTED_LEVEL_EXTERNAL: "Setting trust level to TRUSTED for external requests bypasses all security checks.",
|
|
151
|
+
EXECUTE_WITHOUT_REVIEW: "EXECUTE permission allows tools to perform arbitrary actions. Review carefully.",
|
|
152
|
+
RATE_LIMIT_ZERO: "A rate limit of 0 means unlimited calls. This removes protection against runaway loops.",
|
|
153
|
+
DISABLED_VALIDATION: "Disabling schema validation removes input sanitization protections."
|
|
154
|
+
};
|
|
155
|
+
function createDeniedToolResult(reason) {
|
|
156
|
+
return {
|
|
157
|
+
content: [
|
|
158
|
+
{
|
|
159
|
+
type: "text",
|
|
160
|
+
text: JSON.stringify({
|
|
161
|
+
error: "POLICY_DENIED",
|
|
162
|
+
message: reason,
|
|
163
|
+
hint: "This tool call was blocked by SolonGate security policy. Check your policy configuration."
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
],
|
|
167
|
+
isError: true
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
var DEFAULT_OPTIONS = {
|
|
171
|
+
maxDepth: MAX_ARGUMENT_DEPTH,
|
|
172
|
+
maxSizeBytes: MAX_ARGUMENTS_SIZE_BYTES,
|
|
173
|
+
stripUnknown: false
|
|
174
|
+
};
|
|
175
|
+
function validateToolInput(schema, input, options) {
|
|
176
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
177
|
+
const errors = [];
|
|
178
|
+
const sizeError = checkInputSize(input, opts.maxSizeBytes);
|
|
179
|
+
if (sizeError) {
|
|
180
|
+
return { valid: false, errors: [sizeError], sanitized: null };
|
|
181
|
+
}
|
|
182
|
+
const depthError = checkInputDepth(input, opts.maxDepth);
|
|
183
|
+
if (depthError) {
|
|
184
|
+
return { valid: false, errors: [depthError], sanitized: null };
|
|
185
|
+
}
|
|
186
|
+
const result = schema.safeParse(input);
|
|
187
|
+
if (!result.success) {
|
|
188
|
+
for (const issue of result.error.issues) {
|
|
189
|
+
const path = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
190
|
+
errors.push(`${path}: ${issue.message}`);
|
|
191
|
+
}
|
|
192
|
+
return { valid: false, errors, sanitized: null };
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
valid: true,
|
|
196
|
+
errors: [],
|
|
197
|
+
sanitized: result.data
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function checkInputSize(input, maxBytes) {
|
|
201
|
+
let serialized;
|
|
202
|
+
try {
|
|
203
|
+
serialized = JSON.stringify(input);
|
|
204
|
+
} catch {
|
|
205
|
+
return "Input cannot be serialized to JSON";
|
|
206
|
+
}
|
|
207
|
+
const sizeBytes = new TextEncoder().encode(serialized).length;
|
|
208
|
+
if (sizeBytes > maxBytes) {
|
|
209
|
+
return `Input size ${sizeBytes} bytes exceeds maximum ${maxBytes} bytes`;
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
function checkInputDepth(input, maxDepth) {
|
|
214
|
+
const depth = measureDepth(input, 0);
|
|
215
|
+
if (depth > maxDepth) {
|
|
216
|
+
return `Input depth ${depth} exceeds maximum ${maxDepth}`;
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
function measureDepth(value, currentDepth) {
|
|
221
|
+
if (currentDepth > MAX_ARGUMENT_DEPTH + 1) {
|
|
222
|
+
return currentDepth;
|
|
223
|
+
}
|
|
224
|
+
if (value === null || value === void 0 || typeof value !== "object") {
|
|
225
|
+
return currentDepth;
|
|
226
|
+
}
|
|
227
|
+
if (Array.isArray(value)) {
|
|
228
|
+
let maxChildDepth2 = currentDepth + 1;
|
|
229
|
+
for (const item of value) {
|
|
230
|
+
const childDepth = measureDepth(item, currentDepth + 1);
|
|
231
|
+
if (childDepth > maxChildDepth2) maxChildDepth2 = childDepth;
|
|
232
|
+
}
|
|
233
|
+
return maxChildDepth2;
|
|
234
|
+
}
|
|
235
|
+
let maxChildDepth = currentDepth + 1;
|
|
236
|
+
for (const key of Object.keys(value)) {
|
|
237
|
+
const childDepth = measureDepth(
|
|
238
|
+
value[key],
|
|
239
|
+
currentDepth + 1
|
|
240
|
+
);
|
|
241
|
+
if (childDepth > maxChildDepth) maxChildDepth = childDepth;
|
|
242
|
+
}
|
|
243
|
+
return maxChildDepth;
|
|
244
|
+
}
|
|
245
|
+
var DEFAULT_INPUT_GUARD_CONFIG = Object.freeze({
|
|
246
|
+
pathTraversal: true,
|
|
247
|
+
shellInjection: true,
|
|
248
|
+
wildcardAbuse: true,
|
|
249
|
+
lengthLimit: 4096,
|
|
250
|
+
entropyLimit: true,
|
|
251
|
+
ssrf: true,
|
|
252
|
+
sqlInjection: true
|
|
253
|
+
});
|
|
254
|
+
var PATH_TRAVERSAL_PATTERNS = [
|
|
255
|
+
/\.\.\//,
|
|
256
|
+
// ../
|
|
257
|
+
/\.\.\\/,
|
|
258
|
+
// ..\
|
|
259
|
+
/%2e%2e/i,
|
|
260
|
+
// URL-encoded ..
|
|
261
|
+
/%2e\./i,
|
|
262
|
+
// partial URL-encoded
|
|
263
|
+
/\.%2e/i,
|
|
264
|
+
// partial URL-encoded
|
|
265
|
+
/%252e%252e/i,
|
|
266
|
+
// double URL-encoded
|
|
267
|
+
/\.\.\0/
|
|
268
|
+
// null byte variant
|
|
269
|
+
];
|
|
270
|
+
var SENSITIVE_PATHS = [
|
|
271
|
+
/\/etc\/passwd/i,
|
|
272
|
+
/\/etc\/shadow/i,
|
|
273
|
+
/\/proc\//i,
|
|
274
|
+
/\/dev\//i,
|
|
275
|
+
/c:\\windows\\system32/i,
|
|
276
|
+
/c:\\windows\\syswow64/i,
|
|
277
|
+
/\/root\//i,
|
|
278
|
+
/~\//,
|
|
279
|
+
/\.env(\.|$)/i,
|
|
280
|
+
// .env, .env.local, .env.production
|
|
281
|
+
/\.aws\/credentials/i,
|
|
282
|
+
// AWS credentials
|
|
283
|
+
/\.ssh\/id_/i,
|
|
284
|
+
// SSH keys
|
|
285
|
+
/\.kube\/config/i,
|
|
286
|
+
// Kubernetes config
|
|
287
|
+
/wp-config\.php/i,
|
|
288
|
+
// WordPress config
|
|
289
|
+
/\.git\/config/i,
|
|
290
|
+
// Git config
|
|
291
|
+
/\.npmrc/i,
|
|
292
|
+
// npm credentials
|
|
293
|
+
/\.pypirc/i
|
|
294
|
+
// PyPI credentials
|
|
295
|
+
];
|
|
296
|
+
function detectPathTraversal(value) {
|
|
297
|
+
for (const pattern of PATH_TRAVERSAL_PATTERNS) {
|
|
298
|
+
if (pattern.test(value)) return true;
|
|
299
|
+
}
|
|
300
|
+
for (const pattern of SENSITIVE_PATHS) {
|
|
301
|
+
if (pattern.test(value)) return true;
|
|
302
|
+
}
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
var SHELL_INJECTION_PATTERNS = [
|
|
306
|
+
/[;|&`]/,
|
|
307
|
+
// Command separators and backtick execution
|
|
308
|
+
/\$\(/,
|
|
309
|
+
// Command substitution $(...)
|
|
310
|
+
/\$\{/,
|
|
311
|
+
// Variable expansion ${...}
|
|
312
|
+
/>\s*/,
|
|
313
|
+
// Output redirect
|
|
314
|
+
/<\s*/,
|
|
315
|
+
// Input redirect
|
|
316
|
+
/&&/,
|
|
317
|
+
// AND chaining
|
|
318
|
+
/\|\|/,
|
|
319
|
+
// OR chaining
|
|
320
|
+
/\beval\b/i,
|
|
321
|
+
// eval command
|
|
322
|
+
/\bexec\b/i,
|
|
323
|
+
// exec command
|
|
324
|
+
/\bsystem\b/i,
|
|
325
|
+
// system call
|
|
326
|
+
/%0a/i,
|
|
327
|
+
// URL-encoded newline
|
|
328
|
+
/%0d/i,
|
|
329
|
+
// URL-encoded carriage return
|
|
330
|
+
/%09/i,
|
|
331
|
+
// URL-encoded tab
|
|
332
|
+
/\r\n/,
|
|
333
|
+
// CRLF injection
|
|
334
|
+
/\n/
|
|
335
|
+
// Newline (command separator on Unix)
|
|
336
|
+
];
|
|
337
|
+
function detectShellInjection(value) {
|
|
338
|
+
for (const pattern of SHELL_INJECTION_PATTERNS) {
|
|
339
|
+
if (pattern.test(value)) return true;
|
|
340
|
+
}
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
var MAX_WILDCARDS_PER_VALUE = 3;
|
|
344
|
+
function detectWildcardAbuse(value) {
|
|
345
|
+
if (value.includes("**")) return true;
|
|
346
|
+
const wildcardCount = (value.match(/\*/g) || []).length;
|
|
347
|
+
if (wildcardCount > MAX_WILDCARDS_PER_VALUE) return true;
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
var SSRF_PATTERNS = [
|
|
351
|
+
/^https?:\/\/localhost\b/i,
|
|
352
|
+
/^https?:\/\/127\.\d{1,3}\.\d{1,3}\.\d{1,3}/,
|
|
353
|
+
/^https?:\/\/0\.0\.0\.0/,
|
|
354
|
+
/^https?:\/\/\[::1\]/,
|
|
355
|
+
// IPv6 loopback
|
|
356
|
+
/^https?:\/\/10\.\d{1,3}\.\d{1,3}\.\d{1,3}/,
|
|
357
|
+
// 10.x.x.x
|
|
358
|
+
/^https?:\/\/172\.(1[6-9]|2\d|3[01])\./,
|
|
359
|
+
// 172.16-31.x.x
|
|
360
|
+
/^https?:\/\/192\.168\./,
|
|
361
|
+
// 192.168.x.x
|
|
362
|
+
/^https?:\/\/169\.254\./,
|
|
363
|
+
// Link-local / AWS metadata
|
|
364
|
+
/metadata\.google\.internal/i,
|
|
365
|
+
// GCP metadata
|
|
366
|
+
/^https?:\/\/metadata\b/i,
|
|
367
|
+
// Generic metadata endpoint
|
|
368
|
+
// IPv6 bypass patterns
|
|
369
|
+
/^https?:\/\/\[fe80:/i,
|
|
370
|
+
// IPv6 link-local
|
|
371
|
+
/^https?:\/\/\[fc00:/i,
|
|
372
|
+
// IPv6 unique local
|
|
373
|
+
/^https?:\/\/\[fd[0-9a-f]{2}:/i,
|
|
374
|
+
// IPv6 unique local (fd00::/8)
|
|
375
|
+
/^https?:\/\/\[::ffff:127\./i,
|
|
376
|
+
// IPv4-mapped IPv6 loopback
|
|
377
|
+
/^https?:\/\/\[::ffff:10\./i,
|
|
378
|
+
// IPv4-mapped IPv6 private
|
|
379
|
+
/^https?:\/\/\[::ffff:172\.(1[6-9]|2\d|3[01])\./i,
|
|
380
|
+
// IPv4-mapped IPv6 private
|
|
381
|
+
/^https?:\/\/\[::ffff:192\.168\./i,
|
|
382
|
+
// IPv4-mapped IPv6 private
|
|
383
|
+
/^https?:\/\/\[::ffff:169\.254\./i,
|
|
384
|
+
// IPv4-mapped IPv6 link-local
|
|
385
|
+
// Hex IP bypass (e.g., 0x7f000001 = 127.0.0.1)
|
|
386
|
+
/^https?:\/\/0x[0-9a-f]+\b/i,
|
|
387
|
+
// Octal IP bypass (e.g., 0177.0.0.1 = 127.0.0.1)
|
|
388
|
+
/^https?:\/\/0[0-7]{1,3}\./
|
|
389
|
+
];
|
|
390
|
+
function detectDecimalIP(value) {
|
|
391
|
+
const match = value.match(/^https?:\/\/(\d{8,10})(?:[:/]|$)/);
|
|
392
|
+
if (!match || !match[1]) return false;
|
|
393
|
+
const decimal = parseInt(match[1], 10);
|
|
394
|
+
if (isNaN(decimal) || decimal > 4294967295) return false;
|
|
395
|
+
return decimal >= 2130706432 && decimal <= 2147483647 || // 127.0.0.0/8
|
|
396
|
+
decimal >= 167772160 && decimal <= 184549375 || // 10.0.0.0/8
|
|
397
|
+
decimal >= 2886729728 && decimal <= 2887778303 || // 172.16.0.0/12
|
|
398
|
+
decimal >= 3232235520 && decimal <= 3232301055 || // 192.168.0.0/16
|
|
399
|
+
decimal >= 2851995648 && decimal <= 2852061183 || // 169.254.0.0/16
|
|
400
|
+
decimal === 0;
|
|
401
|
+
}
|
|
402
|
+
function detectSSRF(value) {
|
|
403
|
+
for (const pattern of SSRF_PATTERNS) {
|
|
404
|
+
if (pattern.test(value)) return true;
|
|
405
|
+
}
|
|
406
|
+
if (detectDecimalIP(value)) return true;
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
var SQL_INJECTION_PATTERNS = [
|
|
410
|
+
/'\s{0,20}(OR|AND)\s{0,20}'.{0,200}'/i,
|
|
411
|
+
// ' OR '1'='1 — bounded to prevent ReDoS
|
|
412
|
+
/'\s{0,10};\s{0,10}(DROP|DELETE|UPDATE|INSERT|ALTER|CREATE|EXEC)/i,
|
|
413
|
+
// '; DROP TABLE
|
|
414
|
+
/UNION\s+(ALL\s+)?SELECT/i,
|
|
415
|
+
// UNION SELECT
|
|
416
|
+
/--\s*$/m,
|
|
417
|
+
// SQL comment at end of line
|
|
418
|
+
/\/\*.{0,500}?\*\//,
|
|
419
|
+
// SQL block comment — bounded + non-greedy
|
|
420
|
+
/\bSLEEP\s*\(/i,
|
|
421
|
+
// Time-based injection
|
|
422
|
+
/\bBENCHMARK\s*\(/i,
|
|
423
|
+
// MySQL benchmark
|
|
424
|
+
/\bWAITFOR\s+DELAY/i,
|
|
425
|
+
// MSSQL delay
|
|
426
|
+
/\b(LOAD_FILE|INTO\s+OUTFILE|INTO\s+DUMPFILE)\b/i
|
|
427
|
+
// File operations
|
|
428
|
+
];
|
|
429
|
+
function detectSQLInjection(value) {
|
|
430
|
+
for (const pattern of SQL_INJECTION_PATTERNS) {
|
|
431
|
+
if (pattern.test(value)) return true;
|
|
432
|
+
}
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
function checkLengthLimits(value, maxLength = 4096) {
|
|
436
|
+
return value.length <= maxLength;
|
|
437
|
+
}
|
|
438
|
+
var ENTROPY_THRESHOLD = 4.5;
|
|
439
|
+
var MIN_LENGTH_FOR_ENTROPY_CHECK = 32;
|
|
440
|
+
function checkEntropyLimits(value) {
|
|
441
|
+
if (value.length < MIN_LENGTH_FOR_ENTROPY_CHECK) return true;
|
|
442
|
+
const entropy = calculateShannonEntropy(value);
|
|
443
|
+
return entropy <= ENTROPY_THRESHOLD;
|
|
444
|
+
}
|
|
445
|
+
function calculateShannonEntropy(str) {
|
|
446
|
+
const freq = /* @__PURE__ */ new Map();
|
|
447
|
+
for (const char of str) {
|
|
448
|
+
freq.set(char, (freq.get(char) ?? 0) + 1);
|
|
449
|
+
}
|
|
450
|
+
let entropy = 0;
|
|
451
|
+
const len = str.length;
|
|
452
|
+
for (const count of freq.values()) {
|
|
453
|
+
const p = count / len;
|
|
454
|
+
if (p > 0) {
|
|
455
|
+
entropy -= p * Math.log2(p);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return entropy;
|
|
459
|
+
}
|
|
460
|
+
function sanitizeInput(field, value, config = DEFAULT_INPUT_GUARD_CONFIG) {
|
|
461
|
+
const threats = [];
|
|
462
|
+
if (typeof value !== "string") {
|
|
463
|
+
if (typeof value === "object" && value !== null) {
|
|
464
|
+
return sanitizeObject(field, value, config);
|
|
465
|
+
}
|
|
466
|
+
return { safe: true, threats: [] };
|
|
467
|
+
}
|
|
468
|
+
if (config.pathTraversal && detectPathTraversal(value)) {
|
|
469
|
+
threats.push({
|
|
470
|
+
type: "PATH_TRAVERSAL",
|
|
471
|
+
field,
|
|
472
|
+
value: truncate(value, 100),
|
|
473
|
+
description: "Path traversal pattern detected"
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
if (config.shellInjection && detectShellInjection(value)) {
|
|
477
|
+
threats.push({
|
|
478
|
+
type: "SHELL_INJECTION",
|
|
479
|
+
field,
|
|
480
|
+
value: truncate(value, 100),
|
|
481
|
+
description: "Shell injection pattern detected"
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
if (config.wildcardAbuse && detectWildcardAbuse(value)) {
|
|
485
|
+
threats.push({
|
|
486
|
+
type: "WILDCARD_ABUSE",
|
|
487
|
+
field,
|
|
488
|
+
value: truncate(value, 100),
|
|
489
|
+
description: "Wildcard abuse pattern detected"
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
if (!checkLengthLimits(value, config.lengthLimit)) {
|
|
493
|
+
threats.push({
|
|
494
|
+
type: "LENGTH_EXCEEDED",
|
|
495
|
+
field,
|
|
496
|
+
value: `[${value.length} chars]`,
|
|
497
|
+
description: `Value exceeds maximum length of ${config.lengthLimit}`
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
if (config.entropyLimit && !checkEntropyLimits(value)) {
|
|
501
|
+
threats.push({
|
|
502
|
+
type: "HIGH_ENTROPY",
|
|
503
|
+
field,
|
|
504
|
+
value: truncate(value, 100),
|
|
505
|
+
description: "High entropy string detected - possible encoded payload"
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
if (config.ssrf && detectSSRF(value)) {
|
|
509
|
+
threats.push({
|
|
510
|
+
type: "SSRF",
|
|
511
|
+
field,
|
|
512
|
+
value: truncate(value, 100),
|
|
513
|
+
description: "Server-side request forgery pattern detected \u2014 internal/metadata URL blocked"
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
if (config.sqlInjection && detectSQLInjection(value)) {
|
|
517
|
+
threats.push({
|
|
518
|
+
type: "SQL_INJECTION",
|
|
519
|
+
field,
|
|
520
|
+
value: truncate(value, 100),
|
|
521
|
+
description: "SQL injection pattern detected"
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
return { safe: threats.length === 0, threats };
|
|
525
|
+
}
|
|
526
|
+
function sanitizeObject(basePath, obj, config) {
|
|
527
|
+
const threats = [];
|
|
528
|
+
if (Array.isArray(obj)) {
|
|
529
|
+
for (let i = 0; i < obj.length; i++) {
|
|
530
|
+
const result = sanitizeInput(`${basePath}[${i}]`, obj[i], config);
|
|
531
|
+
threats.push(...result.threats);
|
|
532
|
+
}
|
|
533
|
+
} else {
|
|
534
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
535
|
+
const result = sanitizeInput(`${basePath}.${key}`, val, config);
|
|
536
|
+
threats.push(...result.threats);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return { safe: threats.length === 0, threats };
|
|
540
|
+
}
|
|
541
|
+
function truncate(str, maxLen) {
|
|
542
|
+
return str.length > maxLen ? str.slice(0, maxLen) + "..." : str;
|
|
543
|
+
}
|
|
544
|
+
var DEFAULT_TOKEN_TTL_SECONDS = 30;
|
|
545
|
+
var TOKEN_ALGORITHM = "HS256";
|
|
546
|
+
var MIN_SECRET_LENGTH = 32;
|
|
547
|
+
function normalizePath(path) {
|
|
548
|
+
let normalized = path.replace(/\\/g, "/");
|
|
549
|
+
if (normalized.length > 1 && normalized.endsWith("/")) {
|
|
550
|
+
normalized = normalized.slice(0, -1);
|
|
551
|
+
}
|
|
552
|
+
const parts = normalized.split("/");
|
|
553
|
+
const resolved = [];
|
|
554
|
+
for (const part of parts) {
|
|
555
|
+
if (part === "." || part === "") {
|
|
556
|
+
if (resolved.length === 0) resolved.push("");
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
if (part === "..") {
|
|
560
|
+
if (resolved.length > 1) {
|
|
561
|
+
resolved.pop();
|
|
562
|
+
}
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
resolved.push(part);
|
|
566
|
+
}
|
|
567
|
+
return resolved.join("/") || "/";
|
|
568
|
+
}
|
|
569
|
+
function isWithinRoot(path, root) {
|
|
570
|
+
const normalizedPath = normalizePath(path);
|
|
571
|
+
const normalizedRoot = normalizePath(root);
|
|
572
|
+
if (normalizedPath === normalizedRoot) return true;
|
|
573
|
+
return normalizedPath.startsWith(normalizedRoot + "/");
|
|
574
|
+
}
|
|
575
|
+
function matchPathPattern(path, pattern) {
|
|
576
|
+
const normalizedPath = normalizePath(path);
|
|
577
|
+
const normalizedPattern = normalizePath(pattern);
|
|
578
|
+
if (normalizedPattern === "*") return true;
|
|
579
|
+
if (normalizedPattern === normalizedPath) return true;
|
|
580
|
+
const patternParts = normalizedPattern.split("/");
|
|
581
|
+
const pathParts = normalizedPath.split("/");
|
|
582
|
+
return matchParts(pathParts, 0, patternParts, 0);
|
|
583
|
+
}
|
|
584
|
+
function matchParts(pathParts, pi, patternParts, qi) {
|
|
585
|
+
while (pi < pathParts.length && qi < patternParts.length) {
|
|
586
|
+
const pattern = patternParts[qi];
|
|
587
|
+
if (pattern === "**") {
|
|
588
|
+
if (qi === patternParts.length - 1) return true;
|
|
589
|
+
for (let i = pi; i <= pathParts.length; i++) {
|
|
590
|
+
if (matchParts(pathParts, i, patternParts, qi + 1)) {
|
|
591
|
+
return true;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return false;
|
|
595
|
+
}
|
|
596
|
+
if (pattern === "*") {
|
|
597
|
+
pi++;
|
|
598
|
+
qi++;
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
if (pattern !== pathParts[pi]) {
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
pi++;
|
|
605
|
+
qi++;
|
|
606
|
+
}
|
|
607
|
+
while (qi < patternParts.length && patternParts[qi] === "**") {
|
|
608
|
+
qi++;
|
|
609
|
+
}
|
|
610
|
+
return pi === pathParts.length && qi === patternParts.length;
|
|
611
|
+
}
|
|
612
|
+
function isPathAllowed(path, constraints) {
|
|
613
|
+
if (constraints.rootDirectory) {
|
|
614
|
+
if (!isWithinRoot(path, constraints.rootDirectory)) {
|
|
615
|
+
return false;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
if (constraints.denied && constraints.denied.length > 0) {
|
|
619
|
+
for (const pattern of constraints.denied) {
|
|
620
|
+
if (matchPathPattern(path, pattern)) {
|
|
621
|
+
return false;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
if (constraints.allowed && constraints.allowed.length > 0) {
|
|
626
|
+
let matchesAllowed = false;
|
|
627
|
+
for (const pattern of constraints.allowed) {
|
|
628
|
+
if (matchPathPattern(path, pattern)) {
|
|
629
|
+
matchesAllowed = true;
|
|
630
|
+
break;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
if (!matchesAllowed) return false;
|
|
634
|
+
}
|
|
635
|
+
return true;
|
|
636
|
+
}
|
|
637
|
+
function extractPathArguments(args) {
|
|
638
|
+
const paths = [];
|
|
639
|
+
for (const value of Object.values(args)) {
|
|
640
|
+
if (typeof value === "string" && (value.includes("/") || value.includes("\\"))) {
|
|
641
|
+
paths.push(value);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
return paths;
|
|
645
|
+
}
|
|
646
|
+
function ruleMatchesRequest(rule, request) {
|
|
647
|
+
if (!rule.enabled) return false;
|
|
648
|
+
if (rule.permission !== request.requiredPermission) return false;
|
|
649
|
+
if (!toolPatternMatches(rule.toolPattern, request.toolName)) return false;
|
|
650
|
+
if (!trustLevelMeetsMinimum(request.context.trustLevel, rule.minimumTrustLevel)) {
|
|
651
|
+
return false;
|
|
652
|
+
}
|
|
653
|
+
if (rule.argumentConstraints) {
|
|
654
|
+
if (!argumentConstraintsMatch(rule.argumentConstraints, request.arguments)) {
|
|
655
|
+
return false;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
if (rule.pathConstraints) {
|
|
659
|
+
if (!pathConstraintsMatch(rule.pathConstraints, request.arguments)) {
|
|
660
|
+
return false;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return true;
|
|
664
|
+
}
|
|
665
|
+
function toolPatternMatches(pattern, toolName) {
|
|
666
|
+
if (pattern === "*") return true;
|
|
667
|
+
const startsWithStar = pattern.startsWith("*");
|
|
668
|
+
const endsWithStar = pattern.endsWith("*");
|
|
669
|
+
if (startsWithStar && endsWithStar) {
|
|
670
|
+
const infix = pattern.slice(1, -1);
|
|
671
|
+
return infix.length > 0 && toolName.includes(infix);
|
|
672
|
+
}
|
|
673
|
+
if (endsWithStar) {
|
|
674
|
+
const prefix = pattern.slice(0, -1);
|
|
675
|
+
return toolName.startsWith(prefix);
|
|
676
|
+
}
|
|
677
|
+
if (startsWithStar) {
|
|
678
|
+
const suffix = pattern.slice(1);
|
|
679
|
+
return toolName.endsWith(suffix);
|
|
680
|
+
}
|
|
681
|
+
return pattern === toolName;
|
|
682
|
+
}
|
|
683
|
+
var TRUST_LEVEL_ORDER = {
|
|
684
|
+
[TrustLevel.UNTRUSTED]: 0,
|
|
685
|
+
[TrustLevel.VERIFIED]: 1,
|
|
686
|
+
[TrustLevel.TRUSTED]: 2
|
|
687
|
+
};
|
|
688
|
+
function trustLevelMeetsMinimum(actual, minimum) {
|
|
689
|
+
return (TRUST_LEVEL_ORDER[actual] ?? -1) >= (TRUST_LEVEL_ORDER[minimum] ?? Infinity);
|
|
690
|
+
}
|
|
691
|
+
function argumentConstraintsMatch(constraints, args) {
|
|
692
|
+
for (const [key, constraint] of Object.entries(constraints)) {
|
|
693
|
+
if (!(key in args)) return false;
|
|
694
|
+
const argValue = args[key];
|
|
695
|
+
if (typeof constraint === "string") {
|
|
696
|
+
if (constraint === "*") continue;
|
|
697
|
+
if (typeof argValue === "string") {
|
|
698
|
+
if (argValue !== constraint) return false;
|
|
699
|
+
} else {
|
|
700
|
+
return false;
|
|
701
|
+
}
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
if (typeof constraint === "object" && constraint !== null && !Array.isArray(constraint)) {
|
|
705
|
+
const ops = constraint;
|
|
706
|
+
const strValue = typeof argValue === "string" ? argValue : void 0;
|
|
707
|
+
const numValue = typeof argValue === "number" ? argValue : void 0;
|
|
708
|
+
if ("$contains" in ops && typeof ops.$contains === "string") {
|
|
709
|
+
if (!strValue || !strValue.includes(ops.$contains)) return false;
|
|
710
|
+
}
|
|
711
|
+
if ("$notContains" in ops && typeof ops.$notContains === "string") {
|
|
712
|
+
if (strValue && strValue.includes(ops.$notContains)) return false;
|
|
713
|
+
}
|
|
714
|
+
if ("$startsWith" in ops && typeof ops.$startsWith === "string") {
|
|
715
|
+
if (!strValue || !strValue.startsWith(ops.$startsWith)) return false;
|
|
716
|
+
}
|
|
717
|
+
if ("$endsWith" in ops && typeof ops.$endsWith === "string") {
|
|
718
|
+
if (!strValue || !strValue.endsWith(ops.$endsWith)) return false;
|
|
719
|
+
}
|
|
720
|
+
if ("$in" in ops && Array.isArray(ops.$in)) {
|
|
721
|
+
if (!ops.$in.includes(argValue)) return false;
|
|
722
|
+
}
|
|
723
|
+
if ("$notIn" in ops && Array.isArray(ops.$notIn)) {
|
|
724
|
+
if (ops.$notIn.includes(argValue)) return false;
|
|
725
|
+
}
|
|
726
|
+
if ("$gt" in ops && typeof ops.$gt === "number") {
|
|
727
|
+
if (numValue === void 0 || numValue <= ops.$gt) return false;
|
|
728
|
+
}
|
|
729
|
+
if ("$lt" in ops && typeof ops.$lt === "number") {
|
|
730
|
+
if (numValue === void 0 || numValue >= ops.$lt) return false;
|
|
731
|
+
}
|
|
732
|
+
if ("$gte" in ops && typeof ops.$gte === "number") {
|
|
733
|
+
if (numValue === void 0 || numValue < ops.$gte) return false;
|
|
734
|
+
}
|
|
735
|
+
if ("$lte" in ops && typeof ops.$lte === "number") {
|
|
736
|
+
if (numValue === void 0 || numValue > ops.$lte) return false;
|
|
737
|
+
}
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
return true;
|
|
742
|
+
}
|
|
743
|
+
function pathConstraintsMatch(constraints, args) {
|
|
744
|
+
const paths = extractPathArguments(args);
|
|
745
|
+
if (paths.length === 0) return true;
|
|
746
|
+
return paths.every((path) => isPathAllowed(path, constraints));
|
|
747
|
+
}
|
|
748
|
+
function evaluatePolicy(policySet, request) {
|
|
749
|
+
const startTime = performance.now();
|
|
750
|
+
const sortedRules = [...policySet.rules].sort(
|
|
751
|
+
(a, b) => a.priority - b.priority
|
|
752
|
+
);
|
|
753
|
+
for (const rule of sortedRules) {
|
|
754
|
+
if (ruleMatchesRequest(rule, request)) {
|
|
755
|
+
const endTime2 = performance.now();
|
|
756
|
+
return {
|
|
757
|
+
effect: rule.effect,
|
|
758
|
+
matchedRule: rule,
|
|
759
|
+
reason: `Matched rule "${rule.id}": ${rule.description}`,
|
|
760
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
761
|
+
evaluationTimeMs: endTime2 - startTime
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
const endTime = performance.now();
|
|
766
|
+
return {
|
|
767
|
+
effect: DEFAULT_POLICY_EFFECT,
|
|
768
|
+
matchedRule: null,
|
|
769
|
+
reason: "No matching policy rule found. Default action: DENY.",
|
|
770
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
771
|
+
evaluationTimeMs: endTime - startTime,
|
|
772
|
+
metadata: {
|
|
773
|
+
evaluatedRules: sortedRules.length,
|
|
774
|
+
ruleIds: sortedRules.map((r) => r.id),
|
|
775
|
+
requestContext: {
|
|
776
|
+
tool: request.toolName,
|
|
777
|
+
arguments: Object.keys(request.arguments ?? {})
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
function validatePolicyRule(input) {
|
|
783
|
+
const errors = [];
|
|
784
|
+
const warnings = [];
|
|
785
|
+
const result = PolicyRuleSchema.safeParse(input);
|
|
786
|
+
if (!result.success) {
|
|
787
|
+
return {
|
|
788
|
+
valid: false,
|
|
789
|
+
errors: result.error.errors.map(
|
|
790
|
+
(e) => `${e.path.join(".")}: ${e.message}`
|
|
791
|
+
),
|
|
792
|
+
warnings: []
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
const rule = result.data;
|
|
796
|
+
if (rule.toolPattern === "*" && rule.effect === "ALLOW") {
|
|
797
|
+
warnings.push(UNSAFE_CONFIGURATION_WARNINGS.WILDCARD_ALLOW);
|
|
798
|
+
}
|
|
799
|
+
if (rule.minimumTrustLevel === "TRUSTED") {
|
|
800
|
+
warnings.push(UNSAFE_CONFIGURATION_WARNINGS.TRUSTED_LEVEL_EXTERNAL);
|
|
801
|
+
}
|
|
802
|
+
if (rule.permission === "EXECUTE") {
|
|
803
|
+
warnings.push(UNSAFE_CONFIGURATION_WARNINGS.EXECUTE_WITHOUT_REVIEW);
|
|
804
|
+
}
|
|
805
|
+
return { valid: true, errors, warnings };
|
|
806
|
+
}
|
|
807
|
+
function validatePolicySet(input) {
|
|
808
|
+
const errors = [];
|
|
809
|
+
const warnings = [];
|
|
810
|
+
const result = PolicySetSchema.safeParse(input);
|
|
811
|
+
if (!result.success) {
|
|
812
|
+
return {
|
|
813
|
+
valid: false,
|
|
814
|
+
errors: result.error.errors.map(
|
|
815
|
+
(e) => `${e.path.join(".")}: ${e.message}`
|
|
816
|
+
),
|
|
817
|
+
warnings: []
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
const policySet = result.data;
|
|
821
|
+
if (policySet.rules.length > MAX_RULES_PER_POLICY_SET) {
|
|
822
|
+
errors.push(
|
|
823
|
+
`Policy set exceeds maximum of ${MAX_RULES_PER_POLICY_SET} rules`
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
const ruleIds = /* @__PURE__ */ new Set();
|
|
827
|
+
for (const rule of policySet.rules) {
|
|
828
|
+
if (ruleIds.has(rule.id)) {
|
|
829
|
+
errors.push(`Duplicate rule ID: "${rule.id}"`);
|
|
830
|
+
}
|
|
831
|
+
ruleIds.add(rule.id);
|
|
832
|
+
}
|
|
833
|
+
for (const rule of policySet.rules) {
|
|
834
|
+
const ruleResult = validatePolicyRule(rule);
|
|
835
|
+
warnings.push(...ruleResult.warnings);
|
|
836
|
+
}
|
|
837
|
+
const hasDenyRule = policySet.rules.some((r) => r.effect === "DENY");
|
|
838
|
+
if (!hasDenyRule && policySet.rules.length > 0) {
|
|
839
|
+
warnings.push(
|
|
840
|
+
"Policy set contains only ALLOW rules. The default-deny fallback is the only protection."
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
return {
|
|
844
|
+
valid: errors.length === 0,
|
|
845
|
+
errors,
|
|
846
|
+
warnings
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
function analyzeSecurityWarnings(policySet) {
|
|
850
|
+
const warnings = [];
|
|
851
|
+
for (const rule of policySet.rules) {
|
|
852
|
+
warnings.push(...analyzeRuleWarnings(rule));
|
|
853
|
+
}
|
|
854
|
+
const allowRules = policySet.rules.filter(
|
|
855
|
+
(r) => r.effect === "ALLOW" && r.enabled
|
|
856
|
+
);
|
|
857
|
+
const wildcardAllows = allowRules.filter((r) => r.toolPattern === "*");
|
|
858
|
+
if (wildcardAllows.length > 0) {
|
|
859
|
+
warnings.push({
|
|
860
|
+
level: "CRITICAL",
|
|
861
|
+
code: "WILDCARD_ALLOW",
|
|
862
|
+
message: UNSAFE_CONFIGURATION_WARNINGS.WILDCARD_ALLOW,
|
|
863
|
+
recommendation: "Replace wildcard ALLOW rules with specific tool patterns."
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
return warnings;
|
|
867
|
+
}
|
|
868
|
+
function analyzeRuleWarnings(rule) {
|
|
869
|
+
const warnings = [];
|
|
870
|
+
if (rule.effect === "ALLOW" && rule.minimumTrustLevel === "UNTRUSTED") {
|
|
871
|
+
warnings.push({
|
|
872
|
+
level: "CRITICAL",
|
|
873
|
+
code: "ALLOW_UNTRUSTED",
|
|
874
|
+
message: `Rule "${rule.id}" allows execution for UNTRUSTED requests. Unverified LLM requests can execute tools.`,
|
|
875
|
+
ruleId: rule.id,
|
|
876
|
+
recommendation: "Set minimumTrustLevel to VERIFIED or higher for ALLOW rules."
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
if (rule.effect === "ALLOW" && rule.permission === "EXECUTE") {
|
|
880
|
+
warnings.push({
|
|
881
|
+
level: "WARNING",
|
|
882
|
+
code: "ALLOW_EXECUTE",
|
|
883
|
+
message: UNSAFE_CONFIGURATION_WARNINGS.EXECUTE_WITHOUT_REVIEW,
|
|
884
|
+
ruleId: rule.id,
|
|
885
|
+
recommendation: "Ensure EXECUTE permissions are intentional and scoped to specific tools."
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
return warnings;
|
|
889
|
+
}
|
|
890
|
+
function createDefaultDenyPolicySet() {
|
|
891
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
892
|
+
return {
|
|
893
|
+
id: "default-deny",
|
|
894
|
+
name: "Default Deny All",
|
|
895
|
+
description: "Denies all tool executions. Add explicit ALLOW rules to grant access to specific tools.",
|
|
896
|
+
version: 1,
|
|
897
|
+
rules: [
|
|
898
|
+
{
|
|
899
|
+
id: "deny-all-execute",
|
|
900
|
+
description: "Explicitly deny all tool executions",
|
|
901
|
+
effect: PolicyEffect.DENY,
|
|
902
|
+
priority: 1e4,
|
|
903
|
+
toolPattern: "*",
|
|
904
|
+
permission: Permission.EXECUTE,
|
|
905
|
+
minimumTrustLevel: TrustLevel.UNTRUSTED,
|
|
906
|
+
enabled: true,
|
|
907
|
+
createdAt: now,
|
|
908
|
+
updatedAt: now
|
|
909
|
+
},
|
|
910
|
+
{
|
|
911
|
+
id: "deny-all-write",
|
|
912
|
+
description: "Explicitly deny all write operations",
|
|
913
|
+
effect: PolicyEffect.DENY,
|
|
914
|
+
priority: 1e4,
|
|
915
|
+
toolPattern: "*",
|
|
916
|
+
permission: Permission.WRITE,
|
|
917
|
+
minimumTrustLevel: TrustLevel.UNTRUSTED,
|
|
918
|
+
enabled: true,
|
|
919
|
+
createdAt: now,
|
|
920
|
+
updatedAt: now
|
|
921
|
+
},
|
|
922
|
+
{
|
|
923
|
+
id: "deny-all-read",
|
|
924
|
+
description: "Explicitly deny all read operations",
|
|
925
|
+
effect: PolicyEffect.DENY,
|
|
926
|
+
priority: 1e4,
|
|
927
|
+
toolPattern: "*",
|
|
928
|
+
permission: Permission.READ,
|
|
929
|
+
minimumTrustLevel: TrustLevel.UNTRUSTED,
|
|
930
|
+
enabled: true,
|
|
931
|
+
createdAt: now,
|
|
932
|
+
updatedAt: now
|
|
933
|
+
}
|
|
934
|
+
],
|
|
935
|
+
createdAt: now,
|
|
936
|
+
updatedAt: now
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
function createReadOnlyPolicySet(toolPattern) {
|
|
940
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
941
|
+
return {
|
|
942
|
+
id: `read-only-${toolPattern}`,
|
|
943
|
+
name: `Read-Only: ${toolPattern}`,
|
|
944
|
+
description: `Allows read access to tools matching "${toolPattern}". Denies write and execute.`,
|
|
945
|
+
version: 1,
|
|
946
|
+
rules: [
|
|
947
|
+
{
|
|
948
|
+
id: `allow-read-${toolPattern}`,
|
|
949
|
+
description: `Allow read access to ${toolPattern}`,
|
|
950
|
+
effect: PolicyEffect.ALLOW,
|
|
951
|
+
priority: 100,
|
|
952
|
+
toolPattern,
|
|
953
|
+
permission: Permission.READ,
|
|
954
|
+
minimumTrustLevel: TrustLevel.VERIFIED,
|
|
955
|
+
enabled: true,
|
|
956
|
+
createdAt: now,
|
|
957
|
+
updatedAt: now
|
|
958
|
+
}
|
|
959
|
+
],
|
|
960
|
+
createdAt: now,
|
|
961
|
+
updatedAt: now
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
var PolicyEngine = class {
|
|
965
|
+
policySet;
|
|
966
|
+
timeoutMs;
|
|
967
|
+
store;
|
|
968
|
+
constructor(options) {
|
|
969
|
+
this.policySet = options?.policySet ?? createDefaultDenyPolicySet();
|
|
970
|
+
this.timeoutMs = options?.timeoutMs ?? POLICY_EVALUATION_TIMEOUT_MS;
|
|
971
|
+
this.store = options?.store ?? null;
|
|
972
|
+
}
|
|
973
|
+
/**
|
|
974
|
+
* Evaluates an execution request against the current policy set.
|
|
975
|
+
* Never throws for denials - denial is a normal outcome, not an error.
|
|
976
|
+
*/
|
|
977
|
+
evaluate(request) {
|
|
978
|
+
const startTime = performance.now();
|
|
979
|
+
const decision = evaluatePolicy(this.policySet, request);
|
|
980
|
+
const elapsed = performance.now() - startTime;
|
|
981
|
+
if (elapsed > this.timeoutMs) {
|
|
982
|
+
console.warn(
|
|
983
|
+
`[SolonGate] Policy evaluation took ${elapsed.toFixed(1)}ms (limit: ${this.timeoutMs}ms) for tool "${request.toolName}"`
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
return decision;
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* Loads a new policy set, replacing the current one.
|
|
990
|
+
* Validates before accepting. Auto-saves version when store is present.
|
|
991
|
+
*/
|
|
992
|
+
loadPolicySet(policySet, options) {
|
|
993
|
+
const validation = validatePolicySet(policySet);
|
|
994
|
+
if (!validation.valid) {
|
|
995
|
+
return validation;
|
|
996
|
+
}
|
|
997
|
+
this.policySet = policySet;
|
|
998
|
+
if (this.store) {
|
|
999
|
+
this.store.saveVersion(
|
|
1000
|
+
policySet,
|
|
1001
|
+
options?.reason ?? "Policy updated",
|
|
1002
|
+
options?.createdBy ?? "system"
|
|
1003
|
+
);
|
|
1004
|
+
}
|
|
1005
|
+
return validation;
|
|
1006
|
+
}
|
|
1007
|
+
/**
|
|
1008
|
+
* Rolls back to a previous policy version.
|
|
1009
|
+
* Only available when a PolicyStore is configured.
|
|
1010
|
+
*/
|
|
1011
|
+
rollback(version) {
|
|
1012
|
+
if (!this.store) {
|
|
1013
|
+
throw new Error("PolicyStore not configured - cannot rollback");
|
|
1014
|
+
}
|
|
1015
|
+
const policyVersion = this.store.rollback(this.policySet.id, version);
|
|
1016
|
+
this.policySet = policyVersion.policySet;
|
|
1017
|
+
return policyVersion;
|
|
1018
|
+
}
|
|
1019
|
+
getPolicySet() {
|
|
1020
|
+
return this.policySet;
|
|
1021
|
+
}
|
|
1022
|
+
getSecurityWarnings() {
|
|
1023
|
+
return analyzeSecurityWarnings(this.policySet);
|
|
1024
|
+
}
|
|
1025
|
+
getStore() {
|
|
1026
|
+
return this.store;
|
|
1027
|
+
}
|
|
1028
|
+
reset() {
|
|
1029
|
+
this.policySet = createDefaultDenyPolicySet();
|
|
1030
|
+
}
|
|
1031
|
+
};
|
|
1032
|
+
var PolicyStore = class {
|
|
1033
|
+
versions = /* @__PURE__ */ new Map();
|
|
1034
|
+
/**
|
|
1035
|
+
* Saves a new version of a policy set.
|
|
1036
|
+
* The version number auto-increments.
|
|
1037
|
+
*/
|
|
1038
|
+
saveVersion(policySet, reason, createdBy) {
|
|
1039
|
+
const id = policySet.id;
|
|
1040
|
+
const history = this.versions.get(id) ?? [];
|
|
1041
|
+
const latestVersion = history.length > 0 ? history[history.length - 1].version : 0;
|
|
1042
|
+
const version = {
|
|
1043
|
+
version: latestVersion + 1,
|
|
1044
|
+
policySet: Object.freeze({ ...policySet }),
|
|
1045
|
+
hash: this.computeHash(policySet),
|
|
1046
|
+
reason,
|
|
1047
|
+
createdBy,
|
|
1048
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1049
|
+
};
|
|
1050
|
+
const newHistory = [...history, version];
|
|
1051
|
+
this.versions.set(id, newHistory);
|
|
1052
|
+
return version;
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Gets a specific version of a policy set.
|
|
1056
|
+
*/
|
|
1057
|
+
getVersion(id, version) {
|
|
1058
|
+
const history = this.versions.get(id);
|
|
1059
|
+
if (!history) return null;
|
|
1060
|
+
return history.find((v) => v.version === version) ?? null;
|
|
1061
|
+
}
|
|
1062
|
+
/**
|
|
1063
|
+
* Gets the latest version of a policy set.
|
|
1064
|
+
*/
|
|
1065
|
+
getLatest(id) {
|
|
1066
|
+
const history = this.versions.get(id);
|
|
1067
|
+
if (!history || history.length === 0) return null;
|
|
1068
|
+
return history[history.length - 1];
|
|
1069
|
+
}
|
|
1070
|
+
/**
|
|
1071
|
+
* Gets the full version history of a policy set.
|
|
1072
|
+
*/
|
|
1073
|
+
getHistory(id) {
|
|
1074
|
+
return this.versions.get(id) ?? [];
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* Rolls back to a previous version by creating a new version
|
|
1078
|
+
* with the same content as the target version.
|
|
1079
|
+
*/
|
|
1080
|
+
rollback(id, toVersion) {
|
|
1081
|
+
const target = this.getVersion(id, toVersion);
|
|
1082
|
+
if (!target) {
|
|
1083
|
+
throw new Error(`Version ${toVersion} not found for policy "${id}"`);
|
|
1084
|
+
}
|
|
1085
|
+
return this.saveVersion(
|
|
1086
|
+
target.policySet,
|
|
1087
|
+
`Rollback to version ${toVersion}`,
|
|
1088
|
+
"system"
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
1091
|
+
/**
|
|
1092
|
+
* Computes a diff between two policy versions.
|
|
1093
|
+
*/
|
|
1094
|
+
diff(v1, v2) {
|
|
1095
|
+
const oldRulesMap = new Map(v1.policySet.rules.map((r) => [r.id, r]));
|
|
1096
|
+
const newRulesMap = new Map(v2.policySet.rules.map((r) => [r.id, r]));
|
|
1097
|
+
const added = [];
|
|
1098
|
+
const removed = [];
|
|
1099
|
+
const modified = [];
|
|
1100
|
+
for (const [id, newRule] of newRulesMap) {
|
|
1101
|
+
const oldRule = oldRulesMap.get(id);
|
|
1102
|
+
if (!oldRule) {
|
|
1103
|
+
added.push(newRule);
|
|
1104
|
+
} else if (JSON.stringify(oldRule) !== JSON.stringify(newRule)) {
|
|
1105
|
+
modified.push({ old: oldRule, new: newRule });
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
for (const [id, oldRule] of oldRulesMap) {
|
|
1109
|
+
if (!newRulesMap.has(id)) {
|
|
1110
|
+
removed.push(oldRule);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
return { added, removed, modified };
|
|
1114
|
+
}
|
|
1115
|
+
/**
|
|
1116
|
+
* Computes SHA256 hash of a policy set for integrity verification.
|
|
1117
|
+
*/
|
|
1118
|
+
computeHash(policySet) {
|
|
1119
|
+
const serialized = JSON.stringify(policySet, Object.keys(policySet).sort());
|
|
1120
|
+
return createHash("sha256").update(serialized).digest("hex");
|
|
1121
|
+
}
|
|
1122
|
+
};
|
|
1123
|
+
|
|
1124
|
+
// src/config.ts
|
|
9
1125
|
var DEFAULT_CONFIG = Object.freeze({
|
|
10
1126
|
validateSchemas: true,
|
|
11
1127
|
enableLogging: true,
|
|
@@ -384,6 +1500,8 @@ var ServerVerifier = class {
|
|
|
384
1500
|
return { valid: true };
|
|
385
1501
|
}
|
|
386
1502
|
};
|
|
1503
|
+
|
|
1504
|
+
// src/rate-limiter.ts
|
|
387
1505
|
var RateLimiter = class {
|
|
388
1506
|
windowMs;
|
|
389
1507
|
records = /* @__PURE__ */ new Map();
|
|
@@ -703,6 +1821,8 @@ var SecureMcpServer = class extends McpServer {
|
|
|
703
1821
|
return this.gate;
|
|
704
1822
|
}
|
|
705
1823
|
};
|
|
1824
|
+
|
|
1825
|
+
// src/api-client.ts
|
|
706
1826
|
var DEFAULT_API_URL = "https://api.solongate.com";
|
|
707
1827
|
var API_VERSION = "v1";
|
|
708
1828
|
var SDK_VERSION = "0.2.0";
|
|
@@ -922,6 +2042,6 @@ var SolonGateAPI = class {
|
|
|
922
2042
|
}
|
|
923
2043
|
};
|
|
924
2044
|
|
|
925
|
-
export { APIError, AuthenticationError, DEFAULT_CONFIG, LicenseError, RateLimitError2 as RateLimitError, RateLimiter, SecureMcpServer, SecurityLogger, ServerVerifier, SolonGate, SolonGateAPI, TokenIssuer, interceptToolCall, resolveConfig };
|
|
2045
|
+
export { APIError, AuthenticationError, RateLimitError as CoreRateLimitError, DEFAULT_CONFIG, InputGuardError, LicenseError, NetworkError, Permission, PolicyDeniedError, PolicyEffect, PolicyEngine, PolicyStore, RateLimitError2 as RateLimitError, RateLimiter, SchemaValidationError, SecureMcpServer, SecurityLogger, ServerVerifier, SolonGate, SolonGateAPI, SolonGateError, TokenIssuer, TrustLevel, checkEntropyLimits, checkLengthLimits, createDefaultDenyPolicySet, createDeniedToolResult, createReadOnlyPolicySet, createSecurityContext, detectPathTraversal, detectSQLInjection, detectSSRF, detectShellInjection, detectWildcardAbuse, interceptToolCall, resolveConfig, sanitizeInput, validateToolInput };
|
|
926
2046
|
//# sourceMappingURL=index.js.map
|
|
927
2047
|
//# sourceMappingURL=index.js.map
|