@solongate/proxy 0.15.5 → 0.16.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/dist/index.js CHANGED
@@ -306,6 +306,7 @@ var init_exports = {};
306
306
  import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
307
307
  import { resolve as resolve2, join, dirname as dirname2 } from "path";
308
308
  import { fileURLToPath } from "url";
309
+ import { execSync } from "child_process";
309
310
  import { createInterface } from "readline";
310
311
  function findConfigFile(explicitPath, createIfMissing = false) {
311
312
  if (explicitPath) {
@@ -514,10 +515,37 @@ function installHooks(selectedTools = []) {
514
515
  console.log(` Created ${settingsPath}`);
515
516
  activatedNames.push(client.name);
516
517
  }
518
+ const protectedDirs = [".solongate", ...clients.map((c3) => c3.dir)];
519
+ try {
520
+ if (process.platform === "win32") {
521
+ for (const dir of protectedDirs) {
522
+ const fullDir = resolve2(dir);
523
+ if (existsSync3(fullDir)) {
524
+ try {
525
+ execSync(`attrib +R /S /D "${fullDir}"`, { stdio: "ignore" });
526
+ } catch {
527
+ }
528
+ }
529
+ }
530
+ } else {
531
+ for (const dir of protectedDirs) {
532
+ const fullDir = resolve2(dir);
533
+ if (existsSync3(fullDir)) {
534
+ try {
535
+ execSync(`chmod -R a-w "${fullDir}"`, { stdio: "ignore" });
536
+ } catch {
537
+ }
538
+ }
539
+ }
540
+ }
541
+ console.log(" OS-level read-only protection applied");
542
+ } catch {
543
+ }
517
544
  console.log("");
518
545
  console.log(" Hooks installed:");
519
546
  console.log(" guard.mjs \u2192 blocks policy-violating calls (pre-execution)");
520
547
  console.log(" audit.mjs \u2192 logs all calls to dashboard (post-execution)");
548
+ console.log(" File system \u2192 read-only (OS-level protection)");
521
549
  console.log(` Activated for: ${activatedNames.join(", ")}`);
522
550
  }
523
551
  function ensureEnvFile() {
@@ -804,7 +832,7 @@ var init_init = __esm({
804
832
  var inject_exports = {};
805
833
  import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync4, copyFileSync } from "fs";
806
834
  import { resolve as resolve3 } from "path";
807
- import { execSync } from "child_process";
835
+ import { execSync as execSync2 } from "child_process";
808
836
  function parseInjectArgs(argv) {
809
837
  const args = argv.slice(2);
810
838
  const opts = {
@@ -951,7 +979,7 @@ function installSdk() {
951
979
  const cmd = pm === "yarn" ? "yarn add @solongate/sdk" : `${pm} install @solongate/sdk`;
952
980
  log3(` Installing @solongate/sdk via ${pm}...`);
953
981
  try {
954
- execSync(cmd, { stdio: "pipe", cwd: process.cwd() });
982
+ execSync2(cmd, { stdio: "pipe", cwd: process.cwd() });
955
983
  return true;
956
984
  } catch (err) {
957
985
  log3(` Failed to install: ${err instanceof Error ? err.message : String(err)}`);
@@ -1176,7 +1204,7 @@ var init_inject = __esm({
1176
1204
  var create_exports = {};
1177
1205
  import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4, existsSync as existsSync5 } from "fs";
1178
1206
  import { resolve as resolve4, join as join2 } from "path";
1179
- import { execSync as execSync2 } from "child_process";
1207
+ import { execSync as execSync3 } from "child_process";
1180
1208
  function log4(msg) {
1181
1209
  process.stderr.write(msg + "\n");
1182
1210
  }
@@ -1420,7 +1448,7 @@ async function main3() {
1420
1448
  });
1421
1449
  if (!opts.noInstall) {
1422
1450
  withSpinner("Installing dependencies...", () => {
1423
- execSync2("npm install", { cwd: dir, stdio: "pipe" });
1451
+ execSync3("npm install", { cwd: dir, stdio: "pipe" });
1424
1452
  });
1425
1453
  }
1426
1454
  log4("");
package/dist/init.js CHANGED
@@ -4,6 +4,7 @@
4
4
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
5
5
  import { resolve, join, dirname } from "path";
6
6
  import { fileURLToPath } from "url";
7
+ import { execSync } from "child_process";
7
8
  import { createInterface } from "readline";
8
9
  var SEARCH_PATHS = [
9
10
  ".mcp.json",
@@ -221,10 +222,37 @@ function installHooks(selectedTools = []) {
221
222
  console.log(` Created ${settingsPath}`);
222
223
  activatedNames.push(client.name);
223
224
  }
225
+ const protectedDirs = [".solongate", ...clients.map((c) => c.dir)];
226
+ try {
227
+ if (process.platform === "win32") {
228
+ for (const dir of protectedDirs) {
229
+ const fullDir = resolve(dir);
230
+ if (existsSync(fullDir)) {
231
+ try {
232
+ execSync(`attrib +R /S /D "${fullDir}"`, { stdio: "ignore" });
233
+ } catch {
234
+ }
235
+ }
236
+ }
237
+ } else {
238
+ for (const dir of protectedDirs) {
239
+ const fullDir = resolve(dir);
240
+ if (existsSync(fullDir)) {
241
+ try {
242
+ execSync(`chmod -R a-w "${fullDir}"`, { stdio: "ignore" });
243
+ } catch {
244
+ }
245
+ }
246
+ }
247
+ }
248
+ console.log(" OS-level read-only protection applied");
249
+ } catch {
250
+ }
224
251
  console.log("");
225
252
  console.log(" Hooks installed:");
226
253
  console.log(" guard.mjs \u2192 blocks policy-violating calls (pre-execution)");
227
254
  console.log(" audit.mjs \u2192 logs all calls to dashboard (post-execution)");
255
+ console.log(" File system \u2192 read-only (OS-level protection)");
228
256
  console.log(` Activated for: ${activatedNames.join(", ")}`);
229
257
  }
230
258
  function ensureEnvFile() {
package/hooks/guard.mjs CHANGED
@@ -296,21 +296,93 @@ process.stdin.on('end', async () => {
296
296
  'policy.json', '.mcp.json',
297
297
  ];
298
298
 
299
- // Strip shell quotes/escapes: .sol'on'gate .solongate, .sol"on"gate .solongate
299
+ // Block helper logs to cloud + exits with code 2
300
+ async function blockSelfProtection(reason) {
301
+ if (API_KEY && API_KEY.startsWith('sg_live_')) {
302
+ try {
303
+ await fetch(API_URL + '/api/v1/audit-logs', {
304
+ method: 'POST',
305
+ headers: { 'Authorization': 'Bearer ' + API_KEY, 'Content-Type': 'application/json' },
306
+ body: JSON.stringify({
307
+ tool: data.tool_name || '', arguments: args,
308
+ decision: 'DENY', reason,
309
+ source: 'claude-code-guard',
310
+ }),
311
+ signal: AbortSignal.timeout(3000),
312
+ });
313
+ } catch {}
314
+ }
315
+ process.stderr.write(reason);
316
+ process.exit(2);
317
+ }
318
+
319
+ // ── Normalization layers ──
320
+
321
+ // 1. Decode ANSI-C quoting: $'\x72' → r, $'\162' → r, $'\n' → newline
322
+ function decodeAnsiC(s) {
323
+ return s.replace(/\$'([^']*)'/g, (_, content) => {
324
+ return content
325
+ .replace(/\\x([0-9a-fA-F]{2})/g, (__, hex) => String.fromCharCode(parseInt(hex, 16)))
326
+ .replace(/\\([0-7]{1,3})/g, (__, oct) => String.fromCharCode(parseInt(oct, 8)))
327
+ .replace(/\\u([0-9a-fA-F]{4})/g, (__, hex) => String.fromCharCode(parseInt(hex, 16)))
328
+ .replace(/\\n/g, '\n').replace(/\\t/g, '\t').replace(/\\r/g, '\r')
329
+ .replace(/\\(.)/g, '$1');
330
+ });
331
+ }
332
+
333
+ // 2. Strip all shell quoting: empty quotes, single, double, backslash escapes
300
334
  function stripShellQuotes(s) {
301
- return s.replace(/\\(.)/g, '$1').replace(/'/g, '').replace(/"/g, '');
335
+ let r = s;
336
+ r = r.replace(/""/g, ''); // empty double quotes
337
+ r = r.replace(/''/g, ''); // empty single quotes
338
+ r = r.replace(/\\(.)/g, '$1'); // backslash escapes
339
+ r = r.replace(/'/g, ''); // remaining single quotes
340
+ r = r.replace(/"/g, ''); // remaining double quotes
341
+ return r;
342
+ }
343
+
344
+ // 3. Full normalization pipeline
345
+ function normalizeShell(s) {
346
+ return stripShellQuotes(decodeAnsiC(s));
347
+ }
348
+
349
+ // 4. Extract inner commands from eval, bash -c, sh -c, pipe to bash/sh
350
+ function extractInnerCommands(s) {
351
+ const inner = [];
352
+ // eval "cmd" or eval 'cmd' or eval cmd
353
+ for (const m of s.matchAll(/\beval\s+["']([^"']+)["']/gi)) inner.push(m[1]);
354
+ for (const m of s.matchAll(/\beval\s+([^;"'|&]+)/gi)) inner.push(m[1]);
355
+ // bash -c "cmd" or sh -c "cmd"
356
+ for (const m of s.matchAll(/\b(?:bash|sh)\s+-c\s+["']([^"']+)["']/gi)) inner.push(m[1]);
357
+ // echo "cmd" | bash/sh or printf "cmd" | bash/sh
358
+ for (const m of s.matchAll(/(?:echo|printf)\s+["']([^"']+)["']\s*\|\s*(?:bash|sh)\b/gi)) inner.push(m[1]);
359
+ // find ... -name "pattern" ... -exec ...
360
+ for (const m of s.matchAll(/-name\s+["']?([^\s"']+)["']?/gi)) inner.push(m[1]);
361
+ return inner;
362
+ }
363
+
364
+ // 5. Check variable assignments for protected path fragments
365
+ // e.g. X=".solon" && rm -rf ${X}gate → detects ".solon" as prefix of ".solongate"
366
+ function checkVarAssignments(s) {
367
+ const assignments = [...s.matchAll(/(\w+)=["']?([^"'\s&|;]+)["']?/g)];
368
+ for (const [, , value] of assignments) {
369
+ const v = value.toLowerCase();
370
+ if (v.length < 3) continue; // avoid false positives
371
+ for (const p of protectedPaths) {
372
+ if (p.startsWith(v) || p.includes(v)) return p;
373
+ }
374
+ }
375
+ return null;
302
376
  }
303
377
 
304
- // Check if a glob/wildcard pattern could match any protected path
305
- // e.g. ".antig*" matches ".antigravity", "/path/.sol*" matches ".solongate"
378
+ // 6. Check if a glob/wildcard could match any protected path
306
379
  function globMatchesProtected(s) {
307
380
  if (!s.includes('*') && !s.includes('?')) return null;
308
- // Extract all path segments and the full string to test
309
381
  const segments = s.split('/').filter(Boolean);
310
382
  const candidates = [s, ...segments];
311
383
  for (const candidate of candidates) {
312
384
  if (!candidate.includes('*') && !candidate.includes('?')) continue;
313
- // Simple prefix match: ".antig*" → prefix ".antig", check if any protected path starts with it
385
+ // Prefix match: ".antig*" → prefix ".antig"
314
386
  const starIdx = candidate.indexOf('*');
315
387
  const qIdx = candidate.indexOf('?');
316
388
  const firstWild = starIdx === -1 ? qIdx : qIdx === -1 ? starIdx : Math.min(starIdx, qIdx);
@@ -320,79 +392,125 @@ process.stdin.on('end', async () => {
320
392
  if (p.startsWith(prefix)) return p;
321
393
  }
322
394
  }
323
- // Also try regex for complex patterns like ".cl?ude"
395
+ // Regex match for patterns like ".cl?ude"
324
396
  try {
325
397
  const escaped = candidate.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\\\*/g, '.*').replace(/\\\?/g, '.');
326
398
  const re = new RegExp('^' + escaped + '$', 'i');
327
399
  for (const p of protectedPaths) {
328
400
  if (re.test(p)) return p;
329
401
  }
330
- } catch { /* invalid regex, skip */ }
402
+ } catch {}
331
403
  }
332
404
  return null;
333
405
  }
334
406
 
335
- // Normalize: lowercase, forward slashes, strip shell quotes
407
+ // ── Build all candidate strings from tool input ──
336
408
  const rawStrings = scanStrings(args).map(s => s.replace(/\\/g, '/').toLowerCase());
337
- const allStrings = [];
409
+ const allStrings = new Set();
410
+
338
411
  for (const s of rawStrings) {
339
- allStrings.push(s);
340
- // Also add quote-stripped version
341
- const stripped = stripShellQuotes(s);
342
- if (stripped !== s) allStrings.push(stripped);
343
- // Split by spaces (for commands like "rm -rf .sol'on'gate .cl*")
344
- for (const tok of s.split(/\s+/)) {
345
- if (tok !== s) {
346
- allStrings.push(tok);
347
- const strippedTok = stripShellQuotes(tok);
348
- if (strippedTok !== tok) allStrings.push(strippedTok);
412
+ // Raw string
413
+ allStrings.add(s);
414
+ // Normalized (ANSI-C decoded, quotes stripped)
415
+ const norm = normalizeShell(s);
416
+ allStrings.add(norm);
417
+ // Extract inner commands (eval, bash -c, pipe to bash, find -name)
418
+ for (const inner of extractInnerCommands(s)) {
419
+ allStrings.add(inner.toLowerCase());
420
+ allStrings.add(normalizeShell(inner.toLowerCase()));
421
+ }
422
+ // Split by spaces + shell operators for token-level checks
423
+ for (const tok of s.split(/[\s;&|]+/)) {
424
+ if (tok) {
425
+ allStrings.add(tok);
426
+ allStrings.add(normalizeShell(tok));
349
427
  }
350
428
  }
429
+ // Also split normalized version
430
+ for (const tok of norm.split(/[\s;&|]+/)) {
431
+ if (tok) allStrings.add(tok);
432
+ }
351
433
  }
352
434
 
435
+ // ── Check all candidates ──
353
436
  for (const s of allStrings) {
354
437
  // Direct match
355
438
  for (const p of protectedPaths) {
356
439
  if (s.includes(p)) {
357
- const msg = 'SOLONGATE: Access to protected file "' + p + '" is blocked';
358
- if (API_KEY && API_KEY.startsWith('sg_live_')) {
359
- try {
360
- await fetch(API_URL + '/api/v1/audit-logs', {
361
- method: 'POST',
362
- headers: { 'Authorization': 'Bearer ' + API_KEY, 'Content-Type': 'application/json' },
363
- body: JSON.stringify({
364
- tool: data.tool_name || '', arguments: args,
365
- decision: 'DENY', reason: msg,
366
- source: 'claude-code-guard',
367
- }),
368
- signal: AbortSignal.timeout(3000),
369
- });
370
- } catch {}
371
- }
372
- process.stderr.write(msg);
373
- process.exit(2);
440
+ await blockSelfProtection('SOLONGATE: Access to protected path "' + p + '" is blocked');
374
441
  }
375
442
  }
376
443
  // Wildcard/glob match
377
444
  const globHit = globMatchesProtected(s);
378
445
  if (globHit) {
379
- const msg = 'SOLONGATE: Wildcard pattern "' + s + '" matches protected path "' + globHit + '" — blocked';
380
- if (API_KEY && API_KEY.startsWith('sg_live_')) {
381
- try {
382
- await fetch(API_URL + '/api/v1/audit-logs', {
383
- method: 'POST',
384
- headers: { 'Authorization': 'Bearer ' + API_KEY, 'Content-Type': 'application/json' },
385
- body: JSON.stringify({
386
- tool: data.tool_name || '', arguments: args,
387
- decision: 'DENY', reason: msg,
388
- source: 'claude-code-guard',
389
- }),
390
- signal: AbortSignal.timeout(3000),
391
- });
392
- } catch {}
446
+ await blockSelfProtection('SOLONGATE: Wildcard "' + s + '" matches protected "' + globHit + '" — blocked');
447
+ }
448
+ // Variable assignment targeting protected paths
449
+ const varHit = checkVarAssignments(s);
450
+ if (varHit) {
451
+ await blockSelfProtection('SOLONGATE: Variable assignment targets protected "' + varHit + '" — blocked');
452
+ }
453
+ }
454
+
455
+ // ── Layer 7: Dangerous execution pattern detection ──
456
+ // These can construct ANY string at runtime — block when touching protected dirs
457
+ const fullCmd = rawStrings.join(' ');
458
+
459
+ // 7a. Inline interpreter execution: node -e, python -c, perl -e, ruby -e
460
+ // Extract the -e/-c argument and scan it
461
+ const interpreterPatterns = [
462
+ /\bnode\s+(?:-e|--eval)\s+["']([^"']+)["']/gi,
463
+ /\bnode\s+(?:-e|--eval)\s+([^;&|"']+)/gi,
464
+ /\bpython[23]?\s+-c\s+["']([^"']+)["']/gi,
465
+ /\bperl\s+-e\s+["']([^"']+)["']/gi,
466
+ /\bruby\s+-e\s+["']([^"']+)["']/gi,
467
+ ];
468
+ for (const pat of interpreterPatterns) {
469
+ for (const m of fullCmd.matchAll(pat)) {
470
+ const code = m[1].toLowerCase();
471
+ for (const p of protectedPaths) {
472
+ if (code.includes(p)) {
473
+ await blockSelfProtection('SOLONGATE: Interpreter code targets "' + p + '" — blocked');
474
+ }
475
+ }
476
+ // Also check the normalized version
477
+ const normCode = normalizeShell(code);
478
+ for (const p of protectedPaths) {
479
+ if (normCode.includes(p)) {
480
+ await blockSelfProtection('SOLONGATE: Interpreter code targets "' + p + '" — blocked');
481
+ }
482
+ }
483
+ }
484
+ }
485
+
486
+ // 7b. Base64 decode piped to execution — always block
487
+ if (/\bbase64\s+-d\b.*\|\s*(?:bash|sh|node|python|perl|ruby)\b/i.test(fullCmd) ||
488
+ /\bbase64\s+--decode\b.*\|\s*(?:bash|sh|node|python|perl|ruby)\b/i.test(fullCmd)) {
489
+ await blockSelfProtection('SOLONGATE: base64 decode piped to interpreter — blocked');
490
+ }
491
+
492
+ // 7c. Temp script file execution: bash /path/file, sh /path/file
493
+ // If "bash <file>" or "sh <file>" and the file is not a well-known script
494
+ if (/\b(?:bash|sh)\s+(?:\/tmp\/|\/var\/tmp\/|~\/\.|\.\/[^.s])/i.test(fullCmd)) {
495
+ await blockSelfProtection('SOLONGATE: Temp script execution detected — blocked');
496
+ }
497
+
498
+ // 7d. Process substitution and here-strings that could construct protected paths
499
+ if (/>\s*\(\s*(?:rm|mv|cp|cat)\b/i.test(fullCmd) || /<<<.*(?:rm|mv|cp|cat)\b/i.test(fullCmd)) {
500
+ for (const p of protectedPaths) {
501
+ const prefix = p.slice(0, 4); // e.g. ".sol", ".cla"
502
+ if (fullCmd.includes(prefix)) {
503
+ await blockSelfProtection('SOLONGATE: Process substitution near protected path "' + p + '" — blocked');
504
+ }
505
+ }
506
+ }
507
+
508
+ // 7e. xargs with destructive operations
509
+ if (/\bxargs\b.*\b(?:rm|mv|cp|rmdir|unlink)\b/i.test(fullCmd)) {
510
+ for (const p of protectedPaths) {
511
+ if (fullCmd.includes(p.slice(0, 4))) {
512
+ await blockSelfProtection('SOLONGATE: xargs with destructive op near "' + p + '" — blocked');
393
513
  }
394
- process.stderr.write(msg);
395
- process.exit(2);
396
514
  }
397
515
  }
398
516
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.15.5",
3
+ "version": "0.16.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": {