@aliou/pi-guardrails 0.12.0 → 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/core/shell/command-args.ts +71 -0
- 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 -94
- package/src/shared/matching.test.ts +0 -86
- package/src/shared/paths/bash-paths.test.ts +0 -150
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
createPermissionGateRule,
|
|
4
|
-
formatAutoDenyReason,
|
|
5
|
-
matchCommandPattern,
|
|
6
|
-
matchesAnyCommandPattern,
|
|
7
|
-
} from "./rules";
|
|
8
|
-
|
|
9
|
-
describe("createPermissionGateRule", () => {
|
|
10
|
-
it("passes file actions", async () => {
|
|
11
|
-
const rule = createPermissionGateRule({
|
|
12
|
-
patterns: [{ pattern: "rm -rf", description: "recursive delete" }],
|
|
13
|
-
useBuiltinMatchers: false,
|
|
14
|
-
});
|
|
15
|
-
expect(rule.check({ kind: "file", path: "package.json" })).toEqual({
|
|
16
|
-
kind: "pass",
|
|
17
|
-
});
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it("matches configured dangerous command patterns", async () => {
|
|
21
|
-
const rule = createPermissionGateRule({
|
|
22
|
-
patterns: [
|
|
23
|
-
{ pattern: "terraform destroy", description: "Destroy infra" },
|
|
24
|
-
],
|
|
25
|
-
useBuiltinMatchers: false,
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
expect(
|
|
29
|
-
rule.check({
|
|
30
|
-
kind: "command",
|
|
31
|
-
command: "terraform destroy -auto-approve",
|
|
32
|
-
}),
|
|
33
|
-
).toEqual({
|
|
34
|
-
kind: "match",
|
|
35
|
-
reason: "Destroy infra",
|
|
36
|
-
metadata: {
|
|
37
|
-
command: "terraform destroy -auto-approve",
|
|
38
|
-
description: "Destroy infra",
|
|
39
|
-
pattern: "terraform destroy",
|
|
40
|
-
},
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("can use builtin dangerous command matchers", async () => {
|
|
45
|
-
const rule = createPermissionGateRule({
|
|
46
|
-
patterns: [],
|
|
47
|
-
useBuiltinMatchers: true,
|
|
48
|
-
});
|
|
49
|
-
expect(
|
|
50
|
-
rule.check({ kind: "command", command: "rm -rf dist" }),
|
|
51
|
-
).toMatchObject({ kind: "match" });
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
describe("matchesAnyCommandPattern", () => {
|
|
56
|
-
it("matches substring and regex command patterns", () => {
|
|
57
|
-
expect(
|
|
58
|
-
matchesAnyCommandPattern("npm publish --dry-run", [
|
|
59
|
-
{ pattern: "npm publish" },
|
|
60
|
-
]),
|
|
61
|
-
).toBe(true);
|
|
62
|
-
expect(
|
|
63
|
-
matchesAnyCommandPattern("DROP TABLE users", [
|
|
64
|
-
{ pattern: "^DROP TABLE", regex: true },
|
|
65
|
-
]),
|
|
66
|
-
).toBe(true);
|
|
67
|
-
expect(
|
|
68
|
-
matchesAnyCommandPattern("npm test", [{ pattern: "npm publish" }]),
|
|
69
|
-
).toBe(false);
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
describe("matchCommandPattern", () => {
|
|
74
|
-
it("returns the matched PatternConfig", () => {
|
|
75
|
-
const patterns = [{ pattern: "npm publish" }, { pattern: "rm -rf" }];
|
|
76
|
-
expect(matchCommandPattern("npm publish --dry-run", patterns)).toBe(
|
|
77
|
-
patterns[0],
|
|
78
|
-
);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it("returns the matching regex pattern", () => {
|
|
82
|
-
const patterns = [{ pattern: "^DROP TABLE", regex: true }];
|
|
83
|
-
expect(matchCommandPattern("DROP TABLE users", patterns)).toBe(patterns[0]);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it("returns null when no pattern matches", () => {
|
|
87
|
-
expect(
|
|
88
|
-
matchCommandPattern("npm test", [{ pattern: "npm publish" }]),
|
|
89
|
-
).toBeNull();
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it("preserves description on the returned pattern", () => {
|
|
93
|
-
const patterns = [
|
|
94
|
-
{
|
|
95
|
-
pattern: "python -m venv",
|
|
96
|
-
description: "Use the project .venv instead",
|
|
97
|
-
},
|
|
98
|
-
];
|
|
99
|
-
const result = matchCommandPattern("python -m venv .venv", patterns);
|
|
100
|
-
expect(result).not.toBeNull();
|
|
101
|
-
expect(result?.description).toBe("Use the project .venv instead");
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
describe("formatAutoDenyReason", () => {
|
|
106
|
-
it("uses description when present", () => {
|
|
107
|
-
expect(
|
|
108
|
-
formatAutoDenyReason({
|
|
109
|
-
pattern: "python -m venv",
|
|
110
|
-
description: "Use the project .venv instead",
|
|
111
|
-
}),
|
|
112
|
-
).toBe("Command auto-denied: Use the project .venv instead");
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it("falls back to generic reason when description is missing", () => {
|
|
116
|
-
expect(formatAutoDenyReason({ pattern: "python -m venv" })).toBe(
|
|
117
|
-
"Command matched auto-deny pattern and was blocked automatically.",
|
|
118
|
-
);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("falls back when description is empty string", () => {
|
|
122
|
-
expect(
|
|
123
|
-
formatAutoDenyReason({ pattern: "python -m venv", description: "" }),
|
|
124
|
-
).toBe("Command matched auto-deny pattern and was blocked automatically.");
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it("falls back when description is whitespace-only", () => {
|
|
128
|
-
expect(
|
|
129
|
-
formatAutoDenyReason({ pattern: "python -m venv", description: " " }),
|
|
130
|
-
).toBe("Command matched auto-deny pattern and was blocked automatically.");
|
|
131
|
-
});
|
|
132
|
-
});
|
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
|
-
});
|