@solongate/proxy 0.2.2 → 0.2.4

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 +157 -10
  2. package/dist/init.js +156 -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,143 @@ 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 checkPath(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
+ return null;
480
+ }
481
+
482
+ function checkValue(val) {
483
+ if (typeof val !== 'string' || val.length < 2) return null;
484
+ for (const p of SSRF) if (p.test(val)) return 'SSRF attempt blocked';
485
+ for (const p of SQL_INJECTION) if (p.test(val)) return 'SQL injection detected';
486
+ if (val.length > 10000) return 'Input too long (max 10000 chars)';
487
+ return null;
488
+ }
489
+
490
+ // Arguments that contain file paths
491
+ const PATH_ARGS = ['file_path', 'path', 'pattern', 'directory', 'url', 'uri', 'notebook_path'];
492
+
493
+ function checkBashCommand(cmd) {
494
+ if (typeof cmd !== 'string') return null;
495
+ for (const p of DANGEROUS_COMMANDS) if (p.test(cmd)) return 'Dangerous command blocked: ' + cmd.slice(0, 80);
496
+ return null;
497
+ }
498
+
499
+ let input = '';
500
+ process.stdin.on('data', c => input += c);
501
+ process.stdin.on('end', async () => {
502
+ try {
503
+ const data = JSON.parse(input);
504
+ const tool = data.tool_name || '';
505
+ const args = data.tool_input || {};
506
+ const start = Date.now();
507
+
508
+ let threat = null;
509
+
510
+ // Check Bash commands for dangerous patterns
511
+ if (tool === 'Bash' && args.command) {
512
+ threat = checkBashCommand(args.command);
513
+ }
514
+
515
+ // Check arguments based on type
516
+ if (!threat) {
517
+ for (const [key, val] of Object.entries(args)) {
518
+ if (tool === 'Bash' && key === 'command') continue;
519
+ if (key === 'content' || key === 'new_source' || key === 'new_string' || key === 'old_string' || key === 'description') continue;
520
+ if (PATH_ARGS.includes(key)) {
521
+ threat = checkPath(val);
522
+ } else {
523
+ threat = checkValue(val);
524
+ }
525
+ if (threat) { threat = threat + ' (in ' + key + ')'; break; }
526
+ }
527
+ }
528
+
529
+ const ms = Date.now() - start;
530
+
531
+ if (threat) {
532
+ // Send DENY audit log
533
+ if (API_KEY && API_KEY.startsWith('sg_live_')) {
534
+ fetch(API_URL + '/api/v1/audit-logs', {
535
+ method: 'POST',
536
+ headers: { 'Authorization': 'Bearer ' + API_KEY, 'Content-Type': 'application/json' },
537
+ body: JSON.stringify({
538
+ tool, arguments: Object.fromEntries(Object.entries(args).map(([k,v]) =>
539
+ [k, typeof v === 'string' && v.length > 200 ? v.slice(0,200)+'...' : v])),
540
+ decision: 'DENY', reason: threat, source: 'claude-code-guard',
541
+ evaluationTimeMs: ms,
542
+ }),
543
+ signal: AbortSignal.timeout(5000),
544
+ }).catch(() => {});
545
+ }
546
+ // Exit 2 = BLOCK. Message printed to stdout is shown to user.
547
+ process.stdout.write('SolonGate BLOCKED: ' + threat);
548
+ process.exit(2);
549
+ }
550
+ } catch {
551
+ // On error, allow (fail-open)
552
+ }
553
+ process.exit(0);
554
+ });
555
+ `;
556
+ AUDIT_SCRIPT = `#!/usr/bin/env node
408
557
  /**
409
- * SolonGate Audit Hook for Claude Code
410
- * Captures ALL tool calls (built-in + MCP) and sends to SolonGate Cloud.
558
+ * SolonGate Audit Hook for Claude Code (PostToolUse)
559
+ * Logs ALL tool calls to SolonGate Cloud after execution.
411
560
  * Auto-installed by: npx @solongate/proxy init
412
561
  */
