@solongate/proxy 0.2.2 → 0.2.3

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 (3) hide show
  1. package/dist/index.js +153 -10
  2. package/dist/init.js +152 -9
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -140,9 +140,12 @@ POLICY PRESETS
140
140
  function installClaudeCodeHooks(apiKey) {
141
141
  const hooksDir = resolve2(".claude", "hooks");
142
142
  mkdirSync(hooksDir, { recursive: true });
143
- const hookPath = join(hooksDir, "audit.mjs");
144
- writeFileSync(hookPath, HOOK_SCRIPT);
145
- console.error(` Created ${hookPath}`);
143
+ const guardPath = join(hooksDir, "guard.mjs");
144
+ writeFileSync(guardPath, GUARD_SCRIPT);
145
+ console.error(` Created ${guardPath}`);
146
+ const auditPath = join(hooksDir, "audit.mjs");
147
+ writeFileSync(auditPath, AUDIT_SCRIPT);
148
+ console.error(` Created ${auditPath}`);
146
149
  const settingsPath = resolve2(".claude", "settings.json");
147
150
  let settings = {};
148
151
  if (existsSync2(settingsPath)) {
@@ -152,6 +155,18 @@ function installClaudeCodeHooks(apiKey) {
152
155
  }
153
156
  }
154
157
  settings.hooks = {
158
+ PreToolUse: [
159
+ {
160
+ matcher: ".*",
161
+ hooks: [
162
+ {
163
+ type: "command",
164
+ command: "node .claude/hooks/guard.mjs",
165
+ timeout: 5
166
+ }
167
+ ]
168
+ }
169
+ ],
155
170
  PostToolUse: [
156
171
  {
157
172
  matcher: ".*",
@@ -173,7 +188,8 @@ function installClaudeCodeHooks(apiKey) {
173
188
  console.error(` Created ${settingsPath}`);
174
189
  console.error("");
175
190
  console.error(" Claude Code hooks installed!");
176
- console.error(" Built-in tools (Read, Write, Edit, Bash) will now be audit-logged.");
191
+ console.error(" PreToolUse \u2192 guard.mjs (blocks dangerous calls)");
192
+ console.error(" PostToolUse \u2192 audit.mjs (logs all calls to dashboard)");
177
193
  }
178
194
  function ensureEnvFile() {
179
195
  const envPath = resolve2(".env");
@@ -393,7 +409,7 @@ async function main() {
393
409
  console.error(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
394
410
  console.error("");
395
411
  }
396
- var POLICY_PRESETS, SEARCH_PATHS, CLAUDE_DESKTOP_PATHS, HOOK_SCRIPT;
412
+ var POLICY_PRESETS, SEARCH_PATHS, CLAUDE_DESKTOP_PATHS, GUARD_SCRIPT, AUDIT_SCRIPT;
397
413
  var init_init = __esm({
398
414
  "src/init.ts"() {
399
415
  "use strict";
@@ -404,10 +420,139 @@ var init_init = __esm({
404
420
  ".claude/mcp.json"
405
421
  ];
406
422
  CLAUDE_DESKTOP_PATHS = process.platform === "win32" ? [join(process.env["APPDATA"] ?? "", "Claude", "claude_desktop_config.json")] : process.platform === "darwin" ? [join(process.env["HOME"] ?? "", "Library", "Application Support", "Claude", "claude_desktop_config.json")] : [join(process.env["HOME"] ?? "", ".config", "claude", "claude_desktop_config.json")];
407
- HOOK_SCRIPT = `#!/usr/bin/env node
423
+ GUARD_SCRIPT = `#!/usr/bin/env node
424
+ /**
425
+ * SolonGate Guard Hook for Claude Code (PreToolUse)
426
+ * Blocks dangerous tool calls BEFORE they execute.
427
+ * Exit code 2 = BLOCK, exit code 0 = ALLOW.
428
+ * Auto-installed by: npx @solongate/proxy init
429
+ */
430
+
431
+ const API_KEY = process.env.SOLONGATE_API_KEY || '';
432
+ const API_URL = process.env.SOLONGATE_API_URL || 'https://api.solongate.com';
433
+
434
+ // \u2500\u2500 Input Guard Patterns \u2500\u2500
435
+ const PATH_TRAVERSAL = [
436
+ /\\.\\.\\//, /\\\\.\\.\\\\\\\\/, /%2e%2e/i, /%2e\\./i, /\\.%2e/i, /%252e%252e/i,
437
+ ];
438
+ const SENSITIVE_PATHS = [
439
+ /\\/etc\\/passwd/i, /\\/etc\\/shadow/i, /\\/proc\\//i,
440
+ /c:\\\\windows\\\\system32/i, /\\.env(\\.|$)/i,
441
+ /\\.aws\\/credentials/i, /\\.ssh\\/id_/i, /\\.kube\\/config/i,
442
+ /\\.git\\/config/i, /\\.npmrc/i, /\\.pypirc/i,
443
+ ];
444
+ const SHELL_INJECTION = [
445
+ /\\$\\(/, /\\$\\{/, /\\\`/, /\\beval\\b/i, /\\bexec\\b/i, /\\bsystem\\b/i,
446
+ /%0a/i, /%0d/i,
447
+ ];
448
+ const SSRF = [
449
+ /^https?:\\/\\/localhost\\b/i, /^https?:\\/\\/127\\./, /^https?:\\/\\/0\\.0\\.0\\.0/,
450
+ /^https?:\\/\\/10\\./, /^https?:\\/\\/172\\.(1[6-9]|2\\d|3[01])\\./,
451
+ /^https?:\\/\\/192\\.168\\./, /^https?:\\/\\/169\\.254\\./,
452
+ /metadata\\.google\\.internal/i,
453
+ ];
454
+ const SQL_INJECTION = [
455
+ /'\\s{0,20}(OR|AND)\\s{0,20}'.{0,200}'/i,
456
+ /'\\s{0,10};\\s{0,10}(DROP|DELETE|UPDATE|INSERT|ALTER|CREATE|EXEC)/i,
457
+ /UNION\\s+(ALL\\s+)?SELECT/i, /\\bSLEEP\\s*\\(/i, /\\bWAITFOR\\s+DELAY/i,
458
+ ];
459
+
460
+ // Dangerous command patterns for Bash tool
461
+ const DANGEROUS_COMMANDS = [
462
+ /\\brm\\s+(-[a-zA-Z]*f|-[a-zA-Z]*r|--force|--recursive)/i,
463
+ /\\brm\\s+-rf\\b/i,
464
+ /\\bmkfs\\b/i, /\\bdd\\s+if=/i,
465
+ /\\b(shutdown|reboot|halt|poweroff)\\b/i,
466
+ /\\bchmod\\s+777\\b/,
467
+ /\\bcurl\\b.{0,200}\\|\\s*(sh|bash)\\b/i,
468
+ /\\bwget\\b.{0,200}\\|\\s*(sh|bash)\\b/i,
469
+ /\\bnc\\s+-[a-z]*l/i, // netcat listener
470
+ />(\\s*)\\/dev\\/sd/, // writing to raw disk
471
+ /\\bgit\\s+push\\s+.*--force\\b/i,
472
+ /\\bgit\\s+reset\\s+--hard\\b/i,
473
+ ];
474
+
475
+ function checkValue(val) {
476
+ if (typeof val !== 'string' || val.length < 2) return null;
477
+ for (const p of PATH_TRAVERSAL) if (p.test(val)) return 'Path traversal detected';
478
+ for (const p of SENSITIVE_PATHS) if (p.test(val)) return 'Sensitive file access blocked';
479
+ for (const p of SSRF) if (p.test(val)) return 'SSRF attempt blocked';
480
+ for (const p of SQL_INJECTION) if (p.test(val)) return 'SQL injection detected';
481
+ if (val.length > 10000) return 'Input too long (max 10000 chars)';
482
+ return null;
483
+ }
484
+
485
+ function checkBashCommand(cmd) {
486
+ if (typeof cmd !== 'string') return null;
487
+ for (const p of DANGEROUS_COMMANDS) if (p.test(cmd)) return 'Dangerous command blocked: ' + cmd.slice(0, 80);
488
+ for (const p of SHELL_INJECTION) if (p.test(cmd)) return null; // shell injection is normal for Bash
489
+ return null;
490
+ }
491
+
492
+ let input = '';
493
+ process.stdin.on('data', c => input += c);
494
+ process.stdin.on('end', async () => {
495
+ try {
496
+ const data = JSON.parse(input);
497
+ const tool = data.tool_name || '';
498
+ const args = data.tool_input || {};
499
+ const start = Date.now();
500
+
501
+ let threat = null;
502
+
503
+ // Check Bash commands for dangerous patterns
504
+ if (tool === 'Bash' && args.command) {
505
+ threat = checkBashCommand(args.command);
506
+ }
507
+
508
+ // Check all string arguments for injection patterns
509
+ if (!threat) {
510
+ for (const [key, val] of Object.entries(args)) {
511
+ if (tool === 'Bash' && key === 'command') continue; // already checked
512
+ threat = checkValue(val);
513
+ if (threat) { threat = threat + ' (in ' + key + ')'; break; }
514
+ // Check nested strings
515
+ if (typeof val === 'object' && val) {
516
+ for (const v of Object.values(val)) {
517
+ threat = checkValue(v);
518
+ if (threat) { threat = threat + ' (in ' + key + ')'; break; }
519
+ }
520
+ if (threat) break;
521
+ }
522
+ }
523
+ }
524
+
525
+ const ms = Date.now() - start;
526
+
527
+ if (threat) {
528
+ // Send DENY audit log
529
+ if (API_KEY && API_KEY.startsWith('sg_live_')) {
530
+ fetch(API_URL + '/api/v1/audit-logs', {
531
+ method: 'POST',
532
+ headers: { 'Authorization': 'Bearer ' + API_KEY, 'Content-Type': 'application/json' },
533
+ body: JSON.stringify({
534
+ tool, arguments: Object.fromEntries(Object.entries(args).map(([k,v]) =>
535
+ [k, typeof v === 'string' && v.length > 200 ? v.slice(0,200)+'...' : v])),
536
+ decision: 'DENY', reason: threat, source: 'claude-code-guard',
537
+ evaluationTimeMs: ms,
538
+ }),
539
+ signal: AbortSignal.timeout(5000),
540
+ }).catch(() => {});
541
+ }
542
+ // Exit 2 = BLOCK. Message printed to stdout is shown to user.
543
+ process.stdout.write('SolonGate BLOCKED: ' + threat);
544
+ process.exit(2);
545
+ }
546
+ } catch {
547
+ // On error, allow (fail-open)
548
+ }
549
+ process.exit(0);
550
+ });
551
+ `;
552
+ AUDIT_SCRIPT = `#!/usr/bin/env node
408
553
  /**
409
- * SolonGate Audit Hook for Claude Code
410
- * Captures ALL tool calls (built-in + MCP) and sends to SolonGate Cloud.
554
+ * SolonGate Audit Hook for Claude Code (PostToolUse)
555
+ * Logs ALL tool calls to SolonGate Cloud after execution.
411
556
  * Auto-installed by: npx @solongate/proxy init
412
557
  */
413
558
 
@@ -424,7 +569,6 @@ process.stdin.on('end', async () => {
424
569
  const toolName = data.tool_name || 'unknown';
425
570
  const toolInput = data.tool_input || {};
426
571
 
427
- // Skip logging the audit hook itself to avoid loops
428
572
  if (toolName === 'Bash' && JSON.stringify(toolInput).includes('audit-logs')) {
429
573
  process.exit(0);
430
574
  }
@@ -433,7 +577,6 @@ process.stdin.on('end', async () => {
433
577
  data.tool_response?.exitCode > 0 ||
434
578
  data.tool_response?.isError;
435
579
 
436
- // Truncate large argument values for security
437
580
  const argsSummary = {};
438
581
  for (const [k, v] of Object.entries(toolInput)) {
439
582
  argsSummary[k] = typeof v === 'string' && v.length > 200
package/dist/init.js CHANGED
@@ -139,10 +139,139 @@ POLICY PRESETS
139
139
  `;
140
140
  console.error(help);
141
141
  }
142
- var HOOK_SCRIPT = `#!/usr/bin/env node
142
+ var GUARD_SCRIPT = `#!/usr/bin/env node
143
143
  /**
144
- * SolonGate Audit Hook for Claude Code
145
- * Captures ALL tool calls (built-in + MCP) and sends to SolonGate Cloud.
144
+ * SolonGate Guard Hook for Claude Code (PreToolUse)
145
+ * Blocks dangerous tool calls BEFORE they execute.
146
+ * Exit code 2 = BLOCK, exit code 0 = ALLOW.
147
+ * Auto-installed by: npx @solongate/proxy init
148
+ */
149
+
150
+ const API_KEY = process.env.SOLONGATE_API_KEY || '';
151
+ const API_URL = process.env.SOLONGATE_API_URL || 'https://api.solongate.com';
152
+
153
+ // \u2500\u2500 Input Guard Patterns \u2500\u2500
154
+ const PATH_TRAVERSAL = [
155
+ /\\.\\.\\//, /\\\\.\\.\\\\\\\\/, /%2e%2e/i, /%2e\\./i, /\\.%2e/i, /%252e%252e/i,
156
+ ];
157
+ const SENSITIVE_PATHS = [
158
+ /\\/etc\\/passwd/i, /\\/etc\\/shadow/i, /\\/proc\\//i,
159
+ /c:\\\\windows\\\\system32/i, /\\.env(\\.|$)/i,
160
+ /\\.aws\\/credentials/i, /\\.ssh\\/id_/i, /\\.kube\\/config/i,
161
+ /\\.git\\/config/i, /\\.npmrc/i, /\\.pypirc/i,
162
+ ];
163
+ const SHELL_INJECTION = [
164
+ /\\$\\(/, /\\$\\{/, /\\\`/, /\\beval\\b/i, /\\bexec\\b/i, /\\bsystem\\b/i,
165
+ /%0a/i, /%0d/i,
166
+ ];
167
+ const SSRF = [
168
+ /^https?:\\/\\/localhost\\b/i, /^https?:\\/\\/127\\./, /^https?:\\/\\/0\\.0\\.0\\.0/,
169
+ /^https?:\\/\\/10\\./, /^https?:\\/\\/172\\.(1[6-9]|2\\d|3[01])\\./,
170
+ /^https?:\\/\\/192\\.168\\./, /^https?:\\/\\/169\\.254\\./,
171
+ /metadata\\.google\\.internal/i,
172
+ ];
173
+ const SQL_INJECTION = [
174
+ /'\\s{0,20}(OR|AND)\\s{0,20}'.{0,200}'/i,
175
+ /'\\s{0,10};\\s{0,10}(DROP|DELETE|UPDATE|INSERT|ALTER|CREATE|EXEC)/i,
176
+ /UNION\\s+(ALL\\s+)?SELECT/i, /\\bSLEEP\\s*\\(/i, /\\bWAITFOR\\s+DELAY/i,
177
+ ];
178
+
179
+ // Dangerous command patterns for Bash tool
180
+ const DANGEROUS_COMMANDS = [
181
+ /\\brm\\s+(-[a-zA-Z]*f|-[a-zA-Z]*r|--force|--recursive)/i,
182
+ /\\brm\\s+-rf\\b/i,
183
+ /\\bmkfs\\b/i, /\\bdd\\s+if=/i,
184
+ /\\b(shutdown|reboot|halt|poweroff)\\b/i,
185
+ /\\bchmod\\s+777\\b/,
186
+ /\\bcurl\\b.{0,200}\\|\\s*(sh|bash)\\b/i,
187
+ /\\bwget\\b.{0,200}\\|\\s*(sh|bash)\\b/i,
188
+ /\\bnc\\s+-[a-z]*l/i, // netcat listener
189
+ />(\\s*)\\/dev\\/sd/, // writing to raw disk
190
+ /\\bgit\\s+push\\s+.*--force\\b/i,
191
+ /\\bgit\\s+reset\\s+--hard\\b/i,
192
+ ];
193
+
194
+ function checkValue(val) {
195
+ if (typeof val !== 'string' || val.length < 2) return null;
196
+ for (const p of PATH_TRAVERSAL) if (p.test(val)) return 'Path traversal detected';
197
+ for (const p of SENSITIVE_PATHS) if (p.test(val)) return 'Sensitive file access blocked';
198
+ for (const p of SSRF) if (p.test(val)) return 'SSRF attempt blocked';
199
+ for (const p of SQL_INJECTION) if (p.test(val)) return 'SQL injection detected';
200
+ if (val.length > 10000) return 'Input too long (max 10000 chars)';
201
+ return null;
202
+ }
203
+
204
+ function checkBashCommand(cmd) {
205
+ if (typeof cmd !== 'string') return null;
206
+ for (const p of DANGEROUS_COMMANDS) if (p.test(cmd)) return 'Dangerous command blocked: ' + cmd.slice(0, 80);
207
+ for (const p of SHELL_INJECTION) if (p.test(cmd)) return null; // shell injection is normal for Bash
208
+ return null;
209
+ }
210
+
211
+ let input = '';
212
+ process.stdin.on('data', c => input += c);
213
+ process.stdin.on('end', async () => {
214
+ try {
215
+ const data = JSON.parse(input);
216
+ const tool = data.tool_name || '';
217
+ const args = data.tool_input || {};
218
+ const start = Date.now();
219
+
220
+ let threat = null;
221
+
222
+ // Check Bash commands for dangerous patterns
223
+ if (tool === 'Bash' && args.command) {
224
+ threat = checkBashCommand(args.command);
225
+ }
226
+
227
+ // Check all string arguments for injection patterns
228
+ if (!threat) {
229
+ for (const [key, val] of Object.entries(args)) {
230
+ if (tool === 'Bash' && key === 'command') continue; // already checked
231
+ threat = checkValue(val);
232
+ if (threat) { threat = threat + ' (in ' + key + ')'; break; }
233
+ // Check nested strings
234
+ if (typeof val === 'object' && val) {
235
+ for (const v of Object.values(val)) {
236
+ threat = checkValue(v);
237
+ if (threat) { threat = threat + ' (in ' + key + ')'; break; }
238
+ }
239
+ if (threat) break;
240
+ }
241
+ }
242
+ }
243
+
244
+ const ms = Date.now() - start;
245
+
246
+ if (threat) {
247
+ // Send DENY audit log
248
+ if (API_KEY && API_KEY.startsWith('sg_live_')) {
249
+ fetch(API_URL + '/api/v1/audit-logs', {
250
+ method: 'POST',
251
+ headers: { 'Authorization': 'Bearer ' + API_KEY, 'Content-Type': 'application/json' },
252
+ body: JSON.stringify({
253
+ tool, arguments: Object.fromEntries(Object.entries(args).map(([k,v]) =>
254
+ [k, typeof v === 'string' && v.length > 200 ? v.slice(0,200)+'...' : v])),
255
+ decision: 'DENY', reason: threat, source: 'claude-code-guard',
256
+ evaluationTimeMs: ms,
257
+ }),
258
+ signal: AbortSignal.timeout(5000),
259
+ }).catch(() => {});
260
+ }
261
+ // Exit 2 = BLOCK. Message printed to stdout is shown to user.
262
+ process.stdout.write('SolonGate BLOCKED: ' + threat);
263
+ process.exit(2);
264
+ }
265
+ } catch {
266
+ // On error, allow (fail-open)
267
+ }
268
+ process.exit(0);
269
+ });
270
+ `;
271
+ var AUDIT_SCRIPT = `#!/usr/bin/env node
272
+ /**
273
+ * SolonGate Audit Hook for Claude Code (PostToolUse)
274
+ * Logs ALL tool calls to SolonGate Cloud after execution.
146
275
  * Auto-installed by: npx @solongate/proxy init
147
276
  */
148
277
 
@@ -159,7 +288,6 @@ process.stdin.on('end', async () => {
159
288
  const toolName = data.tool_name || 'unknown';
160
289
  const toolInput = data.tool_input || {};
161
290
 
162
- // Skip logging the audit hook itself to avoid loops
163
291
  if (toolName === 'Bash' && JSON.stringify(toolInput).includes('audit-logs')) {
164
292
  process.exit(0);
165
293
  }
@@ -168,7 +296,6 @@ process.stdin.on('end', async () => {
168
296
  data.tool_response?.exitCode > 0 ||
169
297
  data.tool_response?.isError;
170
298
 
171
- // Truncate large argument values for security
172
299
  const argsSummary = {};
173
300
  for (const [k, v] of Object.entries(toolInput)) {
174
301
  argsSummary[k] = typeof v === 'string' && v.length > 200
@@ -201,9 +328,12 @@ process.stdin.on('end', async () => {
201
328
  function installClaudeCodeHooks(apiKey) {
202
329
  const hooksDir = resolve(".claude", "hooks");
203
330
  mkdirSync(hooksDir, { recursive: true });
204
- const hookPath = join(hooksDir, "audit.mjs");
205
- writeFileSync(hookPath, HOOK_SCRIPT);
206
- console.error(` Created ${hookPath}`);
331
+ const guardPath = join(hooksDir, "guard.mjs");
332
+ writeFileSync(guardPath, GUARD_SCRIPT);
333
+ console.error(` Created ${guardPath}`);
334
+ const auditPath = join(hooksDir, "audit.mjs");
335
+ writeFileSync(auditPath, AUDIT_SCRIPT);
336
+ console.error(` Created ${auditPath}`);
207
337
  const settingsPath = resolve(".claude", "settings.json");
208
338
  let settings = {};
209
339
  if (existsSync(settingsPath)) {
@@ -213,6 +343,18 @@ function installClaudeCodeHooks(apiKey) {
213
343
  }
214
344
  }
215
345
  settings.hooks = {
346
+ PreToolUse: [
347
+ {
348
+ matcher: ".*",
349
+ hooks: [
350
+ {
351
+ type: "command",
352
+ command: "node .claude/hooks/guard.mjs",
353
+ timeout: 5
354
+ }
355
+ ]
356
+ }
357
+ ],
216
358
  PostToolUse: [
217
359
  {
218
360
  matcher: ".*",
@@ -234,7 +376,8 @@ function installClaudeCodeHooks(apiKey) {
234
376
  console.error(` Created ${settingsPath}`);
235
377
  console.error("");
236
378
  console.error(" Claude Code hooks installed!");
237
- console.error(" Built-in tools (Read, Write, Edit, Bash) will now be audit-logged.");
379
+ console.error(" PreToolUse \u2192 guard.mjs (blocks dangerous calls)");
380
+ console.error(" PostToolUse \u2192 audit.mjs (logs all calls to dashboard)");
238
381
  }
239
382
  function ensureEnvFile() {
240
383
  const envPath = resolve(".env");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "MCP security proxy \u00e2\u20ac\u201d protect any MCP server with policies, input validation, rate limiting, and audit logging. Zero code changes required.",
5
5
  "type": "module",
6
6
  "bin": {