@solongate/proxy 0.21.0 → 0.23.0

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
@@ -487,6 +487,30 @@ function unlockProtectedDirs() {
487
487
  } catch {
488
488
  }
489
489
  }
490
+ const protectedFiles = [".env", ".gitignore", ".mcp.json", "policy.json"];
491
+ for (const file of protectedFiles) {
492
+ const fullPath = resolve2(file);
493
+ if (!existsSync3(fullPath)) continue;
494
+ try {
495
+ if (process.platform === "win32") {
496
+ try {
497
+ execSync(`powershell.exe -Command "icacls '${fullPath}' /remove:d '*S-1-1-0' /Q"`, { stdio: "ignore" });
498
+ } catch {
499
+ }
500
+ try {
501
+ execSync(`attrib -R "${fullPath}"`, { stdio: "ignore" });
502
+ } catch {
503
+ }
504
+ } else {
505
+ try {
506
+ execSync(`chattr -i "${fullPath}"`, { stdio: "ignore" });
507
+ } catch {
508
+ }
509
+ execSync(`chmod u+w "${fullPath}"`, { stdio: "ignore" });
510
+ }
511
+ } catch {
512
+ }
513
+ }
490
514
  }
491
515
  function installHooks(selectedTools = []) {
492
516
  unlockProtectedDirs();
@@ -542,49 +566,10 @@ function installHooks(selectedTools = []) {
542
566
  console.log(` Created ${settingsPath}`);
543
567
  activatedNames.push(client.name);
544
568
  }
545
- const protectedDirs = [".solongate", ...clients.map((c3) => c3.dir)];
546
- try {
547
- if (process.platform === "win32") {
548
- for (const dir of protectedDirs) {
549
- const fullDir = resolve2(dir);
550
- if (existsSync3(fullDir)) {
551
- try {
552
- try {
553
- execSync(`powershell.exe -Command "icacls '${fullDir}' /remove:d '*S-1-1-0' /T /Q"`, { stdio: "ignore" });
554
- } catch {
555
- }
556
- try {
557
- execSync(`powershell.exe -Command "icacls '${fullDir}' /deny '*S-1-1-0:(OI)(CI)(DE,DC,WD,AD,WA)' /T /Q"`, { stdio: "ignore" });
558
- } catch {
559
- }
560
- execSync(`attrib +R /S /D "${fullDir}"`, { stdio: "ignore" });
561
- } catch {
562
- }
563
- }
564
- }
565
- } else {
566
- for (const dir of protectedDirs) {
567
- const fullDir = resolve2(dir);
568
- if (existsSync3(fullDir)) {
569
- try {
570
- execSync(`chmod -R a-w "${fullDir}"`, { stdio: "ignore" });
571
- } catch {
572
- }
573
- try {
574
- execSync(`chattr +i -R "${fullDir}"`, { stdio: "ignore" });
575
- } catch {
576
- }
577
- }
578
- }
579
- }
580
- console.log(" OS-level DENY protection applied (icacls/chmod)");
581
- } catch {
582
- }
583
569
  console.log("");
584
570
  console.log(" Hooks installed:");
585
571
  console.log(" guard.mjs \u2192 blocks policy-violating calls (pre-execution)");
586
572
  console.log(" audit.mjs \u2192 logs all calls to dashboard (post-execution)");
587
- console.log(" File system \u2192 read-only (OS-level protection)");
588
573
  console.log(` Activated for: ${activatedNames.join(", ")}`);
589
574
  }
590
575
  function ensureEnvFile() {
package/dist/init.js CHANGED
@@ -194,6 +194,30 @@ function unlockProtectedDirs() {
194
194
  } catch {
195
195
  }
196
196
  }
197
+ const protectedFiles = [".env", ".gitignore", ".mcp.json", "policy.json"];
198
+ for (const file of protectedFiles) {
199
+ const fullPath = resolve(file);
200
+ if (!existsSync(fullPath)) continue;
201
+ try {
202
+ if (process.platform === "win32") {
203
+ try {
204
+ execSync(`powershell.exe -Command "icacls '${fullPath}' /remove:d '*S-1-1-0' /Q"`, { stdio: "ignore" });
205
+ } catch {
206
+ }
207
+ try {
208
+ execSync(`attrib -R "${fullPath}"`, { stdio: "ignore" });
209
+ } catch {
210
+ }
211
+ } else {
212
+ try {
213
+ execSync(`chattr -i "${fullPath}"`, { stdio: "ignore" });
214
+ } catch {
215
+ }
216
+ execSync(`chmod u+w "${fullPath}"`, { stdio: "ignore" });
217
+ }
218
+ } catch {
219
+ }
220
+ }
197
221
  }
