@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.
- package/hooks/guard.mjs +165 -11
- 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;
|
|
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.
|
|
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": {
|