@diegopetrucci/pi-extensions 0.1.74 → 0.1.78
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 +2 -0
- package/README.md +17 -12
- package/extensions/agent-workflow-audit/README.md +1 -1
- package/extensions/agent-workflow-audit/index.ts +1631 -1009
- package/extensions/agent-workflow-audit/package.json +1 -1
- package/extensions/annotate-git-diff/README.md +3 -1
- package/extensions/annotate-git-diff/clipboard.ts +109 -100
- package/extensions/annotate-git-diff/git.ts +846 -768
- package/extensions/annotate-git-diff/glimpseui.d.ts +87 -78
- package/extensions/annotate-git-diff/index.ts +712 -671
- package/extensions/annotate-git-diff/package.json +1 -1
- package/extensions/annotate-git-diff/prompt.ts +45 -45
- package/extensions/annotate-git-diff/quiet-glimpse.ts +161 -155
- package/extensions/annotate-git-diff/review-server.ts +337 -311
- package/extensions/annotate-git-diff/types.ts +132 -132
- package/extensions/annotate-git-diff/ui.ts +27 -27
- package/extensions/annotate-git-diff/watch.ts +74 -74
- package/extensions/annotate-git-diff/web/app.js +1947 -1884
- package/extensions/annotate-git-diff/web/index.html +5 -6
- package/extensions/annotate-last-message/README.md +1 -1
- package/extensions/annotate-last-message/glimpseui.d.ts +87 -78
- package/extensions/annotate-last-message/index.ts +210 -206
- package/extensions/annotate-last-message/package.json +1 -1
- package/extensions/annotate-last-message/prompt.ts +87 -66
- package/extensions/annotate-last-message/quiet-glimpse.ts +161 -155
- package/extensions/annotate-last-message/session.ts +94 -89
- package/extensions/annotate-last-message/theme.ts +199 -193
- package/extensions/annotate-last-message/types.ts +23 -23
- package/extensions/annotate-last-message/ui.ts +27 -25
- package/extensions/annotate-last-message/web/app.js +4 -7
- package/extensions/annotate-last-message/web/index.html +3 -3
- package/extensions/annotate-last-message/web/md-renderer.js +1 -1
- package/extensions/brrr/README.md +1 -1
- package/extensions/brrr/index.ts +213 -191
- package/extensions/brrr/package.json +1 -1
- package/extensions/claude-fast/README.md +5 -79
- package/extensions/claude-fast/index.ts +31 -289
- package/extensions/claude-fast/package.json +2 -3
- package/extensions/code-reviewer/README.md +6 -6
- package/extensions/code-reviewer/index.ts +1825 -1373
- package/extensions/code-reviewer/package.json +1 -1
- package/extensions/confirm-destructive/README.md +1 -1
- package/extensions/confirm-destructive/index.ts +50 -52
- package/extensions/confirm-destructive/package.json +1 -1
- package/extensions/context-cap/README.md +9 -9
- package/extensions/context-cap/index.ts +313 -305
- package/extensions/context-cap/package.json +2 -2
- package/extensions/context-inspector/index.ts +1211 -1064
- package/extensions/context-inspector/package.json +1 -1
- package/extensions/contrarian/README.md +4 -3
- package/extensions/contrarian/index.ts +1863 -1737
- package/extensions/contrarian/package.json +1 -1
- package/extensions/dirty-repo-guard/index.ts +37 -41
- package/extensions/dirty-repo-guard/package.json +1 -1
- package/extensions/dynamic-context-pruning/README.md +11 -43
- package/extensions/dynamic-context-pruning/index.ts +2131 -2105
- package/extensions/dynamic-context-pruning/package.json +1 -1
- package/extensions/fast/README.md +52 -14
- package/extensions/fast/index.ts +513 -317
- package/extensions/fast/package.json +1 -1
- package/extensions/git-footer/README.md +1 -1
- package/extensions/git-footer/index.ts +458 -411
- package/extensions/git-footer/package.json +2 -3
- package/extensions/gnosis/index.ts +180 -130
- package/extensions/gnosis/package.json +1 -1
- package/extensions/inline-bash/index.ts +87 -103
- package/extensions/inline-bash/package.json +1 -1
- package/extensions/librarian/README.md +1 -1
- package/extensions/librarian/index.ts +1877 -1419
- package/extensions/librarian/package.json +1 -1
- package/extensions/minimal-footer/README.md +5 -5
- package/extensions/minimal-footer/index.ts +987 -1012
- package/extensions/minimal-footer/minimal-footer.example.json +1 -1
- package/extensions/minimal-footer/openai-usage.ts +195 -190
- package/extensions/minimal-footer/package.json +1 -1
- package/extensions/notify/index.ts +222 -230
- package/extensions/notify/package.json +1 -1
- package/extensions/openai-fast/README.md +5 -73
- package/extensions/openai-fast/index.ts +32 -272
- package/extensions/openai-fast/package.json +2 -3
- package/extensions/oracle/README.md +4 -3
- package/extensions/oracle/index.ts +1740 -1627
- package/extensions/oracle/package.json +1 -1
- package/extensions/permission-gate/README.md +3 -0
- package/extensions/permission-gate/index.ts +785 -612
- package/extensions/permission-gate/package.json +1 -1
- package/extensions/permission-gate/powershell-safety.ts +840 -793
- package/extensions/project-mcp-json/.pi-fleet-tested-version +1 -0
- package/extensions/project-mcp-json/README.md +199 -0
- package/extensions/project-mcp-json/config.ts +564 -0
- package/extensions/project-mcp-json/index.ts +119 -0
- package/extensions/project-mcp-json/package.json +38 -0
- package/extensions/quiet-tools/.pi-fleet-tested-version +1 -1
- package/extensions/quiet-tools/README.md +30 -19
- package/extensions/quiet-tools/index.ts +376 -291
- package/extensions/quiet-tools/package.json +2 -2
- package/extensions/review/README.md +2 -2
- package/extensions/review/index.ts +1745 -1872
- package/extensions/review/package.json +1 -1
- package/extensions/todo/index.ts +280 -280
- package/extensions/todo/package.json +1 -1
- package/extensions/triage-comments/README.md +6 -4
- package/extensions/triage-comments/index.ts +2620 -1888
- package/extensions/triage-comments/package.json +1 -1
- package/package.json +9 -6
- package/assets/social-preview.png +0 -0
- package/extensions/claude-fast/claude-fast.example.json +0 -4
- package/extensions/openai-fast/openai-fast.example.json +0 -4
|
@@ -1,80 +1,88 @@
|
|
|
1
|
-
const powerShellRemoveCommands = new Set([
|
|
1
|
+
const powerShellRemoveCommands = new Set([
|
|
2
|
+
"remove-item",
|
|
3
|
+
"del",
|
|
4
|
+
"erase",
|
|
5
|
+
"rd",
|
|
6
|
+
"ri",
|
|
7
|
+
"rm",
|
|
8
|
+
"rmdir",
|
|
9
|
+
]);
|
|
2
10
|
const dynamicPowerShellCommands = new Set([
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
11
|
+
"cmd",
|
|
12
|
+
"cmd.exe",
|
|
13
|
+
"icm",
|
|
14
|
+
"ii",
|
|
15
|
+
"iex",
|
|
16
|
+
"invoke-command",
|
|
17
|
+
"invoke-expression",
|
|
18
|
+
"invoke-item",
|
|
19
|
+
"powershell",
|
|
20
|
+
"powershell.exe",
|
|
21
|
+
"pwsh",
|
|
22
|
+
"pwsh.exe",
|
|
23
|
+
"saps",
|
|
24
|
+
"sajb",
|
|
25
|
+
"start",
|
|
26
|
+
"start-job",
|
|
27
|
+
"start-process",
|
|
28
|
+
"start-threadjob",
|
|
21
29
|
]);
|
|
22
30
|
const shellPowerShellCommands = new Set(["bash", "bash.exe", "sh", "sh.exe", "wsl", "wsl.exe"]);
|
|
23
31
|
const commandResolutionPowerShellCommands = new Set([
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
32
|
+
"import-alias",
|
|
33
|
+
"import-module",
|
|
34
|
+
"import-pssession",
|
|
35
|
+
"ipal",
|
|
36
|
+
"ipmo",
|
|
37
|
+
"ipsn",
|
|
38
|
+
"nal",
|
|
39
|
+
"new-alias",
|
|
40
|
+
"new-module",
|
|
41
|
+
"nmo",
|
|
42
|
+
"ral",
|
|
43
|
+
"remove-alias",
|
|
44
|
+
"remove-module",
|
|
45
|
+
"rmo",
|
|
46
|
+
"sal",
|
|
47
|
+
"set-alias",
|
|
40
48
|
]);
|
|
41
49
|
const commandDefinitionPowerShellKeywords = new Set(["filter", "function", "workflow"]);
|
|
42
50
|
const providerMutationPowerShellCommands = new Set([
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
51
|
+
"ac",
|
|
52
|
+
"add-content",
|
|
53
|
+
"clear-content",
|
|
54
|
+
"clear-item",
|
|
55
|
+
"cd",
|
|
56
|
+
"chdir",
|
|
57
|
+
"clc",
|
|
58
|
+
"cli",
|
|
59
|
+
"copy-item",
|
|
60
|
+
"cp",
|
|
61
|
+
"cpi",
|
|
62
|
+
"del",
|
|
63
|
+
"erase",
|
|
64
|
+
"move-item",
|
|
65
|
+
"mi",
|
|
66
|
+
"move",
|
|
67
|
+
"mv",
|
|
68
|
+
"new-item",
|
|
69
|
+
"ni",
|
|
70
|
+
"push-location",
|
|
71
|
+
"pushd",
|
|
72
|
+
"rd",
|
|
73
|
+
"remove-item",
|
|
74
|
+
"rename-item",
|
|
75
|
+
"ren",
|
|
76
|
+
"ri",
|
|
77
|
+
"rm",
|
|
78
|
+
"rmdir",
|
|
79
|
+
"rni",
|
|
80
|
+
"sc",
|
|
81
|
+
"set-content",
|
|
82
|
+
"set-item",
|
|
83
|
+
"set-location",
|
|
84
|
+
"si",
|
|
85
|
+
"sl",
|
|
78
86
|
]);
|
|
79
87
|
|
|
80
88
|
const MAX_LEXICAL_DEPTH = 32;
|
|
@@ -87,7 +95,7 @@ export const POWERSHELL_AST_MARKER = "PI_PERMISSION_GATE_AST:";
|
|
|
87
95
|
* It parses but never invokes the candidate command and returns only the
|
|
88
96
|
* bounded command metadata needed by the guard.
|
|
89
97
|
*/
|
|
90
|
-
|
|
98
|
+
const POWERSHELL_AST_SCRIPT = String.raw`
|
|
91
99
|
$ErrorActionPreference = 'Stop'
|
|
92
100
|
try {
|
|
93
101
|
try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {}
|
|
@@ -178,829 +186,868 @@ try {
|
|
|
178
186
|
`;
|
|
179
187
|
|
|
180
188
|
export function buildPowerShellAstCommand(command: string): string {
|
|
181
|
-
|
|
182
|
-
|
|
189
|
+
const encodedCommand = Buffer.from(command, "utf8").toString("base64");
|
|
190
|
+
return `$encodedSource = '${encodedCommand}'\n${POWERSHELL_AST_SCRIPT}`;
|
|
183
191
|
}
|
|
184
192
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
193
|
+
type PowerShellSafetyAnalysis = {
|
|
194
|
+
risky: boolean;
|
|
195
|
+
reason?: string;
|
|
188
196
|
};
|
|
189
197
|
|
|
190
198
|
type PowerShellLexToken = {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
199
|
+
kind: "word" | "string" | "operator" | "separator" | "nested";
|
|
200
|
+
text: string;
|
|
201
|
+
dynamic?: boolean;
|
|
202
|
+
nestedSources?: string[];
|
|
195
203
|
};
|
|
196
204
|
|
|
197
205
|
type PowerShellLexResult = {
|
|
198
|
-
|
|
199
|
-
|
|
206
|
+
tokens: PowerShellLexToken[];
|
|
207
|
+
malformed: boolean;
|
|
200
208
|
};
|
|
201
209
|
|
|
202
210
|
type PowerShellAstElement = {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
211
|
+
type: string;
|
|
212
|
+
text: string;
|
|
213
|
+
parameter: string | null;
|
|
214
|
+
argument: string | null;
|
|
215
|
+
splatted: boolean;
|
|
208
216
|
};
|
|
209
217
|
|
|
210
218
|
type PowerShellAstCommand = {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
219
|
+
name: string | null;
|
|
220
|
+
invocationOperator: string;
|
|
221
|
+
elements: PowerShellAstElement[];
|
|
214
222
|
};
|
|
215
223
|
|
|
216
224
|
function risky(reason: string): PowerShellSafetyAnalysis {
|
|
217
|
-
|
|
225
|
+
return { risky: true, reason };
|
|
218
226
|
}
|
|
219
227
|
|
|
220
228
|
const safe: PowerShellSafetyAnalysis = { risky: false };
|
|
221
229
|
|
|
222
230
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
223
|
-
|
|
231
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
224
232
|
}
|
|
225
233
|
|
|
226
234
|
function powerShellCommandName(rawName: string): string {
|
|
227
|
-
|
|
228
|
-
|
|
235
|
+
const basename = rawName.trim().split(/[\\/]/).at(-1) ?? rawName;
|
|
236
|
+
return basename.toLowerCase();
|
|
229
237
|
}
|
|
230
238
|
|
|
231
239
|
function isPowerShellScriptCommand(commandName: string): boolean {
|
|
232
|
-
|
|
240
|
+
return /\.(?:ps1|psm1)$/i.test(commandName);
|
|
233
241
|
}
|
|
234
242
|
|
|
235
243
|
function containsCommandProviderPath(values: Iterable<string>): boolean {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
244
|
+
for (const value of values) {
|
|
245
|
+
if (/(?:alias|function)\s*:/i.test(value)) return true;
|
|
246
|
+
}
|
|
247
|
+
return false;
|
|
240
248
|
}
|
|
241
249
|
|
|
242
250
|
function hasDynamicLexicalArguments(tokens: PowerShellLexToken[]): boolean {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
251
|
+
return tokens.some(
|
|
252
|
+
(token) =>
|
|
253
|
+
token.kind === "nested" ||
|
|
254
|
+
token.dynamic === true ||
|
|
255
|
+
(token.kind === "word" && /^(?:\$|@(?!\{))/.test(token.text)),
|
|
256
|
+
);
|
|
249
257
|
}
|
|
250
258
|
|
|
251
259
|
function hasDynamicAstArguments(elements: PowerShellAstElement[]): boolean {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
260
|
+
return elements.some(
|
|
261
|
+
(element) =>
|
|
262
|
+
element.splatted ||
|
|
263
|
+
!["CommandParameterAst", "ConstantExpressionAst", "StringConstantExpressionAst"].includes(
|
|
264
|
+
element.type,
|
|
265
|
+
),
|
|
266
|
+
);
|
|
257
267
|
}
|
|
258
268
|
|
|
259
269
|
function isDynamicMemberInvocationToken(token: PowerShellLexToken): boolean {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
270
|
+
if (token.kind !== "word") return false;
|
|
271
|
+
const normalized = token.text.replace(/`\r?\n/g, "").replace(/`(.)/gs, "$1");
|
|
272
|
+
return (
|
|
273
|
+
/\.\$/.test(normalized) ||
|
|
274
|
+
/^(?:\$|@).+\.$/.test(normalized) ||
|
|
275
|
+
/^\[.+\]::$/.test(normalized) ||
|
|
276
|
+
/\.(?:begininvoke|create|createprocess\w*|endinvoke|exec|execute|invoke(?:returnasis|script|withcontext)?|run|shellexecute\w*|start|winexec)$/i.test(
|
|
277
|
+
normalized,
|
|
278
|
+
) ||
|
|
279
|
+
/^\[(?:system\.)?diagnostics\.process\]::start$/i.test(normalized) ||
|
|
280
|
+
/::(?:createprocess\w*|shellexecute\w*|winexec)$/i.test(normalized) ||
|
|
281
|
+
/^\[(?:system\.)?(?:management\.automation\.)?scriptblock\]::create$/i.test(normalized) ||
|
|
282
|
+
/^\[(?:(?:system\.)?management\.automation\.)?powershell\]::create$/i.test(normalized)
|
|
283
|
+
);
|
|
274
284
|
}
|
|
275
285
|
|
|
276
286
|
function hasDangerousQuotedMemberInvocation(tokens: PowerShellLexToken[]): boolean {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
287
|
+
for (let index = 1; index < tokens.length - 1; index += 1) {
|
|
288
|
+
const previous = tokens[index - 1];
|
|
289
|
+
const member = tokens[index];
|
|
290
|
+
const callArguments = tokens[index + 1];
|
|
291
|
+
if (
|
|
292
|
+
member.kind !== "string" ||
|
|
293
|
+
previous.kind !== "word" ||
|
|
294
|
+
!/(?:\.|::)$/.test(previous.text) ||
|
|
295
|
+
callArguments.kind !== "nested"
|
|
296
|
+
) {
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (
|
|
300
|
+
member.dynamic ||
|
|
301
|
+
/^(?:begininvoke|create|createprocess\w*|endinvoke|exec|execute|invoke(?:returnasis|script|withcontext)?|run|shellexecute\w*|start|winexec)$/i.test(
|
|
302
|
+
member.text,
|
|
303
|
+
)
|
|
304
|
+
) {
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return false;
|
|
299
309
|
}
|
|
300
310
|
|
|
301
311
|
function isFalseLiteral(value: string | null | undefined): boolean {
|
|
302
|
-
|
|
312
|
+
return /^(?:\$?false|0)$/i.test(value?.trim() ?? "");
|
|
303
313
|
}
|
|
304
314
|
|
|
305
315
|
function isTrueLiteral(value: string | null | undefined): boolean {
|
|
306
|
-
|
|
316
|
+
return /^(?:\$?true|1)$/i.test(value?.trim() ?? "");
|
|
307
317
|
}
|
|
308
318
|
|
|
309
319
|
function parameterMatches(parameter: string, kind: "recurse" | "force" | "whatIf"): boolean {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
320
|
+
const normalized = parameter
|
|
321
|
+
.replace(/`\r?\n/g, "")
|
|
322
|
+
.replace(/`(.)/gs, "$1")
|
|
323
|
+
.replace(/^-+/, "")
|
|
324
|
+
.toLowerCase();
|
|
325
|
+
if (kind === "recurse") return /^r(?:e(?:c(?:u(?:r(?:s(?:e)?)?)?)?)?)?$/.test(normalized);
|
|
326
|
+
if (kind === "force") return /^fo(?:r(?:c(?:e)?)?)?$/.test(normalized);
|
|
327
|
+
return /^wh(?:a(?:t(?:i(?:f)?)?)?)?$/.test(normalized);
|
|
318
328
|
}
|
|
319
329
|
|
|
320
330
|
function parseLexicalParameter(token: string): { name: string; value?: string } | undefined {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
331
|
+
if (!token.startsWith("-") || token === "-") return undefined;
|
|
332
|
+
const colon = token.indexOf(":");
|
|
333
|
+
if (colon === -1) return { name: token };
|
|
334
|
+
return { name: token.slice(0, colon), value: token.slice(colon + 1) };
|
|
325
335
|
}
|
|
326
336
|
|
|
327
337
|
function isPotentiallyEnabledSwitch(
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
338
|
+
name: string,
|
|
339
|
+
value: string | null | undefined,
|
|
340
|
+
kind: "recurse" | "force",
|
|
331
341
|
): boolean {
|
|
332
|
-
|
|
342
|
+
return parameterMatches(name, kind) && !isFalseLiteral(value);
|
|
333
343
|
}
|
|
334
344
|
|
|
335
345
|
function isDefinitelyEnabledWhatIf(name: string, value: string | null | undefined): boolean {
|
|
336
|
-
|
|
337
|
-
|
|
346
|
+
if (!parameterMatches(name, "whatIf")) return false;
|
|
347
|
+
return value === undefined || value === null || isTrueLiteral(value);
|
|
338
348
|
}
|
|
339
349
|
|
|
340
350
|
function findClosingParenthesis(source: string, openIndex: number): number {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
351
|
+
let depth = 1;
|
|
352
|
+
let quote: "'" | '"' | undefined;
|
|
353
|
+
let blockCommentDepth = 0;
|
|
354
|
+
|
|
355
|
+
for (let index = openIndex + 1; index < source.length; index += 1) {
|
|
356
|
+
const char = source[index];
|
|
357
|
+
const next = source[index + 1];
|
|
358
|
+
|
|
359
|
+
if (blockCommentDepth > 0) {
|
|
360
|
+
if (char === "<" && next === "#") {
|
|
361
|
+
blockCommentDepth += 1;
|
|
362
|
+
index += 1;
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
if (char === "#" && next === ">") {
|
|
366
|
+
blockCommentDepth -= 1;
|
|
367
|
+
index += 1;
|
|
368
|
+
}
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (quote === "'") {
|
|
373
|
+
if (char === "'" && next === "'") {
|
|
374
|
+
index += 1;
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
if (char === "'") quote = undefined;
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (quote === '"') {
|
|
382
|
+
if (char === "`") {
|
|
383
|
+
if (next === "\r" && source[index + 2] === "\n") index += 2;
|
|
384
|
+
else if (next !== undefined) index += 1;
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
if (char === '"') quote = undefined;
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (char === "`") {
|
|
392
|
+
if (next === "\r" && source[index + 2] === "\n") index += 2;
|
|
393
|
+
else if (next !== undefined) index += 1;
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (char === "#") {
|
|
397
|
+
while (index + 1 < source.length && source[index + 1] !== "\n") index += 1;
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
if (char === "<" && next === "#") {
|
|
401
|
+
blockCommentDepth = 1;
|
|
402
|
+
index += 1;
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
if (char === "'" || char === '"') {
|
|
406
|
+
quote = char;
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
if (char === "(") depth += 1;
|
|
410
|
+
if (char === ")") {
|
|
411
|
+
depth -= 1;
|
|
412
|
+
if (depth === 0) return index;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return -1;
|
|
407
417
|
}
|
|
408
418
|
|
|
409
419
|
function readQuotedToken(
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
420
|
+
source: string,
|
|
421
|
+
startIndex: number,
|
|
422
|
+
quote: "'" | '"',
|
|
413
423
|
): { token: PowerShellLexToken; endIndex: number; malformed: boolean } {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
424
|
+
let text = "";
|
|
425
|
+
let dynamic = false;
|
|
426
|
+
const nestedSources: string[] = [];
|
|
427
|
+
|
|
428
|
+
for (let index = startIndex + 1; index < source.length; index += 1) {
|
|
429
|
+
const char = source[index];
|
|
430
|
+
const next = source[index + 1];
|
|
431
|
+
|
|
432
|
+
if (quote === "'" && char === "'" && next === "'") {
|
|
433
|
+
text += "'";
|
|
434
|
+
index += 1;
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (quote === '"' && char === "`") {
|
|
439
|
+
if (next === "\r" && source[index + 2] === "\n") {
|
|
440
|
+
index += 2;
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
if (next === "\n") {
|
|
444
|
+
index += 1;
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
if (next !== undefined) {
|
|
448
|
+
text += next;
|
|
449
|
+
index += 1;
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (char === quote) {
|
|
455
|
+
return {
|
|
456
|
+
token: {
|
|
457
|
+
kind: "string",
|
|
458
|
+
text,
|
|
459
|
+
dynamic,
|
|
460
|
+
...(nestedSources.length ? { nestedSources } : {}),
|
|
461
|
+
},
|
|
462
|
+
endIndex: index,
|
|
463
|
+
malformed: false,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (quote === '"' && char === "$" && next === "(") {
|
|
468
|
+
const closingIndex = findClosingParenthesis(source, index + 1);
|
|
469
|
+
if (closingIndex === -1) {
|
|
470
|
+
return {
|
|
471
|
+
token: { kind: "string", text, dynamic: true, nestedSources },
|
|
472
|
+
endIndex: source.length - 1,
|
|
473
|
+
malformed: true,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
nestedSources.push(source.slice(index + 2, closingIndex));
|
|
477
|
+
dynamic = true;
|
|
478
|
+
index = closingIndex;
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (quote === '"' && char === "$" && /[A-Za-z_{]/.test(next ?? "")) dynamic = true;
|
|
483
|
+
text += char;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return {
|
|
487
|
+
token: {
|
|
488
|
+
kind: "string",
|
|
489
|
+
text,
|
|
490
|
+
dynamic: true,
|
|
491
|
+
...(nestedSources.length ? { nestedSources } : {}),
|
|
492
|
+
},
|
|
493
|
+
endIndex: source.length - 1,
|
|
494
|
+
malformed: true,
|
|
495
|
+
};
|
|
481
496
|
}
|
|
482
497
|
|
|
483
498
|
function readHereStringToken(
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
499
|
+
source: string,
|
|
500
|
+
startIndex: number,
|
|
501
|
+
quote: "'" | '"',
|
|
487
502
|
): { token: PowerShellLexToken; endIndex: number; malformed: boolean } | undefined {
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
503
|
+
const openerEnd = startIndex + 2;
|
|
504
|
+
let contentStart: number;
|
|
505
|
+
if (source[openerEnd] === "\r" && source[openerEnd + 1] === "\n") contentStart = openerEnd + 2;
|
|
506
|
+
else if (source[openerEnd] === "\n") contentStart = openerEnd + 1;
|
|
507
|
+
else return undefined;
|
|
508
|
+
|
|
509
|
+
let closingMarker = -1;
|
|
510
|
+
let contentEnd = source.length;
|
|
511
|
+
for (let lineStart = contentStart; lineStart <= source.length;) {
|
|
512
|
+
let marker = lineStart;
|
|
513
|
+
while (source[marker] === " " || source[marker] === "\t") marker += 1;
|
|
514
|
+
if (source[marker] === quote && source[marker + 1] === "@") {
|
|
515
|
+
closingMarker = marker;
|
|
516
|
+
contentEnd = lineStart;
|
|
517
|
+
break;
|
|
518
|
+
}
|
|
519
|
+
const newline = source.indexOf("\n", lineStart);
|
|
520
|
+
if (newline === -1) break;
|
|
521
|
+
lineStart = newline + 1;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const text = source.slice(contentStart, contentEnd);
|
|
525
|
+
let dynamic = false;
|
|
526
|
+
let malformed = closingMarker === -1;
|
|
527
|
+
const nestedSources: string[] = [];
|
|
528
|
+
if (quote === '"') {
|
|
529
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
530
|
+
const char = text[index];
|
|
531
|
+
const next = text[index + 1];
|
|
532
|
+
if (char === "`") {
|
|
533
|
+
if (next !== undefined) index += 1;
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
if (char === "$" && next === "(") {
|
|
537
|
+
const closingIndex = findClosingParenthesis(text, index + 1);
|
|
538
|
+
if (closingIndex === -1) {
|
|
539
|
+
malformed = true;
|
|
540
|
+
break;
|
|
541
|
+
}
|
|
542
|
+
nestedSources.push(text.slice(index + 2, closingIndex));
|
|
543
|
+
dynamic = true;
|
|
544
|
+
index = closingIndex;
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
if (char === "$" && /[A-Za-z_{]/.test(next ?? "")) dynamic = true;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return {
|
|
552
|
+
token: {
|
|
553
|
+
kind: "string",
|
|
554
|
+
text,
|
|
555
|
+
dynamic,
|
|
556
|
+
...(nestedSources.length ? { nestedSources } : {}),
|
|
557
|
+
},
|
|
558
|
+
endIndex: closingMarker === -1 ? source.length - 1 : closingMarker + 1,
|
|
559
|
+
malformed,
|
|
560
|
+
};
|
|
546
561
|
}
|
|
547
562
|
|
|
548
563
|
function tokenizePowerShell(source: string): PowerShellLexResult {
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
function segmentCommandStart(
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
564
|
+
const tokens: PowerShellLexToken[] = [];
|
|
565
|
+
let current = "";
|
|
566
|
+
let malformed = false;
|
|
567
|
+
let braceDepth = 0;
|
|
568
|
+
|
|
569
|
+
const flush = () => {
|
|
570
|
+
if (!current) return;
|
|
571
|
+
tokens.push({ kind: "word", text: current });
|
|
572
|
+
current = "";
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
576
|
+
const char = source[index];
|
|
577
|
+
const next = source[index + 1];
|
|
578
|
+
|
|
579
|
+
if (char === "`") {
|
|
580
|
+
if (next === "\r" && source[index + 2] === "\n") {
|
|
581
|
+
index += 2;
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
if (next === "\n") {
|
|
585
|
+
index += 1;
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
if (next === undefined) {
|
|
589
|
+
malformed = true;
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
current += next;
|
|
593
|
+
index += 1;
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (char === "<" && next === "#") {
|
|
598
|
+
flush();
|
|
599
|
+
let depth = 1;
|
|
600
|
+
index += 2;
|
|
601
|
+
for (; index < source.length && depth > 0; index += 1) {
|
|
602
|
+
if (source[index] === "<" && source[index + 1] === "#") {
|
|
603
|
+
depth += 1;
|
|
604
|
+
index += 1;
|
|
605
|
+
} else if (source[index] === "#" && source[index + 1] === ">") {
|
|
606
|
+
depth -= 1;
|
|
607
|
+
index += 1;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
if (depth > 0) malformed = true;
|
|
611
|
+
index -= 1;
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (char === "#") {
|
|
616
|
+
flush();
|
|
617
|
+
while (index + 1 < source.length && source[index + 1] !== "\n") index += 1;
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (char === "@" && (next === "'" || next === '"')) {
|
|
622
|
+
const hereString = readHereStringToken(source, index, next);
|
|
623
|
+
if (hereString) {
|
|
624
|
+
flush();
|
|
625
|
+
tokens.push(hereString.token);
|
|
626
|
+
malformed ||= hereString.malformed;
|
|
627
|
+
index = hereString.endIndex;
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
if (char === "'" || char === '"') {
|
|
633
|
+
flush();
|
|
634
|
+
const quoted = readQuotedToken(source, index, char);
|
|
635
|
+
tokens.push(quoted.token);
|
|
636
|
+
malformed ||= quoted.malformed;
|
|
637
|
+
index = quoted.endIndex;
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
if ((char === "$" || char === "@") && next === "(") {
|
|
642
|
+
flush();
|
|
643
|
+
const closingIndex = findClosingParenthesis(source, index + 1);
|
|
644
|
+
if (closingIndex === -1) {
|
|
645
|
+
malformed = true;
|
|
646
|
+
break;
|
|
647
|
+
}
|
|
648
|
+
tokens.push({ kind: "nested", text: source.slice(index + 2, closingIndex) });
|
|
649
|
+
index = closingIndex;
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
if (char === "(") {
|
|
654
|
+
flush();
|
|
655
|
+
const closingIndex = findClosingParenthesis(source, index);
|
|
656
|
+
if (closingIndex === -1) {
|
|
657
|
+
malformed = true;
|
|
658
|
+
break;
|
|
659
|
+
}
|
|
660
|
+
tokens.push({ kind: "nested", text: source.slice(index + 1, closingIndex) });
|
|
661
|
+
index = closingIndex;
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (char === ")") {
|
|
666
|
+
flush();
|
|
667
|
+
malformed = true;
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
if ((char === "&" && next === "&") || (char === "|" && next === "|")) {
|
|
672
|
+
flush();
|
|
673
|
+
tokens.push({ kind: "separator", text: `${char}${next}` });
|
|
674
|
+
index += 1;
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
if (char === "{") {
|
|
679
|
+
flush();
|
|
680
|
+
braceDepth += 1;
|
|
681
|
+
tokens.push({ kind: "separator", text: char });
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if (char === "}") {
|
|
686
|
+
flush();
|
|
687
|
+
if (braceDepth === 0) malformed = true;
|
|
688
|
+
else braceDepth -= 1;
|
|
689
|
+
tokens.push({ kind: "separator", text: char });
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
if (char === ";" || char === "|" || char === "\n") {
|
|
694
|
+
flush();
|
|
695
|
+
tokens.push({ kind: "separator", text: char });
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
if (char === "&" || char === "=") {
|
|
700
|
+
flush();
|
|
701
|
+
tokens.push({ kind: "operator", text: char });
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
if (/\s/.test(char)) {
|
|
706
|
+
flush();
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
current += char;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
flush();
|
|
714
|
+
if (braceDepth !== 0) malformed = true;
|
|
715
|
+
return { tokens, malformed };
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function segmentCommandStart(
|
|
719
|
+
tokens: PowerShellLexToken[],
|
|
720
|
+
): { index: number; dynamic: boolean } | undefined {
|
|
721
|
+
let index = 0;
|
|
722
|
+
while (
|
|
723
|
+
tokens[index]?.kind === "word" &&
|
|
724
|
+
["try", "finally", "catch", "begin", "process", "end", "else"].includes(
|
|
725
|
+
tokens[index].text.toLowerCase(),
|
|
726
|
+
)
|
|
727
|
+
) {
|
|
728
|
+
index += 1;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
let assignment = false;
|
|
732
|
+
for (let candidate = index; candidate < tokens.length; candidate += 1) {
|
|
733
|
+
if (tokens[candidate]?.kind === "operator" && tokens[candidate].text === "=") {
|
|
734
|
+
index = candidate + 1;
|
|
735
|
+
assignment = true;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
let usedCallOperator = false;
|
|
740
|
+
while (
|
|
741
|
+
tokens[index] &&
|
|
742
|
+
((tokens[index].kind === "operator" && tokens[index].text === "&") ||
|
|
743
|
+
(tokens[index].kind === "word" && tokens[index].text === "."))
|
|
744
|
+
) {
|
|
745
|
+
usedCallOperator = true;
|
|
746
|
+
index += 1;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const head = tokens[index];
|
|
750
|
+
if (!head) return undefined;
|
|
751
|
+
if (head.kind === "nested") return usedCallOperator ? { index, dynamic: true } : undefined;
|
|
752
|
+
if (head.kind !== "word" && head.kind !== "string") return undefined;
|
|
753
|
+
if (head.kind === "string")
|
|
754
|
+
return usedCallOperator ? { index, dynamic: head.dynamic === true } : undefined;
|
|
755
|
+
if (/^(?:\$|@|\[|\d)/.test(head.text))
|
|
756
|
+
return usedCallOperator ? { index, dynamic: true } : undefined;
|
|
757
|
+
if (assignment && head.text === "{") return undefined;
|
|
758
|
+
return { index, dynamic: false };
|
|
735
759
|
}
|
|
736
760
|
|
|
737
761
|
function hasNativeRecursiveForceFlags(tokens: PowerShellLexToken[], startIndex: number): boolean {
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
function analyzeRemoveInvocation(
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
762
|
+
let sawRecursive = false;
|
|
763
|
+
let sawForce = false;
|
|
764
|
+
for (const token of tokens.slice(startIndex + 1)) {
|
|
765
|
+
if (token.kind !== "word") continue;
|
|
766
|
+
if (token.text === "--") break;
|
|
767
|
+
if (/^--recursive$/i.test(token.text)) sawRecursive = true;
|
|
768
|
+
if (/^--force$/i.test(token.text)) sawForce = true;
|
|
769
|
+
if (/^-[^-]+$/.test(token.text)) {
|
|
770
|
+
const flags = token.text.slice(1);
|
|
771
|
+
if (/[rR]/.test(flags)) sawRecursive = true;
|
|
772
|
+
if (/f/.test(flags)) sawForce = true;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
return sawRecursive && sawForce;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function analyzeRemoveInvocation(
|
|
779
|
+
tokens: PowerShellLexToken[],
|
|
780
|
+
startIndex: number,
|
|
781
|
+
): PowerShellSafetyAnalysis {
|
|
782
|
+
let sawRecursive = false;
|
|
783
|
+
let sawForce = false;
|
|
784
|
+
let sawWhatIf = false;
|
|
785
|
+
|
|
786
|
+
for (const token of tokens.slice(startIndex + 1)) {
|
|
787
|
+
if (token.kind !== "word") continue;
|
|
788
|
+
if (token.text.startsWith("@") && token.text.length > 1) {
|
|
789
|
+
return risky("Remove-Item parameter splatting requires confirmation");
|
|
790
|
+
}
|
|
791
|
+
const parameter = parseLexicalParameter(token.text);
|
|
792
|
+
if (!parameter) continue;
|
|
793
|
+
if (isPotentiallyEnabledSwitch(parameter.name, parameter.value, "recurse")) sawRecursive = true;
|
|
794
|
+
if (isPotentiallyEnabledSwitch(parameter.name, parameter.value, "force")) sawForce = true;
|
|
795
|
+
if (isDefinitelyEnabledWhatIf(parameter.name, parameter.value)) sawWhatIf = true;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
if (sawRecursive && sawForce && !sawWhatIf) {
|
|
799
|
+
return risky("recursive forced Remove-Item invocation");
|
|
800
|
+
}
|
|
801
|
+
return safe;
|
|
775
802
|
}
|
|
776
803
|
|
|
777
804
|
function analyzeLexicalSegment(tokens: PowerShellLexToken[]): PowerShellSafetyAnalysis {
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
805
|
+
if (tokens.length === 0) return safe;
|
|
806
|
+
if (tokens.some(isDynamicMemberInvocationToken) || hasDangerousQuotedMemberInvocation(tokens)) {
|
|
807
|
+
return risky("dynamic PowerShell script-block invocation requires confirmation");
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
const start = segmentCommandStart(tokens);
|
|
811
|
+
if (!start) return safe;
|
|
812
|
+
if (start.dynamic) return risky("dynamic PowerShell command invocation requires confirmation");
|
|
813
|
+
|
|
814
|
+
const head = tokens[start.index];
|
|
815
|
+
const commandName = powerShellCommandName(head.text);
|
|
816
|
+
if (!commandName) {
|
|
817
|
+
return risky("dynamic PowerShell command invocation requires confirmation");
|
|
818
|
+
}
|
|
819
|
+
if (dynamicPowerShellCommands.has(commandName) || shellPowerShellCommands.has(commandName)) {
|
|
820
|
+
return risky(`nested or dynamic PowerShell execution through ${commandName}`);
|
|
821
|
+
}
|
|
822
|
+
if (commandResolutionPowerShellCommands.has(commandName)) {
|
|
823
|
+
return risky(`PowerShell command-resolution mutation through ${commandName}`);
|
|
824
|
+
}
|
|
825
|
+
if (commandDefinitionPowerShellKeywords.has(commandName)) {
|
|
826
|
+
return risky(`PowerShell command-resolution mutation through ${commandName} definition`);
|
|
827
|
+
}
|
|
828
|
+
if (
|
|
829
|
+
providerMutationPowerShellCommands.has(commandName) &&
|
|
830
|
+
(containsCommandProviderPath(tokens.slice(start.index + 1).map((token) => token.text)) ||
|
|
831
|
+
hasDynamicLexicalArguments(tokens.slice(start.index + 1)))
|
|
832
|
+
) {
|
|
833
|
+
return risky(`PowerShell command-provider mutation through ${commandName}`);
|
|
834
|
+
}
|
|
835
|
+
if (
|
|
836
|
+
commandName === "using" &&
|
|
837
|
+
tokens.slice(start.index + 1).some((token) => token.text.toLowerCase() === "module")
|
|
838
|
+
) {
|
|
839
|
+
return risky("PowerShell module loading requires confirmation");
|
|
840
|
+
}
|
|
841
|
+
if (isPowerShellScriptCommand(commandName)) {
|
|
842
|
+
return risky("nested PowerShell script invocation requires confirmation");
|
|
843
|
+
}
|
|
844
|
+
if (commandName === "sudo" || commandName === "sudo.exe") return risky("sudo invocation");
|
|
845
|
+
|
|
846
|
+
if (powerShellRemoveCommands.has(commandName)) {
|
|
847
|
+
const analysis = analyzeRemoveInvocation(tokens, start.index);
|
|
848
|
+
if (analysis.risky) return analysis;
|
|
849
|
+
if (commandName === "rm" && hasNativeRecursiveForceFlags(tokens, start.index)) {
|
|
850
|
+
return risky("recursive forced rm invocation");
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
if (commandName === "rm.exe" && hasNativeRecursiveForceFlags(tokens, start.index)) {
|
|
855
|
+
return risky("recursive forced native rm invocation");
|
|
856
|
+
}
|
|
857
|
+
if (["chmod", "chmod.exe", "chown", "chown.exe"].includes(commandName)) {
|
|
858
|
+
if (
|
|
859
|
+
tokens.slice(start.index + 1).some((token) => token.kind === "word" && /777/.test(token.text))
|
|
860
|
+
) {
|
|
861
|
+
return risky(`${commandName} 777 invocation`);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
return safe;
|
|
837
866
|
}
|
|
838
867
|
|
|
839
868
|
export function analyzePowerShellLexically(command: string, depth = 0): PowerShellSafetyAnalysis {
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
869
|
+
if (depth > MAX_LEXICAL_DEPTH) return risky("PowerShell safety analysis nesting limit exceeded");
|
|
870
|
+
|
|
871
|
+
const lexed = tokenizePowerShell(command);
|
|
872
|
+
if (lexed.malformed) return risky("malformed PowerShell syntax requires confirmation");
|
|
873
|
+
|
|
874
|
+
for (const token of lexed.tokens) {
|
|
875
|
+
const nestedSources = token.kind === "nested" ? [token.text] : (token.nestedSources ?? []);
|
|
876
|
+
for (const nestedSource of nestedSources) {
|
|
877
|
+
const nested = analyzePowerShellLexically(nestedSource, depth + 1);
|
|
878
|
+
if (nested.risky) return nested;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
let segment: PowerShellLexToken[] = [];
|
|
883
|
+
for (const token of lexed.tokens) {
|
|
884
|
+
if (token.kind === "separator") {
|
|
885
|
+
const analysis = analyzeLexicalSegment(segment);
|
|
886
|
+
if (analysis.risky) return analysis;
|
|
887
|
+
segment = [];
|
|
888
|
+
continue;
|
|
889
|
+
}
|
|
890
|
+
segment.push(token);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
return analyzeLexicalSegment(segment);
|
|
865
894
|
}
|
|
866
895
|
|
|
867
896
|
function parseAstElement(value: unknown): PowerShellAstElement | undefined {
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
897
|
+
if (!isRecord(value)) return undefined;
|
|
898
|
+
if (
|
|
899
|
+
typeof value.type !== "string" ||
|
|
900
|
+
typeof value.text !== "string" ||
|
|
901
|
+
typeof value.splatted !== "boolean"
|
|
902
|
+
) {
|
|
903
|
+
return undefined;
|
|
904
|
+
}
|
|
905
|
+
if (value.parameter !== null && typeof value.parameter !== "string") return undefined;
|
|
906
|
+
if (value.argument !== null && typeof value.argument !== "string") return undefined;
|
|
907
|
+
return {
|
|
908
|
+
type: value.type,
|
|
909
|
+
text: value.text,
|
|
910
|
+
parameter: value.parameter,
|
|
911
|
+
argument: value.argument,
|
|
912
|
+
splatted: value.splatted,
|
|
913
|
+
};
|
|
881
914
|
}
|
|
882
915
|
|
|
883
916
|
function parseAstCommand(value: unknown): PowerShellAstCommand | undefined {
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
917
|
+
if (
|
|
918
|
+
!isRecord(value) ||
|
|
919
|
+
!Array.isArray(value.elements) ||
|
|
920
|
+
typeof value.invocationOperator !== "string"
|
|
921
|
+
)
|
|
922
|
+
return undefined;
|
|
923
|
+
if (value.name !== null && typeof value.name !== "string") return undefined;
|
|
924
|
+
const elements = value.elements.map(parseAstElement);
|
|
925
|
+
if (elements.length === 0 || elements.some((element) => !element)) return undefined;
|
|
926
|
+
return {
|
|
927
|
+
name: value.name,
|
|
928
|
+
invocationOperator: value.invocationOperator,
|
|
929
|
+
elements: elements as PowerShellAstElement[],
|
|
930
|
+
};
|
|
893
931
|
}
|
|
894
932
|
|
|
895
933
|
function analyzeAstRemoveInvocation(command: PowerShellAstCommand): PowerShellSafetyAnalysis {
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
934
|
+
let sawRecursive = false;
|
|
935
|
+
let sawForce = false;
|
|
936
|
+
let sawWhatIf = false;
|
|
937
|
+
|
|
938
|
+
for (const element of command.elements.slice(1)) {
|
|
939
|
+
if (element.splatted) return risky("Remove-Item parameter splatting requires confirmation");
|
|
940
|
+
if (element.type !== "CommandParameterAst" || element.parameter === null) continue;
|
|
941
|
+
if (isPotentiallyEnabledSwitch(element.parameter, element.argument, "recurse"))
|
|
942
|
+
sawRecursive = true;
|
|
943
|
+
if (isPotentiallyEnabledSwitch(element.parameter, element.argument, "force")) sawForce = true;
|
|
944
|
+
if (isDefinitelyEnabledWhatIf(element.parameter, element.argument)) sawWhatIf = true;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
if (sawRecursive && sawForce && !sawWhatIf) {
|
|
948
|
+
return risky("recursive forced Remove-Item invocation");
|
|
949
|
+
}
|
|
950
|
+
return safe;
|
|
912
951
|
}
|
|
913
952
|
|
|
914
953
|
function analyzeAstCommand(command: PowerShellAstCommand): PowerShellSafetyAnalysis {
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
954
|
+
if (!command.name) return risky("dynamic PowerShell command invocation requires confirmation");
|
|
955
|
+
const commandName = powerShellCommandName(command.name);
|
|
956
|
+
if (dynamicPowerShellCommands.has(commandName) || shellPowerShellCommands.has(commandName)) {
|
|
957
|
+
return risky(`nested or dynamic PowerShell execution through ${commandName}`);
|
|
958
|
+
}
|
|
959
|
+
if (commandResolutionPowerShellCommands.has(commandName)) {
|
|
960
|
+
return risky(`PowerShell command-resolution mutation through ${commandName}`);
|
|
961
|
+
}
|
|
962
|
+
if (
|
|
963
|
+
providerMutationPowerShellCommands.has(commandName) &&
|
|
964
|
+
(containsCommandProviderPath(command.elements.slice(1).map((element) => element.text)) ||
|
|
965
|
+
hasDynamicAstArguments(command.elements.slice(1)))
|
|
966
|
+
) {
|
|
967
|
+
return risky(`PowerShell command-provider mutation through ${commandName}`);
|
|
968
|
+
}
|
|
969
|
+
if (isPowerShellScriptCommand(commandName)) {
|
|
970
|
+
return risky("nested PowerShell script invocation requires confirmation");
|
|
971
|
+
}
|
|
972
|
+
if (commandName === "sudo" || commandName === "sudo.exe") return risky("sudo invocation");
|
|
973
|
+
|
|
974
|
+
if (powerShellRemoveCommands.has(commandName)) {
|
|
975
|
+
const removeAnalysis = analyzeAstRemoveInvocation(command);
|
|
976
|
+
if (removeAnalysis.risky) return removeAnalysis;
|
|
977
|
+
const tokens = command.elements.map((element) => ({
|
|
978
|
+
kind: "word" as const,
|
|
979
|
+
text: element.text,
|
|
980
|
+
}));
|
|
981
|
+
if (commandName === "rm" && hasNativeRecursiveForceFlags(tokens, 0)) {
|
|
982
|
+
return risky("recursive forced rm invocation");
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
if (commandName === "rm.exe") {
|
|
987
|
+
const tokens = command.elements.map((element) => ({
|
|
988
|
+
kind: "word" as const,
|
|
989
|
+
text: element.text,
|
|
990
|
+
}));
|
|
991
|
+
if (hasNativeRecursiveForceFlags(tokens, 0))
|
|
992
|
+
return risky("recursive forced native rm invocation");
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
if (["chmod", "chmod.exe", "chown", "chown.exe"].includes(commandName)) {
|
|
996
|
+
if (command.elements.slice(1).some((element) => /777/.test(element.text))) {
|
|
997
|
+
return risky(`${commandName} 777 invocation`);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
return safe;
|
|
956
1002
|
}
|
|
957
1003
|
|
|
958
1004
|
export function analyzePowerShellAstPayload(payload: unknown): PowerShellSafetyAnalysis {
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1005
|
+
if (!isRecord(payload)) return risky("malformed PowerShell AST analyzer response");
|
|
1006
|
+
if (Object.hasOwn(payload, "analyzerError")) {
|
|
1007
|
+
return typeof payload.analyzerError === "string"
|
|
1008
|
+
? risky(`PowerShell AST analyzer failed: ${payload.analyzerError}`)
|
|
1009
|
+
: risky("malformed PowerShell AST analyzer response");
|
|
1010
|
+
}
|
|
1011
|
+
if (
|
|
1012
|
+
!Array.isArray(payload.parseErrors) ||
|
|
1013
|
+
!Array.isArray(payload.commands) ||
|
|
1014
|
+
!Number.isSafeInteger(payload.dynamicInvocationCount) ||
|
|
1015
|
+
(payload.dynamicInvocationCount as number) < 0 ||
|
|
1016
|
+
!Number.isSafeInteger(payload.functionDefinitionCount) ||
|
|
1017
|
+
(payload.functionDefinitionCount as number) < 0
|
|
1018
|
+
) {
|
|
1019
|
+
return risky("malformed PowerShell AST analyzer response");
|
|
1020
|
+
}
|
|
1021
|
+
if (payload.parseErrors.some((error) => typeof error !== "string")) {
|
|
1022
|
+
return risky("malformed PowerShell AST parser errors");
|
|
1023
|
+
}
|
|
1024
|
+
if (payload.parseErrors.length > 0) return risky("PowerShell parser reported invalid syntax");
|
|
1025
|
+
if ((payload.dynamicInvocationCount as number) > 0) {
|
|
1026
|
+
return risky("dynamic PowerShell invocation requires confirmation");
|
|
1027
|
+
}
|
|
1028
|
+
if ((payload.functionDefinitionCount as number) > 0) {
|
|
1029
|
+
return risky("PowerShell function or filter definition changes command resolution");
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
const commands = payload.commands.map(parseAstCommand);
|
|
1033
|
+
if (commands.some((command) => !command))
|
|
1034
|
+
return risky("malformed PowerShell AST command metadata");
|
|
1035
|
+
for (const command of commands as PowerShellAstCommand[]) {
|
|
1036
|
+
const analysis = analyzeAstCommand(command);
|
|
1037
|
+
if (analysis.risky) return analysis;
|
|
1038
|
+
}
|
|
1039
|
+
return safe;
|
|
993
1040
|
}
|
|
994
1041
|
|
|
995
1042
|
export function parsePowerShellAstOutput(stdout: string): PowerShellSafetyAnalysis {
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1043
|
+
const markerLine = stdout
|
|
1044
|
+
.split(/\r?\n/)
|
|
1045
|
+
.reverse()
|
|
1046
|
+
.find((line) => line.startsWith(POWERSHELL_AST_MARKER));
|
|
1047
|
+
if (!markerLine) return risky("PowerShell AST analyzer produced no result");
|
|
1048
|
+
try {
|
|
1049
|
+
return analyzePowerShellAstPayload(JSON.parse(markerLine.slice(POWERSHELL_AST_MARKER.length)));
|
|
1050
|
+
} catch {
|
|
1051
|
+
return risky("PowerShell AST analyzer produced invalid JSON");
|
|
1052
|
+
}
|
|
1006
1053
|
}
|