@solongate/proxy 0.13.0 → 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 +93 -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,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,8 +315,8 @@ process.stdin.on('end', async () => {
|
|
|
314
315
|
}
|
|
315
316
|
}
|
|
316
317
|
|
|
317
|
-
// ──
|
|
318
|
-
let
|
|
318
|
+
// ── Fetch PI config from Cloud ──
|
|
319
|
+
let piCfg = { piEnabled: true, piThreshold: 0.5, piMode: 'block', piWhitelist: [], piToolConfig: {}, piCustomPatterns: [], piWebhookUrl: null };
|
|
319
320
|
if (API_KEY && API_KEY.startsWith('sg_live_')) {
|
|
320
321
|
try {
|
|
321
322
|
const cfgRes = await fetch(API_URL + '/api/v1/project-config', {
|
|
@@ -324,44 +325,117 @@ process.stdin.on('end', async () => {
|
|
|
324
325
|
});
|
|
325
326
|
if (cfgRes.ok) {
|
|
326
327
|
const cfg = await cfgRes.json();
|
|
327
|
-
|
|
328
|
+
piCfg = { ...piCfg, ...cfg };
|
|
328
329
|
}
|
|
329
|
-
} catch {} // Fallback:
|
|
330
|
+
} catch {} // Fallback: defaults (safe)
|
|
330
331
|
}
|
|
331
332
|
|
|
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) ──
|
|
333
343
|
const allText = scanStrings(args).join(' ');
|
|
334
|
-
|
|
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;
|
|
335
377
|
|
|
336
378
|
if (piResult && piResult.blocked) {
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
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(', ') + ')';
|
|
340
387
|
|
|
388
|
+
// Log to Cloud
|
|
341
389
|
if (API_KEY && API_KEY.startsWith('sg_live_')) {
|
|
342
390
|
try {
|
|
343
391
|
await fetch(API_URL + '/api/v1/audit-logs', {
|
|
344
392
|
method: 'POST',
|
|
345
393
|
headers: { 'Authorization': 'Bearer ' + API_KEY, 'Content-Type': 'application/json' },
|
|
346
394
|
body: JSON.stringify({
|
|
347
|
-
tool:
|
|
395
|
+
tool: toolName,
|
|
348
396
|
arguments: args,
|
|
349
|
-
decision: 'DENY',
|
|
397
|
+
decision: isLogOnly ? 'ALLOW' : 'DENY',
|
|
350
398
|
reason: msg,
|
|
351
399
|
source: 'claude-code-guard',
|
|
352
400
|
pi_detected: true,
|
|
353
401
|
pi_trust_score: piResult.trustScore,
|
|
354
|
-
pi_blocked:
|
|
402
|
+
pi_blocked: !isLogOnly,
|
|
355
403
|
pi_categories: JSON.stringify(piResult.categories),
|
|
356
404
|
pi_stage_scores: JSON.stringify({ rules: piResult.score, embedding: 0, classifier: 0 }),
|
|
357
405
|
}),
|
|
358
406
|
signal: AbortSignal.timeout(3000),
|
|
359
407
|
});
|
|
360
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
|
+
}
|
|
361
429
|
}
|
|
362
430
|
|
|
363
|
-
|
|
364
|
-
|
|
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
|
+
}
|
|
365
439
|
}
|
|
366
440
|
|
|
367
441
|
// Load policy (use cwd from hook data if available)
|
|
@@ -378,7 +452,7 @@ process.stdin.on('end', async () => {
|
|
|
378
452
|
method: 'POST',
|
|
379
453
|
headers: { 'Authorization': 'Bearer ' + API_KEY, 'Content-Type': 'application/json' },
|
|
380
454
|
body: JSON.stringify({
|
|
381
|
-
tool:
|
|
455
|
+
tool: toolName,
|
|
382
456
|
arguments: args,
|
|
383
457
|
decision: 'ALLOW',
|
|
384
458
|
reason: 'Prompt injection detected but below threshold (trust: ' + (piResult.trustScore * 100).toFixed(0) + '%)',
|
|
@@ -403,7 +477,7 @@ process.stdin.on('end', async () => {
|
|
|
403
477
|
if (API_KEY && API_KEY.startsWith('sg_live_')) {
|
|
404
478
|
try {
|
|
405
479
|
const logEntry = {
|
|
406
|
-
tool:
|
|
480
|
+
tool: toolName, arguments: args,
|
|
407
481
|
decision, reason: reason || 'allowed by policy',
|
|
408
482
|
source: 'claude-code-guard',
|
|
409
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": {
|