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