@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.
- package/dist/index.js +203 -111
- package/dist/init.js +203 -111
- 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,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
|
|
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
|
-
|
|
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
|
|
1136
|
+
const reason = evaluate(policy, args);
|
|
1037
1137
|
|
|
1038
|
-
if (
|
|
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:
|
|
1047
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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:
|
|
271
|
-
|
|
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
|
-
|
|
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
|
|
359
|
-
console.log(" guard.mjs \u2192 blocks
|
|
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.
|
|
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": {
|