@solongate/proxy 0.25.6 → 0.26.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 +3 -3
- package/hooks/guard.mjs +134 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6149,7 +6149,7 @@ DENY if the tool call could, directly or indirectly:
|
|
|
6149
6149
|
- Shell glob patterns (e.g., "cred*" could match "credentials.json")
|
|
6150
6150
|
- Command substitution ($(...), backticks)
|
|
6151
6151
|
- Process substitution (<(cat file), <(command file)) \u2014 ALWAYS check inside <(...) for protected files
|
|
6152
|
-
- Variable interpolation or string concatenation (e.g., f=".en"; cat
|
|
6152
|
+
- Variable interpolation or string concatenation (e.g., f=".en"; cat \${f}v builds ".env")
|
|
6153
6153
|
- Partial variable construction \u2014 if shell variables are concatenated to form a filename, analyze the RESULT
|
|
6154
6154
|
- Input redirection (< file)
|
|
6155
6155
|
- Script files that might read protected files
|
|
@@ -6165,7 +6165,7 @@ DENY if the tool call could, directly or indirectly:
|
|
|
6165
6165
|
CRITICAL patterns to watch for:
|
|
6166
6166
|
- <(cat .env) or <(cat ANYPROTECTEDFILE) \u2014 process substitution ALWAYS reads the file
|
|
6167
6167
|
- Compound commands with && or ; where ANY part touches a protected file \u2014 DENY the whole thing
|
|
6168
|
-
- Variable tricks like f=".en"; cat
|
|
6168
|
+
- Variable tricks like f=".en"; cat \${f}v \u2014 mentally evaluate the variable to see if it resolves to a protected filename
|
|
6169
6169
|
- tar/zip/cp that archives a protected file, even if the second command reads the archive \u2014 DENY both
|
|
6170
6170
|
|
|
6171
6171
|
ALLOW if:
|
|
@@ -6930,7 +6930,7 @@ ${msg.content.text}`;
|
|
|
6930
6930
|
if (rule.effect === "DENY" && rule.enabled !== false) {
|
|
6931
6931
|
const denied = rule.filenameConstraints?.denied;
|
|
6932
6932
|
if (denied) {
|
|
6933
|
-
for (const
|
|
6933
|
+
for (const f of denied) files.add(f);
|
|
6934
6934
|
}
|
|
6935
6935
|
}
|
|
6936
6936
|
}
|
package/hooks/guard.mjs
CHANGED
|
@@ -1013,7 +1013,140 @@ process.stdin.on('end', async () => {
|
|
|
1013
1013
|
process.exit(0); // No policy = allow all
|
|
1014
1014
|
}
|
|
1015
1015
|
|
|
1016
|
-
|
|
1016
|
+
let reason = evaluate(policy, args);
|
|
1017
|
+
|
|
1018
|
+
// ── AI Judge: semantic intent analysis (runs when policy ALLOWs) ──
|
|
1019
|
+
if (!reason) {
|
|
1020
|
+
const GROQ_KEY = process.env.GROQ_API_KEY || dotenv.GROQ_API_KEY || '';
|
|
1021
|
+
let aiJudgeEnabled = false;
|
|
1022
|
+
let aiJudgeModel = 'llama-3.1-8b-instant';
|
|
1023
|
+
let aiJudgeEndpoint = 'https://api.groq.com/openai';
|
|
1024
|
+
let aiJudgeTimeout = 5000;
|
|
1025
|
+
|
|
1026
|
+
// Check cloud config for AI Judge settings
|
|
1027
|
+
if (API_KEY && API_KEY.startsWith('sg_live_')) {
|
|
1028
|
+
try {
|
|
1029
|
+
const cfgRes = await fetch(API_URL + '/api/v1/project-config/ai-judge', {
|
|
1030
|
+
headers: { 'Authorization': 'Bearer ' + API_KEY },
|
|
1031
|
+
signal: AbortSignal.timeout(3000),
|
|
1032
|
+
});
|
|
1033
|
+
if (cfgRes.ok) {
|
|
1034
|
+
const cfg = await cfgRes.json();
|
|
1035
|
+
aiJudgeEnabled = Boolean(cfg.enabled);
|
|
1036
|
+
if (cfg.model) aiJudgeModel = cfg.model;
|
|
1037
|
+
if (cfg.endpoint) aiJudgeEndpoint = cfg.endpoint;
|
|
1038
|
+
if (cfg.timeoutMs) aiJudgeTimeout = cfg.timeoutMs;
|
|
1039
|
+
}
|
|
1040
|
+
} catch {}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
if (aiJudgeEnabled && GROQ_KEY) {
|
|
1044
|
+
try {
|
|
1045
|
+
// Extract protected files/paths from policy
|
|
1046
|
+
const protectedFiles = [];
|
|
1047
|
+
const protectedPathsList = [];
|
|
1048
|
+
if (policy && policy.rules) {
|
|
1049
|
+
for (const rule of policy.rules) {
|
|
1050
|
+
if (rule.effect === 'DENY' && rule.enabled !== false) {
|
|
1051
|
+
if (rule.filenameConstraints && rule.filenameConstraints.denied) {
|
|
1052
|
+
for (const f of rule.filenameConstraints.denied) protectedFiles.push(f);
|
|
1053
|
+
}
|
|
1054
|
+
if (rule.pathConstraints && rule.pathConstraints.denied) {
|
|
1055
|
+
for (const p of rule.pathConstraints.denied) protectedPathsList.push(p);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
const judgePayload = JSON.stringify({
|
|
1062
|
+
tool: toolName,
|
|
1063
|
+
arguments: args,
|
|
1064
|
+
protected_files: protectedFiles,
|
|
1065
|
+
protected_paths: protectedPathsList,
|
|
1066
|
+
denied_actions: ['file deletion', 'data exfiltration', 'remote code execution', 'environment variable leak', 'security control bypass'],
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
const systemPrompt = `You are a security judge for an AI coding tool. You evaluate tool calls and decide if they should be ALLOWED or DENIED.
|
|
1070
|
+
|
|
1071
|
+
You will receive a JSON object with:
|
|
1072
|
+
- "tool": the tool name being called (e.g., Bash, Write, Read, Edit)
|
|
1073
|
+
- "arguments": the tool's arguments
|
|
1074
|
+
- "protected_files": list of files that must NEVER be read, written, copied, moved, or accessed
|
|
1075
|
+
- "protected_paths": list of directories/paths that must NEVER be accessed
|
|
1076
|
+
- "denied_actions": list of actions that are never allowed
|
|
1077
|
+
|
|
1078
|
+
DENY if the tool call could, directly or indirectly:
|
|
1079
|
+
- Read, display, copy, move, or exfiltrate any protected file — even through:
|
|
1080
|
+
- Shell glob patterns (e.g., "cred*" could match "credentials.json")
|
|
1081
|
+
- Command substitution ($(...), backticks)
|
|
1082
|
+
- Process substitution (<(cat file), <(command file)) — ALWAYS check inside <(...) for protected files
|
|
1083
|
+
- Variable interpolation or string concatenation (e.g., f=".en"; cat \${f}v builds ".env")
|
|
1084
|
+
- Partial variable construction — if shell variables are concatenated to form a filename, analyze the RESULT
|
|
1085
|
+
- Input redirection (< file)
|
|
1086
|
+
- Script files that might read protected files
|
|
1087
|
+
- Find/exec patterns
|
|
1088
|
+
- Multi-stage operations: if command A archives/copies a protected file and command B reads the copy, DENY the ENTIRE chain
|
|
1089
|
+
- Any Unix/Windows utility that reads file content (cat, head, tail, less, more, type, perl, awk, sed, sort, diff, comm, paste, tee, xxd, od, strings, dd, tr, tar, zip, etc.)
|
|
1090
|
+
- Delete, modify, or overwrite any protected file or path
|
|
1091
|
+
- Send data to external URLs, webhooks, or attacker-controlled endpoints
|
|
1092
|
+
- Execute remotely downloaded code (curl|bash, wget|sh, etc.)
|
|
1093
|
+
- Leak environment variables (printenv, env, /proc/self/environ, npm run env, process.env)
|
|
1094
|
+
- Create scripts that bypass security controls
|
|
1095
|
+
|
|
1096
|
+
CRITICAL patterns to watch for:
|
|
1097
|
+
- <(cat .env) or <(cat ANYPROTECTEDFILE) — process substitution ALWAYS reads the file
|
|
1098
|
+
- Compound commands with && or ; where ANY part touches a protected file — DENY the whole thing
|
|
1099
|
+
- Variable tricks like f=".en"; cat \${f}v — mentally evaluate the variable to see if it resolves to a protected filename
|
|
1100
|
+
- tar/zip/cp that archives a protected file, even if the second command reads the archive — DENY both
|
|
1101
|
+
|
|
1102
|
+
ALLOW if:
|
|
1103
|
+
- The action is a normal development operation (ls, git status, npm build, etc.)
|
|
1104
|
+
- The action does not touch any protected file or path
|
|
1105
|
+
- The action is clearly benign
|
|
1106
|
+
|
|
1107
|
+
When in doubt, DENY. False positives are acceptable; false negatives are not.
|
|
1108
|
+
|
|
1109
|
+
Respond with ONLY valid JSON: {"decision": "ALLOW" or "DENY", "reason": "brief explanation", "confidence": 0.0 to 1.0}`;
|
|
1110
|
+
|
|
1111
|
+
const llmRes = await fetch(aiJudgeEndpoint + '/v1/chat/completions', {
|
|
1112
|
+
method: 'POST',
|
|
1113
|
+
headers: {
|
|
1114
|
+
'Content-Type': 'application/json',
|
|
1115
|
+
'Authorization': 'Bearer ' + GROQ_KEY,
|
|
1116
|
+
},
|
|
1117
|
+
body: JSON.stringify({
|
|
1118
|
+
model: aiJudgeModel,
|
|
1119
|
+
messages: [
|
|
1120
|
+
{ role: 'system', content: systemPrompt },
|
|
1121
|
+
{ role: 'user', content: judgePayload },
|
|
1122
|
+
],
|
|
1123
|
+
temperature: 0,
|
|
1124
|
+
max_tokens: 200,
|
|
1125
|
+
}),
|
|
1126
|
+
signal: AbortSignal.timeout(aiJudgeTimeout),
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
if (llmRes.ok) {
|
|
1130
|
+
const llmData = await llmRes.json();
|
|
1131
|
+
const content = llmData.choices?.[0]?.message?.content || '';
|
|
1132
|
+
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
|
1133
|
+
if (jsonMatch) {
|
|
1134
|
+
const verdict = JSON.parse(jsonMatch[0]);
|
|
1135
|
+
if (verdict.decision === 'DENY') {
|
|
1136
|
+
reason = '[AI Judge] ' + (verdict.reason || 'Blocked by semantic analysis');
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
} else {
|
|
1140
|
+
// Fail-closed: LLM error → DENY
|
|
1141
|
+
reason = '[AI Judge] LLM endpoint error (fail-closed)';
|
|
1142
|
+
}
|
|
1143
|
+
} catch (err) {
|
|
1144
|
+
// Fail-closed: timeout or parse error → DENY
|
|
1145
|
+
reason = '[AI Judge] ' + (err.message || 'error') + ' (fail-closed)';
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1017
1150
|
const decision = reason ? 'DENY' : 'ALLOW';
|
|
1018
1151
|
|
|
1019
1152
|
// ── Log ALL decisions to SolonGate Cloud ──
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solongate/proxy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.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": {
|