sovr-mcp-proxy 6.0.1 → 7.0.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/LICENSE +54 -19
- package/README.md +387 -164
- package/dist/cli.js +1553 -24
- package/dist/cli.mjs +185 -18
- package/dist/commandNormalizer.d.mts +95 -0
- package/dist/commandNormalizer.d.ts +95 -0
- package/dist/commandNormalizer.js +365 -0
- package/dist/commandNormalizer.mjs +336 -0
- package/dist/hooksAdapter.d.mts +122 -0
- package/dist/hooksAdapter.d.ts +122 -0
- package/dist/hooksAdapter.js +321 -0
- package/dist/hooksAdapter.mjs +291 -0
- package/dist/index.js +1065 -2
- package/dist/index.mjs +98 -2
- package/dist/toolReplacement.d.mts +108 -0
- package/dist/toolReplacement.d.ts +108 -0
- package/dist/toolReplacement.js +234 -0
- package/dist/toolReplacement.mjs +204 -0
- package/dist/whitelistEngine.d.mts +167 -0
- package/dist/whitelistEngine.d.ts +167 -0
- package/dist/whitelistEngine.js +435 -0
- package/dist/whitelistEngine.mjs +403 -0
- package/package.json +46 -41
- package/server.json +0 -14
package/dist/cli.mjs
CHANGED
|
@@ -9,6 +9,10 @@ async function main(args) {
|
|
|
9
9
|
await initCli(args.slice(1));
|
|
10
10
|
return;
|
|
11
11
|
}
|
|
12
|
+
case "hooks": {
|
|
13
|
+
await hooksCli(args.slice(1));
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
12
16
|
case "keys": {
|
|
13
17
|
const { keysCli } = await import("./apiKeyManager.mjs");
|
|
14
18
|
await keysCli(args.slice(1));
|
|
@@ -52,15 +56,151 @@ async function main(args) {
|
|
|
52
56
|
}
|
|
53
57
|
}
|
|
54
58
|
}
|
|
59
|
+
async function hooksCli(args) {
|
|
60
|
+
const { installHooks, uninstallHooks, detectClaudeCode, generateHooksConfig } = await import("./hooksAdapter.mjs");
|
|
61
|
+
const subcommand = args[0] || "install";
|
|
62
|
+
let failMode = "open";
|
|
63
|
+
let sovrEndpoint = "http://localhost:3271";
|
|
64
|
+
let projectDir = process.cwd();
|
|
65
|
+
for (let i = 1; i < args.length; i++) {
|
|
66
|
+
if (args[i] === "--fail-closed") failMode = "closed";
|
|
67
|
+
if (args[i] === "--fail-open") failMode = "open";
|
|
68
|
+
if (args[i] === "--endpoint" && args[i + 1]) sovrEndpoint = args[++i];
|
|
69
|
+
if (args[i] === "--dir" && args[i + 1]) projectDir = args[++i];
|
|
70
|
+
}
|
|
71
|
+
const config = {
|
|
72
|
+
endpoint: sovrEndpoint,
|
|
73
|
+
failMode,
|
|
74
|
+
toolPatterns: ["Bash", "Write", "Edit", "MultiEdit", "NotebookEdit"],
|
|
75
|
+
addAuditHook: true
|
|
76
|
+
};
|
|
77
|
+
switch (subcommand) {
|
|
78
|
+
case "install": {
|
|
79
|
+
const hooksConfig = generateHooksConfig(config);
|
|
80
|
+
const { generateHookScript, generateAuditHookScript } = await import("./hooksAdapter.mjs");
|
|
81
|
+
const preToolScript = generateHookScript(config);
|
|
82
|
+
const postToolScript = config.addAuditHook ? generateAuditHookScript(config) : null;
|
|
83
|
+
const fs = await import("fs");
|
|
84
|
+
const path = await import("path");
|
|
85
|
+
const settingsDir = path.join(projectDir, ".claude");
|
|
86
|
+
const settingsFile = path.join(settingsDir, "settings.json");
|
|
87
|
+
if (!fs.existsSync(settingsDir)) {
|
|
88
|
+
fs.mkdirSync(settingsDir, { recursive: true });
|
|
89
|
+
}
|
|
90
|
+
let existingSettings = {};
|
|
91
|
+
if (fs.existsSync(settingsFile)) {
|
|
92
|
+
try {
|
|
93
|
+
existingSettings = JSON.parse(fs.readFileSync(settingsFile, "utf-8"));
|
|
94
|
+
} catch {
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const newSettings = {
|
|
98
|
+
...existingSettings,
|
|
99
|
+
hooks: hooksConfig
|
|
100
|
+
};
|
|
101
|
+
fs.writeFileSync(settingsFile, JSON.stringify(newSettings, null, 2));
|
|
102
|
+
const scriptsDir = path.join(settingsDir, "hooks");
|
|
103
|
+
if (!fs.existsSync(scriptsDir)) {
|
|
104
|
+
fs.mkdirSync(scriptsDir, { recursive: true });
|
|
105
|
+
}
|
|
106
|
+
fs.writeFileSync(
|
|
107
|
+
path.join(scriptsDir, "sovr-pre-tool.sh"),
|
|
108
|
+
preToolScript,
|
|
109
|
+
{ mode: 493 }
|
|
110
|
+
);
|
|
111
|
+
if (postToolScript) {
|
|
112
|
+
fs.writeFileSync(
|
|
113
|
+
path.join(scriptsDir, "sovr-post-tool.sh"),
|
|
114
|
+
postToolScript,
|
|
115
|
+
{ mode: 493 }
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
process.stderr.write(`
|
|
119
|
+
\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
|
|
120
|
+
\u2551 SOVR Claude Code Hooks Installed \u2551
|
|
121
|
+
\u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563
|
|
122
|
+
\u2551 \u2551
|
|
123
|
+
\u2551 Settings: ${settingsFile.padEnd(47)}\u2551
|
|
124
|
+
\u2551 Scripts: ${scriptsDir.padEnd(47)}\u2551
|
|
125
|
+
\u2551 Fail mode: ${failMode.padEnd(47)}\u2551
|
|
126
|
+
\u2551 Endpoint: ${sovrEndpoint.padEnd(47)}\u2551
|
|
127
|
+
\u2551 \u2551
|
|
128
|
+
\u2551 Every Bash/Write/Edit call will now pass through SOVR. \u2551
|
|
129
|
+
\u2551 Use 'sovr-mcp-proxy hooks status' to verify. \u2551
|
|
130
|
+
\u2551 \u2551
|
|
131
|
+
\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
|
|
132
|
+
`);
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
case "uninstall": {
|
|
136
|
+
const fs = await import("fs");
|
|
137
|
+
const path = await import("path");
|
|
138
|
+
const settingsFile = path.join(projectDir, ".claude", "settings.json");
|
|
139
|
+
if (fs.existsSync(settingsFile)) {
|
|
140
|
+
try {
|
|
141
|
+
const settings = JSON.parse(fs.readFileSync(settingsFile, "utf-8"));
|
|
142
|
+
delete settings.hooks;
|
|
143
|
+
fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2));
|
|
144
|
+
process.stderr.write("[SOVR] Hooks removed from .claude/settings.json\n");
|
|
145
|
+
} catch (err) {
|
|
146
|
+
process.stderr.write(`[SOVR] Error removing hooks: ${err.message}
|
|
147
|
+
`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const scriptsDir = path.join(projectDir, ".claude", "hooks");
|
|
151
|
+
for (const f of ["sovr-pre-tool.sh", "sovr-post-tool.sh"]) {
|
|
152
|
+
const fp = path.join(scriptsDir, f);
|
|
153
|
+
if (fs.existsSync(fp)) fs.unlinkSync(fp);
|
|
154
|
+
}
|
|
155
|
+
process.stderr.write("[SOVR] Hooks uninstalled successfully.\n");
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
case "status": {
|
|
159
|
+
const fs = await import("fs");
|
|
160
|
+
const path = await import("path");
|
|
161
|
+
const settingsFile = path.join(projectDir, ".claude", "settings.json");
|
|
162
|
+
if (!fs.existsSync(settingsFile)) {
|
|
163
|
+
process.stderr.write("[SOVR] No .claude/settings.json found. Hooks not installed.\n");
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
const settings = JSON.parse(fs.readFileSync(settingsFile, "utf-8"));
|
|
168
|
+
if (settings.hooks) {
|
|
169
|
+
const preHooks = settings.hooks.PreToolUse || [];
|
|
170
|
+
const postHooks = settings.hooks.PostToolUse || [];
|
|
171
|
+
process.stderr.write(`[SOVR] Hooks Status:
|
|
172
|
+
`);
|
|
173
|
+
process.stderr.write(` PreToolUse hooks: ${preHooks.length}
|
|
174
|
+
`);
|
|
175
|
+
process.stderr.write(` PostToolUse hooks: ${postHooks.length}
|
|
176
|
+
`);
|
|
177
|
+
process.stderr.write(` Settings file: ${settingsFile}
|
|
178
|
+
`);
|
|
179
|
+
} else {
|
|
180
|
+
process.stderr.write("[SOVR] No hooks configured in settings.json\n");
|
|
181
|
+
}
|
|
182
|
+
} catch (err) {
|
|
183
|
+
process.stderr.write(`[SOVR] Error reading settings: ${err.message}
|
|
184
|
+
`);
|
|
185
|
+
}
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
default:
|
|
189
|
+
process.stderr.write(`Unknown hooks subcommand: ${subcommand}
|
|
190
|
+
Usage: sovr-mcp-proxy hooks [install|uninstall|status]
|
|
191
|
+
`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
55
194
|
function printHelp() {
|
|
56
195
|
process.stderr.write(`
|
|
57
|
-
sovr-mcp-proxy \u2014 The Responsibility Layer for AI Agents
|
|
196
|
+
sovr-mcp-proxy v7.0.0 \u2014 The Responsibility Layer for AI Agents
|
|
58
197
|
|
|
59
198
|
USAGE:
|
|
60
199
|
sovr-mcp-proxy [COMMAND] [OPTIONS]
|
|
61
200
|
|
|
62
201
|
COMMANDS:
|
|
63
202
|
init One-command setup for AI coding platforms
|
|
203
|
+
hooks Install/manage Claude Code Hooks (PreToolUse)
|
|
64
204
|
keys Manage SOVR API keys
|
|
65
205
|
usage View usage statistics
|
|
66
206
|
daemon Manage persistent background daemon
|
|
@@ -68,10 +208,25 @@ COMMANDS:
|
|
|
68
208
|
|
|
69
209
|
PROXY OPTIONS (default mode):
|
|
70
210
|
--upstream, -u Downstream MCP server command to wrap
|
|
211
|
+
--mode, -m Security mode: exclusive|enforce|advisory|monitor
|
|
212
|
+
exclusive \u2014 Replace all native tools (strongest)
|
|
213
|
+
enforce \u2014 Intercept + block violations (default)
|
|
214
|
+
advisory \u2014 Warn but don't block
|
|
215
|
+
monitor \u2014 Silent audit only
|
|
216
|
+
--whitelist, -w Whitelist preset or path: readonly|developer|production|<file>
|
|
71
217
|
--rules, -r Path to custom policy rules file
|
|
72
218
|
--timeout, -t Startup timeout in ms (default: 30000)
|
|
73
219
|
--verbose, -v Verbose output
|
|
74
220
|
|
|
221
|
+
HOOKS OPTIONS:
|
|
222
|
+
install Install Claude Code PreToolUse hooks (default)
|
|
223
|
+
uninstall Remove SOVR hooks from .claude/settings.json
|
|
224
|
+
status Show current hooks configuration
|
|
225
|
+
--fail-closed Block on SOVR unreachable (default: fail-open)
|
|
226
|
+
--fail-open Allow on SOVR unreachable
|
|
227
|
+
--endpoint URL SOVR daemon endpoint (default: http://localhost:3271)
|
|
228
|
+
--dir PATH Project directory (default: cwd)
|
|
229
|
+
|
|
75
230
|
INIT OPTIONS:
|
|
76
231
|
--claude-code Configure Claude Code
|
|
77
232
|
--claude-desktop Configure Claude Desktop
|
|
@@ -100,31 +255,43 @@ DAEMON OPTIONS:
|
|
|
100
255
|
--foreground Run in foreground (for debugging)
|
|
101
256
|
--verbose Verbose logging
|
|
102
257
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
258
|
+
SECURITY MODES:
|
|
259
|
+
\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
|
|
260
|
+
\u2502 exclusive \u2502 SOVR replaces native Bash/Shell tools. \u2502
|
|
261
|
+
\u2502 \u2502 LLM can ONLY execute through sovr_exec. \u2502
|
|
262
|
+
\u2502 \u2502 Bypass is architecturally impossible. \u2502
|
|
263
|
+
\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524
|
|
264
|
+
\u2502 enforce \u2502 Native tools preserved but all calls intercepted.\u2502
|
|
265
|
+
\u2502 \u2502 Violations are blocked with error response. \u2502
|
|
266
|
+
\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524
|
|
267
|
+
\u2502 advisory \u2502 Violations trigger warnings in stderr. \u2502
|
|
268
|
+
\u2502 \u2502 Calls are still forwarded to upstream. \u2502
|
|
269
|
+
\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524
|
|
270
|
+
\u2502 monitor \u2502 Silent audit logging only. \u2502
|
|
271
|
+
\u2502 \u2502 No warnings, no blocking. \u2502
|
|
272
|
+
\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
|
|
106
273
|
|
|
107
|
-
|
|
108
|
-
|
|
274
|
+
WHITELIST PRESETS:
|
|
275
|
+
readonly \u2014 git status/log/diff, ls, cat, find, grep (read-only ops)
|
|
276
|
+
developer \u2014 readonly + git add/commit/push, npm/pnpm, tsc, eslint
|
|
277
|
+
production \u2014 developer + docker, kubectl, terraform (with restrictions)
|
|
109
278
|
|
|
110
|
-
|
|
111
|
-
|
|
279
|
+
EXAMPLES:
|
|
280
|
+
# Maximum security: exclusive mode + readonly whitelist
|
|
281
|
+
npx sovr-mcp-proxy --upstream "npx @mcp/server-fs /tmp" --mode=exclusive --whitelist=readonly
|
|
112
282
|
|
|
113
|
-
#
|
|
114
|
-
npx sovr-mcp-proxy
|
|
283
|
+
# Developer workflow: enforce mode + developer whitelist
|
|
284
|
+
npx sovr-mcp-proxy --upstream "npx @mcp/server-fs ." --mode=enforce --whitelist=developer
|
|
115
285
|
|
|
116
|
-
#
|
|
117
|
-
npx sovr-mcp-proxy
|
|
286
|
+
# Install Claude Code hooks (no upstream needed)
|
|
287
|
+
npx sovr-mcp-proxy hooks install --fail-closed
|
|
118
288
|
|
|
119
|
-
#
|
|
120
|
-
npx sovr-mcp-proxy
|
|
289
|
+
# One-command setup (auto-detect platforms)
|
|
290
|
+
npx sovr-mcp-proxy init
|
|
121
291
|
|
|
122
|
-
# Start daemon with fail-closed
|
|
292
|
+
# Start daemon with fail-closed
|
|
123
293
|
npx sovr-mcp-proxy daemon start --fail-closed
|
|
124
294
|
|
|
125
|
-
# Check daemon status
|
|
126
|
-
npx sovr-mcp-proxy daemon status
|
|
127
|
-
|
|
128
295
|
ENVIRONMENT:
|
|
129
296
|
SOVR_API_KEY API key for authentication
|
|
130
297
|
SOVR_ENDPOINT Custom SOVR endpoint URL
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sovr/proxy-mcp — Command Normalizer
|
|
3
|
+
*
|
|
4
|
+
* Solves the Reddit feedback:
|
|
5
|
+
* "If you ban `rm`, I can just `bash -c 'rm -rf /'`" — coloradical5280
|
|
6
|
+
* "find / -delete, TRUNCATE TABLE, etc." — multiple users
|
|
7
|
+
*
|
|
8
|
+
* The normalizer decomposes complex commands into atomic operations
|
|
9
|
+
* that can each be individually evaluated against the whitelist.
|
|
10
|
+
*
|
|
11
|
+
* Key capabilities:
|
|
12
|
+
* 1. Unwrap shell wrappers: bash -c, sh -c, env, xargs, etc.
|
|
13
|
+
* 2. Split pipe chains: cmd1 | cmd2 | cmd3 → [cmd1, cmd2, cmd3]
|
|
14
|
+
* 3. Split command chains: cmd1 && cmd2; cmd3 → [cmd1, cmd2, cmd3]
|
|
15
|
+
* 4. Detect encoding tricks: base64 -d, hex encoding, etc.
|
|
16
|
+
* 5. Detect alias/function tricks: alias rm='rm -rf'
|
|
17
|
+
* 6. Extract the "effective command" from each segment
|
|
18
|
+
*
|
|
19
|
+
* Every segment must pass the whitelist independently.
|
|
20
|
+
* If ANY segment fails, the entire command is blocked.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```
|
|
24
|
+
* normalize('bash -c "rm -rf /tmp && echo done"')
|
|
25
|
+
* → [
|
|
26
|
+
* { raw: 'bash -c "rm -rf /tmp && echo done"', effective: 'rm -rf /tmp', type: 'unwrapped' },
|
|
27
|
+
* { raw: 'bash -c "rm -rf /tmp && echo done"', effective: 'echo done', type: 'chain-segment' },
|
|
28
|
+
* ]
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
/** A normalized command segment */
|
|
32
|
+
interface NormalizedSegment {
|
|
33
|
+
/** The original raw command (or the segment of it) */
|
|
34
|
+
raw: string;
|
|
35
|
+
/** The effective command after normalization */
|
|
36
|
+
effective: string;
|
|
37
|
+
/** How this segment was derived */
|
|
38
|
+
type: 'direct' | 'unwrapped' | 'pipe-segment' | 'chain-segment' | 'subshell' | 'heredoc' | 'encoded';
|
|
39
|
+
/** Warning flags */
|
|
40
|
+
warnings: string[];
|
|
41
|
+
}
|
|
42
|
+
/** Result of normalization */
|
|
43
|
+
interface NormalizationResult {
|
|
44
|
+
/** All command segments that need individual evaluation */
|
|
45
|
+
segments: NormalizedSegment[];
|
|
46
|
+
/** Whether the command structure itself is suspicious */
|
|
47
|
+
suspicious: boolean;
|
|
48
|
+
/** Reasons for suspicion */
|
|
49
|
+
suspicion_reasons: string[];
|
|
50
|
+
/** The original command */
|
|
51
|
+
original: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Normalize a command into atomic segments for individual evaluation.
|
|
55
|
+
*
|
|
56
|
+
* This is the main entry point. It:
|
|
57
|
+
* 1. Splits command chains (&&, ||, ;)
|
|
58
|
+
* 2. Splits pipe chains (|)
|
|
59
|
+
* 3. Unwraps shell wrappers (bash -c, sudo, env, etc.)
|
|
60
|
+
* 4. Detects encoding tricks
|
|
61
|
+
* 5. Detects destructive equivalents
|
|
62
|
+
*
|
|
63
|
+
* Returns all segments that need to be individually evaluated.
|
|
64
|
+
*/
|
|
65
|
+
declare function normalize(command: string): NormalizationResult;
|
|
66
|
+
/**
|
|
67
|
+
* Get the base command name (first word) from a command string.
|
|
68
|
+
*/
|
|
69
|
+
declare function getBaseCommand(command: string): string;
|
|
70
|
+
/**
|
|
71
|
+
* Check if a command contains any destructive equivalent patterns.
|
|
72
|
+
*/
|
|
73
|
+
declare function hasDestructiveEquivalent(command: string): {
|
|
74
|
+
found: boolean;
|
|
75
|
+
matches: Array<{
|
|
76
|
+
equivalent: string;
|
|
77
|
+
description: string;
|
|
78
|
+
}>;
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Check if a command uses any encoding/obfuscation tricks.
|
|
82
|
+
*/
|
|
83
|
+
declare function hasEncodingTricks(command: string): {
|
|
84
|
+
found: boolean;
|
|
85
|
+
tricks: Array<{
|
|
86
|
+
name: string;
|
|
87
|
+
risk: string;
|
|
88
|
+
}>;
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Summarize normalization result in a human-readable format.
|
|
92
|
+
*/
|
|
93
|
+
declare function summarize(result: NormalizationResult): string;
|
|
94
|
+
|
|
95
|
+
export { type NormalizationResult, type NormalizedSegment, getBaseCommand, hasDestructiveEquivalent, hasEncodingTricks, normalize, summarize };
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sovr/proxy-mcp — Command Normalizer
|
|
3
|
+
*
|
|
4
|
+
* Solves the Reddit feedback:
|
|
5
|
+
* "If you ban `rm`, I can just `bash -c 'rm -rf /'`" — coloradical5280
|
|
6
|
+
* "find / -delete, TRUNCATE TABLE, etc." — multiple users
|
|
7
|
+
*
|
|
8
|
+
* The normalizer decomposes complex commands into atomic operations
|
|
9
|
+
* that can each be individually evaluated against the whitelist.
|
|
10
|
+
*
|
|
11
|
+
* Key capabilities:
|
|
12
|
+
* 1. Unwrap shell wrappers: bash -c, sh -c, env, xargs, etc.
|
|
13
|
+
* 2. Split pipe chains: cmd1 | cmd2 | cmd3 → [cmd1, cmd2, cmd3]
|
|
14
|
+
* 3. Split command chains: cmd1 && cmd2; cmd3 → [cmd1, cmd2, cmd3]
|
|
15
|
+
* 4. Detect encoding tricks: base64 -d, hex encoding, etc.
|
|
16
|
+
* 5. Detect alias/function tricks: alias rm='rm -rf'
|
|
17
|
+
* 6. Extract the "effective command" from each segment
|
|
18
|
+
*
|
|
19
|
+
* Every segment must pass the whitelist independently.
|
|
20
|
+
* If ANY segment fails, the entire command is blocked.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```
|
|
24
|
+
* normalize('bash -c "rm -rf /tmp && echo done"')
|
|
25
|
+
* → [
|
|
26
|
+
* { raw: 'bash -c "rm -rf /tmp && echo done"', effective: 'rm -rf /tmp', type: 'unwrapped' },
|
|
27
|
+
* { raw: 'bash -c "rm -rf /tmp && echo done"', effective: 'echo done', type: 'chain-segment' },
|
|
28
|
+
* ]
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
/** A normalized command segment */
|
|
32
|
+
interface NormalizedSegment {
|
|
33
|
+
/** The original raw command (or the segment of it) */
|
|
34
|
+
raw: string;
|
|
35
|
+
/** The effective command after normalization */
|
|
36
|
+
effective: string;
|
|
37
|
+
/** How this segment was derived */
|
|
38
|
+
type: 'direct' | 'unwrapped' | 'pipe-segment' | 'chain-segment' | 'subshell' | 'heredoc' | 'encoded';
|
|
39
|
+
/** Warning flags */
|
|
40
|
+
warnings: string[];
|
|
41
|
+
}
|
|
42
|
+
/** Result of normalization */
|
|
43
|
+
interface NormalizationResult {
|
|
44
|
+
/** All command segments that need individual evaluation */
|
|
45
|
+
segments: NormalizedSegment[];
|
|
46
|
+
/** Whether the command structure itself is suspicious */
|
|
47
|
+
suspicious: boolean;
|
|
48
|
+
/** Reasons for suspicion */
|
|
49
|
+
suspicion_reasons: string[];
|
|
50
|
+
/** The original command */
|
|
51
|
+
original: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Normalize a command into atomic segments for individual evaluation.
|
|
55
|
+
*
|
|
56
|
+
* This is the main entry point. It:
|
|
57
|
+
* 1. Splits command chains (&&, ||, ;)
|
|
58
|
+
* 2. Splits pipe chains (|)
|
|
59
|
+
* 3. Unwraps shell wrappers (bash -c, sudo, env, etc.)
|
|
60
|
+
* 4. Detects encoding tricks
|
|
61
|
+
* 5. Detects destructive equivalents
|
|
62
|
+
*
|
|
63
|
+
* Returns all segments that need to be individually evaluated.
|
|
64
|
+
*/
|
|
65
|
+
declare function normalize(command: string): NormalizationResult;
|
|
66
|
+
/**
|
|
67
|
+
* Get the base command name (first word) from a command string.
|
|
68
|
+
*/
|
|
69
|
+
declare function getBaseCommand(command: string): string;
|
|
70
|
+
/**
|
|
71
|
+
* Check if a command contains any destructive equivalent patterns.
|
|
72
|
+
*/
|
|
73
|
+
declare function hasDestructiveEquivalent(command: string): {
|
|
74
|
+
found: boolean;
|
|
75
|
+
matches: Array<{
|
|
76
|
+
equivalent: string;
|
|
77
|
+
description: string;
|
|
78
|
+
}>;
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Check if a command uses any encoding/obfuscation tricks.
|
|
82
|
+
*/
|
|
83
|
+
declare function hasEncodingTricks(command: string): {
|
|
84
|
+
found: boolean;
|
|
85
|
+
tricks: Array<{
|
|
86
|
+
name: string;
|
|
87
|
+
risk: string;
|
|
88
|
+
}>;
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Summarize normalization result in a human-readable format.
|
|
92
|
+
*/
|
|
93
|
+
declare function summarize(result: NormalizationResult): string;
|
|
94
|
+
|
|
95
|
+
export { type NormalizationResult, type NormalizedSegment, getBaseCommand, hasDestructiveEquivalent, hasEncodingTricks, normalize, summarize };
|