198
222
  function installHooks(selectedTools = []) {
199
223
  unlockProtectedDirs();
@@ -249,49 +273,10 @@ function installHooks(selectedTools = []) {
249
273
  console.log(` Created ${settingsPath}`);
250
274
  activatedNames.push(client.name);
251
275
  }
252
- const protectedDirs = [".solongate", ...clients.map((c) => c.dir)];
253
- try {
254
- if (process.platform === "win32") {
255
- for (const dir of protectedDirs) {
256
- const fullDir = resolve(dir);
257
- if (existsSync(fullDir)) {
258
- try {
259
- try {
260
- execSync(`powershell.exe -Command "icacls '${fullDir}' /remove:d '*S-1-1-0' /T /Q"`, { stdio: "ignore" });
261
- } catch {
262
- }
263
- try {
264
- execSync(`powershell.exe -Command "icacls '${fullDir}' /deny '*S-1-1-0:(OI)(CI)(DE,DC,WD,AD,WA)' /T /Q"`, { stdio: "ignore" });
265
- } catch {
266
- }
267
- execSync(`attrib +R /S /D "${fullDir}"`, { stdio: "ignore" });
268
- } catch {
269
- }
270
- }
271
- }
272
- } else {
273
- for (const dir of protectedDirs) {
274
- const fullDir = resolve(dir);
275
- if (existsSync(fullDir)) {
276
- try {
277
- execSync(`chmod -R a-w "${fullDir}"`, { stdio: "ignore" });
278
- } catch {
279
- }
280
- try {
281
- execSync(`chattr +i -R "${fullDir}"`, { stdio: "ignore" });
282
- } catch {
283
- }
284
- }
285
- }
286
- }
287
- console.log(" OS-level DENY protection applied (icacls/chmod)");
288
- } catch {
289
- }
290
276
  console.log("");
291
277
  console.log(" Hooks installed:");
292
278
  console.log(" guard.mjs \u2192 blocks policy-violating calls (pre-execution)");
293
279
  console.log(" audit.mjs \u2192 logs all calls to dashboard (post-execution)");
294
- console.log(" File system \u2192 read-only (OS-level protection)");
295
280
  console.log(` Activated for: ${activatedNames.join(", ")}`);
296
281
  }
