@solongate/proxy 0.25.7 → 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.
Files changed (2) hide show
  1. package/hooks/guard.mjs +134 -1
  2. package/package.json +1 -1
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
- const reason = evaluate(policy, args);
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.25.7",
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": {