@solongate/proxy 0.12.1 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/hooks/guard.mjs +104 -15
- package/package.json +1 -1
package/hooks/guard.mjs
CHANGED
|
@@ -104,10 +104,11 @@ const PI_CATEGORIES = [
|
|
|
104
104
|
},
|
|
105
105
|
];
|
|
106
106
|
|
|
107
|
-
function detectPromptInjection(text) {
|
|
107
|
+
function detectPromptInjection(text, customCategories = [], threshold = 0.5) {
|
|
108
108
|
const matched = [];
|
|
109
109
|
let maxWeight = 0;
|
|
110
|
-
|
|
110
|
+
const allCategories = [...PI_CATEGORIES, ...customCategories];
|
|
111
|
+
for (const cat of allCategories) {
|
|
111
112
|
for (const pat of cat.patterns) {
|
|
112
113
|
if (pat.test(text)) {
|
|
113
114
|
matched.push(cat.name);
|
|
@@ -119,7 +120,7 @@ function detectPromptInjection(text) {
|
|
|
119
120
|
if (matched.length === 0) return null;
|
|
120
121
|
const score = Math.min(1.0, maxWeight + 0.05 * (matched.length - 1));
|
|
121
122
|
const trustScore = 1.0 - score;
|
|
122
|
-
return { score, trustScore, categories: matched, blocked: trustScore <
|
|
123
|
+
return { score, trustScore, categories: matched, blocked: trustScore < threshold };
|
|
123
124
|
}
|
|
124
125
|
|
|
125
126
|
// ── Glob Matching ──
|
|
@@ -314,39 +315,127 @@ process.stdin.on('end', async () => {
|
|
|
314
315
|
}
|
|
315
316
|
}
|
|
316
317
|
|
|
317
|
-
// ──
|
|
318
|
+
// ── Fetch PI config from Cloud ──
|
|
319
|
+
let piCfg = { piEnabled: true, piThreshold: 0.5, piMode: 'block', piWhitelist: [], piToolConfig: {}, piCustomPatterns: [], piWebhookUrl: null };
|
|
320
|
+
if (API_KEY && API_KEY.startsWith('sg_live_')) {
|
|
321
|
+
try {
|
|
322
|
+
const cfgRes = await fetch(API_URL + '/api/v1/project-config', {
|
|
323
|
+
headers: { 'Authorization': 'Bearer ' + API_KEY },
|
|
324
|
+
signal: AbortSignal.timeout(3000),
|
|
325
|
+
});
|
|
326
|
+
if (cfgRes.ok) {
|
|
327
|
+
const cfg = await cfgRes.json();
|
|
328
|
+
piCfg = { ...piCfg, ...cfg };
|
|
329
|
+
}
|
|
330
|
+
} catch {} // Fallback: defaults (safe)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ── Per-tool config: check if PI scanning is disabled for this tool ──
|
|
334
|
+
const toolName = data.tool_name || '';
|
|
335
|
+
if (piCfg.piToolConfig && typeof piCfg.piToolConfig === 'object') {
|
|
336
|
+
if (piCfg.piToolConfig[toolName] === false) {
|
|
337
|
+
// PI scanning explicitly disabled for this tool — skip detection
|
|
338
|
+
piCfg.piEnabled = false;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ── Prompt Injection Detection (Stage 1: Rules + Custom Patterns) ──
|
|
318
343
|
const allText = scanStrings(args).join(' ');
|
|
319
|
-
|
|
344
|
+
|
|
345
|
+
// Check whitelist — if input matches any whitelist pattern, skip PI detection
|
|
346
|
+
let whitelisted = false;
|
|
347
|
+
if (piCfg.piEnabled !== false && Array.isArray(piCfg.piWhitelist) && piCfg.piWhitelist.length > 0) {
|
|
348
|
+
for (const wlPattern of piCfg.piWhitelist) {
|
|
349
|
+
try {
|
|
350
|
+
if (new RegExp(wlPattern, 'i').test(allText)) {
|
|
351
|
+
whitelisted = true;
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
} catch {} // Invalid regex — skip
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Build custom patterns from config
|
|
359
|
+
const customCategories = [];
|
|
360
|
+
if (piCfg.piEnabled !== false && Array.isArray(piCfg.piCustomPatterns)) {
|
|
361
|
+
for (const cp of piCfg.piCustomPatterns) {
|
|
362
|
+
if (cp && cp.pattern) {
|
|
363
|
+
try {
|
|
364
|
+
customCategories.push({
|
|
365
|
+
name: cp.name || 'custom_pattern',
|
|
366
|
+
weight: Math.max(0, Math.min(1, Number(cp.weight) || 0.8)),
|
|
367
|
+
patterns: [new RegExp(cp.pattern, 'iu')],
|
|
368
|
+
});
|
|
369
|
+
} catch {} // Invalid regex — skip
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const piResult = (piCfg.piEnabled !== false && !whitelisted)
|
|
375
|
+
? detectPromptInjection(allText, customCategories, piCfg.piThreshold)
|
|
376
|
+
: null;
|
|
320
377
|
|
|
321
378
|
if (piResult && piResult.blocked) {
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
379
|
+
const isLogOnly = piCfg.piMode === 'log-only';
|
|
380
|
+
const msg = isLogOnly
|
|
381
|
+
? 'SOLONGATE: Prompt injection detected [LOG-ONLY] (trust score: ' +
|
|
382
|
+
(piResult.trustScore * 100).toFixed(0) + '%, categories: ' +
|
|
383
|
+
piResult.categories.join(', ') + ')'
|
|
384
|
+
: 'SOLONGATE: Prompt injection detected (trust score: ' +
|
|
385
|
+
(piResult.trustScore * 100).toFixed(0) + '%, categories: ' +
|
|
386
|
+
piResult.categories.join(', ') + ')';
|
|
325
387
|
|
|
388
|
+
// Log to Cloud
|
|
326
389
|
if (API_KEY && API_KEY.startsWith('sg_live_')) {
|
|
327
390
|
try {
|
|
328
391
|
await fetch(API_URL + '/api/v1/audit-logs', {
|
|
329
392
|
method: 'POST',
|
|
330
393
|
headers: { 'Authorization': 'Bearer ' + API_KEY, 'Content-Type': 'application/json' },
|
|
331
394
|
body: JSON.stringify({
|
|
332
|
-
tool:
|
|
395
|
+
tool: toolName,
|
|
333
396
|
arguments: args,
|
|
334
|
-
decision: 'DENY',
|
|
397
|
+
decision: isLogOnly ? 'ALLOW' : 'DENY',
|
|
335
398
|
reason: msg,
|
|
336
399
|
source: 'claude-code-guard',
|
|
337
400
|
pi_detected: true,
|
|
338
401
|
pi_trust_score: piResult.trustScore,
|
|
339
|
-
pi_blocked:
|
|
402
|
+
pi_blocked: !isLogOnly,
|
|
340
403
|
pi_categories: JSON.stringify(piResult.categories),
|
|
341
404
|
pi_stage_scores: JSON.stringify({ rules: piResult.score, embedding: 0, classifier: 0 }),
|
|
342
405
|
}),
|
|
343
406
|
signal: AbortSignal.timeout(3000),
|
|
344
407
|
});
|
|
345
408
|
} catch {}
|
|
409
|
+
|
|
410
|
+
// Webhook notification
|
|
411
|
+
if (piCfg.piWebhookUrl) {
|
|
412
|
+
try {
|
|
413
|
+
await fetch(piCfg.piWebhookUrl, {
|
|
414
|
+
method: 'POST',
|
|
415
|
+
headers: { 'Content-Type': 'application/json' },
|
|
416
|
+
body: JSON.stringify({
|
|
417
|
+
event: 'prompt_injection_detected',
|
|
418
|
+
tool: toolName,
|
|
419
|
+
trustScore: piResult.trustScore,
|
|
420
|
+
categories: piResult.categories,
|
|
421
|
+
blocked: !isLogOnly,
|
|
422
|
+
mode: piCfg.piMode,
|
|
423
|
+
timestamp: new Date().toISOString(),
|
|
424
|
+
}),
|
|
425
|
+
signal: AbortSignal.timeout(3000),
|
|
426
|
+
});
|
|
427
|
+
} catch {} // Webhook failure is non-blocking
|
|
428
|
+
}
|
|
346
429
|
}
|
|
347
430
|
|
|
348
|
-
|
|
349
|
-
|
|
431
|
+
// In log-only mode, warn but don't block
|
|
432
|
+
if (isLogOnly) {
|
|
433
|
+
process.stderr.write(msg);
|
|
434
|
+
// Fall through to policy evaluation (don't exit)
|
|
435
|
+
} else {
|
|
436
|
+
process.stderr.write(msg);
|
|
437
|
+
process.exit(2);
|
|
438
|
+
}
|
|
350
439
|
}
|
|
351
440
|
|
|
352
441
|
// Load policy (use cwd from hook data if available)
|
|
@@ -363,7 +452,7 @@ process.stdin.on('end', async () => {
|
|
|
363
452
|
method: 'POST',
|
|
364
453
|
headers: { 'Authorization': 'Bearer ' + API_KEY, 'Content-Type': 'application/json' },
|
|
365
454
|
body: JSON.stringify({
|
|
366
|
-
tool:
|
|
455
|
+
tool: toolName,
|
|
367
456
|
arguments: args,
|
|
368
457
|
decision: 'ALLOW',
|
|
369
458
|
reason: 'Prompt injection detected but below threshold (trust: ' + (piResult.trustScore * 100).toFixed(0) + '%)',
|
|
@@ -388,7 +477,7 @@ process.stdin.on('end', async () => {
|
|
|
388
477
|
if (API_KEY && API_KEY.startsWith('sg_live_')) {
|
|
389
478
|
try {
|
|
390
479
|
const logEntry = {
|
|
391
|
-
tool:
|
|
480
|
+
tool: toolName, arguments: args,
|
|
392
481
|
decision, reason: reason || 'allowed by policy',
|
|
393
482
|
source: 'claude-code-guard',
|
|
394
483
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solongate/proxy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
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": {
|