@solongate/proxy 0.5.9 → 0.6.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.
Files changed (3) hide show
  1. package/dist/index.js +214 -109
  2. package/dist/init.js +214 -109
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -664,15 +664,42 @@ function installHooks() {
664
664
  const auditPath = join(hooksDir, "audit.mjs");
665
665
  writeFileSync2(auditPath, AUDIT_SCRIPT);
666
666
  console.log(` Created ${auditPath}`);
667
+ const claudeDir = resolve2(".claude");
668
+ mkdirSync(claudeDir, { recursive: true });
669
+ const settingsPath = join(claudeDir, "settings.json");
670
+ const settings = {
671
+ hooks: {
672
+ PreToolUse: [
673
+ {
674
+ matcher: "",
675
+ hooks: [
676
+ { type: "command", command: "node .solongate/hooks/guard.mjs" }
677
+ ]
678
+ }
679
+ ],
680
+ PostToolUse: [
681
+ {
682
+ matcher: "",
683
+ hooks: [
684
+ { type: "command", command: "node .solongate/hooks/audit.mjs" }
685
+ ]
686
+ }
687
+ ]
688
+ }
689
+ };
690
+ let existing = {};
691
+ try {
692
+ existing = JSON.parse(readFileSync3(settingsPath, "utf-8"));
693
+ } catch {
694
+ }
695
+ const merged = { ...existing, hooks: settings.hooks };
696
+ writeFileSync2(settingsPath, JSON.stringify(merged, null, 2) + "\n");
697
+ console.log(` Created ${settingsPath}`);
667
698
  console.log("");
668
- console.log(" Hooks installed in .solongate/hooks/");
669
- console.log(" guard.mjs \u2192 blocks dangerous calls (pre-execution)");
699
+ console.log(" Hooks installed:");
700
+ console.log(" guard.mjs \u2192 blocks policy-violating calls (pre-execution)");
670
701
  console.log(" audit.mjs \u2192 logs all calls to dashboard (post-execution)");
671
- console.log("");
672
- console.log(" To activate hooks in your MCP client:");
673
- console.log(" Claude Code \u2192 .claude/settings.json");
674
- console.log(" Cursor \u2192 .cursor/settings.json");
675
- console.log(" Or run: node .solongate/hooks/guard.mjs (stdin: JSON)");
702
+ console.log(" .claude/settings.json \u2192 hooks activated for Claude Code");
676
703
  }
