@aliou/pi-guardrails 0.12.1 → 0.13.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/README.md +8 -6
- package/extensions/path-access/index.ts +12 -0
- package/extensions/permission-gate/index.ts +12 -0
- package/package.json +7 -5
- package/src/shared/events.ts +28 -0
- package/extensions/guardrails/rules.test.ts +0 -107
- package/extensions/guardrails/targets.test.ts +0 -44
- package/extensions/path-access/grants.test.ts +0 -47
- package/extensions/path-access/rules.test.ts +0 -46
- package/extensions/path-access/targets.test.ts +0 -40
- package/extensions/permission-gate/rules.test.ts +0 -132
- package/src/core/check.test.ts +0 -169
- package/src/core/commands/dangerous.test.ts +0 -468
- package/src/core/paths/access.test.ts +0 -150
- package/src/core/paths/path.test.ts +0 -293
- package/src/core/shell/command-args.test.ts +0 -142
- package/src/shared/matching.test.ts +0 -86
- package/src/shared/paths/bash-paths.test.ts +0 -171
package/src/core/check.test.ts
DELETED
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { checkAction, resolveDecision } from "./check";
|
|
3
|
-
import type { Action, Rule, Safety } from "./types";
|
|
4
|
-
|
|
5
|
-
const commandAction: Action = { kind: "command", command: "rm -rf /tmp/test" };
|
|
6
|
-
|
|
7
|
-
type TestMeta = { pattern: string; source: "test" };
|
|
8
|
-
|
|
9
|
-
const testMetadata: TestMeta = { pattern: "rm -rf", source: "test" };
|
|
10
|
-
|
|
11
|
-
describe("checkAction", () => {
|
|
12
|
-
it("returns safe when no rules match", async () => {
|
|
13
|
-
const rules: Rule[] = [
|
|
14
|
-
{
|
|
15
|
-
key: "sudo",
|
|
16
|
-
check: () => ({ kind: "pass" }),
|
|
17
|
-
},
|
|
18
|
-
];
|
|
19
|
-
|
|
20
|
-
await expect(checkAction(commandAction, rules)).resolves.toEqual({
|
|
21
|
-
kind: "safe",
|
|
22
|
-
});
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it("returns dangerous for the first matching rule", async () => {
|
|
26
|
-
const secondCheck = vi.fn(() => ({
|
|
27
|
-
kind: "match" as const,
|
|
28
|
-
reason: "second match",
|
|
29
|
-
metadata: testMetadata,
|
|
30
|
-
}));
|
|
31
|
-
const rules: Rule<TestMeta>[] = [
|
|
32
|
-
{
|
|
33
|
-
key: "first",
|
|
34
|
-
check: () => ({
|
|
35
|
-
kind: "match",
|
|
36
|
-
reason: "first match",
|
|
37
|
-
metadata: testMetadata,
|
|
38
|
-
}),
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
key: "second",
|
|
42
|
-
check: secondCheck,
|
|
43
|
-
},
|
|
44
|
-
];
|
|
45
|
-
|
|
46
|
-
await expect(checkAction(commandAction, rules)).resolves.toEqual({
|
|
47
|
-
kind: "dangerous",
|
|
48
|
-
action: commandAction,
|
|
49
|
-
key: "first",
|
|
50
|
-
reason: "first match",
|
|
51
|
-
metadata: testMetadata,
|
|
52
|
-
});
|
|
53
|
-
expect(secondCheck).not.toHaveBeenCalled();
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("supports async rules", async () => {
|
|
57
|
-
const rules: Rule[] = [
|
|
58
|
-
{
|
|
59
|
-
key: "async",
|
|
60
|
-
check: async (action) =>
|
|
61
|
-
action.kind === "command"
|
|
62
|
-
? {
|
|
63
|
-
kind: "match",
|
|
64
|
-
reason: "async match",
|
|
65
|
-
metadata: null,
|
|
66
|
-
}
|
|
67
|
-
: { kind: "pass" },
|
|
68
|
-
},
|
|
69
|
-
];
|
|
70
|
-
|
|
71
|
-
await expect(checkAction(commandAction, rules)).resolves.toMatchObject({
|
|
72
|
-
kind: "dangerous",
|
|
73
|
-
key: "async",
|
|
74
|
-
reason: "async match",
|
|
75
|
-
metadata: null,
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("propagates rule errors", async () => {
|
|
80
|
-
const error = new Error("rule failed");
|
|
81
|
-
const rules: Rule[] = [
|
|
82
|
-
{
|
|
83
|
-
key: "broken",
|
|
84
|
-
check: () => {
|
|
85
|
-
throw error;
|
|
86
|
-
},
|
|
87
|
-
},
|
|
88
|
-
];
|
|
89
|
-
|
|
90
|
-
await expect(checkAction(commandAction, rules)).rejects.toThrow(error);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it("propagates async rule rejections", async () => {
|
|
94
|
-
const error = new Error("async rule failed");
|
|
95
|
-
const rules: Rule[] = [
|
|
96
|
-
{
|
|
97
|
-
key: "broken-async",
|
|
98
|
-
check: async () => {
|
|
99
|
-
throw error;
|
|
100
|
-
},
|
|
101
|
-
},
|
|
102
|
-
];
|
|
103
|
-
|
|
104
|
-
await expect(checkAction(commandAction, rules)).rejects.toThrow(error);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it("preserves typed match metadata", async () => {
|
|
108
|
-
const rules: Rule<TestMeta>[] = [
|
|
109
|
-
{
|
|
110
|
-
key: "typed",
|
|
111
|
-
check: () => ({
|
|
112
|
-
kind: "match",
|
|
113
|
-
reason: "typed match",
|
|
114
|
-
metadata: testMetadata,
|
|
115
|
-
}),
|
|
116
|
-
},
|
|
117
|
-
];
|
|
118
|
-
|
|
119
|
-
const safety = await checkAction(commandAction, rules);
|
|
120
|
-
|
|
121
|
-
if (safety.kind !== "dangerous") {
|
|
122
|
-
throw new Error("expected dangerous safety");
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
expect(safety.metadata.pattern).toBe("rm -rf");
|
|
126
|
-
|
|
127
|
-
const decision = resolveDecision(safety, "prompt");
|
|
128
|
-
|
|
129
|
-
if (decision.kind !== "prompt") {
|
|
130
|
-
throw new Error("expected prompt decision");
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
expect(decision.risk.metadata.pattern).toBe("rm -rf");
|
|
134
|
-
});
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
describe("resolveDecision", () => {
|
|
138
|
-
const dangerous: Safety = {
|
|
139
|
-
kind: "dangerous",
|
|
140
|
-
action: commandAction,
|
|
141
|
-
key: "rm-rf",
|
|
142
|
-
reason: "recursive force delete",
|
|
143
|
-
metadata: null,
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
it("allows safe actions", () => {
|
|
147
|
-
expect(resolveDecision({ kind: "safe" }, "denied")).toEqual({
|
|
148
|
-
kind: "allow",
|
|
149
|
-
});
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it("allows dangerous actions when permission is granted", () => {
|
|
153
|
-
expect(resolveDecision(dangerous, "granted")).toEqual({ kind: "allow" });
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it("denies dangerous actions when permission is denied", () => {
|
|
157
|
-
expect(resolveDecision(dangerous, "denied")).toEqual({
|
|
158
|
-
kind: "deny",
|
|
159
|
-
reason: "recursive force delete",
|
|
160
|
-
});
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it("prompts for dangerous actions when permission is prompt", () => {
|
|
164
|
-
expect(resolveDecision(dangerous, "prompt")).toEqual({
|
|
165
|
-
kind: "prompt",
|
|
166
|
-
risk: dangerous,
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
});
|
|
@@ -1,468 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
BUILTIN_MATCHERS,
|
|
4
|
-
checkDangerousCommand,
|
|
5
|
-
compileCommandPatterns,
|
|
6
|
-
matchDangerousCommand,
|
|
7
|
-
} from "./dangerous";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Helper to run all matchers against a command string.
|
|
11
|
-
* Returns the first match description, or undefined if none match.
|
|
12
|
-
*/
|
|
13
|
-
function findMatch(words: string[]): string | undefined {
|
|
14
|
-
for (const matcher of BUILTIN_MATCHERS) {
|
|
15
|
-
const result = matcher(words);
|
|
16
|
-
if (result) return result;
|
|
17
|
-
}
|
|
18
|
-
return undefined;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
describe("rm matcher", () => {
|
|
22
|
-
it("matches rm -rf", () => {
|
|
23
|
-
expect(findMatch(["rm", "-rf", "/tmp/test"])).toBe(
|
|
24
|
-
"recursive force delete",
|
|
25
|
-
);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it("matches rm -fr (reversed flags)", () => {
|
|
29
|
-
expect(findMatch(["rm", "-fr", "/tmp/test"])).toBe(
|
|
30
|
-
"recursive force delete",
|
|
31
|
-
);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it("matches rm -r -f (separate flags)", () => {
|
|
35
|
-
expect(findMatch(["rm", "-r", "-f", "/tmp/test"])).toBe(
|
|
36
|
-
"recursive force delete",
|
|
37
|
-
);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("matches rm --recursive --force (long options)", () => {
|
|
41
|
-
expect(findMatch(["rm", "--recursive", "--force", "/tmp/test"])).toBe(
|
|
42
|
-
"recursive force delete",
|
|
43
|
-
);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("matches rm --force --recursive (reversed long options)", () => {
|
|
47
|
-
expect(findMatch(["rm", "--force", "--recursive", "/tmp/test"])).toBe(
|
|
48
|
-
"recursive force delete",
|
|
49
|
-
);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it("matches rm -Rfv (grouped with extra flags)", () => {
|
|
53
|
-
expect(findMatch(["rm", "-Rfv", "/tmp/test"])).toBe(
|
|
54
|
-
"recursive force delete",
|
|
55
|
-
);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("does not match rm -r (no force)", () => {
|
|
59
|
-
expect(findMatch(["rm", "-r", "/tmp/test"])).toBeUndefined();
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("does not match rm -f (no recursive)", () => {
|
|
63
|
-
expect(findMatch(["rm", "-f", "/tmp/test"])).toBeUndefined();
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("does not match echo rm -rf", () => {
|
|
67
|
-
expect(findMatch(["echo", "rm", "-rf", "/"])).toBeUndefined();
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
describe("sudo matcher", () => {
|
|
72
|
-
it("matches sudo", () => {
|
|
73
|
-
expect(findMatch(["sudo", "apt", "update"])).toBe("superuser command");
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it("matches sudo at start only", () => {
|
|
77
|
-
expect(findMatch(["echo", "sudo", "something"])).toBeUndefined();
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
describe("doas matcher", () => {
|
|
82
|
-
it("matches doas", () => {
|
|
83
|
-
expect(findMatch(["doas", "pkg_add", "vim"])).toBe(
|
|
84
|
-
"privileged command execution",
|
|
85
|
-
);
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
describe("pkexec matcher", () => {
|
|
90
|
-
it("matches pkexec", () => {
|
|
91
|
-
expect(findMatch(["pkexec", "apt", "install", "firefox"])).toBe(
|
|
92
|
-
"privileged command execution",
|
|
93
|
-
);
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
describe("dd matcher", () => {
|
|
98
|
-
it("matches dd with of= (output file)", () => {
|
|
99
|
-
expect(findMatch(["dd", "if=/dev/zero", "of=/dev/sda"])).toBe(
|
|
100
|
-
"disk write operation",
|
|
101
|
-
);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it("matches dd with of= in any order", () => {
|
|
105
|
-
expect(findMatch(["dd", "of=/dev/sda", "if=/dev/zero"])).toBe(
|
|
106
|
-
"disk write operation",
|
|
107
|
-
);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it("matches dd with progress and of=", () => {
|
|
111
|
-
expect(
|
|
112
|
-
findMatch(["dd", "status=progress", "of=/dev/sdb", "if=image.img"]),
|
|
113
|
-
).toBe("disk write operation");
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it("matches dd writing to /dev/null", () => {
|
|
117
|
-
expect(findMatch(["dd", "if=/dev/sda", "of=/dev/null"])).toBe(
|
|
118
|
-
"disk write operation",
|
|
119
|
-
);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it("does not match dd with only if= (read-only)", () => {
|
|
123
|
-
expect(findMatch(["dd", "if=/dev/sda"])).toBeUndefined();
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
describe("mkfs matcher", () => {
|
|
128
|
-
it("matches mkfs.ext4", () => {
|
|
129
|
-
expect(findMatch(["mkfs.ext4", "/dev/sda1"])).toBe("filesystem format");
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it("matches mkfs.xfs", () => {
|
|
133
|
-
expect(findMatch(["mkfs.xfs", "/dev/sdb1"])).toBe("filesystem format");
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it("matches plain mkfs", () => {
|
|
137
|
-
expect(findMatch(["mkfs", "/dev/sda1"])).toBe("filesystem format");
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it("matches mkfs.vfat", () => {
|
|
141
|
-
expect(findMatch(["mkfs.vfat", "/dev/sdc1"])).toBe("filesystem format");
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
describe("shred matcher", () => {
|
|
146
|
-
it("matches shred", () => {
|
|
147
|
-
expect(findMatch(["shred", "-u", "secret.txt"])).toBe(
|
|
148
|
-
"secure file overwrite",
|
|
149
|
-
);
|
|
150
|
-
});
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
describe("wipefs matcher", () => {
|
|
154
|
-
it("matches wipefs", () => {
|
|
155
|
-
expect(findMatch(["wipefs", "-a", "/dev/sda"])).toBe(
|
|
156
|
-
"filesystem signature wipe",
|
|
157
|
-
);
|
|
158
|
-
});
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
describe("blkdiscard matcher", () => {
|
|
162
|
-
it("matches blkdiscard", () => {
|
|
163
|
-
expect(findMatch(["blkdiscard", "/dev/nvme0n1"])).toBe(
|
|
164
|
-
"block device discard",
|
|
165
|
-
);
|
|
166
|
-
});
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
describe("fdisk matcher", () => {
|
|
170
|
-
it("matches fdisk", () => {
|
|
171
|
-
expect(findMatch(["fdisk", "/dev/sda"])).toBe("disk partitioning");
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it("matches sfdisk", () => {
|
|
175
|
-
expect(findMatch(["sfdisk", "/dev/sda"])).toBe("disk partitioning");
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it("matches cfdisk", () => {
|
|
179
|
-
expect(findMatch(["cfdisk", "/dev/sda"])).toBe("disk partitioning");
|
|
180
|
-
});
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
describe("parted matcher", () => {
|
|
184
|
-
it("matches parted", () => {
|
|
185
|
-
expect(findMatch(["parted", "/dev/sda"])).toBe("disk partitioning");
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
it("matches sgdisk", () => {
|
|
189
|
-
expect(findMatch(["sgdisk", "-l", "/dev/sda"])).toBe("disk partitioning");
|
|
190
|
-
});
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
describe("chmod matcher", () => {
|
|
194
|
-
it("matches chmod -R 777", () => {
|
|
195
|
-
expect(findMatch(["chmod", "-R", "777", "/tmp"])).toBe(
|
|
196
|
-
"insecure recursive permissions",
|
|
197
|
-
);
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
it("matches chmod --recursive 777", () => {
|
|
201
|
-
expect(findMatch(["chmod", "--recursive", "777", "/tmp"])).toBe(
|
|
202
|
-
"insecure recursive permissions",
|
|
203
|
-
);
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it("matches chmod -R 0777", () => {
|
|
207
|
-
expect(findMatch(["chmod", "-R", "0777", "/tmp"])).toBe(
|
|
208
|
-
"insecure recursive permissions",
|
|
209
|
-
);
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
it("matches chmod -R a+rwx", () => {
|
|
213
|
-
expect(findMatch(["chmod", "-R", "a+rwx", "/tmp"])).toBe(
|
|
214
|
-
"insecure recursive permissions",
|
|
215
|
-
);
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
it("matches chmod -R ugo+rwx", () => {
|
|
219
|
-
expect(findMatch(["chmod", "-R", "ugo+rwx", "/tmp"])).toBe(
|
|
220
|
-
"insecure recursive permissions",
|
|
221
|
-
);
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
it("does not match chmod 755 (not world-writable)", () => {
|
|
225
|
-
expect(findMatch(["chmod", "755", "file"])).toBeUndefined();
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
it("does not match chmod -R 755 (not world-writable)", () => {
|
|
229
|
-
expect(findMatch(["chmod", "-R", "755", "/tmp"])).toBeUndefined();
|
|
230
|
-
});
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
describe("chown matcher", () => {
|
|
234
|
-
it("matches chown -R", () => {
|
|
235
|
-
expect(findMatch(["chown", "-R", "user:group", "/tmp"])).toBe(
|
|
236
|
-
"recursive ownership change",
|
|
237
|
-
);
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
it("matches chown --recursive", () => {
|
|
241
|
-
expect(findMatch(["chown", "--recursive", "user", "/tmp"])).toBe(
|
|
242
|
-
"recursive ownership change",
|
|
243
|
-
);
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
it("does not match chown without -R", () => {
|
|
247
|
-
expect(findMatch(["chown", "user:group", "/tmp/file"])).toBeUndefined();
|
|
248
|
-
});
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
describe("container matcher (docker/podman)", () => {
|
|
252
|
-
describe("docker", () => {
|
|
253
|
-
it("matches docker run --privileged", () => {
|
|
254
|
-
expect(findMatch(["docker", "run", "--privileged", "alpine"])).toBe(
|
|
255
|
-
"container with privileged mode",
|
|
256
|
-
);
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
it("matches docker run --pid=host", () => {
|
|
260
|
-
expect(findMatch(["docker", "run", "--pid=host", "alpine"])).toBe(
|
|
261
|
-
"container with host PID namespace",
|
|
262
|
-
);
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
it("matches docker run --network=host", () => {
|
|
266
|
-
expect(findMatch(["docker", "run", "--network=host", "alpine"])).toBe(
|
|
267
|
-
"container with host network",
|
|
268
|
-
);
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
it("matches docker run --userns=host", () => {
|
|
272
|
-
expect(findMatch(["docker", "run", "--userns=host", "alpine"])).toBe(
|
|
273
|
-
"container with host user namespace",
|
|
274
|
-
);
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
it("matches docker run --uts=host", () => {
|
|
278
|
-
expect(findMatch(["docker", "run", "--uts=host", "alpine"])).toBe(
|
|
279
|
-
"container with host UTS namespace",
|
|
280
|
-
);
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
it("matches docker run --ipc=host", () => {
|
|
284
|
-
expect(findMatch(["docker", "run", "--ipc=host", "alpine"])).toBe(
|
|
285
|
-
"container with host IPC",
|
|
286
|
-
);
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
it("matches docker run with root mount", () => {
|
|
290
|
-
expect(findMatch(["docker", "run", "-v/:/host", "alpine"])).toBe(
|
|
291
|
-
"container with root filesystem mount",
|
|
292
|
-
);
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
it("matches docker run with docker socket", () => {
|
|
296
|
-
expect(
|
|
297
|
-
findMatch([
|
|
298
|
-
"docker",
|
|
299
|
-
"run",
|
|
300
|
-
"-v",
|
|
301
|
-
"/var/run/docker.sock:/var/run/docker.sock",
|
|
302
|
-
"alpine",
|
|
303
|
-
]),
|
|
304
|
-
).toBe("container with docker socket access");
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
it("does not match docker build", () => {
|
|
308
|
-
expect(
|
|
309
|
-
findMatch(["docker", "build", "-t", "myimage", "."]),
|
|
310
|
-
).toBeUndefined();
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
it("does not match docker run without dangerous flags", () => {
|
|
314
|
-
expect(
|
|
315
|
-
findMatch(["docker", "run", "alpine", "echo", "hello"]),
|
|
316
|
-
).toBeUndefined();
|
|
317
|
-
});
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
describe("podman", () => {
|
|
321
|
-
it("matches podman run --privileged", () => {
|
|
322
|
-
expect(findMatch(["podman", "run", "--privileged", "alpine"])).toBe(
|
|
323
|
-
"container with privileged mode",
|
|
324
|
-
);
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
it("matches podman create --privileged", () => {
|
|
328
|
-
expect(findMatch(["podman", "create", "--privileged", "alpine"])).toBe(
|
|
329
|
-
"container with privileged mode",
|
|
330
|
-
);
|
|
331
|
-
});
|
|
332
|
-
});
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
describe("checkDangerousCommand", () => {
|
|
336
|
-
it("matches built-in dangerous commands structurally", () => {
|
|
337
|
-
const result = checkDangerousCommand({
|
|
338
|
-
command: "rm -rf /tmp/example",
|
|
339
|
-
patterns: compileCommandPatterns([
|
|
340
|
-
{ pattern: "rm -rf", description: "recursive force delete" },
|
|
341
|
-
]),
|
|
342
|
-
useBuiltinMatchers: true,
|
|
343
|
-
fallbackPatterns: [
|
|
344
|
-
{ pattern: "rm -rf", description: "recursive force delete" },
|
|
345
|
-
],
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
expect(result).toEqual({
|
|
349
|
-
description: "recursive force delete",
|
|
350
|
-
pattern: "(structural)",
|
|
351
|
-
});
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
it("skips built-in substring matches after a successful parse", () => {
|
|
355
|
-
const result = checkDangerousCommand({
|
|
356
|
-
command: "echo 'rm -rf /tmp/example'",
|
|
357
|
-
patterns: compileCommandPatterns([
|
|
358
|
-
{ pattern: "rm -rf", description: "recursive force delete" },
|
|
359
|
-
]),
|
|
360
|
-
useBuiltinMatchers: true,
|
|
361
|
-
fallbackPatterns: [
|
|
362
|
-
{ pattern: "rm -rf", description: "recursive force delete" },
|
|
363
|
-
],
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
expect(result).toBeUndefined();
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
it("uses configured regex patterns", () => {
|
|
370
|
-
const result = checkDangerousCommand({
|
|
371
|
-
command: "terraform apply -auto-approve",
|
|
372
|
-
patterns: compileCommandPatterns([
|
|
373
|
-
{
|
|
374
|
-
pattern: "terraform\\s+apply",
|
|
375
|
-
description: "terraform apply",
|
|
376
|
-
regex: true,
|
|
377
|
-
},
|
|
378
|
-
]),
|
|
379
|
-
useBuiltinMatchers: false,
|
|
380
|
-
fallbackPatterns: [],
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
expect(result).toEqual({
|
|
384
|
-
description: "terraform apply",
|
|
385
|
-
pattern: "terraform\\s+apply",
|
|
386
|
-
});
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
it("ignores invalid regex patterns", () => {
|
|
390
|
-
const result = checkDangerousCommand({
|
|
391
|
-
command: "anything",
|
|
392
|
-
patterns: compileCommandPatterns([
|
|
393
|
-
{ pattern: "[", description: "invalid", regex: true },
|
|
394
|
-
]),
|
|
395
|
-
useBuiltinMatchers: false,
|
|
396
|
-
fallbackPatterns: [],
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
expect(result).toBeUndefined();
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
it("uses configured patterns when built-in matchers are disabled", () => {
|
|
403
|
-
const result = checkDangerousCommand({
|
|
404
|
-
command: "deploy production",
|
|
405
|
-
patterns: compileCommandPatterns([
|
|
406
|
-
{ pattern: "deploy production", description: "production deploy" },
|
|
407
|
-
]),
|
|
408
|
-
useBuiltinMatchers: false,
|
|
409
|
-
fallbackPatterns: [],
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
expect(result).toEqual({
|
|
413
|
-
description: "production deploy",
|
|
414
|
-
pattern: "deploy production",
|
|
415
|
-
});
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
it("falls back to raw patterns when parsing fails", () => {
|
|
419
|
-
const result = checkDangerousCommand({
|
|
420
|
-
command: "if then rm -rf /tmp/example",
|
|
421
|
-
patterns: [],
|
|
422
|
-
useBuiltinMatchers: true,
|
|
423
|
-
fallbackPatterns: [
|
|
424
|
-
{ pattern: "rm -rf", description: "recursive force delete" },
|
|
425
|
-
],
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
expect(result).toEqual({
|
|
429
|
-
description: "recursive force delete",
|
|
430
|
-
pattern: "rm -rf",
|
|
431
|
-
});
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
it.each([
|
|
435
|
-
["logical command", "echo ok && sudo true", "superuser command"],
|
|
436
|
-
["pipeline", "echo ok | sudo tee /tmp/out", "superuser command"],
|
|
437
|
-
["subshell", "(sudo true)", "superuser command"],
|
|
438
|
-
])("matches dangerous commands nested in a %s", (_label, command, description) => {
|
|
439
|
-
const result = checkDangerousCommand({
|
|
440
|
-
command,
|
|
441
|
-
patterns: [],
|
|
442
|
-
useBuiltinMatchers: true,
|
|
443
|
-
fallbackPatterns: [],
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
expect(result).toEqual({ description, pattern: "(structural)" });
|
|
447
|
-
});
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
describe("matchDangerousCommand", () => {
|
|
451
|
-
it("returns description and pattern for dangerous commands", () => {
|
|
452
|
-
const result = matchDangerousCommand(["sudo", "apt", "update"]);
|
|
453
|
-
expect(result).toEqual({
|
|
454
|
-
description: "superuser command",
|
|
455
|
-
pattern: "sudo",
|
|
456
|
-
});
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
it("returns undefined for safe commands", () => {
|
|
460
|
-
expect(matchDangerousCommand(["echo", "hello"])).toBeUndefined();
|
|
461
|
-
});
|
|
462
|
-
|
|
463
|
-
it("returns first match when multiple could apply", () => {
|
|
464
|
-
// sudo comes before dd in the matcher list
|
|
465
|
-
const result = matchDangerousCommand(["sudo", "dd", "of=/dev/sda"]);
|
|
466
|
-
expect(result?.pattern).toBe("sudo");
|
|
467
|
-
});
|
|
468
|
-
});
|