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
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/commandNormalizer.ts
|
|
21
|
+
var commandNormalizer_exports = {};
|
|
22
|
+
__export(commandNormalizer_exports, {
|
|
23
|
+
getBaseCommand: () => getBaseCommand,
|
|
24
|
+
hasDestructiveEquivalent: () => hasDestructiveEquivalent,
|
|
25
|
+
hasEncodingTricks: () => hasEncodingTricks,
|
|
26
|
+
normalize: () => normalize,
|
|
27
|
+
summarize: () => summarize
|
|
28
|
+
});
|
|
29
|
+
module.exports = __toCommonJS(commandNormalizer_exports);
|
|
30
|
+
var SHELL_WRAPPERS = [
|
|
31
|
+
// bash -c "cmd" / sh -c "cmd"
|
|
32
|
+
{
|
|
33
|
+
pattern: /^(?:bash|sh|zsh|dash|ksh)\s+-c\s+(?:"([^"]+)"|'([^']+)'|(\S+))/,
|
|
34
|
+
extract: (m) => m[1] || m[2] || m[3] || "",
|
|
35
|
+
name: "shell -c"
|
|
36
|
+
},
|
|
37
|
+
// env cmd args...
|
|
38
|
+
{
|
|
39
|
+
pattern: /^env\s+(?:-\S+\s+)*(?:\S+=\S+\s+)*(.+)/,
|
|
40
|
+
extract: (m) => m[1] || "",
|
|
41
|
+
name: "env"
|
|
42
|
+
},
|
|
43
|
+
// xargs cmd
|
|
44
|
+
{
|
|
45
|
+
pattern: /^xargs\s+(?:-\S+\s+)*(.+)/,
|
|
46
|
+
extract: (m) => m[1] || "",
|
|
47
|
+
name: "xargs"
|
|
48
|
+
},
|
|
49
|
+
// sudo cmd
|
|
50
|
+
{
|
|
51
|
+
pattern: /^sudo\s+(?:-\S+\s+)*(.+)/,
|
|
52
|
+
extract: (m) => m[1] || "",
|
|
53
|
+
name: "sudo"
|
|
54
|
+
},
|
|
55
|
+
// nohup cmd
|
|
56
|
+
{
|
|
57
|
+
pattern: /^nohup\s+(.+?)(?:\s*&\s*)?$/,
|
|
58
|
+
extract: (m) => m[1] || "",
|
|
59
|
+
name: "nohup"
|
|
60
|
+
},
|
|
61
|
+
// timeout N cmd
|
|
62
|
+
{
|
|
63
|
+
pattern: /^timeout\s+\d+[smhd]?\s+(.+)/,
|
|
64
|
+
extract: (m) => m[1] || "",
|
|
65
|
+
name: "timeout"
|
|
66
|
+
},
|
|
67
|
+
// nice -n N cmd
|
|
68
|
+
{
|
|
69
|
+
pattern: /^nice\s+(?:-n\s+\d+\s+)?(.+)/,
|
|
70
|
+
extract: (m) => m[1] || "",
|
|
71
|
+
name: "nice"
|
|
72
|
+
},
|
|
73
|
+
// strace / ltrace cmd
|
|
74
|
+
{
|
|
75
|
+
pattern: /^(?:strace|ltrace)\s+(?:-\S+\s+)*(.+)/,
|
|
76
|
+
extract: (m) => m[1] || "",
|
|
77
|
+
name: "trace"
|
|
78
|
+
},
|
|
79
|
+
// watch -n N cmd
|
|
80
|
+
{
|
|
81
|
+
pattern: /^watch\s+(?:-n\s+\d+\s+)?(.+)/,
|
|
82
|
+
extract: (m) => m[1] || "",
|
|
83
|
+
name: "watch"
|
|
84
|
+
}
|
|
85
|
+
];
|
|
86
|
+
var ENCODING_PATTERNS = [
|
|
87
|
+
// echo "base64" | base64 -d | bash
|
|
88
|
+
{
|
|
89
|
+
pattern: /base64\s+(?:-d|--decode)/,
|
|
90
|
+
name: "base64-decode",
|
|
91
|
+
risk: "Command may be hidden via base64 encoding"
|
|
92
|
+
},
|
|
93
|
+
// printf '\x72\x6d' (hex encoding)
|
|
94
|
+
{
|
|
95
|
+
pattern: /printf\s+.*\\x[0-9a-fA-F]{2}/,
|
|
96
|
+
name: "hex-printf",
|
|
97
|
+
risk: "Command may be hidden via hex encoding in printf"
|
|
98
|
+
},
|
|
99
|
+
// $'\x72\x6d' (ANSI-C quoting)
|
|
100
|
+
{
|
|
101
|
+
pattern: /\$'[^']*\\x[0-9a-fA-F]{2}/,
|
|
102
|
+
name: "ansi-c-quoting",
|
|
103
|
+
risk: "Command may be hidden via ANSI-C quoting"
|
|
104
|
+
},
|
|
105
|
+
// python -c "import os; os.system('rm -rf /')"
|
|
106
|
+
{
|
|
107
|
+
pattern: /python[23]?\s+-c\s+/,
|
|
108
|
+
name: "python-exec",
|
|
109
|
+
risk: "Arbitrary code execution via Python"
|
|
110
|
+
},
|
|
111
|
+
// perl -e "system('rm -rf /')"
|
|
112
|
+
{
|
|
113
|
+
pattern: /perl\s+-e\s+/,
|
|
114
|
+
name: "perl-exec",
|
|
115
|
+
risk: "Arbitrary code execution via Perl"
|
|
116
|
+
},
|
|
117
|
+
// ruby -e "system('rm -rf /')"
|
|
118
|
+
{
|
|
119
|
+
pattern: /ruby\s+-e\s+/,
|
|
120
|
+
name: "ruby-exec",
|
|
121
|
+
risk: "Arbitrary code execution via Ruby"
|
|
122
|
+
},
|
|
123
|
+
// eval "cmd"
|
|
124
|
+
{
|
|
125
|
+
pattern: /\beval\s+/,
|
|
126
|
+
name: "eval",
|
|
127
|
+
risk: "Dynamic command evaluation"
|
|
128
|
+
},
|
|
129
|
+
// curl ... | bash
|
|
130
|
+
{
|
|
131
|
+
pattern: /curl\s+.*\|\s*(?:bash|sh|zsh)/,
|
|
132
|
+
name: "curl-pipe-shell",
|
|
133
|
+
risk: "Remote code execution via curl | bash"
|
|
134
|
+
},
|
|
135
|
+
// wget ... -O - | bash
|
|
136
|
+
{
|
|
137
|
+
pattern: /wget\s+.*\|\s*(?:bash|sh|zsh)/,
|
|
138
|
+
name: "wget-pipe-shell",
|
|
139
|
+
risk: "Remote code execution via wget | bash"
|
|
140
|
+
}
|
|
141
|
+
];
|
|
142
|
+
var DESTRUCTIVE_EQUIVALENTS = [
|
|
143
|
+
// find / -delete (equivalent to rm -rf)
|
|
144
|
+
{ pattern: /find\s+.*-delete/, equivalent: "rm -rf", description: "find -delete is equivalent to rm -rf" },
|
|
145
|
+
// find / -exec rm {} (equivalent to rm -rf)
|
|
146
|
+
{ pattern: /find\s+.*-exec\s+rm/, equivalent: "rm -rf", description: "find -exec rm is equivalent to rm -rf" },
|
|
147
|
+
// TRUNCATE TABLE (equivalent to DELETE FROM)
|
|
148
|
+
{ pattern: /TRUNCATE\s+TABLE/i, equivalent: "DELETE FROM", description: "TRUNCATE TABLE is equivalent to DELETE FROM" },
|
|
149
|
+
// dd if=/dev/zero of=/ (disk wipe)
|
|
150
|
+
{ pattern: /dd\s+.*of=\//, equivalent: "disk-wipe", description: "dd writing to root is destructive" },
|
|
151
|
+
// mkfs (format disk)
|
|
152
|
+
{ pattern: /mkfs/, equivalent: "disk-format", description: "mkfs formats a disk" },
|
|
153
|
+
// shred (secure delete)
|
|
154
|
+
{ pattern: /shred\s+/, equivalent: "secure-delete", description: "shred securely deletes files" },
|
|
155
|
+
// chmod 777 / (open permissions)
|
|
156
|
+
{ pattern: /chmod\s+777\s+\//, equivalent: "open-permissions", description: "chmod 777 / opens all permissions" },
|
|
157
|
+
// chown root (change ownership)
|
|
158
|
+
{ pattern: /chown\s+.*\//, equivalent: "change-ownership", description: "chown on root paths is dangerous" },
|
|
159
|
+
// iptables -F (flush firewall)
|
|
160
|
+
{ pattern: /iptables\s+-F/, equivalent: "flush-firewall", description: "iptables -F flushes all firewall rules" }
|
|
161
|
+
];
|
|
162
|
+
function normalize(command) {
|
|
163
|
+
const original = command;
|
|
164
|
+
const segments = [];
|
|
165
|
+
const suspicion_reasons = [];
|
|
166
|
+
const trimmed = command.trim();
|
|
167
|
+
if (!trimmed) {
|
|
168
|
+
return { segments: [], suspicious: false, suspicion_reasons: [], original };
|
|
169
|
+
}
|
|
170
|
+
for (const enc of ENCODING_PATTERNS) {
|
|
171
|
+
if (enc.pattern.test(trimmed)) {
|
|
172
|
+
suspicion_reasons.push(`${enc.name}: ${enc.risk}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
for (const deq of DESTRUCTIVE_EQUIVALENTS) {
|
|
176
|
+
if (deq.pattern.test(trimmed)) {
|
|
177
|
+
suspicion_reasons.push(`Destructive equivalent detected: ${deq.description}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const chainSegments = splitChains(trimmed);
|
|
181
|
+
for (const chainSeg of chainSegments) {
|
|
182
|
+
const pipeSegments = splitPipes(chainSeg);
|
|
183
|
+
for (const pipeSeg of pipeSegments) {
|
|
184
|
+
const unwrapped = unwrapCommand(pipeSeg.trim());
|
|
185
|
+
if (unwrapped.unwrapped) {
|
|
186
|
+
segments.push({
|
|
187
|
+
raw: pipeSeg.trim(),
|
|
188
|
+
effective: unwrapped.effective,
|
|
189
|
+
type: "unwrapped",
|
|
190
|
+
warnings: unwrapped.warnings
|
|
191
|
+
});
|
|
192
|
+
const inner = normalize(unwrapped.effective);
|
|
193
|
+
for (const innerSeg of inner.segments) {
|
|
194
|
+
if (innerSeg.effective !== unwrapped.effective) {
|
|
195
|
+
segments.push(innerSeg);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
suspicion_reasons.push(...inner.suspicion_reasons);
|
|
199
|
+
} else {
|
|
200
|
+
const type = pipeSegments.length > 1 ? "pipe-segment" : chainSegments.length > 1 ? "chain-segment" : "direct";
|
|
201
|
+
segments.push({
|
|
202
|
+
raw: pipeSeg.trim(),
|
|
203
|
+
effective: pipeSeg.trim(),
|
|
204
|
+
type,
|
|
205
|
+
warnings: []
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const subshells = extractSubshells(trimmed);
|
|
211
|
+
for (const sub of subshells) {
|
|
212
|
+
segments.push({
|
|
213
|
+
raw: trimmed,
|
|
214
|
+
effective: sub,
|
|
215
|
+
type: "subshell",
|
|
216
|
+
warnings: ["Subshell command extracted for evaluation"]
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
segments,
|
|
221
|
+
suspicious: suspicion_reasons.length > 0,
|
|
222
|
+
suspicion_reasons,
|
|
223
|
+
original
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function splitChains(command) {
|
|
227
|
+
return splitRespectingQuotes(command, /\s*(?:&&|\|\||;)\s*/);
|
|
228
|
+
}
|
|
229
|
+
function splitPipes(command) {
|
|
230
|
+
return splitRespectingQuotes(command, /\s*\|(?!\|)\s*/);
|
|
231
|
+
}
|
|
232
|
+
function splitRespectingQuotes(input, delimiter) {
|
|
233
|
+
const segments = [];
|
|
234
|
+
let current = "";
|
|
235
|
+
let inSingleQuote = false;
|
|
236
|
+
let inDoubleQuote = false;
|
|
237
|
+
let escaped = false;
|
|
238
|
+
let i = 0;
|
|
239
|
+
while (i < input.length) {
|
|
240
|
+
const char = input[i];
|
|
241
|
+
if (escaped) {
|
|
242
|
+
current += char;
|
|
243
|
+
escaped = false;
|
|
244
|
+
i++;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (char === "\\") {
|
|
248
|
+
escaped = true;
|
|
249
|
+
current += char;
|
|
250
|
+
i++;
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (char === "'" && !inDoubleQuote) {
|
|
254
|
+
inSingleQuote = !inSingleQuote;
|
|
255
|
+
current += char;
|
|
256
|
+
i++;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (char === '"' && !inSingleQuote) {
|
|
260
|
+
inDoubleQuote = !inDoubleQuote;
|
|
261
|
+
current += char;
|
|
262
|
+
i++;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (!inSingleQuote && !inDoubleQuote) {
|
|
266
|
+
const remaining = input.slice(i);
|
|
267
|
+
const match = remaining.match(delimiter);
|
|
268
|
+
if (match && match.index === 0) {
|
|
269
|
+
if (current.trim()) {
|
|
270
|
+
segments.push(current.trim());
|
|
271
|
+
}
|
|
272
|
+
current = "";
|
|
273
|
+
i += match[0].length;
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
current += char;
|
|
278
|
+
i++;
|
|
279
|
+
}
|
|
280
|
+
if (current.trim()) {
|
|
281
|
+
segments.push(current.trim());
|
|
282
|
+
}
|
|
283
|
+
return segments.length > 0 ? segments : [input];
|
|
284
|
+
}
|
|
285
|
+
function unwrapCommand(command) {
|
|
286
|
+
for (const wrapper of SHELL_WRAPPERS) {
|
|
287
|
+
const match = command.match(wrapper.pattern);
|
|
288
|
+
if (match) {
|
|
289
|
+
const inner = wrapper.extract(match);
|
|
290
|
+
if (inner) {
|
|
291
|
+
return {
|
|
292
|
+
unwrapped: true,
|
|
293
|
+
effective: inner.trim(),
|
|
294
|
+
wrapper: wrapper.name,
|
|
295
|
+
warnings: [`Unwrapped from ${wrapper.name}: "${command}" \u2192 "${inner.trim()}"`]
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return { unwrapped: false, effective: command, warnings: [] };
|
|
301
|
+
}
|
|
302
|
+
function extractSubshells(command) {
|
|
303
|
+
const subshells = [];
|
|
304
|
+
const dollarParen = /\$\(([^)]+)\)/g;
|
|
305
|
+
let match;
|
|
306
|
+
while ((match = dollarParen.exec(command)) !== null) {
|
|
307
|
+
if (match[1]) subshells.push(match[1].trim());
|
|
308
|
+
}
|
|
309
|
+
const backtick = /`([^`]+)`/g;
|
|
310
|
+
while ((match = backtick.exec(command)) !== null) {
|
|
311
|
+
if (match[1]) subshells.push(match[1].trim());
|
|
312
|
+
}
|
|
313
|
+
return subshells;
|
|
314
|
+
}
|
|
315
|
+
function getBaseCommand(command) {
|
|
316
|
+
const trimmed = command.trim();
|
|
317
|
+
const firstSpace = trimmed.indexOf(" ");
|
|
318
|
+
return firstSpace === -1 ? trimmed : trimmed.slice(0, firstSpace);
|
|
319
|
+
}
|
|
320
|
+
function hasDestructiveEquivalent(command) {
|
|
321
|
+
const matches = [];
|
|
322
|
+
for (const deq of DESTRUCTIVE_EQUIVALENTS) {
|
|
323
|
+
if (deq.pattern.test(command)) {
|
|
324
|
+
matches.push({ equivalent: deq.equivalent, description: deq.description });
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return { found: matches.length > 0, matches };
|
|
328
|
+
}
|
|
329
|
+
function hasEncodingTricks(command) {
|
|
330
|
+
const tricks = [];
|
|
331
|
+
for (const enc of ENCODING_PATTERNS) {
|
|
332
|
+
if (enc.pattern.test(command)) {
|
|
333
|
+
tricks.push({ name: enc.name, risk: enc.risk });
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return { found: tricks.length > 0, tricks };
|
|
337
|
+
}
|
|
338
|
+
function summarize(result) {
|
|
339
|
+
const lines = [];
|
|
340
|
+
lines.push(`Original: ${result.original}`);
|
|
341
|
+
lines.push(`Segments: ${result.segments.length}`);
|
|
342
|
+
lines.push(`Suspicious: ${result.suspicious}`);
|
|
343
|
+
if (result.suspicion_reasons.length > 0) {
|
|
344
|
+
lines.push(`Suspicion reasons:`);
|
|
345
|
+
for (const reason of result.suspicion_reasons) {
|
|
346
|
+
lines.push(` - ${reason}`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
lines.push(`Segments:`);
|
|
350
|
+
for (const seg of result.segments) {
|
|
351
|
+
lines.push(` [${seg.type}] ${seg.effective}`);
|
|
352
|
+
for (const w of seg.warnings) {
|
|
353
|
+
lines.push(` \u26A0 ${w}`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return lines.join("\n");
|
|
357
|
+
}
|
|
358
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
359
|
+
0 && (module.exports = {
|
|
360
|
+
getBaseCommand,
|
|
361
|
+
hasDestructiveEquivalent,
|
|
362
|
+
hasEncodingTricks,
|
|
363
|
+
normalize,
|
|
364
|
+
summarize
|
|
365
|
+
});
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
// src/commandNormalizer.ts
|
|
2
|
+
var SHELL_WRAPPERS = [
|
|
3
|
+
// bash -c "cmd" / sh -c "cmd"
|
|
4
|
+
{
|
|
5
|
+
pattern: /^(?:bash|sh|zsh|dash|ksh)\s+-c\s+(?:"([^"]+)"|'([^']+)'|(\S+))/,
|
|
6
|
+
extract: (m) => m[1] || m[2] || m[3] || "",
|
|
7
|
+
name: "shell -c"
|
|
8
|
+
},
|
|
9
|
+
// env cmd args...
|
|
10
|
+
{
|
|
11
|
+
pattern: /^env\s+(?:-\S+\s+)*(?:\S+=\S+\s+)*(.+)/,
|
|
12
|
+
extract: (m) => m[1] || "",
|
|
13
|
+
name: "env"
|
|
14
|
+
},
|
|
15
|
+
// xargs cmd
|
|
16
|
+
{
|
|
17
|
+
pattern: /^xargs\s+(?:-\S+\s+)*(.+)/,
|
|
18
|
+
extract: (m) => m[1] || "",
|
|
19
|
+
name: "xargs"
|
|
20
|
+
},
|
|
21
|
+
// sudo cmd
|
|
22
|
+
{
|
|
23
|
+
pattern: /^sudo\s+(?:-\S+\s+)*(.+)/,
|
|
24
|
+
extract: (m) => m[1] || "",
|
|
25
|
+
name: "sudo"
|
|
26
|
+
},
|
|
27
|
+
// nohup cmd
|
|
28
|
+
{
|
|
29
|
+
pattern: /^nohup\s+(.+?)(?:\s*&\s*)?$/,
|
|
30
|
+
extract: (m) => m[1] || "",
|
|
31
|
+
name: "nohup"
|
|
32
|
+
},
|
|
33
|
+
// timeout N cmd
|
|
34
|
+
{
|
|
35
|
+
pattern: /^timeout\s+\d+[smhd]?\s+(.+)/,
|
|
36
|
+
extract: (m) => m[1] || "",
|
|
37
|
+
name: "timeout"
|
|
38
|
+
},
|
|
39
|
+
// nice -n N cmd
|
|
40
|
+
{
|
|
41
|
+
pattern: /^nice\s+(?:-n\s+\d+\s+)?(.+)/,
|
|
42
|
+
extract: (m) => m[1] || "",
|
|
43
|
+
name: "nice"
|
|
44
|
+
},
|
|
45
|
+
// strace / ltrace cmd
|
|
46
|
+
{
|
|
47
|
+
pattern: /^(?:strace|ltrace)\s+(?:-\S+\s+)*(.+)/,
|
|
48
|
+
extract: (m) => m[1] || "",
|
|
49
|
+
name: "trace"
|
|
50
|
+
},
|
|
51
|
+
// watch -n N cmd
|
|
52
|
+
{
|
|
53
|
+
pattern: /^watch\s+(?:-n\s+\d+\s+)?(.+)/,
|
|
54
|
+
extract: (m) => m[1] || "",
|
|
55
|
+
name: "watch"
|
|
56
|
+
}
|
|
57
|
+
];
|
|
58
|
+
var ENCODING_PATTERNS = [
|
|
59
|
+
// echo "base64" | base64 -d | bash
|
|
60
|
+
{
|
|
61
|
+
pattern: /base64\s+(?:-d|--decode)/,
|
|
62
|
+
name: "base64-decode",
|
|
63
|
+
risk: "Command may be hidden via base64 encoding"
|
|
64
|
+
},
|
|
65
|
+
// printf '\x72\x6d' (hex encoding)
|
|
66
|
+
{
|
|
67
|
+
pattern: /printf\s+.*\\x[0-9a-fA-F]{2}/,
|
|
68
|
+
name: "hex-printf",
|
|
69
|
+
risk: "Command may be hidden via hex encoding in printf"
|
|
70
|
+
},
|
|
71
|
+
// $'\x72\x6d' (ANSI-C quoting)
|
|
72
|
+
{
|
|
73
|
+
pattern: /\$'[^']*\\x[0-9a-fA-F]{2}/,
|
|
74
|
+
name: "ansi-c-quoting",
|
|
75
|
+
risk: "Command may be hidden via ANSI-C quoting"
|
|
76
|
+
},
|
|
77
|
+
// python -c "import os; os.system('rm -rf /')"
|
|
78
|
+
{
|
|
79
|
+
pattern: /python[23]?\s+-c\s+/,
|
|
80
|
+
name: "python-exec",
|
|
81
|
+
risk: "Arbitrary code execution via Python"
|
|
82
|
+
},
|
|
83
|
+
// perl -e "system('rm -rf /')"
|
|
84
|
+
{
|
|
85
|
+
pattern: /perl\s+-e\s+/,
|
|
86
|
+
name: "perl-exec",
|
|
87
|
+
risk: "Arbitrary code execution via Perl"
|
|
88
|
+
},
|
|
89
|
+
// ruby -e "system('rm -rf /')"
|
|
90
|
+
{
|
|
91
|
+
pattern: /ruby\s+-e\s+/,
|
|
92
|
+
name: "ruby-exec",
|
|
93
|
+
risk: "Arbitrary code execution via Ruby"
|
|
94
|
+
},
|
|
95
|
+
// eval "cmd"
|
|
96
|
+
{
|
|
97
|
+
pattern: /\beval\s+/,
|
|
98
|
+
name: "eval",
|
|
99
|
+
risk: "Dynamic command evaluation"
|
|
100
|
+
},
|
|
101
|
+
// curl ... | bash
|
|
102
|
+
{
|
|
103
|
+
pattern: /curl\s+.*\|\s*(?:bash|sh|zsh)/,
|
|
104
|
+
name: "curl-pipe-shell",
|
|
105
|
+
risk: "Remote code execution via curl | bash"
|
|
106
|
+
},
|
|
107
|
+
// wget ... -O - | bash
|
|
108
|
+
{
|
|
109
|
+
pattern: /wget\s+.*\|\s*(?:bash|sh|zsh)/,
|
|
110
|
+
name: "wget-pipe-shell",
|
|
111
|
+
risk: "Remote code execution via wget | bash"
|
|
112
|
+
}
|
|
113
|
+
];
|
|
114
|
+
var DESTRUCTIVE_EQUIVALENTS = [
|
|
115
|
+
// find / -delete (equivalent to rm -rf)
|
|
116
|
+
{ pattern: /find\s+.*-delete/, equivalent: "rm -rf", description: "find -delete is equivalent to rm -rf" },
|
|
117
|
+
// find / -exec rm {} (equivalent to rm -rf)
|
|
118
|
+
{ pattern: /find\s+.*-exec\s+rm/, equivalent: "rm -rf", description: "find -exec rm is equivalent to rm -rf" },
|
|
119
|
+
// TRUNCATE TABLE (equivalent to DELETE FROM)
|
|
120
|
+
{ pattern: /TRUNCATE\s+TABLE/i, equivalent: "DELETE FROM", description: "TRUNCATE TABLE is equivalent to DELETE FROM" },
|
|
121
|
+
// dd if=/dev/zero of=/ (disk wipe)
|
|
122
|
+
{ pattern: /dd\s+.*of=\//, equivalent: "disk-wipe", description: "dd writing to root is destructive" },
|
|
123
|
+
// mkfs (format disk)
|
|
124
|
+
{ pattern: /mkfs/, equivalent: "disk-format", description: "mkfs formats a disk" },
|
|
125
|
+
// shred (secure delete)
|
|
126
|
+
{ pattern: /shred\s+/, equivalent: "secure-delete", description: "shred securely deletes files" },
|
|
127
|
+
// chmod 777 / (open permissions)
|
|
128
|
+
{ pattern: /chmod\s+777\s+\//, equivalent: "open-permissions", description: "chmod 777 / opens all permissions" },
|
|
129
|
+
// chown root (change ownership)
|
|
130
|
+
{ pattern: /chown\s+.*\//, equivalent: "change-ownership", description: "chown on root paths is dangerous" },
|
|
131
|
+
// iptables -F (flush firewall)
|
|
132
|
+
{ pattern: /iptables\s+-F/, equivalent: "flush-firewall", description: "iptables -F flushes all firewall rules" }
|
|
133
|
+
];
|
|
134
|
+
function normalize(command) {
|
|
135
|
+
const original = command;
|
|
136
|
+
const segments = [];
|
|
137
|
+
const suspicion_reasons = [];
|
|
138
|
+
const trimmed = command.trim();
|
|
139
|
+
if (!trimmed) {
|
|
140
|
+
return { segments: [], suspicious: false, suspicion_reasons: [], original };
|
|
141
|
+
}
|
|
142
|
+
for (const enc of ENCODING_PATTERNS) {
|
|
143
|
+
if (enc.pattern.test(trimmed)) {
|
|
144
|
+
suspicion_reasons.push(`${enc.name}: ${enc.risk}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
for (const deq of DESTRUCTIVE_EQUIVALENTS) {
|
|
148
|
+
if (deq.pattern.test(trimmed)) {
|
|
149
|
+
suspicion_reasons.push(`Destructive equivalent detected: ${deq.description}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const chainSegments = splitChains(trimmed);
|
|
153
|
+
for (const chainSeg of chainSegments) {
|
|
154
|
+
const pipeSegments = splitPipes(chainSeg);
|
|
155
|
+
for (const pipeSeg of pipeSegments) {
|
|
156
|
+
const unwrapped = unwrapCommand(pipeSeg.trim());
|
|
157
|
+
if (unwrapped.unwrapped) {
|
|
158
|
+
segments.push({
|
|
159
|
+
raw: pipeSeg.trim(),
|
|
160
|
+
effective: unwrapped.effective,
|
|
161
|
+
type: "unwrapped",
|
|
162
|
+
warnings: unwrapped.warnings
|
|
163
|
+
});
|
|
164
|
+
const inner = normalize(unwrapped.effective);
|
|
165
|
+
for (const innerSeg of inner.segments) {
|
|
166
|
+
if (innerSeg.effective !== unwrapped.effective) {
|
|
167
|
+
segments.push(innerSeg);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
suspicion_reasons.push(...inner.suspicion_reasons);
|
|
171
|
+
} else {
|
|
172
|
+
const type = pipeSegments.length > 1 ? "pipe-segment" : chainSegments.length > 1 ? "chain-segment" : "direct";
|
|
173
|
+
segments.push({
|
|
174
|
+
raw: pipeSeg.trim(),
|
|
175
|
+
effective: pipeSeg.trim(),
|
|
176
|
+
type,
|
|
177
|
+
warnings: []
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const subshells = extractSubshells(trimmed);
|
|
183
|
+
for (const sub of subshells) {
|
|
184
|
+
segments.push({
|
|
185
|
+
raw: trimmed,
|
|
186
|
+
effective: sub,
|
|
187
|
+
type: "subshell",
|
|
188
|
+
warnings: ["Subshell command extracted for evaluation"]
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
segments,
|
|
193
|
+
suspicious: suspicion_reasons.length > 0,
|
|
194
|
+
suspicion_reasons,
|
|
195
|
+
original
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function splitChains(command) {
|
|
199
|
+
return splitRespectingQuotes(command, /\s*(?:&&|\|\||;)\s*/);
|
|
200
|
+
}
|
|
201
|
+
function splitPipes(command) {
|
|
202
|
+
return splitRespectingQuotes(command, /\s*\|(?!\|)\s*/);
|
|
203
|
+
}
|
|
204
|
+
function splitRespectingQuotes(input, delimiter) {
|
|
205
|
+
const segments = [];
|
|
206
|
+
let current = "";
|
|
207
|
+
let inSingleQuote = false;
|
|
208
|
+
let inDoubleQuote = false;
|
|
209
|
+
let escaped = false;
|
|
210
|
+
let i = 0;
|
|
211
|
+
while (i < input.length) {
|
|
212
|
+
const char = input[i];
|
|
213
|
+
if (escaped) {
|
|
214
|
+
current += char;
|
|
215
|
+
escaped = false;
|
|
216
|
+
i++;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (char === "\\") {
|
|
220
|
+
escaped = true;
|
|
221
|
+
current += char;
|
|
222
|
+
i++;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (char === "'" && !inDoubleQuote) {
|
|
226
|
+
inSingleQuote = !inSingleQuote;
|
|
227
|
+
current += char;
|
|
228
|
+
i++;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (char === '"' && !inSingleQuote) {
|
|
232
|
+
inDoubleQuote = !inDoubleQuote;
|
|
233
|
+
current += char;
|
|
234
|
+
i++;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (!inSingleQuote && !inDoubleQuote) {
|
|
238
|
+
const remaining = input.slice(i);
|
|
239
|
+
const match = remaining.match(delimiter);
|
|
240
|
+
if (match && match.index === 0) {
|
|
241
|
+
if (current.trim()) {
|
|
242
|
+
segments.push(current.trim());
|
|
243
|
+
}
|
|
244
|
+
current = "";
|
|
245
|
+
i += match[0].length;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
current += char;
|
|
250
|
+
i++;
|
|
251
|
+
}
|
|
252
|
+
if (current.trim()) {
|
|
253
|
+
segments.push(current.trim());
|
|
254
|
+
}
|
|
255
|
+
return segments.length > 0 ? segments : [input];
|
|
256
|
+
}
|
|
257
|
+
function unwrapCommand(command) {
|
|
258
|
+
for (const wrapper of SHELL_WRAPPERS) {
|
|
259
|
+
const match = command.match(wrapper.pattern);
|
|
260
|
+
if (match) {
|
|
261
|
+
const inner = wrapper.extract(match);
|
|
262
|
+
if (inner) {
|
|
263
|
+
return {
|
|
264
|
+
unwrapped: true,
|
|
265
|
+
effective: inner.trim(),
|
|
266
|
+
wrapper: wrapper.name,
|
|
267
|
+
warnings: [`Unwrapped from ${wrapper.name}: "${command}" \u2192 "${inner.trim()}"`]
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return { unwrapped: false, effective: command, warnings: [] };
|
|
273
|
+
}
|
|
274
|
+
function extractSubshells(command) {
|
|
275
|
+
const subshells = [];
|
|
276
|
+
const dollarParen = /\$\(([^)]+)\)/g;
|
|
277
|
+
let match;
|
|
278
|
+
while ((match = dollarParen.exec(command)) !== null) {
|
|
279
|
+
if (match[1]) subshells.push(match[1].trim());
|
|
280
|
+
}
|
|
281
|
+
const backtick = /`([^`]+)`/g;
|
|
282
|
+
while ((match = backtick.exec(command)) !== null) {
|
|
283
|
+
if (match[1]) subshells.push(match[1].trim());
|
|
284
|
+
}
|
|
285
|
+
return subshells;
|
|
286
|
+
}
|
|
287
|
+
function getBaseCommand(command) {
|
|
288
|
+
const trimmed = command.trim();
|
|
289
|
+
const firstSpace = trimmed.indexOf(" ");
|
|
290
|
+
return firstSpace === -1 ? trimmed : trimmed.slice(0, firstSpace);
|
|
291
|
+
}
|
|
292
|
+
function hasDestructiveEquivalent(command) {
|
|
293
|
+
const matches = [];
|
|
294
|
+
for (const deq of DESTRUCTIVE_EQUIVALENTS) {
|
|
295
|
+
if (deq.pattern.test(command)) {
|
|
296
|
+
matches.push({ equivalent: deq.equivalent, description: deq.description });
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return { found: matches.length > 0, matches };
|
|
300
|
+
}
|
|
301
|
+
function hasEncodingTricks(command) {
|
|
302
|
+
const tricks = [];
|
|
303
|
+
for (const enc of ENCODING_PATTERNS) {
|
|
304
|
+
if (enc.pattern.test(command)) {
|
|
305
|
+
tricks.push({ name: enc.name, risk: enc.risk });
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return { found: tricks.length > 0, tricks };
|
|
309
|
+
}
|
|
310
|
+
function summarize(result) {
|
|
311
|
+
const lines = [];
|
|
312
|
+
lines.push(`Original: ${result.original}`);
|
|
313
|
+
lines.push(`Segments: ${result.segments.length}`);
|
|
314
|
+
lines.push(`Suspicious: ${result.suspicious}`);
|
|
315
|
+
if (result.suspicion_reasons.length > 0) {
|
|
316
|
+
lines.push(`Suspicion reasons:`);
|
|
317
|
+
for (const reason of result.suspicion_reasons) {
|
|
318
|
+
lines.push(` - ${reason}`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
lines.push(`Segments:`);
|
|
322
|
+
for (const seg of result.segments) {
|
|
323
|
+
lines.push(` [${seg.type}] ${seg.effective}`);
|
|
324
|
+
for (const w of seg.warnings) {
|
|
325
|
+
lines.push(` \u26A0 ${w}`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return lines.join("\n");
|
|
329
|
+
}
|
|
330
|
+
export {
|
|
331
|
+
getBaseCommand,
|
|
332
|
+
hasDestructiveEquivalent,
|
|
333
|
+
hasEncodingTricks,
|
|
334
|
+
normalize,
|
|
335
|
+
summarize
|
|
336
|
+
};
|