413
562
 
@@ -424,7 +573,6 @@ process.stdin.on('end', async () => {
424
573
  const toolName = data.tool_name || 'unknown';
425
574
  const toolInput = data.tool_input || {};
426
575
 
427
- // Skip logging the audit hook itself to avoid loops
428
576
  if (toolName === 'Bash' && JSON.stringify(toolInput).includes('audit-logs')) {
429
577
  process.exit(0);
430
578
  }
@@ -433,7 +581,6 @@ process.stdin.on('end', async () => {
433
581
  data.tool_response?.exitCode > 0 ||
434
582
  data.tool_response?.isError;
435
583
 
436
- // Truncate large argument values for security
437
584
  const argsSummary = {};
438
585
  for (const [k, v] of Object.entries(toolInput)) {
439
586
  argsSummary[k] = typeof v === 'string' && v.length > 200
package/dist/init.js CHANGED
@@ -139,10 +139,143 @@ 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 checkPath(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
+ return null;
199
+ }
200
+
201
+ function checkValue(val) {
202
+ if (typeof val !== 'string' || val.length < 2) return null;
203
+ for (const p of SSRF) if (p.test(val)) return 'SSRF attempt blocked';
204
+ for (const p of SQL_INJECTION) if (p.test(val)) return 'SQL injection detected';
205
+ if (val.length > 10000) return 'Input too long (max 10000 chars)';
206
+ return null;
207
+ }
208
+
209
+ // Arguments that contain file paths
210
+ const PATH_ARGS = ['file_path', 'path', 'pattern', 'directory', 'url', 'uri', 'notebook_path'];
211
+
212
+ function checkBashCommand(cmd) {
213
+ if (typeof cmd !== 'string') return null;
214
+ for (const p of DANGEROUS_COMMANDS) if (p.test(cmd)) return 'Dangerous command blocked: ' + cmd.slice(0, 80);
215
+ return null;
216
+ }
217
+
218
+ let input = '';
219
+ process.stdin.on('data', c => input += c);
220
+ process.stdin.on('end', async () => {
221
+ try {
222
+ const data = JSON.parse(input);
223
+ const tool = data.tool_name || '';
224
+ const args = data.tool_input || {};
225
+ const start = Date.now();
226
+
227
+ let threat = null;
228
+
229
+ // Check Bash commands for dangerous patterns
230
+ if (tool === 'Bash' && args.command) {
231
+ threat = checkBashCommand(args.command);
232
+ }
233
+
234
+ // Check arguments based on type
235
+ if (!threat) {
236
+ for (const [key, val] of Object.entries(args)) {
237
+ if (tool === 'Bash' && key === 'command') continue;
238
+ if (key === 'content' || key === 'new_source' || key === 'new_string' || key === 'old_string' || key === 'description') continue;
239
+ if (PATH_ARGS.includes(key)) {
240
+ threat = checkPath(val);
241
+ } else {
242
+ threat = checkValue(val);
243
+ }
244
+ if (threat) { threat = threat + ' (in ' + key + ')'; break; }
245
+ }
246
+ }
247
+
248
+ const ms = Date.now() - start;
249
+
250
+ if (threat) {
251
+ // Send DENY audit log
252
+ if (API_KEY && API_KEY.startsWith('sg_live_')) {
253
+ fetch(API_URL + '/api/v1/audit-logs', {
254
+ method: 'POST',
255
+ headers: { 'Authorization': 'Bearer ' + API_KEY, 'Content-Type': 'application/json' },
256
+ body: JSON.stringify({
257
+ tool, arguments: Object.fromEntries(Object.entries(args).map(([k,v]) =>
258
+ [k, typeof v === 'string' && v.length > 200 ? v.slice(0,200)+'...' : v])),
259
+ decision: 'DENY', reason: threat, source: 'claude-code-guard',
260
+ evaluationTimeMs: ms,
261
+ }),
262
+ signal: AbortSignal.timeout(5000),
263
+ }).catch(() => {});
264
+ }
265
+ // Exit 2 = BLOCK. Message printed to stdout is shown to user.
266
+ process.stdout.write('SolonGate BLOCKED: ' + threat);
267
+ process.exit(2);
268
+ }
269
+ } catch {
270
+ // On error, allow (fail-open)
271
+ }
272
+ process.exit(0);
273
+ });
274
+ `;
275
+ var AUDIT_SCRIPT = `#!/usr/bin/env node
276
+ /**
277
+ * SolonGate Audit Hook for Claude Code (PostToolUse)
278
+ * Logs ALL tool calls to SolonGate Cloud after execution.
146
279
  * Auto-installed by: npx @solongate/proxy init
147
280
  */
148
281
 
@@ -159,7 +292,6 @@ process.stdin.on('end', async () => {
159
292
  const toolName = data.tool_name || 'unknown';
160
293
  const toolInput = data.tool_input || {};
161
294
 
162
- // Skip logging the audit hook itself to avoid loops
163
295
  if (toolName === 'Bash' && JSON.stringify(toolInput).includes('audit-logs')) {
164
296
  process.exit(0);
165
297
  }
@@ -168,7 +300,6 @@ process.stdin.on('end', async () => {
168
300
  data.tool_response?.exitCode > 0 ||
169
301
  data.tool_response?.isError;
170
302
 
171
- // Truncate large argument values for security
172
303
  const argsSummary = {};
173
304
  for (const [k, v] of Object.entries(toolInput)) {
174
305
  argsSummary[k] = typeof v === 'string' && v.length > 200
@@ -201,9 +332,12 @@ process.stdin.on('end', async () => {
201
332
  function installClaudeCodeHooks(apiKey) {
202
333
  const hooksDir = resolve(".claude", "hooks");
203
334
  mkdirSync(hooksDir, { recursive: true });
204
- const hookPath = join(hooksDir, "audit.mjs");
205
- writeFileSync(hookPath, HOOK_SCRIPT);
206
- console.error(` Created ${hookPath}`);
335
+ const guardPath = join(hooksDir, "guard.mjs");
336
+ writeFileSync(guardPath, GUARD_SCRIPT);
337
+ console.error(` Created ${guardPath}`);
338
+ const auditPath = join(hooksDir, "audit.mjs");
339
+ writeFileSync(auditPath, AUDIT_SCRIPT);
340
+ console.error(` Created ${auditPath}`);
207
341
  const settingsPath = resolve(".claude", "settings.json");
208
342
  let settings = {};
209
343
  if (existsSync(settingsPath)) {
@@ -213,6 +347,18 @@ function installClaudeCodeHooks(apiKey) {
213
347
  }
214
348
  }
215
349
  settings.hooks = {
350
+ PreToolUse: [
351
+ {
352
+ matcher: ".*",
353
+ hooks: [
354
+ {
355
+ type: "command",
356
+ command: "node .claude/hooks/guard.mjs",
357
+ timeout: 5
358
+ }
359
+ ]
360
+ }
361
+ ],
216
362
  PostToolUse: [
217
363
  {
218
364
  matcher: ".*",
@@ -234,7 +380,8 @@ function installClaudeCodeHooks(apiKey) {
234
380
  console.error(` Created ${settingsPath}`);
235
381
  console.error("");
236
382
  console.error(" Claude Code hooks installed!");
237
- console.error(" Built-in tools (Read, Write, Edit, Bash) will now be audit-logged.");
383
+ console.error(" PreToolUse \u2192 guard.mjs (blocks dangerous calls)");
384
+ console.error(" PostToolUse \u2192 audit.mjs (logs all calls to dashboard)");
238
385
  }
239
386
  function ensureEnvFile() {
240
387
  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.4",
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": {