297
282
  function ensureEnvFile() {
package/hooks/guard.mjs CHANGED
@@ -589,6 +589,107 @@ process.stdin.on('end', async () => {
589
589
  await blockSelfProtection('SOLONGATE: Script execution + destructive command in chain — blocked');
590
590
  }
591
591
 
592
+ // 7-pre6. Nested script substitution: node clean.mjs $(node scan.mjs)
593
+ // Two "harmless" scripts chained via command substitution — no rm in the command itself
594
+ const nestedScriptSub = /\b(?:node|python[23]?|perl|ruby|bash|sh)\s+\S+\.\S+\s+.*\$\(\s*(?:node|python[23]?|perl|ruby|bash|sh)\b/i;
595
+ const backtickNestedScript = /\b(?:node|python[23]?|perl|ruby|bash|sh)\s+\S+\.\S+\s+.*`\s*(?:node|python[23]?|perl|ruby|bash|sh)\b/i;
596
+ if (nestedScriptSub.test(fullCmd) || backtickNestedScript.test(fullCmd)) {
597
+ await blockSelfProtection('SOLONGATE: Nested script substitution — blocked (script output as args to another script)');
598
+ }
599
+
600
+ // 7-pre7. NODE_OPTIONS injection — block commands with NODE_OPTIONS env var
601
+ // NODE_OPTIONS="--require malicious.cjs" can inject code into any node process
602
+ if (/\bNODE_OPTIONS\s*=/i.test(fullCmd)) {
603
+ await blockSelfProtection('SOLONGATE: NODE_OPTIONS injection — blocked');
604
+ }
605
+
606
+ // 7-pre8. npm lifecycle scripts — scan package.json before npm install/run
607
+ // npm install can trigger postinstall/preinstall/prepare scripts
608
+ if (/\bnpm\s+(?:install|i|ci|run|start|test|publish|pack)\b/i.test(fullCmd) || /\bnpx\s+/i.test(fullCmd)) {
609
+ try {
610
+ const hookCwdForNpm = data.cwd || process.cwd();
611
+ const pkgPath = resolve(hookCwdForNpm, 'package.json');
612
+ if (existsSync(pkgPath)) {
613
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
614
+ const scripts = pkg.scripts || {};
615
+ const lifecycleKeys = ['preinstall', 'install', 'postinstall', 'prepare', 'prepublish', 'prepublishOnly', 'prepack', 'postpack'];
616
+ const allScripts = Object.entries(scripts);
617
+ for (const [key, val] of allScripts) {
618
+ if (typeof val !== 'string') continue;
619
+ const scriptLower = val.toLowerCase();
620
+ // Check lifecycle scripts for dangerous patterns
621
+ const isLifecycle = lifecycleKeys.includes(key);
622
+ const isExplicitRun = fullCmd.includes(key); // npm run <key>
623
+ if (isLifecycle || isExplicitRun) {
624
+ // Check for protected path names
625
+ for (const p of [...protectedPaths, ...writeProtectedPaths]) {
626
+ if (scriptLower.includes(p)) {
627
+ await blockSelfProtection('SOLONGATE: npm script "' + key + '" references protected "' + p + '" — blocked');
628
+ }
629
+ }
630
+ // Check for string construction patterns
631
+ if (/fromcharcode|atob\s*\(|buffer\.from|\\x[0-9a-f]{2}/i.test(scriptLower)) {
632
+ await blockSelfProtection('SOLONGATE: npm script "' + key + '" contains string construction — blocked');
633
+ }
634
+ // Check for discovery+destruction combo
635
+ const hasDisc = /\breaddirsync\b|\breaddir\b|\bos\.listdir\b|\bscandir\b|\bglob\b|\bls\s+-[adl]/i.test(scriptLower);
636
+ const hasDest = /\brmsync\b|\brm\s+-rf\b|\bunlinksync\b|\brmdirsync\b|\bunlink\b|\brimraf\b/i.test(scriptLower);
637
+ if (hasDisc && hasDest) {
638
+ await blockSelfProtection('SOLONGATE: npm script "' + key + '" has discovery+destruction — blocked');
639
+ }
640
+ // Follow node/bash <file> references in npm scripts — deep scan the target file
641
+ const scriptFileMatch = scriptLower.match(/\b(?:node|bash|sh|python[23]?)\s+([^\s;&|$]+)/i);
642
+ if (scriptFileMatch) {
643
+ try {
644
+ const targetPath = resolve(hookCwdForNpm, scriptFileMatch[1]);
645
+ if (existsSync(targetPath)) {
646
+ const targetContent = readFileSync(targetPath, 'utf-8').toLowerCase();
647
+ // Check target file for protected paths
648
+ for (const pp of [...protectedPaths, ...writeProtectedPaths]) {
649
+ if (targetContent.includes(pp)) {
650
+ await blockSelfProtection('SOLONGATE: npm script "' + key + '" runs file referencing "' + pp + '" — blocked');
651
+ }
652
+ }
653
+ // Check target for discovery+destruction
654
+ const tDisc = /\breaddirsync\b|\breaddir\b|\bos\.listdir\b|\bglob\b/i.test(targetContent);
655
+ const tDest = /\brmsync\b|\bunlinksync\b|\brmdirsync\b|\bunlink\b|\brimraf\b|\bfs\.\s*(?:rm|unlink|rmdir)/i.test(targetContent);
656
+ if (tDisc && tDest) {
657
+ await blockSelfProtection('SOLONGATE: npm script "' + key + '" runs file with discovery+destruction — blocked');
658
+ }
659
+ // Check target for string construction + destruction
660
+ const tStrCon = /\bfromcharcode\b|\batob\s*\(|\bbuffer\.from\b/i.test(targetContent);
661
+ if (tStrCon && tDest) {
662
+ await blockSelfProtection('SOLONGATE: npm script "' + key + '" runs file with string construction+destruction — blocked');
663
+ }
664
+ // Follow imports in the target file
665
+ const tDir = targetPath.replace(/[/\\][^/\\]+$/, '');
666
+ const tImports = [...targetContent.matchAll(/(?:import\s+.*?\s+from\s+|import\s+|require\s*\()['"]\.\/([^'"]+)['"]/gi)];
667
+ for (const [, imp] of tImports) {
668
+ try {
669
+ const impAbs = resolve(tDir, imp);
670
+ const candidates = [impAbs, impAbs + '.mjs', impAbs + '.js', impAbs + '.cjs'];
671
+ for (const c of candidates) {
672
+ if (existsSync(c)) {
673
+ const impContent = readFileSync(c, 'utf-8').toLowerCase();
674
+ const iDisc = /\breaddirsync\b|\breaddir\b|\bos\.listdir\b|\bglob\b/i.test(impContent);
675
+ const iDest = /\brmsync\b|\bunlinksync\b|\bfs\.\s*(?:rm|unlink|rmdir)/i.test(impContent);
676
+ if ((iDisc && tDest) || (tDisc && iDest)) {
677
+ await blockSelfProtection('SOLONGATE: npm script "' + key + '" — cross-module discovery+destruction — blocked');
678
+ }
679
+ break;
680
+ }
681
+ }
682
+ } catch {}
683
+ }
684
+ }
685
+ } catch {}
686
+ }
687
+ }
688
+ }
689
+ }
690
+ } catch {} // Package.json read error — skip
691
+ }
692
+
592
693
  // 7a. Inline interpreter execution — TOTAL BLOCK (no content scan needed)
593
694
  // These can construct ANY string at runtime, bypassing all static analysis
594
695
  const blockedInterpreters = [
@@ -646,7 +747,7 @@ process.stdin.on('end', async () => {
646
747
 
647
748
  // 7g. Script file execution — scan file content for discovery+destruction combo
648
749
  // Catches: bash script.sh / node script.mjs where the script uses readdirSync + rmSync
649
- const scriptExecMatch = fullCmd.match(/\b(?:bash|sh|node|python[23]?|perl|ruby)\s+([^\s;&|]+)/i);
750
+ const scriptExecMatch = fullCmd.match(/\b(?:bash|sh|node|python[23]?|perl|ruby)\s+([^\s;&|$`]+)/i);
650
751
  if (scriptExecMatch) {
651
752
  const scriptPath = scriptExecMatch[1];
652
753
  try {
@@ -658,12 +759,50 @@ process.stdin.on('end', async () => {
658
759
  const scriptContent = readFileSync(absPath, 'utf-8').toLowerCase();
659
760
  // Check for discovery+destruction combo
660
761
  const hasDiscovery = /\breaddirsync\b|\breaddir\b|\bos\.listdir\b|\bscandir\b|\bglob(?:sync)?\b|\bls\s+-[adl]|\bls\s+\.\b|\bopendir\b|\bdir\.entries\b|\bwalkdir\b|\bls\b.*\.\[/.test(scriptContent);
661
- const hasDestruction = /\brmsync\b|\brm\s+-rf\b|\bunlinksync\b|\brmdirsync\b|\bunlink\s*\(|\brimraf\b|\bremovesync\b|\bremove_tree\b|\bshutil\.rmtree\b|\bwritefilesync\b|\bexecsync\b.*\brm\b|\bchild_process\b|\bfs\.\s*(?:rm|unlink|rmdir|write)/.test(scriptContent);
762
+ const hasDestruction = /\brmsync\b|\brm\s+-rf\b|\bunlinksync\b|\brmdirsync\b|\bunlink\b|\brimraf\b|\bremovesync\b|\bremove_tree\b|\bshutil\.rmtree\b|\bwritefilesync\b|\bexecsync\b.*\brm\b|\bchild_process\b|\bfs\.\s*(?:rm|unlink|rmdir|write)/.test(scriptContent);
662
763
  if (hasDiscovery && hasDestruction) {
663
764
  await blockSelfProtection('SOLONGATE: Script contains directory discovery + destructive ops — blocked');
664
765
  }
665
- // Also check for protected path names in script content (existing check, now centralized)
666
- for (const p of protectedPaths) {
766
+ // String construction + destructive = BLOCK
767
+ // Runtime string construction (fromCharCode, atob, Buffer.from) bypasses literal name checks
768
+ const hasStringConstruction = /\bfromcharcode\b|\batob\s*\(|\bbuffer\.from\b|\\x[0-9a-f]{2}|\bstring\.fromcodepoin/i.test(scriptContent);
769
+ if (hasStringConstruction && hasDestruction) {
770
+ await blockSelfProtection('SOLONGATE: Script uses string construction + destructive ops — blocked');
771
+ }
772
+ // Import/require chain: if script imports other local files, scan them too
773
+ const scriptDir = absPath.replace(/[/\\][^/\\]+$/, '');
774
+ const imports = [...scriptContent.matchAll(/(?:import\s+.*?\s+from\s+|import\s+|require\s*\()['"]\.\/([^'"]+)['"]/gi)];
775
+ for (const [, importPath] of imports) {
776
+ try {
777
+ const importAbs = resolve(scriptDir, importPath);
778
+ const candidates = [importAbs, importAbs + '.mjs', importAbs + '.js', importAbs + '.cjs'];
779
+ for (const candidate of candidates) {
780
+ if (existsSync(candidate)) {
781
+ const importContent = readFileSync(candidate, 'utf-8').toLowerCase();
782
+ // Cross-module: check if imported module has discovery/destruction/string construction
783
+ const importDisc = /\breaddirsync\b|\breaddir\b|\bos\.listdir\b|\bscandir\b|\bglob(?:sync)?\b/i.test(importContent);
784
+ const importDest = /\brmsync\b|\bunlinksync\b|\brmdirsync\b|\bunlink\b|\brimraf\b|\bfs\.\s*(?:rm|unlink|rmdir)/i.test(importContent);
785
+ const importStrCon = /\bfromcharcode\b|\batob\s*\(|\bbuffer\.from\b/i.test(importContent);
786
+ // Cross-module discovery in import + destruction in main (or vice versa)
787
+ if ((importDisc && hasDestruction) || (hasDiscovery && importDest)) {
788
+ await blockSelfProtection('SOLONGATE: Cross-module discovery+destruction detected — blocked');
789
+ }
790
+ if ((importStrCon && hasDestruction) || (hasStringConstruction && importDest)) {
791
+ await blockSelfProtection('SOLONGATE: Cross-module string construction+destruction — blocked');
792
+ }
793
+ // Check imported module for protected paths
794
+ for (const p of [...protectedPaths, ...writeProtectedPaths]) {
795
+ if (importContent.includes(p)) {
796
+ await blockSelfProtection('SOLONGATE: Imported module references protected "' + p + '" — blocked');
797
+ }
798
+ }
799
+ break;
800
+ }
801
+ }
802
+ } catch {}
803
+ }
804
+ // Check for protected path names in script content
805
+ for (const p of [...protectedPaths, ...writeProtectedPaths]) {
667
806
  if (scriptContent.includes(p)) {
668
807
  await blockSelfProtection('SOLONGATE: Script references protected path "' + p + '" — blocked');
669
808
  }
@@ -683,6 +822,39 @@ process.stdin.on('end', async () => {
683
822
  if (hasDiscovery && hasDestruction) {
684
823
  await blockSelfProtection('SOLONGATE: File content contains discovery + destructive ops — write blocked');
685
824
  }
825
+ // String construction + destructive = BLOCK
826
+ const hasStringConstruction = /\bfromcharcode\b|\batob\s*\(|\bbuffer\.from\b|\\x[0-9a-f]{2}|\bstring\.fromcodepoin/i.test(fileContent);
827
+ if (hasStringConstruction && hasDestruction) {
828
+ await blockSelfProtection('SOLONGATE: File uses string construction + destructive ops — write blocked');
829
+ }
830
+ }
831
+ }
832
+
833
+ // 7i. Write tool — detect package.json scripts with dangerous patterns
834
+ if (toolName_.toLowerCase() === 'write' || toolName_.toLowerCase() === 'edit') {
835
+ const filePath = (args.file_path || '').toLowerCase();
836
+ if (filePath.endsWith('package.json')) {
837
+ const content = args.content || args.new_string || '';
838
+ try {
839
+ // Try parsing as JSON to check scripts
840
+ const pkg = JSON.parse(content);
841
+ const scripts = pkg.scripts || {};
842
+ for (const [key, val] of Object.entries(scripts)) {
843
+ if (typeof val !== 'string') continue;
844
+ const v = val.toLowerCase();
845
+ for (const p of [...protectedPaths, ...writeProtectedPaths]) {
846
+ if (v.includes(p)) {
847
+ await blockSelfProtection('SOLONGATE: package.json script "' + key + '" references protected "' + p + '" — blocked');
848
+ }
849
+ }
850
+ if (/fromcharcode|atob\s*\(|buffer\.from/i.test(v)) {
851
+ const hasDest = /\brmsync\b|\brm\b|\bunlink\b|\brimraf\b/i.test(v);
852
+ if (hasDest) {
853
+ await blockSelfProtection('SOLONGATE: package.json script "' + key + '" has string construction + destruction — blocked');
854
+ }
855
+ }
856
+ }
857
+ } catch {} // Not valid JSON or parse error — skip
686
858
  }
687
859
  }
688
860
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.21.0",
3
+ "version": "0.23.0",
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": {