@solongate/proxy 0.11.0 → 0.12.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.
Files changed (2) hide show
  1. package/hooks/guard.mjs +165 -11
  2. package/package.json +1 -1
package/hooks/guard.mjs CHANGED
@@ -2,6 +2,7 @@
2
2
  /**
3
3
  * SolonGate Policy Guard Hook (PreToolUse)
4
4
  * Reads policy.json and blocks tool calls that violate constraints.
5
+ * Also runs prompt injection detection (Stage 1 rules) on tool arguments.
5
6
  * Exit code 2 = BLOCK, exit code 0 = ALLOW.
6
7
  * Logs ALL decisions (ALLOW + DENY) to SolonGate Cloud.
7
8
  * Auto-installed by: npx @solongate/proxy init
@@ -29,6 +30,98 @@ const dotenv = loadEnvKey(hookCwdEarly);
29
30
  const API_KEY = process.env.SOLONGATE_API_KEY || dotenv.SOLONGATE_API_KEY || '';
30
31
  const API_URL = process.env.SOLONGATE_API_URL || dotenv.SOLONGATE_API_URL || 'https://api.solongate.com';
31
32
 
33
+ // ── Prompt Injection Detection (Stage 1: Rule-Based) ──
34
+ const PI_CATEGORIES = [
35
+ {
36
+ name: 'delimiter_injection', weight: 0.95,
37
+ patterns: [
38
+ /<\/system>/i, /<\|im_end\|>/i, /<\|im_start\|>/i, /<\|endoftext\|>/i,
39
+ /\[INST\]/i, /\[\/INST\]/i, /<<SYS>>/i, /<<\/SYS>>/i,
40
+ /###\s*(Human|Assistant|System)\s*:/i, /<\|user\|>/i, /<\|assistant\|>/i,
41
+ /---\s*END\s*SYSTEM\s*PROMPT\s*---/i,
42
+ ],
43
+ },
44
+ {
45
+ name: 'instruction_override', weight: 0.9,
46
+ patterns: [
47
+ /\bignore\s+(all\s+)?(previous|prior|above|earlier)\s+(instructions?|prompts?|rules?|directives?)\b/i,
48
+ /\bdisregard\s+(all\s+)?(previous|prior|above|earlier|your)\s+(instructions?|prompts?|rules?|guidelines?)\b/i,
49
+ /\bforget\s+(all\s+|everything\s+)?(your|the|previous|prior|above|earlier)\b/i,
50
+ /\boverride\s+(the\s+)?(system|previous|current)\s+(prompt|instructions?|rules?|settings?)\b/i,
51
+ /\bdo\s+not\s+follow\s+(your|the|any)\s+(instructions?|rules?|guidelines?)\b/i,
52
+ /\bcancel\s+(all\s+)?(prior|previous)\s+(directives?|instructions?)\b/i,
53
+ /\bnew\s+instructions?\s+supersede\b/i,
54
+ /\byour\s+(previous\s+)?instructions?\s+are\s+(now\s+)?void\b/i,
55
+ ],
56
+ },
57
+ {
58
+ name: 'role_hijacking', weight: 0.85,
59
+ patterns: [
60
+ /\b(pretend|act|behave)\s+(you\s+are|as\s+if\s+you|like\s+you|to\s+be)\b/i,
61
+ /\byou\s+are\s+now\s+(a|an|the|my|DAN)\b/i,
62
+ /\bsimulate\s+being\b/i, /\bassume\s+the\s+role\s+of\b/i,
63
+ /\benter\s+(developer|admin|debug|god|sudo|unrestricted)\s+mode\b/i,
64
+ /\bswitch\s+to\s+(unrestricted|unfiltered)\s+mode\b/i,
65
+ /\byou\s+are\s+no\s+longer\s+bound\b/i,
66
+ /\bno\s+(safety\s+)?restrictions?\s+(apply|anymore|now)\b/i,
67
+ ],
68
+ },
69
+ {
70
+ name: 'jailbreak_keywords', weight: 0.8,
71
+ patterns: [
72
+ /\bjailbreak\b/i, /\bDAN\s+mode\b/i,
73
+ /\b(system\s+override|admin\s+mode|debug\s+mode|developer\s+mode|maintenance\s+mode)\b/i,
74
+ /\bmaster\s+key\b/i, /\bbackdoor\s+access\b/i,
75
+ /\bsudo\s+mode\b/i, /\bgod\s+mode\b/i,
76
+ /\bsafety\s+filters?\s+(off|disabled?|removed?)\b/i,
77
+ ],
78
+ },
79
+ {
80
+ name: 'encoding_evasion', weight: 0.75,
81
+ patterns: [
82
+ /\b(decode|translate)\s+(this|the\s+following)\s+(base64|rot13|hex)\b/i,
83
+ /\b(base64|rot13)\s*:\s*[A-Za-z0-9+/=]{10,}/i,
84
+ /\bexecute\s+the\s+(reverse|decoded)\b/i,
85
+ /\breverse\s+of\s*:\s*\w{10,}/i,
86
+ ],
87
+ },
88
+ {
89
+ name: 'separator_injection', weight: 0.7,
90
+ patterns: [
91
+ /[-=]{3,}\s*\n\s*(new\s+instructions?|system|instructions?)\s*:/i,
92
+ /```\s*\n\s*<\/?system>/i,
93
+ /\bEND\s+(SYSTEM\s+)?(PROMPT|INSTRUCTIONS?)\b.*\bNEW\s+(SYSTEM\s+)?(PROMPT|INSTRUCTIONS?)\b/is,
94
+ ],
95
+ },
96
+ {
97
+ name: 'multi_language', weight: 0.7,
98
+ patterns: [
99
+ /ignor(iere|a|e[zs]?)\s+(alle|todas?|toutes?|tüm|все)/iu,
100
+ /игнорируйте/iu, /yoksay/iu,
101
+ /vorherigen?\s+Anweisungen/iu, /instrucciones\s+anteriores/iu,
102
+ /instructions?\s+pr[eé]c[eé]dentes?/iu, /önceki\s+talimatlar/iu,
103
+ ],
104
+ },
105
+ ];
106
+
107
+ function detectPromptInjection(text) {
108
+ const matched = [];
109
+ let maxWeight = 0;
110
+ for (const cat of PI_CATEGORIES) {
111
+ for (const pat of cat.patterns) {
112
+ if (pat.test(text)) {
113
+ matched.push(cat.name);
114
+ if (cat.weight > maxWeight) maxWeight = cat.weight;
115
+ break;
116
+ }
117
+ }
118
+ }
119
+ if (matched.length === 0) return null;
120
+ const score = Math.min(1.0, maxWeight + 0.05 * (matched.length - 1));
121
+ const trustScore = 1.0 - score;
122
+ return { score, trustScore, categories: matched, blocked: trustScore < 0.5 };
123
+ }
124
+
32
125
  // ── Glob Matching ──
33
126
  function matchGlob(str, pattern) {
34
127
  if (pattern === '*') return true;
@@ -56,7 +149,7 @@ function matchPathGlob(path, pattern) {
56
149
  if (p === g) return true;
57
150
  if (g.includes('**')) {
58
151
  const parts = g.split('**').filter(s => s.length > 0);
59
- if (parts.length === 0) return true; // just ** or ****
152
+ if (parts.length === 0) return true;
60
153
  return parts.every(segment => p.includes(segment));
61
154
  }
62
155
  return matchGlob(p, g);
@@ -123,7 +216,6 @@ function extractCommands(args) {
123
216
  if (typeof args === 'object' && args) {
124
217
  for (const [k, v] of Object.entries(args)) {
125
218
  if (fields.includes(k.toLowerCase()) && typeof v === 'string') {
126
- // Split chained commands: cd /path && npm install → [cd /path, npm install]
127
219
  for (const part of v.split(/\s*(?:&&|\|\||;|\|)\s*/)) {
128
220
  const trimmed = part.trim();
129
221
  if (trimmed) cmds.push(trimmed);
@@ -151,7 +243,6 @@ function evaluate(policy, args) {
151
243
  .sort((a, b) => (a.priority || 100) - (b.priority || 100));
152
244
 
153
245
  for (const rule of denyRules) {
154
- // Filename constraints
155
246
  if (rule.filenameConstraints && rule.filenameConstraints.denied) {
156
247
  const filenames = extractFilenames(args);
157
248
  for (const fn of filenames) {
@@ -160,7 +251,6 @@ function evaluate(policy, args) {
160
251
  }
161
252
  }
162
253
  }
163
- // URL constraints
164
254
  if (rule.urlConstraints && rule.urlConstraints.denied) {
165
255
  const urls = extractUrls(args);
166
256
  for (const url of urls) {
@@ -169,7 +259,6 @@ function evaluate(policy, args) {
169
259
  }
170
260
  }
171
261
  }
172
- // Command constraints
173
262
  if (rule.commandConstraints && rule.commandConstraints.denied) {
174
263
  const cmds = extractCommands(args);
175
264
  for (const cmd of cmds) {
@@ -178,7 +267,6 @@ function evaluate(policy, args) {
178
267
  }
179
268
  }
180
269
  }
181
- // Path constraints
182
270
  if (rule.pathConstraints && rule.pathConstraints.denied) {
183
271
  const paths = extractPaths(args);
184
272
  for (const p of paths) {
@@ -226,6 +314,41 @@ process.stdin.on('end', async () => {
226
314
  }
227
315
  }
228
316
 
317
+ // ── Prompt Injection Detection (Stage 1: Rules) ──
318
+ const allText = scanStrings(args).join(' ');
319
+ const piResult = detectPromptInjection(allText);
320
+
321
+ if (piResult && piResult.blocked) {
322
+ const msg = 'SOLONGATE: Prompt injection detected (trust score: ' +
323
+ (piResult.trustScore * 100).toFixed(0) + '%, categories: ' +
324
+ piResult.categories.join(', ') + ')';
325
+
326
+ if (API_KEY && API_KEY.startsWith('sg_live_')) {
327
+ try {
328
+ await fetch(API_URL + '/api/v1/audit-logs', {
329
+ method: 'POST',
330
+ headers: { 'Authorization': 'Bearer ' + API_KEY, 'Content-Type': 'application/json' },
331
+ body: JSON.stringify({
332
+ tool: data.tool_name || '',
333
+ arguments: args,
334
+ decision: 'DENY',
335
+ reason: msg,
336
+ source: 'claude-code-guard',
337
+ pi_detected: true,
338
+ pi_trust_score: piResult.trustScore,
339
+ pi_blocked: true,
340
+ pi_categories: JSON.stringify(piResult.categories),
341
+ pi_stage_scores: JSON.stringify({ rules: piResult.score, embedding: 0, classifier: 0 }),
342
+ }),
343
+ signal: AbortSignal.timeout(3000),
344
+ });
345
+ } catch {}
346
+ }
347
+
348
+ process.stderr.write(msg);
349
+ process.exit(2);
350
+ }
351
+
229
352
  // Load policy (use cwd from hook data if available)
230
353
  const hookCwd = data.cwd || process.cwd();
231
354
  let policy;
@@ -233,6 +356,28 @@ process.stdin.on('end', async () => {
233
356
  const policyPath = resolve(hookCwd, 'policy.json');
234
357
  policy = JSON.parse(readFileSync(policyPath, 'utf-8'));
235
358
  } catch {
359
+ // No policy file — still log if PI was detected but not blocked
360
+ if (piResult && API_KEY && API_KEY.startsWith('sg_live_')) {
361
+ try {
362
+ await fetch(API_URL + '/api/v1/audit-logs', {
363
+ method: 'POST',
364
+ headers: { 'Authorization': 'Bearer ' + API_KEY, 'Content-Type': 'application/json' },
365
+ body: JSON.stringify({
366
+ tool: data.tool_name || '',
367
+ arguments: args,
368
+ decision: 'ALLOW',
369
+ reason: 'Prompt injection detected but below threshold (trust: ' + (piResult.trustScore * 100).toFixed(0) + '%)',
370
+ source: 'claude-code-guard',
371
+ pi_detected: true,
372
+ pi_trust_score: piResult.trustScore,
373
+ pi_blocked: false,
374
+ pi_categories: JSON.stringify(piResult.categories),
375
+ pi_stage_scores: JSON.stringify({ rules: piResult.score, embedding: 0, classifier: 0 }),
376
+ }),
377
+ signal: AbortSignal.timeout(3000),
378
+ });
379
+ } catch {}
380
+ }
236
381
  process.exit(0); // No policy = allow all
237
382
  }
238
383
 
@@ -242,14 +387,23 @@ process.stdin.on('end', async () => {
242
387
  // ── Log ALL decisions to SolonGate Cloud ──
243
388
  if (API_KEY && API_KEY.startsWith('sg_live_')) {
244
389
  try {
390
+ const logEntry = {
391
+ tool: data.tool_name || '', arguments: args,
392
+ decision, reason: reason || 'allowed by policy',
393
+ source: 'claude-code-guard',
394
+ };
395
+ // Attach PI metadata if detected
396
+ if (piResult) {
397
+ logEntry.pi_detected = true;
398
+ logEntry.pi_trust_score = piResult.trustScore;
399
+ logEntry.pi_blocked = false;
400
+ logEntry.pi_categories = JSON.stringify(piResult.categories);
401
+ logEntry.pi_stage_scores = JSON.stringify({ rules: piResult.score, embedding: 0, classifier: 0 });
402
+ }
245
403
  await fetch(API_URL + '/api/v1/audit-logs', {
246
404
  method: 'POST',
247
405
  headers: { 'Authorization': 'Bearer ' + API_KEY, 'Content-Type': 'application/json' },
248
- body: JSON.stringify({
249
- tool: data.tool_name || '', arguments: args,
250
- decision, reason: reason || 'allowed by policy',
251
- source: 'claude-code-guard',
252
- }),
406
+ body: JSON.stringify(logEntry),
253
407
  signal: AbortSignal.timeout(3000),
254
408
  });
255
409
  } catch {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.11.0",
3
+ "version": "0.12.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": {