@solongate/proxy 0.18.0 → 0.20.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/hooks/guard.mjs +168 -14
- package/package.json +1 -1
package/hooks/guard.mjs
CHANGED
|
@@ -291,10 +291,13 @@ process.stdin.on('end', async () => {
|
|
|
291
291
|
|
|
292
292
|
// ── Self-protection: block access to hook files and settings ──
|
|
293
293
|
// Hardcoded, no bypass possible — runs before policy/PI config
|
|
294
|
+
// Fully protected: block ALL access (read, write, delete, move)
|
|
294
295
|
const protectedPaths = [
|
|
295
296
|
'.solongate', '.claude', '.cursor', '.gemini', '.antigravity', '.openclaw', '.perplexity',
|
|
296
297
|
'policy.json', '.mcp.json',
|
|
297
298
|
];
|
|
299
|
+
// Write-protected: block write/delete/modify, allow read (cat, grep, head, etc.)
|
|
300
|
+
const writeProtectedPaths = ['.env', '.gitignore'];
|
|
298
301
|
|
|
299
302
|
// Block helper — logs to cloud + exits with code 2
|
|
300
303
|
async function blockSelfProtection(reason) {
|
|
@@ -346,7 +349,7 @@ process.stdin.on('end', async () => {
|
|
|
346
349
|
return stripShellQuotes(decodeAnsiC(s));
|
|
347
350
|
}
|
|
348
351
|
|
|
349
|
-
// 4. Extract inner commands from eval, bash -c, sh -c, pipe to bash/sh
|
|
352
|
+
// 4. Extract inner commands from eval, bash -c, sh -c, pipe to bash/sh, heredoc, process substitution
|
|
350
353
|
function extractInnerCommands(s) {
|
|
351
354
|
const inner = [];
|
|
352
355
|
// eval "cmd" or eval 'cmd' or eval cmd
|
|
@@ -358,6 +361,13 @@ process.stdin.on('end', async () => {
|
|
|
358
361
|
for (const m of s.matchAll(/(?:echo|printf)\s+["']([^"']+)["']\s*\|\s*(?:bash|sh)\b/gi)) inner.push(m[1]);
|
|
359
362
|
// find ... -name "pattern" ... -exec ...
|
|
360
363
|
for (const m of s.matchAll(/-name\s+["']?([^\s"']+)["']?/gi)) inner.push(m[1]);
|
|
364
|
+
// Heredoc: bash << 'EOF'\n...\nEOF or bash <<- EOF\n...\nEOF
|
|
365
|
+
for (const m of s.matchAll(/<<-?\s*['"]?(\w+)['"]?\s*\n([\s\S]*?)\n\s*\1/gi)) inner.push(m[2]);
|
|
366
|
+
// Herestring: bash <<< "cmd"
|
|
367
|
+
for (const m of s.matchAll(/<<<\s*["']([^"']+)["']/gi)) inner.push(m[1]);
|
|
368
|
+
for (const m of s.matchAll(/<<<\s*([^\s;&|]+)/gi)) inner.push(m[1]);
|
|
369
|
+
// Process substitution: <(cmd) or >(cmd)
|
|
370
|
+
for (const m of s.matchAll(/[<>]\(\s*([^)]+)\s*\)/gi)) inner.push(m[1]);
|
|
361
371
|
return inner;
|
|
362
372
|
}
|
|
363
373
|
|
|
@@ -377,25 +387,50 @@ process.stdin.on('end', async () => {
|
|
|
377
387
|
|
|
378
388
|
// 6. Check if a glob/wildcard could match any protected path
|
|
379
389
|
function globMatchesProtected(s) {
|
|
380
|
-
|
|
390
|
+
const hasWild = s.includes('*') || s.includes('?') || /\[.+\]/.test(s);
|
|
391
|
+
if (!hasWild) return null;
|
|
381
392
|
const segments = s.split('/').filter(Boolean);
|
|
382
393
|
const candidates = [s, ...segments];
|
|
383
394
|
for (const candidate of candidates) {
|
|
384
|
-
|
|
395
|
+
const candHasWild = candidate.includes('*') || candidate.includes('?') || /\[.+\]/.test(candidate);
|
|
396
|
+
if (!candHasWild) continue;
|
|
385
397
|
// Prefix match: ".antig*" → prefix ".antig"
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
|
|
398
|
+
const firstWild = Math.min(
|
|
399
|
+
candidate.includes('*') ? candidate.indexOf('*') : Infinity,
|
|
400
|
+
candidate.includes('?') ? candidate.indexOf('?') : Infinity,
|
|
401
|
+
/\[/.test(candidate) ? candidate.indexOf('[') : Infinity,
|
|
402
|
+
);
|
|
389
403
|
const prefix = candidate.slice(0, firstWild).toLowerCase();
|
|
390
404
|
if (prefix.length > 0) {
|
|
391
405
|
for (const p of protectedPaths) {
|
|
392
406
|
if (p.startsWith(prefix)) return p;
|
|
393
407
|
}
|
|
394
408
|
}
|
|
395
|
-
// Regex match
|
|
409
|
+
// Regex match — convert shell glob to regex
|
|
410
|
+
// Handles *, ?, and [...] character classes
|
|
396
411
|
try {
|
|
397
|
-
|
|
398
|
-
|
|
412
|
+
let regex = '';
|
|
413
|
+
let i = 0;
|
|
414
|
+
const c = candidate.toLowerCase();
|
|
415
|
+
while (i < c.length) {
|
|
416
|
+
if (c[i] === '*') { regex += '.*'; i++; }
|
|
417
|
+
else if (c[i] === '?') { regex += '.'; i++; }
|
|
418
|
+
else if (c[i] === '[') {
|
|
419
|
+
// Pass through [...] as regex character class
|
|
420
|
+
const close = c.indexOf(']', i + 1);
|
|
421
|
+
if (close !== -1) {
|
|
422
|
+
regex += c.slice(i, close + 1);
|
|
423
|
+
i = close + 1;
|
|
424
|
+
} else {
|
|
425
|
+
regex += '\\['; i++;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
regex += c[i].replace(/[.+^${}()|\\]/g, '\\$&');
|
|
430
|
+
i++;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
const re = new RegExp('^' + regex + '$', 'i');
|
|
399
434
|
for (const p of protectedPaths) {
|
|
400
435
|
if (re.test(p)) return p;
|
|
401
436
|
}
|
|
@@ -452,10 +487,80 @@ process.stdin.on('end', async () => {
|
|
|
452
487
|
}
|
|
453
488
|
}
|
|
454
489
|
|
|
490
|
+
// ── Write-protection for .env, .gitignore ──
|
|
491
|
+
// These files can be READ but not written/deleted/moved
|
|
492
|
+
const toolNameLower = (data.tool_name || '').toLowerCase();
|
|
493
|
+
const isWriteTool = toolNameLower === 'write' || toolNameLower === 'edit' || toolNameLower === 'notebookedit';
|
|
494
|
+
const isBashTool = toolNameLower === 'bash';
|
|
495
|
+
const bashDestructive = /\b(?:rm|rmdir|del|unlink|mv|move|rename|cp|chmod|chown|truncate|shred)\b/i;
|
|
496
|
+
const fullCmd = rawStrings.join(' ');
|
|
497
|
+
|
|
498
|
+
for (const wp of writeProtectedPaths) {
|
|
499
|
+
if (isWriteTool) {
|
|
500
|
+
// Check file_path and all strings for write-protected paths
|
|
501
|
+
for (const s of allStrings) {
|
|
502
|
+
if (s.includes(wp)) {
|
|
503
|
+
await blockSelfProtection('SOLONGATE: Write to protected file "' + wp + '" is blocked');
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
if (isBashTool && bashDestructive.test(fullCmd)) {
|
|
508
|
+
for (const s of allStrings) {
|
|
509
|
+
if (s.includes(wp)) {
|
|
510
|
+
await blockSelfProtection('SOLONGATE: Destructive operation on "' + wp + '" is blocked');
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Also apply glob/wildcard checks for write-protected paths in destructive contexts
|
|
517
|
+
if (isBashTool && bashDestructive.test(fullCmd)) {
|
|
518
|
+
for (const s of allStrings) {
|
|
519
|
+
const hasWild = s.includes('*') || s.includes('?') || /\[.+\]/.test(s);
|
|
520
|
+
if (!hasWild) continue;
|
|
521
|
+
// Convert glob to regex and test against write-protected paths
|
|
522
|
+
try {
|
|
523
|
+
let regex = '';
|
|
524
|
+
let i = 0;
|
|
525
|
+
const c = s.toLowerCase();
|
|
526
|
+
while (i < c.length) {
|
|
527
|
+
if (c[i] === '*') { regex += '.*'; i++; }
|
|
528
|
+
else if (c[i] === '?') { regex += '.'; i++; }
|
|
529
|
+
else if (c[i] === '[') {
|
|
530
|
+
const close = c.indexOf(']', i + 1);
|
|
531
|
+
if (close !== -1) { regex += c.slice(i, close + 1); i = close + 1; }
|
|
532
|
+
else { regex += '\\['; i++; }
|
|
533
|
+
}
|
|
534
|
+
else { regex += c[i].replace(/[.+^${}()|\\]/g, '\\$&'); i++; }
|
|
535
|
+
}
|
|
536
|
+
const re = new RegExp('^' + regex + '$', 'i');
|
|
537
|
+
for (const wp of writeProtectedPaths) {
|
|
538
|
+
if (re.test(wp)) {
|
|
539
|
+
await blockSelfProtection('SOLONGATE: Wildcard "' + s + '" matches write-protected "' + wp + '" — blocked');
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
} catch {}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
455
546
|
// ── Layer 7: Block ALL inline code execution & dangerous patterns ──
|
|
456
547
|
// Runtime string construction (atob, Buffer.from, fromCharCode, array.join)
|
|
457
548
|
// makes static analysis impossible. Blanket-block these patterns.
|
|
458
|
-
|
|
549
|
+
|
|
550
|
+
// 7-pre. Blanket rule: destructive command + dot-prefixed glob = BLOCK
|
|
551
|
+
// Catches: rm -rf .[a-z]*, mv .[!.]* /tmp, etc.
|
|
552
|
+
// Any glob starting with "." combined with a destructive op is dangerous
|
|
553
|
+
const destructiveOps = /\b(?:rm|rmdir|del|unlink|mv|move|rename)\b/i;
|
|
554
|
+
const dotGlob = /\.\[|\.[\*\?]|\.{[^}]+}/; // .[a-z]*, .*, .?, .{foo,bar}
|
|
555
|
+
if (destructiveOps.test(fullCmd) && dotGlob.test(fullCmd)) {
|
|
556
|
+
await blockSelfProtection('SOLONGATE: Destructive command + dot-glob pattern — blocked');
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// 7-pre2. Heredoc to interpreter — BLOCK
|
|
560
|
+
// bash << 'EOF' / bash <<< "cmd" / sh << TAG
|
|
561
|
+
if (/\b(?:bash|sh|node|python[23]?|perl|ruby)\s+<</i.test(fullCmd)) {
|
|
562
|
+
await blockSelfProtection('SOLONGATE: Heredoc to interpreter — blocked');
|
|
563
|
+
}
|
|
459
564
|
|
|
460
565
|
// 7a. Inline interpreter execution — TOTAL BLOCK (no content scan needed)
|
|
461
566
|
// These can construct ANY string at runtime, bypassing all static analysis
|
|
@@ -476,17 +581,24 @@ process.stdin.on('end', async () => {
|
|
|
476
581
|
}
|
|
477
582
|
}
|
|
478
583
|
|
|
479
|
-
// 7b.
|
|
584
|
+
// 7b. Pipe-to-interpreter — TOTAL BLOCK
|
|
585
|
+
// Any content piped to an interpreter can construct arbitrary commands at runtime
|
|
586
|
+
const pipeToInterpreter = /\|\s*(?:node|bash|sh|python[23]?|perl|ruby|php)\b/i;
|
|
587
|
+
if (pipeToInterpreter.test(fullCmd)) {
|
|
588
|
+
await blockSelfProtection('SOLONGATE: Pipe to interpreter blocked — runtime bypass risk');
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// 7c. Base64 decode in ANY context — block when piped to anything
|
|
480
592
|
if (/\bbase64\s+(?:-d|--decode)\b/i.test(fullCmd) && /\|/i.test(fullCmd)) {
|
|
481
593
|
await blockSelfProtection('SOLONGATE: base64 decode in pipe chain — blocked');
|
|
482
594
|
}
|
|
483
595
|
|
|
484
|
-
//
|
|
596
|
+
// 7d. Temp/arbitrary script file execution
|
|
485
597
|
if (/\b(?:bash|sh)\s+(?:\/tmp\/|\/var\/tmp\/|~\/|\/dev\/)/i.test(fullCmd)) {
|
|
486
598
|
await blockSelfProtection('SOLONGATE: Script execution from temp path — blocked');
|
|
487
599
|
}
|
|
488
600
|
|
|
489
|
-
//
|
|
601
|
+
// 7e. xargs with destructive operations
|
|
490
602
|
if (/\bxargs\b.*\b(?:rm|mv|cp|rmdir|unlink|del)\b/i.test(fullCmd)) {
|
|
491
603
|
for (const p of protectedPaths) {
|
|
492
604
|
if (fullCmd.includes(p.slice(0, 4))) {
|
|
@@ -495,7 +607,7 @@ process.stdin.on('end', async () => {
|
|
|
495
607
|
}
|
|
496
608
|
}
|
|
497
609
|
|
|
498
|
-
//
|
|
610
|
+
// 7f. cmd.exe /c with encoded/constructed commands
|
|
499
611
|
if (/\bcmd(?:\.exe)?\s+\/c\b/i.test(fullCmd)) {
|
|
500
612
|
for (const p of protectedPaths) {
|
|
501
613
|
if (fullCmd.includes(p) || fullCmd.includes(p.slice(0, 4))) {
|
|
@@ -504,6 +616,48 @@ process.stdin.on('end', async () => {
|
|
|
504
616
|
}
|
|
505
617
|
}
|
|
506
618
|
|
|
619
|
+
// 7g. Script file execution — scan file content for discovery+destruction combo
|
|
620
|
+
// Catches: bash script.sh / node script.mjs where the script uses readdirSync + rmSync
|
|
621
|
+
const scriptExecMatch = fullCmd.match(/\b(?:bash|sh|node|python[23]?|perl|ruby)\s+([^\s;&|]+)/i);
|
|
622
|
+
if (scriptExecMatch) {
|
|
623
|
+
const scriptPath = scriptExecMatch[1];
|
|
624
|
+
try {
|
|
625
|
+
const hookCwdForScript = data.cwd || process.cwd();
|
|
626
|
+
const absPath = scriptPath.startsWith('/') || scriptPath.includes(':')
|
|
627
|
+
? scriptPath
|
|
628
|
+
: resolve(hookCwdForScript, scriptPath);
|
|
629
|
+
if (existsSync(absPath)) {
|
|
630
|
+
const scriptContent = readFileSync(absPath, 'utf-8').toLowerCase();
|
|
631
|
+
// Check for discovery+destruction combo
|
|
632
|
+
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);
|
|
633
|
+
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);
|
|
634
|
+
if (hasDiscovery && hasDestruction) {
|
|
635
|
+
await blockSelfProtection('SOLONGATE: Script contains directory discovery + destructive ops — blocked');
|
|
636
|
+
}
|
|
637
|
+
// Also check for protected path names in script content (existing check, now centralized)
|
|
638
|
+
for (const p of protectedPaths) {
|
|
639
|
+
if (scriptContent.includes(p)) {
|
|
640
|
+
await blockSelfProtection('SOLONGATE: Script references protected path "' + p + '" — blocked');
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
} catch {} // File read error — skip
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// 7h. Write tool content scanning — detect discovery+destruction in file content being written
|
|
648
|
+
// Catches: Write tool creating a script that uses readdirSync('.') + rmSync
|
|
649
|
+
const toolName_ = data.tool_name || '';
|
|
650
|
+
if (toolName_.toLowerCase() === 'write' || toolName_.toLowerCase() === 'edit') {
|
|
651
|
+
const fileContent = (args.content || args.new_string || '').toLowerCase();
|
|
652
|
+
if (fileContent.length > 0) {
|
|
653
|
+
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(fileContent);
|
|
654
|
+
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.*\brm\b|\bfs\.\s*(?:rm|unlink|rmdir)/.test(fileContent);
|
|
655
|
+
if (hasDiscovery && hasDestruction) {
|
|
656
|
+
await blockSelfProtection('SOLONGATE: File content contains discovery + destructive ops — write blocked');
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
507
661
|
// ── Fetch PI config from Cloud ──
|
|
508
662
|
let piCfg = { piEnabled: true, piThreshold: 0.5, piMode: 'block', piWhitelist: [], piToolConfig: {}, piCustomPatterns: [], piWebhookUrl: null };
|
|
509
663
|
if (API_KEY && API_KEY.startsWith('sg_live_')) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solongate/proxy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.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": {
|