@zhushanwen/pi-permission 0.0.1
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 +455 -0
- package/index.ts +2 -0
- package/package.json +52 -0
- package/src/__tests__/approval.test.ts +456 -0
- package/src/__tests__/commands.test.ts +487 -0
- package/src/__tests__/config.test.ts +229 -0
- package/src/__tests__/e2e-modes.test.ts +253 -0
- package/src/__tests__/extension.test.ts +170 -0
- package/src/__tests__/index-integration.test.ts +282 -0
- package/src/__tests__/model-picker.test.ts +371 -0
- package/src/__tests__/pipeline.test.ts +976 -0
- package/src/__tests__/production.test.ts +102 -0
- package/src/__tests__/rule-editor-component.test.ts +475 -0
- package/src/__tests__/rule-editor.test.ts +252 -0
- package/src/__tests__/rule-templates.test.ts +264 -0
- package/src/__tests__/statusline.test.ts +221 -0
- package/src/__tests__/types.test.ts +59 -0
- package/src/approval.ts +352 -0
- package/src/ast/__tests__/analyzer.test.ts +262 -0
- package/src/ast/__tests__/loader.test.ts +136 -0
- package/src/ast/analyzer.ts +326 -0
- package/src/ast/index.ts +7 -0
- package/src/ast/loader.ts +113 -0
- package/src/classifier/__tests__/classifier.test.ts +285 -0
- package/src/classifier/__tests__/json-parser.test.ts +118 -0
- package/src/classifier/__tests__/model-resolver.test.ts +173 -0
- package/src/classifier/__tests__/prompt.test.ts +59 -0
- package/src/classifier/classifier.ts +242 -0
- package/src/classifier/index.ts +28 -0
- package/src/classifier/json-parser.ts +127 -0
- package/src/classifier/model-resolver.ts +295 -0
- package/src/classifier/prompt.ts +52 -0
- package/src/commands.ts +234 -0
- package/src/config.ts +216 -0
- package/src/index.ts +291 -0
- package/src/model-picker.ts +398 -0
- package/src/pipeline.ts +541 -0
- package/src/production.ts +102 -0
- package/src/rule-editor-component.ts +883 -0
- package/src/rule-editor.ts +262 -0
- package/src/rule-templates.ts +332 -0
- package/src/rules/__tests__/builtins.test.ts +614 -0
- package/src/rules/__tests__/index.test.ts +26 -0
- package/src/rules/__tests__/matcher.test.ts +378 -0
- package/src/rules/__tests__/wildcard.test.ts +84 -0
- package/src/rules/builtins.ts +680 -0
- package/src/rules/index.ts +27 -0
- package/src/rules/matcher.ts +160 -0
- package/src/rules/wildcard.ts +43 -0
- package/src/statusline.ts +226 -0
- package/src/types.ts +249 -0
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BT 系列:builtins.ts 单元测试。
|
|
3
|
+
*
|
|
4
|
+
* 三组:
|
|
5
|
+
* - BT-safe-*:Codex 50 无条件 + 9 条件安全命令(isKnownSafeCommand)
|
|
6
|
+
* - BT-danger-*:permission-gate 12 危险规则(BUILTIN_DANGER_RULES)
|
|
7
|
+
* - BT-misc:getDefaultRules / findGitSubcommand
|
|
8
|
+
*
|
|
9
|
+
* 移植自 Codex is_safe_command.rs 的 #[test] 用例(known_safe_examples /
|
|
10
|
+
* git_branch_mutating_flags_are_not_safe / git_output_flags_are_not_safe /
|
|
11
|
+
* base64_output_options_are_unsafe / ripgrep_rules / unknown_or_partial 等),
|
|
12
|
+
* 确保白名单行为与源码一致。
|
|
13
|
+
*/
|
|
14
|
+
import { describe, expect, it } from "vitest";
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
BUILTIN_DANGER_RULES,
|
|
18
|
+
BUILTIN_UNCONDITIONAL_SAFE,
|
|
19
|
+
findGitSubcommand,
|
|
20
|
+
getDefaultRules,
|
|
21
|
+
isKnownSafeCommand,
|
|
22
|
+
} from "../builtins.js";
|
|
23
|
+
import { matchRulesForArgv } from "../matcher.js";
|
|
24
|
+
|
|
25
|
+
// ──────────────────────── BT-safe: Codex 白名单 ────────────────────────
|
|
26
|
+
|
|
27
|
+
describe("BT-safe-50: 无条件安全命令(24 Codex + 26 扩充)", () => {
|
|
28
|
+
// 与 is_safe_command.rs `is_safe_to_call_with_exec` 的 match 分支一致
|
|
29
|
+
const cases: string[][] = [
|
|
30
|
+
["cat", "file"],
|
|
31
|
+
["cd", "/tmp"],
|
|
32
|
+
["cut", "-f1"],
|
|
33
|
+
["echo", "hi"],
|
|
34
|
+
["expr", "1+1"],
|
|
35
|
+
["false"],
|
|
36
|
+
["grep", "foo"],
|
|
37
|
+
["head", "file"],
|
|
38
|
+
["id"],
|
|
39
|
+
["ls", "-la"],
|
|
40
|
+
["nl", "-nrz", "Cargo.toml"],
|
|
41
|
+
["paste", "a", "b"],
|
|
42
|
+
["pwd"],
|
|
43
|
+
["rev", "file"],
|
|
44
|
+
["seq", "1", "10"],
|
|
45
|
+
["stat", "file"],
|
|
46
|
+
["tail", "file"],
|
|
47
|
+
["tr", "a", "b"],
|
|
48
|
+
["true"],
|
|
49
|
+
["uname"],
|
|
50
|
+
["uniq", "file"],
|
|
51
|
+
["wc", "file"],
|
|
52
|
+
["which", "node"],
|
|
53
|
+
["whoami"],
|
|
54
|
+
// ── 本扩展扩充(26 条,字母序)──
|
|
55
|
+
["arch"],
|
|
56
|
+
["basename", "/tmp/file.txt"],
|
|
57
|
+
["cksum", "file"],
|
|
58
|
+
["cmp", "a", "b"],
|
|
59
|
+
["column", "-t"],
|
|
60
|
+
["comm", "a", "b"],
|
|
61
|
+
["diff", "a", "b"],
|
|
62
|
+
["dirname", "/tmp/file.txt"],
|
|
63
|
+
["du", "-sh", "."],
|
|
64
|
+
["df", "-h"],
|
|
65
|
+
["expand", "file"],
|
|
66
|
+
["file", "file.txt"],
|
|
67
|
+
["fold", "-w", "80", "file"],
|
|
68
|
+
["groups"],
|
|
69
|
+
["jq", ".", "file.json"],
|
|
70
|
+
["md5sum", "file"],
|
|
71
|
+
["printenv", "PATH"],
|
|
72
|
+
["ps", "aux"],
|
|
73
|
+
["readlink", "link"],
|
|
74
|
+
["realpath", "file"],
|
|
75
|
+
["sha256sum", "file"],
|
|
76
|
+
["shasum", "file"],
|
|
77
|
+
["tsort", "file"],
|
|
78
|
+
["uptime"],
|
|
79
|
+
["whereis", "node"],
|
|
80
|
+
["who"],
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
for (const argv of cases) {
|
|
84
|
+
it(`BT-safe-50: ${argv.join(" ")} → safe`, () => {
|
|
85
|
+
expect(isKnownSafeCommand(argv)).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
it("BT-safe-50: 白名单恰好 50 个命令", () => {
|
|
90
|
+
expect(BUILTIN_UNCONDITIONAL_SAFE.size).toBe(50);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("BT-safe-base64: base64 flag 子检查", () => {
|
|
95
|
+
it("base64 无 -o → safe", () => {
|
|
96
|
+
expect(isKnownSafeCommand(["base64"])).toBe(true);
|
|
97
|
+
expect(isKnownSafeCommand(["base64", "-d"])).toBe(true);
|
|
98
|
+
expect(isKnownSafeCommand(["base64", "input.txt"])).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("base64 -o → unsafe(移植 base64_output_options_are_unsafe)", () => {
|
|
102
|
+
expect(isKnownSafeCommand(["base64", "-o", "out.bin"])).toBe(false);
|
|
103
|
+
expect(isKnownSafeCommand(["base64", "--output", "out.bin"])).toBe(false);
|
|
104
|
+
expect(isKnownSafeCommand(["base64", "--output=out.bin"])).toBe(false);
|
|
105
|
+
expect(isKnownSafeCommand(["base64", "-ob64.txt"])).toBe(false); // 合并 flag
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("base64 -do(合并 -d -o)→ unsafe(G4: 合并 flag 非首位)", () => {
|
|
109
|
+
expect(isKnownSafeCommand(["base64", "-do", "out.bin"])).toBe(false);
|
|
110
|
+
expect(isKnownSafeCommand(["base64", "-fo"])).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe("BT-safe-find: find flag 子检查", () => {
|
|
115
|
+
it("find 无危险选项 → safe", () => {
|
|
116
|
+
expect(isKnownSafeCommand(["find", ".", "-name", "file.txt"])).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("find -exec/-delete/-fls 等 → unsafe(移植 unknown_or_partial)", () => {
|
|
120
|
+
expect(
|
|
121
|
+
isKnownSafeCommand(["find", ".", "-name", "file.txt", "-exec", "rm", "{}", ";"]),
|
|
122
|
+
).toBe(false);
|
|
123
|
+
expect(isKnownSafeCommand(["find", ".", "-delete", "-name", "file.txt"])).toBe(false);
|
|
124
|
+
expect(isKnownSafeCommand(["find", ".", "-fls", "/etc/passwd"])).toBe(false);
|
|
125
|
+
expect(isKnownSafeCommand(["find", ".", "-fprint", "/etc/passwd"])).toBe(false);
|
|
126
|
+
expect(isKnownSafeCommand(["find", ".", "-fprint0", "/etc/passwd"])).toBe(false);
|
|
127
|
+
expect(isKnownSafeCommand(["find", ".", "-fprintf", "/root/suid.txt", "%#m %u %p\n"])).toBe(false);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe("BT-safe-rg: ripgrep flag 子检查(移植 ripgrep_rules)", () => {
|
|
132
|
+
it("rg 无危险 flag → safe", () => {
|
|
133
|
+
expect(isKnownSafeCommand(["rg", "Cargo.toml", "-n"])).toBe(true);
|
|
134
|
+
expect(isKnownSafeCommand(["rg", "pattern"])).toBe(true);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("rg --search-zip / -z → unsafe", () => {
|
|
138
|
+
expect(isKnownSafeCommand(["rg", "--search-zip", "files"])).toBe(false);
|
|
139
|
+
expect(isKnownSafeCommand(["rg", "-z", "files"])).toBe(false);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("rg --pre / --hostname-bin(split 与 = 两种形式)→ unsafe", () => {
|
|
143
|
+
expect(isKnownSafeCommand(["rg", "--pre", "pwned", "files"])).toBe(false);
|
|
144
|
+
expect(isKnownSafeCommand(["rg", "--pre=pwned", "files"])).toBe(false);
|
|
145
|
+
expect(isKnownSafeCommand(["rg", "--hostname-bin", "pwned", "files"])).toBe(false);
|
|
146
|
+
expect(isKnownSafeCommand(["rg", "--hostname-bin=pwned", "files"])).toBe(false);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("BT-safe-git: git 子命令 + 全局选项检查", () => {
|
|
151
|
+
it("git status/log/diff/show → safe(移植 known_safe_examples)", () => {
|
|
152
|
+
expect(isKnownSafeCommand(["git", "status"])).toBe(true);
|
|
153
|
+
expect(isKnownSafeCommand(["git", "log", "-p", "-1"])).toBe(true);
|
|
154
|
+
expect(isKnownSafeCommand(["git", "diff", "-p"])).toBe(true);
|
|
155
|
+
expect(isKnownSafeCommand(["git", "show", "-p", "HEAD"])).toBe(true);
|
|
156
|
+
expect(isKnownSafeCommand(["git", "log", "--oneline"])).toBe(true);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("git push/fetch 等非白名单子命令 → unsafe", () => {
|
|
160
|
+
expect(isKnownSafeCommand(["git", "push"])).toBe(false);
|
|
161
|
+
expect(isKnownSafeCommand(["git", "fetch"])).toBe(false);
|
|
162
|
+
expect(isKnownSafeCommand(["git", "checkout", "status"])).toBe(false); // 第一个位置参数是子命令
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("git branch 只读 → safe", () => {
|
|
166
|
+
expect(isKnownSafeCommand(["git", "branch"])).toBe(true);
|
|
167
|
+
expect(isKnownSafeCommand(["git", "branch", "--show-current"])).toBe(true);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("git branch 变更 flag → unsafe(移植 git_branch_mutating_flags_are_not_safe)", () => {
|
|
171
|
+
expect(isKnownSafeCommand(["git", "branch", "-d", "feature"])).toBe(false);
|
|
172
|
+
expect(isKnownSafeCommand(["git", "branch", "new-branch"])).toBe(false); // 位置参数 = 创建分支
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("git 全局选项 -C/-c/--paginate → unsafe(移植 git_global_override_flags_are_not_safe)", () => {
|
|
176
|
+
expect(isKnownSafeCommand(["git", "-C", ".", "status"])).toBe(false);
|
|
177
|
+
expect(isKnownSafeCommand(["git", "-C.", "status"])).toBe(false);
|
|
178
|
+
expect(isKnownSafeCommand(["git", "-c", "core.pager=cat", "log", "-n", "1"])).toBe(false);
|
|
179
|
+
expect(isKnownSafeCommand(["git", "-ccore.pager=cat", "status"])).toBe(false);
|
|
180
|
+
expect(isKnownSafeCommand(["git", "--paginate", "log", "-1"])).toBe(false);
|
|
181
|
+
expect(isKnownSafeCommand(["git", "-p", "log", "-1"])).toBe(false);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("git 子命令危险输出选项 --output/--ext-diff → unsafe(移植 git_output_flags_are_not_safe)", () => {
|
|
185
|
+
expect(isKnownSafeCommand(["git", "log", "--output=/tmp/out", "-n", "1"])).toBe(false);
|
|
186
|
+
expect(isKnownSafeCommand(["git", "diff", "--output", "/tmp/out"])).toBe(false);
|
|
187
|
+
expect(isKnownSafeCommand(["git", "show", "--output=/tmp/out", "HEAD"])).toBe(false);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe("BT-safe-sed: sed -n {N|M,N}p", () => {
|
|
192
|
+
it("sed -n 1p / sed -n 1,5p → safe(移植 bash_lc_safe_examples)", () => {
|
|
193
|
+
expect(isKnownSafeCommand(["sed", "-n", "1p", "file.txt"])).toBe(true);
|
|
194
|
+
expect(isKnownSafeCommand(["sed", "-n", "1,5p", "file.txt"])).toBe(true);
|
|
195
|
+
expect(isKnownSafeCommand(["sed", "-n", "10p"])).toBe(true);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("sed -i / sed s/a/b/ / sed -n xp → unsafe", () => {
|
|
199
|
+
expect(isKnownSafeCommand(["sed", "-i", "s/a/b/"])).toBe(false);
|
|
200
|
+
expect(isKnownSafeCommand(["sed", "s/a/b/"])).toBe(false);
|
|
201
|
+
expect(isKnownSafeCommand(["sed", "-n", "xp", "file.txt"])).toBe(false); // xp 非数字
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("sed argv > 4 → unsafe", () => {
|
|
205
|
+
// sed -n 1p a b c d(5 参数)超长
|
|
206
|
+
expect(isKnownSafeCommand(["sed", "-n", "1p", "a", "b", "c", "d"])).toBe(false);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe("BT-safe-sort: sort flag 子检查", () => {
|
|
211
|
+
it("sort 无 -o → safe", () => {
|
|
212
|
+
expect(isKnownSafeCommand(["sort", "file.txt"])).toBe(true);
|
|
213
|
+
expect(isKnownSafeCommand(["sort", "-n", "-r"])).toBe(true);
|
|
214
|
+
});
|
|
215
|
+
it("sort -o → unsafe", () => {
|
|
216
|
+
expect(isKnownSafeCommand(["sort", "-o", "out.txt", "in.txt"])).toBe(false);
|
|
217
|
+
expect(isKnownSafeCommand(["sort", "--output", "out.txt"])).toBe(false);
|
|
218
|
+
expect(isKnownSafeCommand(["sort", "--output=out.txt"])).toBe(false);
|
|
219
|
+
expect(isKnownSafeCommand(["sort", "-oout.txt"])).toBe(false);
|
|
220
|
+
});
|
|
221
|
+
it("sort -fo(合并 -f -o)→ unsafe", () => {
|
|
222
|
+
expect(isKnownSafeCommand(["sort", "-fo", "out.txt"])).toBe(false);
|
|
223
|
+
expect(isKnownSafeCommand(["sort", "-nfo", "out.txt"])).toBe(false); // -n -f -o
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
describe("BT-safe-iconv: iconv flag 子检查", () => {
|
|
228
|
+
it("iconv 无 -o → safe", () => {
|
|
229
|
+
expect(isKnownSafeCommand(["iconv", "-f", "UTF-8", "-t", "GBK"])).toBe(true);
|
|
230
|
+
});
|
|
231
|
+
it("iconv -o → unsafe", () => {
|
|
232
|
+
expect(isKnownSafeCommand(["iconv", "-o", "out.txt"])).toBe(false);
|
|
233
|
+
expect(isKnownSafeCommand(["iconv", "--output", "out.txt"])).toBe(false);
|
|
234
|
+
});
|
|
235
|
+
it("iconv -fo(合并)→ unsafe", () => {
|
|
236
|
+
expect(isKnownSafeCommand(["iconv", "-fo", "out.txt"])).toBe(false);
|
|
237
|
+
});
|
|
238
|
+
it("iconv --output= → unsafe", () => {
|
|
239
|
+
expect(isKnownSafeCommand(["iconv", "--output=out.txt"])).toBe(false);
|
|
240
|
+
});
|
|
241
|
+
it("iconv -oout(合并首字符)→ unsafe", () => {
|
|
242
|
+
expect(isKnownSafeCommand(["iconv", "-oout.txt"])).toBe(false);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
describe("BT-safe-shuf: shuf flag 子检查", () => {
|
|
247
|
+
it("shuf 无 -o → safe", () => {
|
|
248
|
+
expect(isKnownSafeCommand(["shuf", "-i", "1-10"])).toBe(true);
|
|
249
|
+
});
|
|
250
|
+
it("shuf -o → unsafe", () => {
|
|
251
|
+
expect(isKnownSafeCommand(["shuf", "-o", "out.txt"])).toBe(false);
|
|
252
|
+
expect(isKnownSafeCommand(["shuf", "--output=out.txt"])).toBe(false);
|
|
253
|
+
});
|
|
254
|
+
it("shuf -fo(合并)→ unsafe", () => {
|
|
255
|
+
expect(isKnownSafeCommand(["shuf", "-fo", "out.txt"])).toBe(false);
|
|
256
|
+
});
|
|
257
|
+
it("shuf --output(空格形式)→ unsafe", () => {
|
|
258
|
+
expect(isKnownSafeCommand(["shuf", "--output", "out.txt"])).toBe(false);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
describe("BT-safe-date: date flag 子检查", () => {
|
|
263
|
+
it("date 无 -s → safe", () => {
|
|
264
|
+
expect(isKnownSafeCommand(["date"])).toBe(true);
|
|
265
|
+
expect(isKnownSafeCommand(["date", "+%Y-%m-%d"])).toBe(true);
|
|
266
|
+
});
|
|
267
|
+
it("date -s → unsafe(设置系统时间)", () => {
|
|
268
|
+
expect(isKnownSafeCommand(["date", "-s", "2025-01-01"])).toBe(false);
|
|
269
|
+
expect(isKnownSafeCommand(["date", "--set", "2025-01-01"])).toBe(false);
|
|
270
|
+
expect(isKnownSafeCommand(["date", "--set=2025-01-01"])).toBe(false);
|
|
271
|
+
});
|
|
272
|
+
it("date -ds(合并 -d -s)→ unsafe", () => {
|
|
273
|
+
expect(isKnownSafeCommand(["date", "-ds", "2025-01-01"])).toBe(false);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
describe("BT-safe-misc: 边界", () => {
|
|
278
|
+
it("空 argv → false", () => {
|
|
279
|
+
expect(isKnownSafeCommand([])).toBe(false);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("未知命令 → false(移植 unknown_or_partial)", () => {
|
|
283
|
+
expect(isKnownSafeCommand(["foo"])).toBe(false);
|
|
284
|
+
expect(isKnownSafeCommand(["cargo", "check"])).toBe(false); // cargo 不在白名单
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("zsh 归一化为 bash(源码行为)", () => {
|
|
288
|
+
// zsh -lc "ls" 在源码里 zsh→bash,但本扩展不实现 bash -lc 解析
|
|
289
|
+
// bash 作为 argv[0] 不在白名单 → false(与 unknown 命令一致)
|
|
290
|
+
expect(isKnownSafeCommand(["zsh", "-lc", "ls"])).toBe(false);
|
|
291
|
+
expect(isKnownSafeCommand(["bash", "-lc", "ls"])).toBe(false);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("带路径前缀的命令名(basename lookup)", () => {
|
|
295
|
+
// executableNameLookupKey 取 basename
|
|
296
|
+
expect(isKnownSafeCommand(["/usr/bin/ls"])).toBe(true);
|
|
297
|
+
expect(isKnownSafeCommand(["/usr/bin/git", "status"])).toBe(true);
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// ──────────────────────── BT-danger: permission-gate 12 危险规则 ────────────────────────
|
|
302
|
+
|
|
303
|
+
describe("BT-danger: BUILTIN_DANGER_RULES 12 条", () => {
|
|
304
|
+
it("BT-danger-001: recursive delete — rm -rf", () => {
|
|
305
|
+
const re = new RegExp(BUILTIN_DANGER_RULES[0].pattern, "i");
|
|
306
|
+
expect(re.test("rm -rf /")).toBe(true);
|
|
307
|
+
expect(re.test("rm --recursive foo")).toBe(true);
|
|
308
|
+
expect(re.test("rm foo")).toBe(false);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("BT-danger-002: sudo", () => {
|
|
312
|
+
const re = new RegExp(BUILTIN_DANGER_RULES[1].pattern, "i");
|
|
313
|
+
expect(re.test("sudo rm /etc/passwd")).toBe(true);
|
|
314
|
+
expect(re.test("ls")).toBe(false);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("BT-danger-003: world-writable — chmod 777", () => {
|
|
318
|
+
const re = new RegExp(BUILTIN_DANGER_RULES[2].pattern, "i");
|
|
319
|
+
expect(re.test("chmod 777 file")).toBe(true);
|
|
320
|
+
expect(re.test("chmod 755 file")).toBe(false);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it("BT-danger-004: raw device — > /dev/sda", () => {
|
|
324
|
+
const re = new RegExp(BUILTIN_DANGER_RULES[3].pattern, "i");
|
|
325
|
+
expect(re.test("dd if=img > /dev/sda")).toBe(true);
|
|
326
|
+
expect(re.test("echo hi")).toBe(false);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it("BT-danger-005: force push — git push --force", () => {
|
|
330
|
+
const re = new RegExp(BUILTIN_DANGER_RULES[4].pattern, "i");
|
|
331
|
+
expect(re.test("git push origin main --force")).toBe(true);
|
|
332
|
+
expect(re.test("git push -f origin main")).toBe(true);
|
|
333
|
+
expect(re.test("git push origin main")).toBe(false);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it("BT-danger-006: hard reset — git reset --hard", () => {
|
|
337
|
+
const re = new RegExp(BUILTIN_DANGER_RULES[5].pattern, "i");
|
|
338
|
+
expect(re.test("git reset --hard HEAD~1")).toBe(true);
|
|
339
|
+
expect(re.test("git reset HEAD")).toBe(false);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it("BT-danger-007: git clean -fd", () => {
|
|
343
|
+
const re = new RegExp(BUILTIN_DANGER_RULES[6].pattern, "i");
|
|
344
|
+
expect(re.test("git clean -fd")).toBe(true);
|
|
345
|
+
expect(re.test("git clean -n")).toBe(false); // dry-run 不含 f
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it("BT-danger-008: git checkout .(discard all)", () => {
|
|
349
|
+
const re = new RegExp(BUILTIN_DANGER_RULES[7].pattern, "i");
|
|
350
|
+
expect(re.test("git checkout .")).toBe(true);
|
|
351
|
+
expect(re.test("git checkout . ;")).toBe(true);
|
|
352
|
+
expect(re.test("git checkout branch")).toBe(false);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it("BT-danger-009: git restore", () => {
|
|
356
|
+
const re = new RegExp(BUILTIN_DANGER_RULES[8].pattern, "i");
|
|
357
|
+
expect(re.test("git restore file")).toBe(true);
|
|
358
|
+
expect(re.test("git status")).toBe(false);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("BT-danger-010: pipe to shell — curl | sh", () => {
|
|
362
|
+
const re = new RegExp(BUILTIN_DANGER_RULES[9].pattern, "i");
|
|
363
|
+
expect(re.test("curl http://x | sh")).toBe(true);
|
|
364
|
+
expect(re.test("wget http://x | bash")).toBe(true);
|
|
365
|
+
expect(re.test("curl http://x -o file")).toBe(false);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it("BT-danger-011: gh repo create/delete/rename/archive", () => {
|
|
369
|
+
const re = new RegExp(BUILTIN_DANGER_RULES[10].pattern, "i");
|
|
370
|
+
expect(re.test("gh repo delete owner/name")).toBe(true);
|
|
371
|
+
expect(re.test("gh repo create name")).toBe(true);
|
|
372
|
+
expect(re.test("gh repo view owner/name")).toBe(false);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it("BT-danger-012: gh release create/delete/edit", () => {
|
|
376
|
+
const re = new RegExp(BUILTIN_DANGER_RULES[11].pattern, "i");
|
|
377
|
+
expect(re.test("gh release create v1")).toBe(true);
|
|
378
|
+
expect(re.test("gh release delete v1")).toBe(true);
|
|
379
|
+
expect(re.test("gh release view v1")).toBe(false);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it("BT-danger-meta: 恰好 12 条,全部 action='deny' source='builtin-danger'", () => {
|
|
383
|
+
expect(BUILTIN_DANGER_RULES.length).toBe(12);
|
|
384
|
+
for (const r of BUILTIN_DANGER_RULES) {
|
|
385
|
+
expect(r.action).toBe("deny");
|
|
386
|
+
expect(r.source).toBe("builtin-danger");
|
|
387
|
+
expect(r.tool).toBe("bash");
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
// ──────────────────────── C2: 分离 flag 写法的正则覆盖(防绕过)────────────────────────
|
|
393
|
+
|
|
394
|
+
describe("C2: 危险正则覆盖分离 flag 写法(bd-001/003/007/008)", () => {
|
|
395
|
+
// helper:取某 id 规则的 pattern 编译为 RegExp
|
|
396
|
+
const patternFor = (id: string): RegExp => {
|
|
397
|
+
const rule = BUILTIN_DANGER_RULES.find((r) => r.id === id);
|
|
398
|
+
if (!rule) throw new Error(`rule ${id} not found`);
|
|
399
|
+
return new RegExp(rule.pattern, "i");
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
it("bd-001: rm 分离 flag `rm -f -r /` 也命中(原来只匹配合并 -rf)", () => {
|
|
403
|
+
const re = patternFor("bd-001");
|
|
404
|
+
// 合并 flag(原有覆盖)
|
|
405
|
+
expect(re.test("rm -rf /")).toBe(true);
|
|
406
|
+
expect(re.test("rm -fr /")).toBe(true);
|
|
407
|
+
// 分离 flag(C2 新覆盖)
|
|
408
|
+
expect(re.test("rm -f -r /")).toBe(true);
|
|
409
|
+
expect(re.test("rm -f -r -f /tmp/x")).toBe(true);
|
|
410
|
+
// 长选项
|
|
411
|
+
expect(re.test("rm --recursive foo")).toBe(true);
|
|
412
|
+
// 非 recursive 不命中
|
|
413
|
+
expect(re.test("rm -f /")).toBe(false);
|
|
414
|
+
expect(re.test("rm foo")).toBe(false);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it("bd-003: chmod 等价写法 a+rwx / ugo+rwx 也命中(原来只匹配 777)", () => {
|
|
418
|
+
const re = patternFor("bd-003");
|
|
419
|
+
// 777(原有覆盖)
|
|
420
|
+
expect(re.test("chmod 777 file")).toBe(true);
|
|
421
|
+
// 等价符号写法(C2 新覆盖)
|
|
422
|
+
expect(re.test("chmod a+rwx file")).toBe(true);
|
|
423
|
+
expect(re.test("chmod ugo+rwx file")).toBe(true);
|
|
424
|
+
expect(re.test("chmod ugo=rwx file")).toBe(true);
|
|
425
|
+
// 非危险权限不命中
|
|
426
|
+
expect(re.test("chmod 755 file")).toBe(false);
|
|
427
|
+
expect(re.test("chmod a+r file")).toBe(false);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it("bd-007: git clean 分离 flag `git clean -d -f` 也命中(原来只匹配合并 -fd)", () => {
|
|
431
|
+
const re = patternFor("bd-007");
|
|
432
|
+
// 合并 flag(原有覆盖)
|
|
433
|
+
expect(re.test("git clean -fd")).toBe(true);
|
|
434
|
+
expect(re.test("git clean -df")).toBe(true);
|
|
435
|
+
// 分离 flag(C2 新覆盖)
|
|
436
|
+
expect(re.test("git clean -d -f")).toBe(true);
|
|
437
|
+
expect(re.test("git clean -x -f")).toBe(true);
|
|
438
|
+
// --force
|
|
439
|
+
expect(re.test("git clean --force")).toBe(true);
|
|
440
|
+
// dry-run(无 f)不命中
|
|
441
|
+
expect(re.test("git clean -n")).toBe(false);
|
|
442
|
+
expect(re.test("git clean -d")).toBe(false);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it("bd-001: 长选项 --verbose 不误报(只匹配短 flag 簇的 r)", () => {
|
|
446
|
+
const re = patternFor("bd-001");
|
|
447
|
+
// 短 flag(原有覆盖)
|
|
448
|
+
expect(re.test("rm -rf /")).toBe(true);
|
|
449
|
+
expect(re.test("rm -fr /")).toBe(true);
|
|
450
|
+
expect(re.test("rm -f -r /")).toBe(true);
|
|
451
|
+
// 长选项含 r 但非 recursive → 不应命中
|
|
452
|
+
expect(re.test("rm --verbose file")).toBe(false);
|
|
453
|
+
expect(re.test("rm --dir foo")).toBe(false);
|
|
454
|
+
// --recursive 命中
|
|
455
|
+
expect(re.test("rm --recursive x")).toBe(true);
|
|
456
|
+
// 短 flag 簇中 r 在任意位置
|
|
457
|
+
expect(re.test("rm -xr /tmp")).toBe(true);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it("bd-007: 长选项 --files 不误报", () => {
|
|
461
|
+
const re = patternFor("bd-007");
|
|
462
|
+
expect(re.test("git clean -fd")).toBe(true);
|
|
463
|
+
expect(re.test("git clean --force")).toBe(true);
|
|
464
|
+
// 长选项含 f 但非 force → 不应命中
|
|
465
|
+
expect(re.test("git clean --files foo")).toBe(false);
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it("bd-008: git checkout -- . 也命中(原来只匹配 `git checkout .`)", () => {
|
|
469
|
+
const re = patternFor("bd-008");
|
|
470
|
+
// 无 -- (原有覆盖)
|
|
471
|
+
expect(re.test("git checkout .")).toBe(true);
|
|
472
|
+
expect(re.test("git checkout . ;")).toBe(true);
|
|
473
|
+
// 带 -- (C2 新覆盖)
|
|
474
|
+
expect(re.test("git checkout -- .")).toBe(true);
|
|
475
|
+
expect(re.test("git checkout -- . &&")).toBe(true);
|
|
476
|
+
// 普通分支切换不命中
|
|
477
|
+
expect(re.test("git checkout branch")).toBe(false);
|
|
478
|
+
expect(re.test("git checkout -- file.txt")).toBe(false);
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
// ──────────────────────── m8: bd-004 设备重定向规则扩展 ────────────────────────
|
|
483
|
+
|
|
484
|
+
describe("m8: bd-004 覆盖 dd of=/dev/... 与现代设备名(nvme/mmcblk)", () => {
|
|
485
|
+
const re = new RegExp(
|
|
486
|
+
BUILTIN_DANGER_RULES.find((r) => r.id === "bd-004")!.pattern,
|
|
487
|
+
"i",
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
it("重定向 > /dev/sda(原有覆盖)", () => {
|
|
491
|
+
expect(re.test("dd if=img > /dev/sda")).toBe(true);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
it("dd of=/dev/sda(C2/m8 新覆盖)", () => {
|
|
495
|
+
expect(re.test("dd if=img of=/dev/sda")).toBe(true);
|
|
496
|
+
expect(re.test("dd of=/dev/sdb bs=4M")).toBe(true);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it("现代设备名 nvme0n1 / mmcblk0(m8 新覆盖)", () => {
|
|
500
|
+
expect(re.test("dd of=/dev/nvme0n1")).toBe(true);
|
|
501
|
+
expect(re.test("dd if=img > /dev/nvme0n1p2")).toBe(true);
|
|
502
|
+
expect(re.test("dd of=/dev/mmcblk0")).toBe(true);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it("非设备写入不误报", () => {
|
|
506
|
+
expect(re.test("echo hi")).toBe(false);
|
|
507
|
+
expect(re.test("cat file > /tmp/out")).toBe(false);
|
|
508
|
+
expect(re.test("echo data > out.txt")).toBe(false);
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
// ──────────────────────── C2 端到端:matchRulesForArgv 真实链路 ────────────────────────
|
|
513
|
+
|
|
514
|
+
describe("C2-e2e: matchRulesForArgv 对分离 flag 写法返回 deny", () => {
|
|
515
|
+
// builtins.test 直接测内置规则正则;此处补真实匹配链路(matchRulesForArgv)
|
|
516
|
+
const rules = getDefaultRules();
|
|
517
|
+
|
|
518
|
+
it("rm -f -r / → deny(bd-001)", () => {
|
|
519
|
+
const r = matchRulesForArgv(["rm", "-f", "-r", "/"], rules);
|
|
520
|
+
expect(r.action).toBe("deny");
|
|
521
|
+
expect(r.matchedRule?.id).toBe("bd-001");
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
it("chmod a+rwx file → deny(bd-003)", () => {
|
|
525
|
+
const r = matchRulesForArgv(["chmod", "a+rwx", "file"], rules);
|
|
526
|
+
expect(r.action).toBe("deny");
|
|
527
|
+
expect(r.matchedRule?.id).toBe("bd-003");
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
it("git clean -d -f → deny(bd-007)", () => {
|
|
531
|
+
const r = matchRulesForArgv(["git", "clean", "-d", "-f"], rules);
|
|
532
|
+
expect(r.action).toBe("deny");
|
|
533
|
+
expect(r.matchedRule?.id).toBe("bd-007");
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
it("git checkout -- . → deny(bd-008)", () => {
|
|
537
|
+
// argv 级:["git","checkout","--","."] join 后 "git checkout -- ."
|
|
538
|
+
const r = matchRulesForArgv(["git", "checkout", "--", "."], rules);
|
|
539
|
+
expect(r.action).toBe("deny");
|
|
540
|
+
expect(r.matchedRule?.id).toBe("bd-008");
|
|
541
|
+
});
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
// ──────────────────────── BT-misc: getDefaultRules / findGitSubcommand ────────────────────────
|
|
545
|
+
|
|
546
|
+
describe("BT-misc: getDefaultRules", () => {
|
|
547
|
+
it("返回 12 条 builtin-danger 规则(深拷贝:新数组 + 新元素对象)", () => {
|
|
548
|
+
const rules = getDefaultRules();
|
|
549
|
+
expect(rules.length).toBe(12);
|
|
550
|
+
// m6:深拷贝——新数组,元素也是新对象(不共享内置常量引用)
|
|
551
|
+
expect(rules).not.toBe(BUILTIN_DANGER_RULES);
|
|
552
|
+
expect(rules[0]).not.toBe(BUILTIN_DANGER_RULES[0]);
|
|
553
|
+
// 但内容(值)相等
|
|
554
|
+
expect(rules[0]).toEqual(BUILTIN_DANGER_RULES[0]);
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
it("深拷贝防外部修改——push 不影响内置常量", () => {
|
|
558
|
+
const rules = getDefaultRules();
|
|
559
|
+
const originalLen = BUILTIN_DANGER_RULES.length;
|
|
560
|
+
rules.push({
|
|
561
|
+
id: "x",
|
|
562
|
+
tool: "bash",
|
|
563
|
+
pattern: "x",
|
|
564
|
+
action: "allow",
|
|
565
|
+
source: "user",
|
|
566
|
+
});
|
|
567
|
+
expect(BUILTIN_DANGER_RULES.length).toBe(originalLen);
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
it("m6:深拷贝防元素属性污染——改 rule.action 不影响内置常量", () => {
|
|
571
|
+
// 浅拷贝的缺陷:元素共享引用,改 action 会污染 BUILTIN_DANGER_RULES。
|
|
572
|
+
// 深拷贝后:修改返回值元素的属性不影响内置常量。
|
|
573
|
+
const rules = getDefaultRules();
|
|
574
|
+
const originalAction = BUILTIN_DANGER_RULES[0]!.action;
|
|
575
|
+
// 篡改返回值的第 0 条规则
|
|
576
|
+
rules[0]!.action = "allow";
|
|
577
|
+
rules[0]!.pattern = "tampered";
|
|
578
|
+
// 内置常量未被污染
|
|
579
|
+
expect(BUILTIN_DANGER_RULES[0]!.action).toBe(originalAction);
|
|
580
|
+
expect(BUILTIN_DANGER_RULES[0]!.pattern).not.toBe("tampered");
|
|
581
|
+
});
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
describe("BT-misc: findGitSubcommand", () => {
|
|
585
|
+
it("直接子命令", () => {
|
|
586
|
+
expect(findGitSubcommand(["git", "status"], ["status", "log"])).toEqual([1, "status"]);
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
it("跳过 -C path(取值 global option)", () => {
|
|
590
|
+
// -C . status:-C 的下一个参数 . 被 skip,找到 status
|
|
591
|
+
expect(findGitSubcommand(["git", "-C", ".", "status"], ["status"])).toEqual([3, "status"]);
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
it("跳过 -Cpath(内联值 global option)", () => {
|
|
595
|
+
expect(findGitSubcommand(["git", "-C.", "status"], ["status"])).toEqual([2, "status"]);
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
it("跳过 --git-dir=path(prefix 内联)", () => {
|
|
599
|
+
expect(findGitSubcommand(["git", "--git-dir=.evil", "diff"], ["diff"])).toEqual([2, "diff"]);
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
it("非 git 命令 → undefined", () => {
|
|
603
|
+
expect(findGitSubcommand(["hg", "status"], ["status"])).toBeUndefined();
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
it("第一个非选项参数不在目标列表 → undefined(不误判后续位置参数)", () => {
|
|
607
|
+
// git checkout status:checkout 不在 [status],返回 undefined
|
|
608
|
+
expect(findGitSubcommand(["git", "checkout", "status"], ["status"])).toBeUndefined();
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
it("无匹配子命令 → undefined", () => {
|
|
612
|
+
expect(findGitSubcommand(["git", "push"], ["status", "log"])).toBeUndefined();
|
|
613
|
+
});
|
|
614
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IT 系列:rules/index.ts barrel 导出验证。
|
|
3
|
+
*
|
|
4
|
+
* 确保公开 API surface 完整,调用方可从 "./rules/index.js" 一处 import。
|
|
5
|
+
*/
|
|
6
|
+
import { describe, expect, it } from "vitest";
|
|
7
|
+
|
|
8
|
+
import * as rulesApi from "../index.js";
|
|
9
|
+
|
|
10
|
+
describe("IT: rules barrel 导出", () => {
|
|
11
|
+
it("导出所有公开函数", () => {
|
|
12
|
+
expect(typeof rulesApi.matchRules).toBe("function");
|
|
13
|
+
expect(typeof rulesApi.matchRulesForArgv).toBe("function");
|
|
14
|
+
expect(typeof rulesApi.resolvePattern).toBe("function");
|
|
15
|
+
expect(typeof rulesApi.getDefaultRules).toBe("function");
|
|
16
|
+
expect(typeof rulesApi.isKnownSafeCommand).toBe("function");
|
|
17
|
+
expect(typeof rulesApi.findGitSubcommand).toBe("function");
|
|
18
|
+
expect(typeof rulesApi.wildcardToRegExp).toBe("function");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("导出常量数据(BUILTIN_DANGER_RULES / BUILTIN_UNCONDITIONAL_SAFE)", () => {
|
|
22
|
+
expect(Array.isArray(rulesApi.BUILTIN_DANGER_RULES)).toBe(true);
|
|
23
|
+
expect(rulesApi.BUILTIN_DANGER_RULES.length).toBe(12);
|
|
24
|
+
expect(rulesApi.BUILTIN_UNCONDITIONAL_SAFE.size).toBe(50);
|
|
25
|
+
});
|
|
26
|
+
});
|