@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.
Files changed (2) hide show
  1. package/hooks/guard.mjs +104 -15
  2. 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
- for (const cat of PI_CATEGORIES) {
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 < 0.5 };
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
- // ── Prompt Injection Detection (Stage 1: Rules) ──
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
- const piResult = detectPromptInjection(allText);
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 msg = 'SOLONGATE: Prompt injection detected (trust score: ' +
323
- (piResult.trustScore * 100).toFixed(0) + '%, categories: ' +
324
- piResult.categories.join(', ') + ')';
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: data.tool_name || '',
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: true,
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
- process.stderr.write(msg);
349
- process.exit(2);
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: data.tool_name || '',
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: data.tool_name || '', arguments: args,
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.12.1",
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": {