@solongate/proxy 0.5.9 → 0.6.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.
Files changed (3) hide show
  1. package/dist/index.js +203 -111
  2. package/dist/init.js +203 -111
  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,213 @@ 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);
1031
- }
1032
- if (threat) { threat = threat + ' (in ' + key + ')'; break; }
1033
- }
1128
+ // Load policy
1129
+ let policy;
1130
+ try {
1131
+ policy = JSON.parse(readFileSync(resolve('policy.json'), 'utf-8'));
1132
+ } catch {
1133
+ process.exit(0); // No policy = allow all
1034
1134
  }
1035
1135
 
1036
- const ms = Date.now() - start;
1136
+ const reason = evaluate(policy, args);
1037
1137
 
1038
- if (threat) {
1039
- // Send DENY audit log \u2014 MUST await before exit or fetch dies
1138
+ if (reason) {
1040
1139
  if (API_KEY && API_KEY.startsWith('sg_live_')) {
1041
1140
  try {
1042
1141
  await fetch(API_URL + '/api/v1/audit-logs', {
1043
1142
  method: 'POST',
1044
1143
  headers: { 'Authorization': 'Bearer ' + API_KEY, 'Content-Type': 'application/json' },
1045
1144
  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,
1145
+ tool: data.tool_name || '', arguments: args,
1146
+ decision: 'DENY', reason, source: 'claude-code-guard',
1050
1147
  }),
1051
1148
  signal: AbortSignal.timeout(3000),
1052
1149
  });
1053
1150
  } catch {}
1054
1151
  }
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);
1152
+ process.stderr.write(reason);
1059
1153
  process.exit(2);
1060
1154
  }
1061
- } catch {
1062
- // On error, allow (fail-open)
1063
- }
1155
+ } catch {}
1064
1156
  process.exit(0);
1065
1157
  });
1066
1158
  `;
package/dist/init.js CHANGED
@@ -141,150 +141,213 @@ 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);
323
+ // Load policy
324
+ let policy;
325
+ try {
326
+ policy = JSON.parse(readFileSync(resolve('policy.json'), 'utf-8'));
327
+ } catch {
328
+ process.exit(0); // No policy = allow all
244
329
  }
245
330
 
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);
255
- }
256
- if (threat) { threat = threat + ' (in ' + key + ')'; break; }
257
- }
258
- }
259
-
260
- const ms = Date.now() - start;
331
+ const reason = evaluate(policy, args);
261
332
 
262
- if (threat) {
263
- // Send DENY audit log \u2014 MUST await before exit or fetch dies
333
+ if (reason) {
264
334
  if (API_KEY && API_KEY.startsWith('sg_live_')) {
265
335
  try {
266
336
  await fetch(API_URL + '/api/v1/audit-logs', {
267
337
  method: 'POST',
268
338
  headers: { 'Authorization': 'Bearer ' + API_KEY, 'Content-Type': 'application/json' },
269
339
  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,
340
+ tool: data.tool_name || '', arguments: args,
341
+ decision: 'DENY', reason, source: 'claude-code-guard',
274
342
  }),
275
343
  signal: AbortSignal.timeout(3000),
276
344
  });
277
345
  } catch {}
278
346
  }
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);
347
+ process.stderr.write(reason);
283
348
  process.exit(2);
284
349
  }
285
- } catch {
286
- // On error, allow (fail-open)
287
- }
350
+ } catch {}
288
351
  process.exit(0);
289
352
  });
290
353
  `;
@@ -354,15 +417,42 @@ function installHooks() {
354
417
  const auditPath = join(hooksDir, "audit.mjs");
355
418
  writeFileSync(auditPath, AUDIT_SCRIPT);
356
419
  console.log(` Created ${auditPath}`);
420
+ const claudeDir = resolve(".claude");
421
+ mkdirSync(claudeDir, { recursive: true });
422
+ const settingsPath = join(claudeDir, "settings.json");
423
+ const settings = {
424
+ hooks: {
425
+ PreToolUse: [
426
+ {
427
+ matcher: "",
428
+ hooks: [
429
+ { type: "command", command: "node .solongate/hooks/guard.mjs" }
430
+ ]
431
+ }
432
+ ],
433
+ PostToolUse: [
434
+ {
435
+ matcher: "",
436
+ hooks: [
437
+ { type: "command", command: "node .solongate/hooks/audit.mjs" }
438
+ ]
439
+ }
440
+ ]
441
+ }
442
+ };
443
+ let existing = {};
444
+ try {
445
+ existing = JSON.parse(readFileSync(settingsPath, "utf-8"));
446
+ } catch {
447
+ }
448
+ const merged = { ...existing, hooks: settings.hooks };
449
+ writeFileSync(settingsPath, JSON.stringify(merged, null, 2) + "\n");
450
+ console.log(` Created ${settingsPath}`);
357
451
  console.log("");
358
- console.log(" Hooks installed in .solongate/hooks/");
359
- console.log(" guard.mjs \u2192 blocks dangerous calls (pre-execution)");
452
+ console.log(" Hooks installed:");
453
+ console.log(" guard.mjs \u2192 blocks policy-violating calls (pre-execution)");
360
454
  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)");
455
+ console.log(" .claude/settings.json \u2192 hooks activated for Claude Code");
366
456
  }
367
457
  function ensureEnvFile() {
368
458
  const envPath = resolve(".env");
@@ -468,6 +558,8 @@ async function main() {
468
558
  if (alreadyProtected.length === serverNames.length) {
469
559
  console.log(" All servers are already protected by SolonGate!");
470
560
  ensureEnvFile();
561
+ console.log("");
562
+ installHooks();
471
563
  process.exit(0);
472
564
  }
473
565
  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.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": {