@parel/security-basic 0.1.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.d.ts +5 -0
- package/dist/index.js +423 -0
- package/package.json +41 -0
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { definePlugin, HookPriority, LifecycleEvent } from "@parel/plugin-sdk";
|
|
3
|
+
var DEFAULT_EXEC_TOOLS = /* @__PURE__ */ new Set(["bash", "shell", "exec", "terminal", "run_command", "run"]);
|
|
4
|
+
var DEFAULT_ALLOWED_PATTERNS = [
|
|
5
|
+
/^(ls|cat|head|tail|wc|grep|find|which|echo|printf|date|pwd|whoami|id|env|printenv)$/,
|
|
6
|
+
/^(cd|mkdir|cp|mv|touch|ln)$/,
|
|
7
|
+
/^(node|npx|npm|pnpm|bun|deno|python|python3|pip|pip3)$/,
|
|
8
|
+
/^(git|gh)$/,
|
|
9
|
+
/^(curl|wget|dig|nslookup|ping)$/,
|
|
10
|
+
/^(jq|yq|sed|awk|sort|uniq|cut|tr|xargs|tee)$/,
|
|
11
|
+
/^(docker|podman|kubectl)$/,
|
|
12
|
+
/^(make|cargo|go|rustc|gcc|g\+\+|javac|java|dotnet)$/,
|
|
13
|
+
/^(tar|zip|unzip|gzip|gunzip)$/,
|
|
14
|
+
/^(vi|vim|nano|less|more|diff|patch)$/,
|
|
15
|
+
/^(test|\[|true|false|read|set|export|source|\.)$/
|
|
16
|
+
];
|
|
17
|
+
var DENY_PATTERNS = [
|
|
18
|
+
/\brm\s+(-[a-z]*r[a-z]*\s+(-[a-z]*f|\/)|(-[a-z]*f[a-z]*\s+(-[a-z]*r|\/))|-rf\s)/,
|
|
19
|
+
/\brm\s+(-[a-z]*f[a-z]*\s+)?\/($|\s)/,
|
|
20
|
+
/\bmkfs\b/,
|
|
21
|
+
/\bdd\b.*\bof=\/dev\//,
|
|
22
|
+
/>\s*\/dev\/sd[a-z]/,
|
|
23
|
+
/\bchmod\b.*\b777\b.*\//,
|
|
24
|
+
/\bchown\b.*-R\b.*\//,
|
|
25
|
+
/\b(shutdown|reboot|halt|poweroff|init\s+[06])\b/,
|
|
26
|
+
/\bwipefs\b/,
|
|
27
|
+
/\bshred\b.*\//
|
|
28
|
+
];
|
|
29
|
+
function createSecretPatterns() {
|
|
30
|
+
return [
|
|
31
|
+
/sk-[a-zA-Z0-9]{20,}/g,
|
|
32
|
+
/sk-ant-[a-zA-Z0-9-]{20,}/g,
|
|
33
|
+
/ghp_[a-zA-Z0-9]{36,}/g,
|
|
34
|
+
/gho_[a-zA-Z0-9]{36,}/g,
|
|
35
|
+
/github_pat_[a-zA-Z0-9_]{20,}/g,
|
|
36
|
+
/AKIA[A-Z0-9]{16}/g,
|
|
37
|
+
/-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g,
|
|
38
|
+
/xoxb-[a-zA-Z0-9-]+/g,
|
|
39
|
+
/xoxp-[a-zA-Z0-9-]+/g,
|
|
40
|
+
/sk_live_[a-zA-Z0-9]+/g,
|
|
41
|
+
/pk_live_[a-zA-Z0-9]+/g,
|
|
42
|
+
/(?<=^|[^a-zA-Z0-9])pk_[a-zA-Z0-9]{24,}/g,
|
|
43
|
+
/eyJ[a-zA-Z0-9_-]{20,}\.[a-zA-Z0-9_-]{20,}\.[a-zA-Z0-9_-]{20,}/g
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
function redactSecrets(text) {
|
|
47
|
+
for (const pattern of createSecretPatterns()) {
|
|
48
|
+
text = text.replace(pattern, "[REDACTED]");
|
|
49
|
+
}
|
|
50
|
+
return text;
|
|
51
|
+
}
|
|
52
|
+
function normalizeForDeny(cmd) {
|
|
53
|
+
return cmd.replace(/\\(.)/g, "$1").replace(/['"]/g, "").replace(/\s+/g, " ");
|
|
54
|
+
}
|
|
55
|
+
var COMMAND_WRAPPERS = /* @__PURE__ */ new Set([
|
|
56
|
+
"sudo",
|
|
57
|
+
"env",
|
|
58
|
+
"nohup",
|
|
59
|
+
"time",
|
|
60
|
+
"command",
|
|
61
|
+
"builtin",
|
|
62
|
+
"exec",
|
|
63
|
+
"nice",
|
|
64
|
+
"ionice",
|
|
65
|
+
"setsid",
|
|
66
|
+
"stdbuf",
|
|
67
|
+
"xargs",
|
|
68
|
+
"watch",
|
|
69
|
+
"timeout"
|
|
70
|
+
]);
|
|
71
|
+
var COMMAND_SEPARATORS = /* @__PURE__ */ new Set(["|", "||", "&&", ";", "&", "|&", "\n"]);
|
|
72
|
+
function tokenize(input) {
|
|
73
|
+
const tokens = [];
|
|
74
|
+
let i = 0;
|
|
75
|
+
const n = input.length;
|
|
76
|
+
let current = "";
|
|
77
|
+
let currentNested = [];
|
|
78
|
+
let hasWord = false;
|
|
79
|
+
const pushWord = () => {
|
|
80
|
+
if (hasWord) {
|
|
81
|
+
tokens.push({ value: current, operator: false, nested: currentNested });
|
|
82
|
+
current = "";
|
|
83
|
+
currentNested = [];
|
|
84
|
+
hasWord = false;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
const pushOperator = (op) => {
|
|
88
|
+
pushWord();
|
|
89
|
+
tokens.push({ value: op, operator: true, nested: [] });
|
|
90
|
+
};
|
|
91
|
+
const readBalanced = (start, open, close) => {
|
|
92
|
+
let depth = 1;
|
|
93
|
+
let j = start;
|
|
94
|
+
let quote = "normal";
|
|
95
|
+
while (j < n) {
|
|
96
|
+
const c = input[j];
|
|
97
|
+
if (quote === "single") {
|
|
98
|
+
if (c === "'") quote = "normal";
|
|
99
|
+
j++;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (quote === "double") {
|
|
103
|
+
if (c === "\\") {
|
|
104
|
+
j += 2;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (c === '"') quote = "normal";
|
|
108
|
+
j++;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (c === "'") {
|
|
112
|
+
quote = "single";
|
|
113
|
+
j++;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (c === '"') {
|
|
117
|
+
quote = "double";
|
|
118
|
+
j++;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (c === "\\") {
|
|
122
|
+
j += 2;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (open && c === open) {
|
|
126
|
+
depth++;
|
|
127
|
+
j++;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (c === close) {
|
|
131
|
+
depth--;
|
|
132
|
+
if (depth === 0) {
|
|
133
|
+
return [input.slice(start, j), j + 1];
|
|
134
|
+
}
|
|
135
|
+
j++;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
j++;
|
|
139
|
+
}
|
|
140
|
+
return [input.slice(start), n];
|
|
141
|
+
};
|
|
142
|
+
const addNested = (inner) => {
|
|
143
|
+
currentNested.push(tokenize(inner));
|
|
144
|
+
hasWord = true;
|
|
145
|
+
};
|
|
146
|
+
while (i < n) {
|
|
147
|
+
const c = input[i];
|
|
148
|
+
if (c === "'") {
|
|
149
|
+
const [inner, next] = readBalanced(i + 1, "", "'");
|
|
150
|
+
current += inner;
|
|
151
|
+
hasWord = true;
|
|
152
|
+
i = next;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (c === '"') {
|
|
156
|
+
let j = i + 1;
|
|
157
|
+
while (j < n) {
|
|
158
|
+
const d = input[j];
|
|
159
|
+
if (d === "\\") {
|
|
160
|
+
if (j + 1 < n) current += input[j + 1];
|
|
161
|
+
j += 2;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (d === '"') {
|
|
165
|
+
j++;
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
if (d === "`") {
|
|
169
|
+
const [inner, next] = readBalanced(j + 1, "", "`");
|
|
170
|
+
addNested(inner);
|
|
171
|
+
j = next;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (d === "$" && input[j + 1] === "(") {
|
|
175
|
+
const [inner, next] = readBalanced(j + 2, "(", ")");
|
|
176
|
+
addNested(inner);
|
|
177
|
+
j = next;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
current += d;
|
|
181
|
+
hasWord = true;
|
|
182
|
+
j++;
|
|
183
|
+
}
|
|
184
|
+
hasWord = true;
|
|
185
|
+
i = j;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (c === "`") {
|
|
189
|
+
const [inner, next] = readBalanced(i + 1, "", "`");
|
|
190
|
+
addNested(inner);
|
|
191
|
+
i = next;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (c === "$" && input[i + 1] === "(") {
|
|
195
|
+
if (input[i + 2] === "(") {
|
|
196
|
+
const [, next2] = readBalanced(i + 3, "(", ")");
|
|
197
|
+
hasWord = true;
|
|
198
|
+
i = input[next2] === ")" ? next2 + 1 : next2;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const [inner, next] = readBalanced(i + 2, "(", ")");
|
|
202
|
+
addNested(inner);
|
|
203
|
+
i = next;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if ((c === "<" || c === ">") && input[i + 1] === "(") {
|
|
207
|
+
const [inner, next] = readBalanced(i + 2, "(", ")");
|
|
208
|
+
addNested(inner);
|
|
209
|
+
i = next;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (c === " " || c === " ") {
|
|
213
|
+
pushWord();
|
|
214
|
+
i++;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (c === "\n") {
|
|
218
|
+
pushOperator("\n");
|
|
219
|
+
i++;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (c === "(") {
|
|
223
|
+
const [inner, next] = readBalanced(i + 1, "(", ")");
|
|
224
|
+
pushWord();
|
|
225
|
+
const innerTokens = tokenize(inner);
|
|
226
|
+
tokens.push({ value: "(", operator: true, nested: [] });
|
|
227
|
+
for (const t of innerTokens) tokens.push(t);
|
|
228
|
+
tokens.push({ value: ")", operator: true, nested: [] });
|
|
229
|
+
i = next;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
const two = input.slice(i, i + 2);
|
|
233
|
+
if (two === "&&" || two === "||" || two === "|&") {
|
|
234
|
+
pushOperator(two);
|
|
235
|
+
i += 2;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (c === "|" || c === ";" || c === "&") {
|
|
239
|
+
pushOperator(c);
|
|
240
|
+
i++;
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (c === ">" || c === "<") {
|
|
244
|
+
let op = c;
|
|
245
|
+
if (input[i + 1] === ">") {
|
|
246
|
+
op += ">";
|
|
247
|
+
i++;
|
|
248
|
+
}
|
|
249
|
+
if (input[i + 1] === "&") {
|
|
250
|
+
op += "&";
|
|
251
|
+
i++;
|
|
252
|
+
}
|
|
253
|
+
pushOperator(op);
|
|
254
|
+
i++;
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
if (c === "\\") {
|
|
258
|
+
if (i + 1 < n) {
|
|
259
|
+
current += input[i + 1];
|
|
260
|
+
hasWord = true;
|
|
261
|
+
}
|
|
262
|
+
i += 2;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
current += c;
|
|
266
|
+
hasWord = true;
|
|
267
|
+
i++;
|
|
268
|
+
}
|
|
269
|
+
pushWord();
|
|
270
|
+
return tokens;
|
|
271
|
+
}
|
|
272
|
+
function collectPrograms(tokens, out) {
|
|
273
|
+
let expectProgram = true;
|
|
274
|
+
let afterRedirection = false;
|
|
275
|
+
let inWrapperArgs = false;
|
|
276
|
+
for (const token of tokens) {
|
|
277
|
+
for (const nested of token.nested) {
|
|
278
|
+
collectPrograms(nested, out);
|
|
279
|
+
}
|
|
280
|
+
if (token.operator) {
|
|
281
|
+
if (token.value === "(") {
|
|
282
|
+
expectProgram = true;
|
|
283
|
+
afterRedirection = false;
|
|
284
|
+
inWrapperArgs = false;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
if (token.value === ")") {
|
|
288
|
+
expectProgram = false;
|
|
289
|
+
afterRedirection = false;
|
|
290
|
+
inWrapperArgs = false;
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
if (token.value[0] === ">" || token.value[0] === "<") {
|
|
294
|
+
afterRedirection = true;
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (COMMAND_SEPARATORS.has(token.value)) {
|
|
298
|
+
expectProgram = true;
|
|
299
|
+
afterRedirection = false;
|
|
300
|
+
inWrapperArgs = false;
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
if (afterRedirection) {
|
|
306
|
+
afterRedirection = false;
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
if (!expectProgram) continue;
|
|
310
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(token.value)) {
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
if (inWrapperArgs && (token.value.startsWith("-") || /^\d+$/.test(token.value))) {
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
if (token.value === "" && token.nested.length > 0) {
|
|
317
|
+
expectProgram = false;
|
|
318
|
+
inWrapperArgs = false;
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
const program = basename(token.value);
|
|
322
|
+
if (COMMAND_WRAPPERS.has(program)) {
|
|
323
|
+
out.push(program);
|
|
324
|
+
expectProgram = true;
|
|
325
|
+
inWrapperArgs = true;
|
|
326
|
+
afterRedirection = false;
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
out.push(program);
|
|
330
|
+
expectProgram = false;
|
|
331
|
+
inWrapperArgs = false;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
function basename(word) {
|
|
335
|
+
const slash = word.lastIndexOf("/");
|
|
336
|
+
return slash >= 0 ? word.slice(slash + 1) : word;
|
|
337
|
+
}
|
|
338
|
+
function extractAllPrograms(command) {
|
|
339
|
+
const tokens = tokenize(command);
|
|
340
|
+
const out = [];
|
|
341
|
+
collectPrograms(tokens, out);
|
|
342
|
+
return out;
|
|
343
|
+
}
|
|
344
|
+
var index_default = definePlugin({
|
|
345
|
+
name: "@parel/security-basic",
|
|
346
|
+
version: "0.0.2",
|
|
347
|
+
async setup(ctx) {
|
|
348
|
+
const mode = ctx.config.mode ?? "allowlist";
|
|
349
|
+
const customExecTools = ctx.config.exec_tools;
|
|
350
|
+
const execTools = customExecTools ? new Set(customExecTools) : DEFAULT_EXEC_TOOLS;
|
|
351
|
+
const customAllowed = ctx.config.allowed_patterns;
|
|
352
|
+
const allowedPatterns = customAllowed ? customAllowed.map((p) => new RegExp(p)) : DEFAULT_ALLOWED_PATTERNS;
|
|
353
|
+
ctx.hook(
|
|
354
|
+
LifecycleEvent.ToolBefore,
|
|
355
|
+
async (hookCtx) => {
|
|
356
|
+
if (!execTools.has(hookCtx.toolCall.name)) return;
|
|
357
|
+
const command = hookCtx.toolCall.arguments.command;
|
|
358
|
+
if (!command) return;
|
|
359
|
+
const denyTargets = [command, normalizeForDeny(command)];
|
|
360
|
+
for (const pattern of DENY_PATTERNS) {
|
|
361
|
+
if (denyTargets.some((target) => pattern.test(target))) {
|
|
362
|
+
return {
|
|
363
|
+
action: "block",
|
|
364
|
+
reason: `Blocked: destructive command pattern detected`
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (mode === "allowlist") {
|
|
369
|
+
const programs = extractAllPrograms(command);
|
|
370
|
+
for (const prog of programs) {
|
|
371
|
+
const allowed = allowedPatterns.some((p) => p.test(prog));
|
|
372
|
+
if (!allowed) {
|
|
373
|
+
return {
|
|
374
|
+
action: "block",
|
|
375
|
+
reason: `Blocked: "${prog}" is not in the allowed command list`
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
},
|
|
381
|
+
{ priority: HookPriority.Security }
|
|
382
|
+
);
|
|
383
|
+
ctx.hook(
|
|
384
|
+
LifecycleEvent.ToolAfter,
|
|
385
|
+
async (hookCtx) => {
|
|
386
|
+
const content = hookCtx.toolResult.content;
|
|
387
|
+
if (typeof content !== "string") return;
|
|
388
|
+
const redacted = redactSecrets(content);
|
|
389
|
+
if (redacted !== content) {
|
|
390
|
+
return {
|
|
391
|
+
action: "continue",
|
|
392
|
+
mutations: { toolResult: { ...hookCtx.toolResult, content: redacted } }
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
},
|
|
396
|
+
{ priority: HookPriority.Security }
|
|
397
|
+
);
|
|
398
|
+
ctx.hook(
|
|
399
|
+
LifecycleEvent.ContextBuild,
|
|
400
|
+
async (hookCtx) => {
|
|
401
|
+
const system = redactSecrets(hookCtx.system);
|
|
402
|
+
const messages = hookCtx.messages.map((msg) => {
|
|
403
|
+
const parts = msg.parts.map((part) => {
|
|
404
|
+
if (part.type === "text") {
|
|
405
|
+
const text = redactSecrets(part.text);
|
|
406
|
+
return text !== part.text ? { ...part, text } : part;
|
|
407
|
+
}
|
|
408
|
+
return part;
|
|
409
|
+
});
|
|
410
|
+
return { ...msg, parts };
|
|
411
|
+
});
|
|
412
|
+
return {
|
|
413
|
+
action: "continue",
|
|
414
|
+
mutations: { system, messages }
|
|
415
|
+
};
|
|
416
|
+
},
|
|
417
|
+
{ priority: HookPriority.Late }
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
export {
|
|
422
|
+
index_default as default
|
|
423
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@parel/security-basic",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "PAREL plugin for basic command and secret safety checks.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/parall-hq/parel-opensource.git",
|
|
9
|
+
"directory": "js/plugins/security-basic"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/parall-hq/parel-opensource/issues"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/parall-hq/parel-opensource#readme",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"import": "./dist/index.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@parel/plugin-sdk": "0.1.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"tsup": "^8.0.0",
|
|
30
|
+
"typescript": "^5.8.0",
|
|
31
|
+
"vitest": "^3.0.0"
|
|
32
|
+
},
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsup",
|
|
38
|
+
"test": "vitest run --passWithNoTests",
|
|
39
|
+
"lint": "biome check src/"
|
|
40
|
+
}
|
|
41
|
+
}
|