@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.
- package/dist/index.js +214 -109
- package/dist/init.js +214 -109
- 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
|
|
669
|
-
console.log(" guard.mjs \u2192 blocks
|
|
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
|
|
921
|
-
*
|
|
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
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
const
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
const
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
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
|
-
//
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
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
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
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
|
|
989
|
-
if (
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
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
|
-
//
|
|
997
|
-
|
|
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
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
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
|
-
//
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
if (
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1039
|
-
|
|
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:
|
|
1047
|
-
|
|
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
|
-
|
|
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
|
|
145
|
-
*
|
|
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
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
//
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
|
213
|
-
if (
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
221
|
-
const
|
|
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
|
|
224
|
-
|
|
225
|
-
for (const
|
|
226
|
-
|
|
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
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
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 (
|
|
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:
|
|
271
|
-
|
|
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
|
-
|
|
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
|
|
359
|
-
console.log(" guard.mjs \u2192 blocks
|
|
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.
|
|
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": {
|