677
704
  function ensureEnvFile() {
678
705
  const envPath = resolve2(".env");
@@ -778,6 +805,8 @@ async function main() {
778
805
  if (alreadyProtected.length === serverNames.length) {
779
806
  console.log(" All servers are already protected by SolonGate!");
780
807
  ensureEnvFile();
808
+ console.log("");
809
+ installHooks();
781
810
  process.exit(0);
782
811
  }
783
812
  if (!options.all) {
@@ -917,150 +946,226 @@ var init_init = __esm({
917
946
  sleep = (ms) => new Promise((r) => setTimeout(r, ms));
918
947
  GUARD_SCRIPT = `#!/usr/bin/env node
919
948
  /**
920
- * SolonGate Guard Hook for Claude Code (PreToolUse)
921
- * Blocks dangerous tool calls BEFORE they execute.
949
+ * SolonGate Policy Guard Hook (PreToolUse)
950
+ * Reads policy.json and blocks tool calls that violate constraints.
922
951
  * Exit code 2 = BLOCK, exit code 0 = ALLOW.
923
952
  * Auto-installed by: npx @solongate/proxy init
924
953
  */
954
+ import { readFileSync } from 'node:fs';
955
+ import { resolve } from 'node:path';
925
956
 
926
957
  const API_KEY = process.env.SOLONGATE_API_KEY || '';
927
958
  const API_URL = process.env.SOLONGATE_API_URL || 'https://api.solongate.com';
928
959
 
929
- // \u2500\u2500 Input Guard Patterns \u2500\u2500
930
- const PATH_TRAVERSAL = [
931
- /\\.\\.\\//, /\\\\.\\.\\\\\\\\/, /%2e%2e/i, /%2e\\./i, /\\.%2e/i, /%252e%252e/i,
932
- ];
933
- const SENSITIVE_PATHS = [
934
- /\\/etc\\/passwd/i, /\\/etc\\/shadow/i, /\\/proc\\//i,
935
- /c:\\\\windows\\\\system32/i, /\\.env(\\.|$)/i,
936
- /\\.aws\\/credentials/i, /\\.ssh\\/id_/i, /\\.kube\\/config/i,
937
- /\\.git\\/config/i, /\\.npmrc/i, /\\.pypirc/i,
938
- ];
939
- const SHELL_INJECTION = [
940
- /\\$\\(/, /\\$\\{/, /\\\`/, /\\beval\\b/i, /\\bexec\\b/i, /\\bsystem\\b/i,
941
- /%0a/i, /%0d/i,
942
- ];
943
- const SSRF = [
944
- /^https?:\\/\\/localhost\\b/i, /^https?:\\/\\/127\\./, /^https?:\\/\\/0\\.0\\.0\\.0/,
945
- /^https?:\\/\\/10\\./, /^https?:\\/\\/172\\.(1[6-9]|2\\d|3[01])\\./,
946
- /^https?:\\/\\/192\\.168\\./, /^https?:\\/\\/169\\.254\\./,
947
- /metadata\\.google\\.internal/i,
948
- ];
949
- const SSRF_IN_CMD = [
950
- /https?:\\/\\/localhost\\b/i, /https?:\\/\\/127\\./, /https?:\\/\\/0\\.0\\.0\\.0/,
951
- /https?:\\/\\/10\\./, /https?:\\/\\/172\\.(1[6-9]|2\\d|3[01])\\./,
952
- /https?:\\/\\/192\\.168\\./, /https?:\\/\\/169\\.254\\./,
953
- /metadata\\.google\\.internal/i,
954
- /\\b127\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b/, /\\b0\\.0\\.0\\.0\\b/,
955
- /\\b10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b/,
956
- /\\b172\\.(1[6-9]|2\\d|3[01])\\.\\d{1,3}\\.\\d{1,3}\\b/,
957
- /\\b192\\.168\\.\\d{1,3}\\.\\d{1,3}\\b/,
958
- /\\b169\\.254\\.\\d{1,3}\\.\\d{1,3}\\b/,
959
- ];
960
- const SQL_INJECTION = [
961
- /'\\s{0,20}(OR|AND)\\s{0,20}'.{0,200}'/i,
962
- /'\\s{0,10};\\s{0,10}(DROP|DELETE|UPDATE|INSERT|ALTER|CREATE|EXEC)/i,
963
- /UNION\\s+(ALL\\s+)?SELECT/i, /\\bSLEEP\\s*\\(/i, /\\bWAITFOR\\s+DELAY/i,
964
- ];
960
+ // \u2500\u2500 Glob Matching \u2500\u2500
961
+ function matchGlob(str, pattern) {
962
+ if (pattern === '*') return true;
963
+ const s = str.toLowerCase();
964
+ const p = pattern.toLowerCase();
965
+ if (s === p) return true;
966
+ const startsW = p.startsWith('*');
967
+ const endsW = p.endsWith('*');
968
+ if (startsW && endsW) { const infix = p.slice(1, -1); return infix.length > 0 && s.includes(infix); }
969
+ if (startsW) return s.endsWith(p.slice(1));
970
+ if (endsW) return s.startsWith(p.slice(0, -1));
971
+ const idx = p.indexOf('*');
972
+ if (idx !== -1) {
973
+ const pre = p.slice(0, idx);
974
+ const suf = p.slice(idx + 1);
975
+ return s.startsWith(pre) && s.endsWith(suf) && s.length >= pre.length + suf.length;
976
+ }
977
+ return false;
978
+ }
965
979
 
966
- // Dangerous command patterns for Bash tool
967
- const DANGEROUS_COMMANDS = [
968
- /\\brm\\s+(-[a-zA-Z]*f|-[a-zA-Z]*r|--force|--recursive)/i,
969
- /\\brm\\s+-rf\\b/i,
970
- /\\bmkfs\\b/i, /\\bdd\\s+if=/i,
971
- /\\b(shutdown|reboot|halt|poweroff)\\b/i,
972
- /\\bchmod\\s+777\\b/,
973
- /\\bcurl\\b.{0,200}\\|\\s*(sh|bash)\\b/i,
974
- /\\bwget\\b.{0,200}\\|\\s*(sh|bash)\\b/i,
975
- /\\bnc\\s+-[a-z]*l/i, // netcat listener
976
- />(\\s*)\\/dev\\/sd/, // writing to raw disk
977
- /\\bgit\\s+push\\s+.*--force\\b/i,
978
- /\\bgit\\s+reset\\s+--hard\\b/i,
979
- ];
980
+ // \u2500\u2500 Path Glob (supports **) \u2500\u2500
981
+ function matchPathGlob(path, pattern) {
982
+ const p = path.replace(/\\\\\\\\/g, '/').toLowerCase();
983
+ const g = pattern.replace(/\\\\\\\\/g, '/').toLowerCase();
984
+ if (p === g) return true;
985
+ if (g.includes('**')) {
986
+ const parts = g.split('**');
987
+ if (parts.length === 2) {
988
+ const [pre, suf] = parts;
989
+ const matchPre = !pre || p.startsWith(pre) || p.includes(pre);
990
+ const matchSuf = !suf || p.endsWith(suf) || p.includes(suf.slice(1));
991
+ return matchPre && matchSuf;
992
+ }
993
+ }
994
+ return matchGlob(p, g);
995
+ }
980
996
 
981
- function checkPath(val) {
982
- if (typeof val !== 'string' || val.length < 2) return null;
983
- for (const p of PATH_TRAVERSAL) if (p.test(val)) return 'Path traversal detected';
984
- for (const p of SENSITIVE_PATHS) if (p.test(val)) return 'Sensitive file access blocked';
985
- return null;
997
+ // \u2500\u2500 Extract Functions (deep scan all string values) \u2500\u2500
998
+ function scanStrings(obj) {
999
+ const strings = [];
1000
+ function walk(v) {
1001
+ if (typeof v === 'string' && v.trim()) strings.push(v.trim());
1002
+ else if (Array.isArray(v)) v.forEach(walk);
1003
+ else if (v && typeof v === 'object') Object.values(v).forEach(walk);
1004
+ }
1005
+ walk(obj);
1006
+ return strings;
986
1007
  }
987
1008
 
988
- function checkValue(val) {
989
- if (typeof val !== 'string' || val.length < 2) return null;
990
- for (const p of SSRF) if (p.test(val)) return 'SSRF attempt blocked';
991
- for (const p of SQL_INJECTION) if (p.test(val)) return 'SQL injection detected';
992
- if (val.length > 10000) return 'Input too long (max 10000 chars)';
993
- return null;
1009
+ function looksLikeFilename(s) {
1010
+ if (s.startsWith('.')) return true;
1011
+ if (/\\.\\w+$/.test(s)) return true;
1012
+ const known = ['id_rsa','id_dsa','id_ecdsa','id_ed25519','authorized_keys','known_hosts','makefile','dockerfile'];
1013
+ return known.includes(s.toLowerCase());
1014
+ }
1015
+
1016
+ function extractFilenames(args) {
1017
+ const names = new Set();
1018
+ for (const s of scanStrings(args)) {
1019
+ if (/^https?:\\/\\//i.test(s)) continue;
1020
+ if (s.includes('/') || s.includes('\\\\')) {
1021
+ const base = s.replace(/\\\\\\\\/g, '/').split('/').pop();
1022
+ if (base) names.add(base);
1023
+ continue;
1024
+ }
1025
+ if (s.includes(' ')) {
1026
+ for (const tok of s.split(/\\s+/)) {
1027
+ if (tok.includes('/') || tok.includes('\\\\')) {
1028
+ const b = tok.replace(/\\\\\\\\/g, '/').split('/').pop();
1029
+ if (b && looksLikeFilename(b)) names.add(b);
1030
+ } else if (looksLikeFilename(tok)) names.add(tok);
1031
+ }
1032
+ continue;
1033
+ }
1034
+ if (looksLikeFilename(s)) names.add(s);
1035
+ }
1036
+ return [...names];
1037
+ }
1038
+
1039
+ function extractUrls(args) {
1040
+ const urls = new Set();
1041
+ for (const s of scanStrings(args)) {
1042
+ if (/^https?:\\/\\//i.test(s)) { urls.add(s); continue; }
1043
+ if (s.includes(' ')) {
1044
+ for (const tok of s.split(/\\s+/)) {
1045
+ if (/^https?:\\/\\//i.test(tok)) urls.add(tok);
1046
+ }
1047
+ }
1048
+ }
1049
+ return [...urls];
1050
+ }
1051
+
1052
+ function extractCommands(args) {
1053
+ const cmds = [];
1054
+ const fields = ['command', 'cmd', 'function', 'script', 'shell'];
1055
+ if (typeof args === 'object' && args) {
1056
+ for (const [k, v] of Object.entries(args)) {
1057
+ if (fields.includes(k.toLowerCase()) && typeof v === 'string') cmds.push(v);
1058
+ }
1059
+ }
1060
+ return cmds;
1061
+ }
1062
+
1063
+ function extractPaths(args) {
1064
+ const paths = [];
1065
+ for (const s of scanStrings(args)) {
1066
+ if (/^https?:\\/\\//i.test(s)) continue;
1067
+ if (s.includes('/') || s.includes('\\\\') || s.startsWith('.')) paths.push(s);
1068
+ }
1069
+ return paths;
994
1070
  }
995
1071
 
996
- // Arguments that contain file paths
997
- const PATH_ARGS = ['file_path', 'path', 'pattern', 'directory', 'url', 'uri', 'notebook_path'];
1072
+ // \u2500\u2500 Policy Evaluation \u2500\u2500
1073
+ function evaluate(policy, args) {
1074
+ if (!policy || !policy.rules) return null;
1075
+ const denyRules = policy.rules
1076
+ .filter(r => r.effect === 'DENY' && r.enabled !== false)
1077
+ .sort((a, b) => (a.priority || 100) - (b.priority || 100));
998
1078
 
999
- function checkBashCommand(cmd) {
1000
- if (typeof cmd !== 'string') return null;
1001
- for (const p of DANGEROUS_COMMANDS) if (p.test(cmd)) return 'Dangerous command blocked: ' + cmd.slice(0, 80);
1002
- for (const p of SSRF_IN_CMD) if (p.test(cmd)) return 'SSRF attempt blocked in command: ' + cmd.slice(0, 80);
1079
+ for (const rule of denyRules) {
1080
+ // Filename constraints
1081
+ if (rule.filenameConstraints && rule.filenameConstraints.denied) {
1082
+ const filenames = extractFilenames(args);
1083
+ for (const fn of filenames) {
1084
+ for (const pat of rule.filenameConstraints.denied) {
1085
+ if (matchGlob(fn, pat)) return 'Blocked by policy: filename "' + fn + '" matches "' + pat + '"';
1086
+ }
1087
+ }
1088
+ }
1089
+ // URL constraints
1090
+ if (rule.urlConstraints && rule.urlConstraints.denied) {
1091
+ const urls = extractUrls(args);
1092
+ for (const url of urls) {
1093
+ for (const pat of rule.urlConstraints.denied) {
1094
+ if (matchGlob(url, pat)) return 'Blocked by policy: URL "' + url + '" matches "' + pat + '"';
1095
+ }
1096
+ }
1097
+ }
1098
+ // Command constraints
1099
+ if (rule.commandConstraints && rule.commandConstraints.denied) {
1100
+ const cmds = extractCommands(args);
1101
+ for (const cmd of cmds) {
1102
+ for (const pat of rule.commandConstraints.denied) {
1103
+ if (matchGlob(cmd, pat)) return 'Blocked by policy: command "' + cmd.slice(0,60) + '" matches "' + pat + '"';
1104
+ }
1105
+ }
1106
+ }
1107
+ // Path constraints
1108
+ if (rule.pathConstraints && rule.pathConstraints.denied) {
1109
+ const paths = extractPaths(args);
1110
+ for (const p of paths) {
1111
+ for (const pat of rule.pathConstraints.denied) {
1112
+ if (matchPathGlob(p, pat)) return 'Blocked by policy: path "' + p + '" matches "' + pat + '"';
1113
+ }
1114
+ }
1115
+ }
1116
+ }
1003
1117
  return null;
1004
1118
  }
1005
1119
 
1120
+ // \u2500\u2500 Main \u2500\u2500
1006
1121
  let input = '';
1007
1122
  process.stdin.on('data', c => input += c);
1008
1123
  process.stdin.on('end', async () => {
1009
1124
  try {
1010
1125
  const data = JSON.parse(input);
1011
- const tool = data.tool_name || '';
1012
1126
  const args = data.tool_input || {};
1013
- const start = Date.now();
1014
-
1015
- let threat = null;
1016
-
1017
- // Check Bash commands for dangerous patterns
1018
- if (tool === 'Bash' && args.command) {
1019
- threat = checkBashCommand(args.command);
1020
- }
1021
1127
 
1022
- // Check arguments based on type
1023
- if (!threat) {
1024
- for (const [key, val] of Object.entries(args)) {
1025
- if (tool === 'Bash' && key === 'command') continue;
1026
- if (key === 'content' || key === 'new_source' || key === 'new_string' || key === 'old_string' || key === 'description') continue;
1027
- if (PATH_ARGS.includes(key)) {
1028
- threat = checkPath(val);
1029
- } else {
1030
- threat = checkValue(val);
1128
+ // \u2500\u2500 Self-protection: block access to hook files and settings \u2500\u2500
1129
+ const allStrings = scanStrings(args).map(s => s.replace(/\\\\\\\\/g, '/').toLowerCase());
1130
+ const protectedPaths = ['.solongate', '.claude/settings.json', 'policy.json'];
1131
+ for (const s of allStrings) {
1132
+ for (const p of protectedPaths) {
1133
+ if (s.includes(p)) {
1134
+ const msg = 'SOLONGATE: Access to protected file "' + p + '" is blocked';
1135
+ process.stderr.write(msg);
1136
+ process.exit(2);
1031
1137
  }
1032
- if (threat) { threat = threat + ' (in ' + key + ')'; break; }
1033
1138
  }
1034
1139
  }
1035
1140
 
1036
- const ms = Date.now() - start;
1141
+ // Load policy
1142
+ let policy;
1143
+ try {
1144
+ policy = JSON.parse(readFileSync(resolve('policy.json'), 'utf-8'));
1145
+ } catch {
1146
+ process.exit(0); // No policy = allow all
1147
+ }
1037
1148
 
1038
- if (threat) {
1039
- // Send DENY audit log \u2014 MUST await before exit or fetch dies
1149
+ const reason = evaluate(policy, args);
1150
+
1151
+ if (reason) {
1040
1152
  if (API_KEY && API_KEY.startsWith('sg_live_')) {
1041
1153
  try {
1042
1154
  await fetch(API_URL + '/api/v1/audit-logs', {
1043
1155
  method: 'POST',
1044
1156
  headers: { 'Authorization': 'Bearer ' + API_KEY, 'Content-Type': 'application/json' },
1045
1157
  body: JSON.stringify({
1046
- tool, arguments: Object.fromEntries(Object.entries(args).map(([k,v]) =>
1047
- [k, typeof v === 'string' && v.length > 200 ? v.slice(0,200)+'...' : v])),
1048
- decision: 'DENY', reason: threat, source: 'claude-code-guard',
1049
- evaluationTimeMs: ms,
1158
+ tool: data.tool_name || '', arguments: args,
1159
+ decision: 'DENY', reason, source: 'claude-code-guard',
1050
1160
  }),
1051
1161
  signal: AbortSignal.timeout(3000),
1052
1162
  });
1053
1163
  } catch {}
1054
1164
  }
1055
- // Exit 2 = BLOCK. Write to both stdout and stderr for visibility.
1056
- const msg = 'SOLONGATE BLOCKED: ' + threat;
1057
- process.stdout.write(msg);
1058
- process.stderr.write(msg);
1165
+ process.stderr.write(reason);
1059
1166
  process.exit(2);
1060
1167
  }
1061
- } catch {
1062
- // On error, allow (fail-open)
1063
- }
1168
+ } catch {}
1064
1169
  process.exit(0);
1065
1170
  });
1066
1171
  `;
package/dist/init.js CHANGED
@@ -141,150 +141,226 @@ EXAMPLES
141
141
  }
142
142
  var GUARD_SCRIPT = `#!/usr/bin/env node
143
143
  /**
144
- * SolonGate Guard Hook for Claude Code (PreToolUse)
145
- * Blocks dangerous tool calls BEFORE they execute.
144
+ * SolonGate Policy Guard Hook (PreToolUse)
145
+ * Reads policy.json and blocks tool calls that violate constraints.
146
146
  * Exit code 2 = BLOCK, exit code 0 = ALLOW.
147
147
  * Auto-installed by: npx @solongate/proxy init
148
148
  */
149
+ import { readFileSync } from 'node:fs';
150
+ import { resolve } from 'node:path';
149
151
 
150
152
  const API_KEY = process.env.SOLONGATE_API_KEY || '';
151
153
  const API_URL = process.env.SOLONGATE_API_URL || 'https://api.solongate.com';
152
154
 
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 SSRF_IN_CMD = [
174
- /https?:\\/\\/localhost\\b/i, /https?:\\/\\/127\\./, /https?:\\/\\/0\\.0\\.0\\.0/,
175
- /https?:\\/\\/10\\./, /https?:\\/\\/172\\.(1[6-9]|2\\d|3[01])\\./,
176
- /https?:\\/\\/192\\.168\\./, /https?:\\/\\/169\\.254\\./,
177
- /metadata\\.google\\.internal/i,
178
- /\\b127\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b/, /\\b0\\.0\\.0\\.0\\b/,
179
- /\\b10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b/,
180
- /\\b172\\.(1[6-9]|2\\d|3[01])\\.\\d{1,3}\\.\\d{1,3}\\b/,
181
- /\\b192\\.168\\.\\d{1,3}\\.\\d{1,3}\\b/,
182
- /\\b169\\.254\\.\\d{1,3}\\.\\d{1,3}\\b/,
183
- ];
184
- const SQL_INJECTION = [
185
- /'\\s{0,20}(OR|AND)\\s{0,20}'.{0,200}'/i,
186
- /'\\s{0,10};\\s{0,10}(DROP|DELETE|UPDATE|INSERT|ALTER|CREATE|EXEC)/i,
187
- /UNION\\s+(ALL\\s+)?SELECT/i, /\\bSLEEP\\s*\\(/i, /\\bWAITFOR\\s+DELAY/i,
188
- ];
155
+ // \u2500\u2500 Glob Matching \u2500\u2500
156
+ function matchGlob(str, pattern) {
157
+ if (pattern === '*') return true;
158
+ const s = str.toLowerCase();
159
+ const p = pattern.toLowerCase();
160
+ if (s === p) return true;
161
+ const startsW = p.startsWith('*');
162
+ const endsW = p.endsWith('*');
163
+ if (startsW && endsW) { const infix = p.slice(1, -1); return infix.length > 0 && s.includes(infix); }
164
+ if (startsW) return s.endsWith(p.slice(1));
165
+ if (endsW) return s.startsWith(p.slice(0, -1));
166
+ const idx = p.indexOf('*');
167
+ if (idx !== -1) {
168
+ const pre = p.slice(0, idx);
169
+ const suf = p.slice(idx + 1);
170
+ return s.startsWith(pre) && s.endsWith(suf) && s.length >= pre.length + suf.length;
171
+ }
172
+ return false;
173
+ }
189
174
 
190
- // Dangerous command patterns for Bash tool
191
- const DANGEROUS_COMMANDS = [
192
- /\\brm\\s+(-[a-zA-Z]*f|-[a-zA-Z]*r|--force|--recursive)/i,
193
- /\\brm\\s+-rf\\b/i,
194
- /\\bmkfs\\b/i, /\\bdd\\s+if=/i,
195
- /\\b(shutdown|reboot|halt|poweroff)\\b/i,
196
- /\\bchmod\\s+777\\b/,
197
- /\\bcurl\\b.{0,200}\\|\\s*(sh|bash)\\b/i,
198
- /\\bwget\\b.{0,200}\\|\\s*(sh|bash)\\b/i,
199
- /\\bnc\\s+-[a-z]*l/i, // netcat listener
200
- />(\\s*)\\/dev\\/sd/, // writing to raw disk
201
- /\\bgit\\s+push\\s+.*--force\\b/i,
202
- /\\bgit\\s+reset\\s+--hard\\b/i,
203
- ];
175
+ // \u2500\u2500 Path Glob (supports **) \u2500\u2500
176
+ function matchPathGlob(path, pattern) {
177
+ const p = path.replace(/\\\\\\\\/g, '/').toLowerCase();
178
+ const g = pattern.replace(/\\\\\\\\/g, '/').toLowerCase();
179
+ if (p === g) return true;
180
+ if (g.includes('**')) {
181
+ const parts = g.split('**');
182
+ if (parts.length === 2) {
183
+ const [pre, suf] = parts;
184
+ const matchPre = !pre || p.startsWith(pre) || p.includes(pre);
185
+ const matchSuf = !suf || p.endsWith(suf) || p.includes(suf.slice(1));
186
+ return matchPre && matchSuf;
187
+ }
188
+ }
189
+ return matchGlob(p, g);
190
+ }
204
191
 
205
- function checkPath(val) {
206
- if (typeof val !== 'string' || val.length < 2) return null;
207
- for (const p of PATH_TRAVERSAL) if (p.test(val)) return 'Path traversal detected';
208
- for (const p of SENSITIVE_PATHS) if (p.test(val)) return 'Sensitive file access blocked';
209
- return null;
192
+ // \u2500\u2500 Extract Functions (deep scan all string values) \u2500\u2500
193
+ function scanStrings(obj) {
194
+ const strings = [];
195
+ function walk(v) {
196
+ if (typeof v === 'string' && v.trim()) strings.push(v.trim());
197
+ else if (Array.isArray(v)) v.forEach(walk);
198
+ else if (v && typeof v === 'object') Object.values(v).forEach(walk);
199
+ }
200
+ walk(obj);
201
+ return strings;
210
202
  }
211
203
 
212
- function checkValue(val) {
213
- if (typeof val !== 'string' || val.length < 2) return null;
214
- for (const p of SSRF) if (p.test(val)) return 'SSRF attempt blocked';
215
- for (const p of SQL_INJECTION) if (p.test(val)) return 'SQL injection detected';
216
- if (val.length > 10000) return 'Input too long (max 10000 chars)';
217
- return null;
204
+ function looksLikeFilename(s) {
205
+ if (s.startsWith('.')) return true;
206
+ if (/\\.\\w+$/.test(s)) return true;
207
+ const known = ['id_rsa','id_dsa','id_ecdsa','id_ed25519','authorized_keys','known_hosts','makefile','dockerfile'];
208
+ return known.includes(s.toLowerCase());
218
209
  }
219
210
 
220
- // Arguments that contain file paths
221
- const PATH_ARGS = ['file_path', 'path', 'pattern', 'directory', 'url', 'uri', 'notebook_path'];
211
+ function extractFilenames(args) {
212
+ const names = new Set();
213
+ for (const s of scanStrings(args)) {
214
+ if (/^https?:\\/\\//i.test(s)) continue;
215
+ if (s.includes('/') || s.includes('\\\\')) {
216
+ const base = s.replace(/\\\\\\\\/g, '/').split('/').pop();
217
+ if (base) names.add(base);
218
+ continue;
219
+ }
220
+ if (s.includes(' ')) {
221
+ for (const tok of s.split(/\\s+/)) {
222
+ if (tok.includes('/') || tok.includes('\\\\')) {
223
+ const b = tok.replace(/\\\\\\\\/g, '/').split('/').pop();
224
+ if (b && looksLikeFilename(b)) names.add(b);
225
+ } else if (looksLikeFilename(tok)) names.add(tok);
226
+ }
227
+ continue;
228
+ }
229
+ if (looksLikeFilename(s)) names.add(s);
230
+ }
231
+ return [...names];
232
+ }
233
+
234
+ function extractUrls(args) {
235
+ const urls = new Set();
236
+ for (const s of scanStrings(args)) {
237
+ if (/^https?:\\/\\//i.test(s)) { urls.add(s); continue; }
238
+ if (s.includes(' ')) {
239
+ for (const tok of s.split(/\\s+/)) {
240
+ if (/^https?:\\/\\//i.test(tok)) urls.add(tok);
241
+ }
242
+ }
243
+ }
244
+ return [...urls];
245
+ }
246
+
247
+ function extractCommands(args) {
248
+ const cmds = [];
249
+ const fields = ['command', 'cmd', 'function', 'script', 'shell'];
250
+ if (typeof args === 'object' && args) {
251
+ for (const [k, v] of Object.entries(args)) {
252
+ if (fields.includes(k.toLowerCase()) && typeof v === 'string') cmds.push(v);
253
+ }
254
+ }
255
+ return cmds;
256
+ }
222
257
 
223
- function checkBashCommand(cmd) {
224
- if (typeof cmd !== 'string') return null;
225
- for (const p of DANGEROUS_COMMANDS) if (p.test(cmd)) return 'Dangerous command blocked: ' + cmd.slice(0, 80);
226
- for (const p of SSRF_IN_CMD) if (p.test(cmd)) return 'SSRF attempt blocked in command: ' + cmd.slice(0, 80);
258
+ function extractPaths(args) {
259
+ const paths = [];
260
+ for (const s of scanStrings(args)) {
261
+ if (/^https?:\\/\\//i.test(s)) continue;
262
+ if (s.includes('/') || s.includes('\\\\') || s.startsWith('.')) paths.push(s);
263
+ }
264
+ return paths;
265
+ }
266
+
267
+ // \u2500\u2500 Policy Evaluation \u2500\u2500
268
+ function evaluate(policy, args) {
269
+ if (!policy || !policy.rules) return null;
270
+ const denyRules = policy.rules
271
+ .filter(r => r.effect === 'DENY' && r.enabled !== false)
272
+ .sort((a, b) => (a.priority || 100) - (b.priority || 100));
273
+
274
+ for (const rule of denyRules) {
275
+ // Filename constraints
276
+ if (rule.filenameConstraints && rule.filenameConstraints.denied) {
277
+ const filenames = extractFilenames(args);
278
+ for (const fn of filenames) {
279
+ for (const pat of rule.filenameConstraints.denied) {
280
+ if (matchGlob(fn, pat)) return 'Blocked by policy: filename "' + fn + '" matches "' + pat + '"';
281
+ }
282
+ }
283
+ }
284
+ // URL constraints
285
+ if (rule.urlConstraints && rule.urlConstraints.denied) {
286
+ const urls = extractUrls(args);
287
+ for (const url of urls) {
288
+ for (const pat of rule.urlConstraints.denied) {
289
+ if (matchGlob(url, pat)) return 'Blocked by policy: URL "' + url + '" matches "' + pat + '"';
290
+ }
291
+ }
292
+ }
293
+ // Command constraints
294
+ if (rule.commandConstraints && rule.commandConstraints.denied) {
295
+ const cmds = extractCommands(args);
296
+ for (const cmd of cmds) {
297
+ for (const pat of rule.commandConstraints.denied) {
298
+ if (matchGlob(cmd, pat)) return 'Blocked by policy: command "' + cmd.slice(0,60) + '" matches "' + pat + '"';
299
+ }
300
+ }
301
+ }
302
+ // Path constraints
303
+ if (rule.pathConstraints && rule.pathConstraints.denied) {
304
+ const paths = extractPaths(args);
305
+ for (const p of paths) {
306
+ for (const pat of rule.pathConstraints.denied) {
307
+ if (matchPathGlob(p, pat)) return 'Blocked by policy: path "' + p + '" matches "' + pat + '"';
308
+ }
309
+ }
310
+ }
311
+ }
227
312
  return null;
228
313
  }
229
314
 
315
+ // \u2500\u2500 Main \u2500\u2500
230
316
  let input = '';
231
317
  process.stdin.on('data', c => input += c);
232
318
  process.stdin.on('end', async () => {
233
319
  try {
234
320
  const data = JSON.parse(input);
235
- const tool = data.tool_name || '';
236
321
  const args = data.tool_input || {};
237
- const start = Date.now();
238
-
239
- let threat = null;
240
322
 
241
- // Check Bash commands for dangerous patterns
242
- if (tool === 'Bash' && args.command) {
243
- threat = checkBashCommand(args.command);
244
- }
245
-
246
- // Check arguments based on type
247
- if (!threat) {
248
- for (const [key, val] of Object.entries(args)) {
249
- if (tool === 'Bash' && key === 'command') continue;
250
- if (key === 'content' || key === 'new_source' || key === 'new_string' || key === 'old_string' || key === 'description') continue;
251
- if (PATH_ARGS.includes(key)) {
252
- threat = checkPath(val);
253
- } else {
254
- threat = checkValue(val);
323
+ // \u2500\u2500 Self-protection: block access to hook files and settings \u2500\u2500
324
+ const allStrings = scanStrings(args).map(s => s.replace(/\\\\\\\\/g, '/').toLowerCase());
325
+ const protectedPaths = ['.solongate', '.claude/settings.json', 'policy.json'];
326
+ for (const s of allStrings) {
327
+ for (const p of protectedPaths) {
328
+ if (s.includes(p)) {
329
+ const msg = 'SOLONGATE: Access to protected file "' + p + '" is blocked';
330
+ process.stderr.write(msg);
331
+ process.exit(2);
255
332
  }
256
- if (threat) { threat = threat + ' (in ' + key + ')'; break; }
257
333
  }
258
334
  }
259
335
 
260
- const ms = Date.now() - start;
336
+ // Load policy
337
+ let policy;
338
+ try {
339
+ policy = JSON.parse(readFileSync(resolve('policy.json'), 'utf-8'));
340
+ } catch {
341
+ process.exit(0); // No policy = allow all
342
+ }
343
+
344
+ const reason = evaluate(policy, args);
261
345
 
262
- if (threat) {
263
- // Send DENY audit log \u2014 MUST await before exit or fetch dies
346
+ if (reason) {
264
347
  if (API_KEY && API_KEY.startsWith('sg_live_')) {
265
348
  try {
266
349
  await fetch(API_URL + '/api/v1/audit-logs', {
267
350
  method: 'POST',
268
351
  headers: { 'Authorization': 'Bearer ' + API_KEY, 'Content-Type': 'application/json' },
269
352
  body: JSON.stringify({
270
- tool, arguments: Object.fromEntries(Object.entries(args).map(([k,v]) =>
271
- [k, typeof v === 'string' && v.length > 200 ? v.slice(0,200)+'...' : v])),
272
- decision: 'DENY', reason: threat, source: 'claude-code-guard',
273
- evaluationTimeMs: ms,
353
+ tool: data.tool_name || '', arguments: args,
354
+ decision: 'DENY', reason, source: 'claude-code-guard',
274
355
  }),
275
356
  signal: AbortSignal.timeout(3000),
276
357
  });
277
358
  } catch {}
278
359
  }
279
- // Exit 2 = BLOCK. Write to both stdout and stderr for visibility.
280
- const msg = 'SOLONGATE BLOCKED: ' + threat;
281
- process.stdout.write(msg);
282
- process.stderr.write(msg);
360
+ process.stderr.write(reason);
283
361
  process.exit(2);
284
362
  }
285
- } catch {
286
- // On error, allow (fail-open)
287
- }
363
+ } catch {}
288
364
  process.exit(0);
289
365
  });
290
366
  `;
@@ -354,15 +430,42 @@ function installHooks() {
354
430
  const auditPath = join(hooksDir, "audit.mjs");
355
431
  writeFileSync(auditPath, AUDIT_SCRIPT);
356
432
  console.log(` Created ${auditPath}`);
433
+ const claudeDir = resolve(".claude");
434
+ mkdirSync(claudeDir, { recursive: true });
435
+ const settingsPath = join(claudeDir, "settings.json");
436
+ const settings = {
437
+ hooks: {
438
+ PreToolUse: [
439
+ {
440
+ matcher: "",
441
+ hooks: [
442
+ { type: "command", command: "node .solongate/hooks/guard.mjs" }
443
+ ]
444
+ }
445
+ ],
446
+ PostToolUse: [
447
+ {
448
+ matcher: "",
449
+ hooks: [
450
+ { type: "command", command: "node .solongate/hooks/audit.mjs" }
451
+ ]
452
+ }
453
+ ]
454
+ }
455
+ };
456
+ let existing = {};
457
+ try {
458
+ existing = JSON.parse(readFileSync(settingsPath, "utf-8"));
459
+ } catch {
460
+ }
461
+ const merged = { ...existing, hooks: settings.hooks };
462
+ writeFileSync(settingsPath, JSON.stringify(merged, null, 2) + "\n");
463
+ console.log(` Created ${settingsPath}`);
357
464
  console.log("");
358
- console.log(" Hooks installed in .solongate/hooks/");
359
- console.log(" guard.mjs \u2192 blocks dangerous calls (pre-execution)");
465
+ console.log(" Hooks installed:");
466
+ console.log(" guard.mjs \u2192 blocks policy-violating calls (pre-execution)");
360
467
  console.log(" audit.mjs \u2192 logs all calls to dashboard (post-execution)");
361
- console.log("");
362
- console.log(" To activate hooks in your MCP client:");
363
- console.log(" Claude Code \u2192 .claude/settings.json");
364
- console.log(" Cursor \u2192 .cursor/settings.json");
365
- console.log(" Or run: node .solongate/hooks/guard.mjs (stdin: JSON)");
468
+ console.log(" .claude/settings.json \u2192 hooks activated for Claude Code");
366
469
  }
367
470
  function ensureEnvFile() {
368
471
  const envPath = resolve(".env");
@@ -468,6 +571,8 @@ async function main() {
468
571
  if (alreadyProtected.length === serverNames.length) {
469
572
  console.log(" All servers are already protected by SolonGate!");
470
573
  ensureEnvFile();
574
+ console.log("");
575
+ installHooks();
471
576
  process.exit(0);
472
577
  }
473
578
  if (!options.all) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.5.9",
3
+ "version": "0.6.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": {