mirror of
https://github.com/anomalyco/opencode.git
synced 2026-10-23 20:29:54 +00:00
604 lines
25 KiB
TypeScript
604 lines
25 KiB
TypeScript
import { expect, test } from "bun:test"
|
||
import { ShellScan } from "../../src/shell/scan.js"
|
||
|
||
const pwsh = process.env.SHELL_SCAN_PWSH ?? Bun.which("pwsh")
|
||
|
||
// These ordinary forms must stay accepted, not disappear behind the oracle's opaque-result filter.
|
||
const supported = [
|
||
"Invoke-ProbeA; Invoke-ProbeB",
|
||
"$result = Invoke-ProbeA; Invoke-ProbeB",
|
||
"if (Invoke-ProbeA) { Invoke-ProbeB } else { Invoke-ProbeC }",
|
||
"foreach ($item in (Invoke-ProbeA)) { Invoke-ProbeB }",
|
||
"function Get-Probe { param($x); Invoke-ProbeB }; Invoke-ProbeA",
|
||
"$x = @{ first = Invoke-ProbeA; second = @(Invoke-ProbeB; Invoke-ProbeC) }",
|
||
'Invoke-ProbeA "$(Invoke-ProbeB "$(Invoke-ProbeC)")"',
|
||
"Invoke-ProbeA | ForEach-Object { Invoke-ProbeB }",
|
||
"Invoke-ProbeA @'\nliteral ; }\n'@; Invoke-ProbeB",
|
||
'Invoke-ProbeA @"\n$(Invoke-ProbeB)\n"@; Invoke-ProbeC',
|
||
"Invoke-ProbeA `\n argument; Invoke-ProbeB",
|
||
"Invoke-ProbeA 2>&1; Invoke-ProbeB",
|
||
"& 'Invoke-ProbeA' argument; Invoke-ProbeB",
|
||
"Invoke-ProbeA --% literal; ignored\nInvoke-ProbeB",
|
||
"$x = $(switch (${probe}#)\n) { default { Invoke-ProbeA; Invoke-ProbeB } })",
|
||
"${probe}<# comment #>; Invoke-ProbeA",
|
||
"Invoke-ProbeA user@'host'; Invoke-ProbeB",
|
||
'"$("a""; Invoke-ProbeA; "b"")"',
|
||
'Invoke-ProbeA "$(Invoke-ProbeB "a"")"',
|
||
'@"\n$(Invoke-ProbeA \'\n"@\')\n"@',
|
||
'@"\n$(Invoke-ProbeA @\'\n"@\n\'@)\n"@',
|
||
"@'\u00a0\n'@; Invoke-ProbeA",
|
||
"$probe[-$(Invoke-ProbeA)]",
|
||
"switch ('a#b') { a#b { Invoke-ProbeA } }",
|
||
]
|
||
|
||
test.each(supported)("accepts supported PowerShell syntax without an opaque escape hatch: %s", (source) => {
|
||
expect(ShellScan.scanPowerShell(source).kind).toBe("scanned")
|
||
})
|
||
|
||
const fixtures = [
|
||
...supported,
|
||
...[
|
||
"$result = Invoke-ProbeA; Invoke-ProbeB",
|
||
"$result = (Invoke-ProbeA); Invoke-ProbeB",
|
||
"[string]$result = Invoke-ProbeA; Invoke-ProbeB",
|
||
"if (Invoke-ProbeA) { Invoke-ProbeB } else { Invoke-ProbeC }",
|
||
"foreach ($item in (Invoke-ProbeA)) { Invoke-ProbeB }",
|
||
"foreach ($item in Invoke-ProbeA) { Invoke-ProbeB }",
|
||
"for ($i=0; $i -lt 2; $i++) { Invoke-ProbeB }",
|
||
"while (Invoke-ProbeA) { Invoke-ProbeB; break }",
|
||
"do { Invoke-ProbeB } until ($true)",
|
||
"function Get-Probe { param($x); Invoke-ProbeB }; Invoke-ProbeA",
|
||
"function Get-Probe($x = (Invoke-ProbeA)) { Invoke-ProbeB }",
|
||
"try { Invoke-ProbeA } catch { Invoke-ProbeB } finally { Invoke-ProbeC }",
|
||
"$x = @{ first = Invoke-ProbeA; second = @(Invoke-ProbeB; Invoke-ProbeC) }",
|
||
"Invoke-ProbeA @(Invoke-ProbeB; Invoke-ProbeC)",
|
||
'Invoke-ProbeA "$(Invoke-ProbeB "$(Invoke-ProbeC)")"',
|
||
"Invoke-ProbeA <# <# ignored } #> #> literal; Invoke-ProbeB",
|
||
'& "Inv`oke-ProbeA"; Invoke-ProbeB',
|
||
'Invoke-ProbeBlock { & "Invoke-ProbeA" literal#value; Invoke-ProbeB }',
|
||
'Invoke-ProbeBlock { . "Invoke-ProbeA" literal#value; Invoke-ProbeB }',
|
||
"Invoke-ProbeBlock { ${probe}# ignored\nInvoke-ProbeB }",
|
||
"Invoke-ProbeBlock { $result = Invoke-ProbeA literal#value; Invoke-ProbeB }",
|
||
"Invoke-ProbeA $probe[$(Invoke-ProbeB)]",
|
||
'Invoke-ProbeA "tab`tnewline`n`u{0041}"; Invoke-ProbeB',
|
||
"Invoke-ProbeA |\n\n# comment\nForEach-Object { Invoke-ProbeB }",
|
||
],
|
||
...[
|
||
"1",
|
||
"+1",
|
||
"-1",
|
||
".1",
|
||
"0x1",
|
||
"0b1",
|
||
"1L",
|
||
"1kb",
|
||
"1.0",
|
||
"1+1",
|
||
"1..2",
|
||
"-not 1",
|
||
"-bnot 1",
|
||
"!1",
|
||
"!!1",
|
||
",1",
|
||
"-join 1",
|
||
"\u2013not 1",
|
||
"\u2014not 1",
|
||
"\u2015not 1",
|
||
"\u2013join 1",
|
||
"\u2014join 1",
|
||
"\u2015join 1",
|
||
"'x' -eq 1",
|
||
'"x" -eq 1',
|
||
"{} -eq 1",
|
||
].flatMap((expression) => [
|
||
`${expression}#'\nInvoke-ProbeB\n#'`,
|
||
`Invoke-ProbeBlock { ${expression}#} '\nInvoke-ProbeB\n} #'`,
|
||
]),
|
||
...[
|
||
"$null",
|
||
"$probe",
|
||
"$HOME",
|
||
"${probe}",
|
||
"$env:PATH",
|
||
"$true",
|
||
"$false",
|
||
"1",
|
||
"1.0",
|
||
"1kb",
|
||
"0x1",
|
||
"-1",
|
||
"x2>&1",
|
||
"x6>&1",
|
||
">$null",
|
||
"> $null",
|
||
"'x'>$null",
|
||
].flatMap((prefix) => [
|
||
`Invoke-ProbeA ${prefix}#'\nInvoke-ProbeB\n#'`,
|
||
`Invoke-ProbeBlock { Invoke-ProbeA ${prefix}#} '\nInvoke-ProbeB\n} #'`,
|
||
]),
|
||
...[" ", "\t", "\v", "\f", "\u0085", "\u00a0", "\u2000", "\u2028", "\u2029", "\ufeff"].flatMap((space) => [
|
||
`Invoke-ProbeA \`${space}#'\nInvoke-ProbeB\n#'`,
|
||
`Invoke-ProbeBlock { Invoke-ProbeA 2>&1\`${space}#} '\nInvoke-ProbeB\n} #'`,
|
||
]),
|
||
...Array.from({ length: 2048 }, (_, index) => {
|
||
const tokens = [
|
||
"x",
|
||
" ",
|
||
"\t",
|
||
"\r",
|
||
"\n",
|
||
";",
|
||
"|",
|
||
"'",
|
||
'"',
|
||
"''",
|
||
'""',
|
||
"#",
|
||
"--%",
|
||
"`",
|
||
"{",
|
||
"}",
|
||
"2>&1",
|
||
"Invoke-ProbeC",
|
||
]
|
||
let seed = index + 1
|
||
const body = Array.from({ length: 8 }, () => {
|
||
seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0
|
||
return tokens[(seed >>> 16) % tokens.length]
|
||
}).join("")
|
||
return index % 2
|
||
? `Invoke-ProbeA ${body}\nInvoke-ProbeB\n# '`
|
||
: `Invoke-ProbeBlock { Invoke-ProbeA ${body}\nInvoke-ProbeB\n} # '`
|
||
}),
|
||
...["&", "."].flatMap((operator) =>
|
||
["'Invoke-ProbeA'", '"Invoke-ProbeA"'].flatMap((head) =>
|
||
["argument", "'argument'", '"argument"', "#ignored", "--% '"].map(
|
||
(tail) => `${operator} ${head}${tail}\nInvoke-ProbeB\n# '`,
|
||
),
|
||
),
|
||
),
|
||
...["'", '"', "\u2018", "\u2019", "\u201c", "\u201d"].flatMap((open) =>
|
||
["'", '"', "\u2018", "\u2019", "\u201c", "\u201d"].flatMap((close) =>
|
||
["", "literal", "`", "'", '"', "''", '""', "{", "}", "#", "--%", "$(Invoke-ProbeB)"].flatMap((content) => [
|
||
`Invoke-ProbeA ${open}${content}${close}; Invoke-ProbeB; Invoke-ProbeC '${content}'`,
|
||
`Invoke-ProbeBlock { Invoke-ProbeA ${open}${content}${close}; Invoke-ProbeB }; Invoke-ProbeC '${content}'`,
|
||
]),
|
||
),
|
||
),
|
||
...["", "literal", "'quoted'", '"quoted"', "{}"].flatMap((prefix) =>
|
||
["#ignored", "<#ignored#>", "--% 'ignored", '--% "ignored'].flatMap((tail) =>
|
||
[";", "|", "\r", "\n", "\r\n"].map(
|
||
(separator) => `Invoke-ProbeA ${prefix}${tail}${separator}Invoke-ProbeB${separator}Invoke-ProbeC`,
|
||
),
|
||
),
|
||
),
|
||
...["''", "'x'", '"x"', "{}", "2>&1", "6>&1", "*>&1", "plain", "plain'quoted'"].flatMap((prefix) =>
|
||
["#", "--%", "--% ", "<# #>#"].flatMap((suffix) =>
|
||
["\r", "\n", "\r\n"].flatMap((newline) => [
|
||
`Invoke-ProbeA ${prefix}${suffix}'${newline}Invoke-ProbeB${newline}# '`,
|
||
`Invoke-ProbeA ${prefix}${suffix}\"${newline}Invoke-ProbeB${newline}# \"`,
|
||
`Invoke-ProbeBlock { Invoke-ProbeA ${prefix}${suffix}} '${newline}Invoke-ProbeB${newline}} # '`,
|
||
]),
|
||
),
|
||
),
|
||
...[
|
||
...Array.from({ length: 33 }, (_, index) => String.fromCharCode(index)),
|
||
"\u0085",
|
||
"\u00a0",
|
||
"\u1680",
|
||
"\u2000",
|
||
"\u2028",
|
||
"\u2029",
|
||
"\u202f",
|
||
"\u205f",
|
||
"\u3000",
|
||
"\ufeff",
|
||
].flatMap((space) => [
|
||
`Invoke-ProbeA${space}argument; Invoke-ProbeB`,
|
||
`Invoke-ProbeA # ignored${space}Invoke-ProbeB`,
|
||
`Invoke-ProbeA${space}Invoke-ProbeB`,
|
||
`&${space}'Invoke-ProbeA'; Invoke-ProbeB`,
|
||
]),
|
||
...["Invoke-ProbeA", "& Invoke-ProbeA", "& 'Invoke-ProbeA'", '. "Invoke-ProbeA"'].flatMap((head) =>
|
||
[
|
||
"plain",
|
||
"'single ; | & # { }'",
|
||
'"double ; | & # { }"',
|
||
"'single''quote'",
|
||
'"double""quote"',
|
||
"'`'",
|
||
'"a`"}b"',
|
||
'"it\'s } literal"',
|
||
"left`#right",
|
||
"left`;right",
|
||
"left`|right",
|
||
"left`&right",
|
||
'"$(Invoke-ProbeB)"',
|
||
'"$probe"',
|
||
'"${probe}"',
|
||
"\u2018smart single\u2019",
|
||
"\u201csmart double\u201d",
|
||
].flatMap((argument) =>
|
||
["; ", "\n", "\r", "\r\n", " | ", " && ", " || "].map(
|
||
(separator) => `${head} ${argument}${separator}Invoke-ProbeC`,
|
||
),
|
||
),
|
||
),
|
||
...["\n", "\r", "\r\n"].flatMap((newline) => [
|
||
`Invoke-ProbeA # '\" } ; ignored${newline}Invoke-ProbeB`,
|
||
`Invoke-ProbeBlock { # } ignored${newline}Invoke-ProbeB }`,
|
||
`Invoke-ProbeBlock { Invoke-ProbeA # } ignored${newline}Invoke-ProbeB }`,
|
||
`Invoke-ProbeA --% \"ignored${newline}Invoke-ProbeB${newline}\"`,
|
||
`Invoke-ProbeBlock { Invoke-ProbeA --% \"ignored${newline}Invoke-ProbeB${newline}\" }`,
|
||
`Invoke-ProbeA \`${newline}argument; Invoke-ProbeB`,
|
||
`Invoke-ProbeA @'${newline}literal ; }${newline}'@; Invoke-ProbeB`,
|
||
`Invoke-ProbeA @\"${newline}$(Invoke-ProbeB)${newline}\"@; Invoke-ProbeC`,
|
||
]),
|
||
...["&", ".", "Invoke-ProbeBlock", "Invoke-ProbeA | ForEach-Object"].flatMap((head) =>
|
||
[
|
||
"Invoke-ProbeB; Invoke-ProbeC",
|
||
"Invoke-ProbeB '`'; Invoke-ProbeC",
|
||
'Invoke-ProbeB "it\'s } literal"; Invoke-ProbeC',
|
||
'Invoke-ProbeB "a`\"}b"; Invoke-ProbeC',
|
||
'Invoke-ProbeB "a\"\"}b"; Invoke-ProbeC',
|
||
"Invoke-ProbeBlock { Invoke-ProbeB }; Invoke-ProbeC",
|
||
"<# <# } #> #> Invoke-ProbeB",
|
||
'Invoke-ProbeB "$(Invoke-ProbeC)"',
|
||
'Invoke-ProbeB "$(Invoke-ProbeA \"}\"); Invoke-ProbeC"',
|
||
].map((body) => `${head} { ${body} }; Invoke-ProbeA`),
|
||
),
|
||
...["> $null", ">> $null", "2>&1", "3>&1", "4>&1", "5>&1", "6>&1", "*>&1", "*> $null"].flatMap((redirect) => [
|
||
`Invoke-ProbeA ${redirect}; Invoke-ProbeB`,
|
||
`Invoke-ProbeBlock { Invoke-ProbeA ${redirect}; Invoke-ProbeB }`,
|
||
]),
|
||
"Invoke-ProbeA --% 'ignored|Invoke-ProbeB|Invoke-ProbeC '",
|
||
"Invoke-ProbeA --% ; Invoke-ProbeB",
|
||
"Invoke-ProbeA --% $(Invoke-ProbeB)",
|
||
"Invoke-ProbeA \u2018a'; Invoke-ProbeB; Invoke-ProbeC 'b\u2019",
|
||
'Invoke-ProbeA \u201ca"; Invoke-ProbeB; Invoke-ProbeC "b\u201d',
|
||
'Invoke-ProbeA "$(Invoke-ProbeB \"quoted\")"; Invoke-ProbeC',
|
||
"& $probe; Invoke-ProbeC",
|
||
"& ('Invoke-' + 'ProbeB'); Invoke-ProbeC",
|
||
"& { Invoke-ProbeB }; Invoke-ProbeC",
|
||
"$(Invoke-ProbeB); Invoke-ProbeC",
|
||
"@(Invoke-ProbeB); Invoke-ProbeC",
|
||
"Invoke-ProbeA <# ignored #>; Invoke-ProbeB",
|
||
"Invoke-ProbeA#literal; Invoke-ProbeB",
|
||
"Invoke-ProbeA ''#literal; Invoke-ProbeB",
|
||
'Invoke-ProbeA ""#literal; Invoke-ProbeB',
|
||
"Invoke-ProbeA { Invoke-ProbeB }#literal; Invoke-ProbeC",
|
||
"Inv'oke'-ProbeA; Invoke-ProbeB",
|
||
'Inv"oke"-ProbeA; Invoke-ProbeB',
|
||
"Invoke-ProbeA,Invoke-ProbeB; Invoke-ProbeC",
|
||
'Invoke-ProbeA --%"literal|Invoke-ProbeB|Invoke-ProbeC"',
|
||
'Invoke-ProbeA x--% "literal|Invoke-ProbeB|Invoke-ProbeC"',
|
||
"Invoke-ProbeA '--%' \"literal|Invoke-ProbeB|Invoke-ProbeC\"",
|
||
'Invoke-ProbeA "--%" "literal|Invoke-ProbeB|Invoke-ProbeC"',
|
||
'Invoke-ProbeA --`% "literal|Invoke-ProbeB|Invoke-ProbeC"',
|
||
'Invoke-ProbeA `-`-`% "literal|Invoke-ProbeB|Invoke-ProbeC"',
|
||
"Invoke-ProbeA ''--% \"literal|Invoke-ProbeB|Invoke-ProbeC\"",
|
||
"Invoke-ProbeA 2>&1; &{Invoke-ProbeB}; Invoke-ProbeC",
|
||
"Invoke-ProbeA 2>&1|Invoke-ProbeB",
|
||
"Invoke-ProbeA 'x'#'\nInvoke-ProbeB\nInvoke-ProbeC ''#'",
|
||
'Invoke-ProbeA "x"#"\nInvoke-ProbeB\nInvoke-ProbeC ""#"',
|
||
"Invoke-ProbeA {}#'\nInvoke-ProbeB\nInvoke-ProbeC ''#'",
|
||
"Invoke-ProbeBlock { Invoke-ProbeA 'x'#'\nInvoke-ProbeB\nInvoke-ProbeC ''#' }",
|
||
"Invoke-ProbeA 'x'# ' \nInvoke-ProbeB\n# '",
|
||
"Invoke-ProbeA { Invoke-ProbeB }# ' \nInvoke-ProbeC\n# '",
|
||
"Invoke-ProbeA { Invoke-ProbeB 2>&1# } ' \nInvoke-ProbeC\n} # '",
|
||
"Invoke-ProbeBlock { Invoke-ProbeB 2>&1# } ' \nInvoke-ProbeC\n} # '",
|
||
"& { Invoke-ProbeB 2>&1# } ' \nInvoke-ProbeC\n} # '",
|
||
"Invoke-ProbeA & Invoke-ProbeB",
|
||
"Invoke-ProbeA 'safe`'; Invoke-ProbeB; '`'",
|
||
'$x = "$(switch ${probe}#)\n{ default { Invoke-ProbeA; Invoke-ProbeB } })"',
|
||
'$x = "$(if (${probe}#)\n) { Invoke-ProbeA; Invoke-ProbeB })"',
|
||
'$x = "$(foreach ($item in ${probe}#)\n) { Invoke-ProbeA; Invoke-ProbeB })"',
|
||
'Invoke-ProbeA "${probe"}$(Invoke-ProbeB)"',
|
||
'Invoke-ProbeA "${probe"\'}"#"\nInvoke-ProbeB\n# "\'',
|
||
"Invoke-ProbeA ${probe`}'}; Invoke-ProbeB; #'",
|
||
"Invoke-ProbeA ${probe``}; Invoke-ProbeB",
|
||
'Invoke-ProbeA "${probe`}b}$(Invoke-ProbeB)"',
|
||
'Invoke-ProbeA @"\n${probe`$(Invoke-ProbeB)}\n$(Invoke-ProbeC)\n"@',
|
||
'Invoke-ProbeA @"\n`$(Invoke-ProbeB)\n``$(Invoke-ProbeC)\n"@',
|
||
"${probe}<# comment #>; Invoke-ProbeA",
|
||
"$(1)<# comment #>; Invoke-ProbeA",
|
||
"@(1)<# comment #>; Invoke-ProbeA",
|
||
"@{a=1}<# comment #>; Invoke-ProbeA",
|
||
"[int]<# comment #>$x = 1; Invoke-ProbeA",
|
||
"$probe<# comment #>; Invoke-ProbeA",
|
||
"1<# comment #>; Invoke-ProbeA",
|
||
"1.0<# comment #>; Invoke-ProbeA",
|
||
"1kb<# comment #>; Invoke-ProbeA",
|
||
"0x1<# comment #>; Invoke-ProbeA",
|
||
"'x'<# comment #>; Invoke-ProbeA",
|
||
'"x"<# comment #>; Invoke-ProbeA',
|
||
"(1)<# comment #>; Invoke-ProbeA",
|
||
"{1}<# comment #>; Invoke-ProbeA",
|
||
"Invoke-ProbeA $(1)<# comment #>; Invoke-ProbeB",
|
||
"Invoke-ProbeA @(1)<# comment #>; Invoke-ProbeB",
|
||
"Invoke-ProbeA @{a=1}<# comment #>; Invoke-ProbeB",
|
||
"Invoke-ProbeA $(1)# comment\nInvoke-ProbeB",
|
||
"Invoke-ProbeA @(1)# comment\nInvoke-ProbeB",
|
||
"Invoke-ProbeA @{a=1}# comment\nInvoke-ProbeB",
|
||
"Invoke-ProbeA pre$(1)#notcomment\nInvoke-ProbeB",
|
||
"Invoke-ProbeA pre@(1)# comment\nInvoke-ProbeB",
|
||
"Invoke-ProbeA pre@{Invoke-ProbeB}# comment\nInvoke-ProbeC",
|
||
"$x<# comment #>= Invoke-ProbeA",
|
||
"${x}<# comment #>= Invoke-ProbeA",
|
||
"$x = <# comment #>Invoke-ProbeA",
|
||
"Invoke-ProbeA $probe@'\n'; Invoke-ProbeB; #\n'@'",
|
||
"Invoke-ProbeA pre@'\n'; Invoke-ProbeB; #\n'@'",
|
||
"Invoke-ProbeA user@'host'; Invoke-ProbeB",
|
||
'Invoke-ProbeA user@"host"; Invoke-ProbeB',
|
||
".'Invoke-ProbeA'# '\nInvoke-ProbeB\n# '",
|
||
'."Invoke-ProbeA"# "\nInvoke-ProbeB\n# "',
|
||
".{ Invoke-ProbeA }; Invoke-ProbeB",
|
||
".(Invoke-ProbeA); Invoke-ProbeB",
|
||
".$probe; Invoke-ProbeB",
|
||
".${probe}; Invoke-ProbeB",
|
||
"&{ Invoke-ProbeA }; Invoke-ProbeB",
|
||
"&(Invoke-ProbeA); Invoke-ProbeB",
|
||
"(& 'Invoke-ProbeA'); Invoke-ProbeB",
|
||
"Invoke-ProbeA [a; Invoke-ProbeB; Invoke-ProbeC ]",
|
||
"Invoke-ProbeA [int]#notcomment\nInvoke-ProbeB",
|
||
"Invoke-ProbeA $probe[(Invoke-ProbeB)]",
|
||
"Invoke-ProbeA $probe[0]# comment\nInvoke-ProbeB",
|
||
"Invoke-ProbeA $probe[0][$(Invoke-ProbeB)]# comment\nInvoke-ProbeC",
|
||
"Invoke-ProbeA 'a'[(Invoke-ProbeB)]# comment\nInvoke-ProbeC",
|
||
'Invoke-ProbeA "a"[(Invoke-ProbeB)]# comment\nInvoke-ProbeC',
|
||
"Invoke-ProbeA (1)[(Invoke-ProbeB)]# comment\nInvoke-ProbeC",
|
||
"[string][int]$x = Invoke-ProbeA; Invoke-ProbeB",
|
||
"[Parameter(Mandatory=$true)] param([string]$x = (Invoke-ProbeA)) Invoke-ProbeB",
|
||
"switch ($probe) { default { Invoke-ProbeA } }",
|
||
"switch -Regex ($probe) { 'a' { Invoke-ProbeA } default { Invoke-ProbeB } }",
|
||
"switch -Wildcard ($probe) { a* { Invoke-ProbeA } default { Invoke-ProbeB } }",
|
||
"switch (Invoke-ProbeA) { (Invoke-ProbeB) { Invoke-ProbeC } }",
|
||
"switch (Invoke-ProbeA) { { Invoke-ProbeB } { Invoke-ProbeC } }",
|
||
"[Flags()] enum Color { Red = 1; Green = 2 } Invoke-ProbeA",
|
||
"[Attribute()] class C { [int]$x = (Invoke-ProbeA); [void] M() { Invoke-ProbeB } } Invoke-ProbeC",
|
||
":outer while (Invoke-ProbeA) { Invoke-ProbeB; break outer }",
|
||
":outer foreach ($i in (Invoke-ProbeA)) { Invoke-ProbeB; continue outer }",
|
||
"while (Invoke-ProbeA) { Invoke-ProbeB; break; }",
|
||
"do { Invoke-ProbeA } until ($true) Invoke-ProbeB",
|
||
"do { Invoke-ProbeA } while ($false) Invoke-ProbeB",
|
||
"function Get-Probe { Invoke-ProbeA; return; } Invoke-ProbeB",
|
||
"Invoke-ProbeA --% arg && Invoke-ProbeB",
|
||
'Invoke-ProbeA --% "arg && still" && Invoke-ProbeB',
|
||
"Invoke-ProbeA --% arg || Invoke-ProbeB",
|
||
"Invoke-ProbeA > --% $null; Invoke-ProbeB",
|
||
"Invoke-ProbeBlock { Invoke-ProbeA --% } | Invoke-ProbeB }",
|
||
"$probe > $null#'\n'; Invoke-ProbeA; #'",
|
||
"$probe>$null#'\n'; Invoke-ProbeA; #'",
|
||
"$probe*>$null; Invoke-ProbeA",
|
||
"${probe}>$null; Invoke-ProbeA",
|
||
"${probe}2>$null; Invoke-ProbeA",
|
||
"1>$null; Invoke-ProbeA",
|
||
"2>$null; Invoke-ProbeA",
|
||
"Invoke-ProbeA `t#'\n'; Invoke-ProbeB; #'",
|
||
"Invoke-ProbeA `n#'\n'; Invoke-ProbeB; #'",
|
||
"Invoke-ProbeA `r#'\n'; Invoke-ProbeB; #'",
|
||
"Invoke-ProbeA `u{0020}#'\n'; Invoke-ProbeB; #'",
|
||
"$probe` # comment\nInvoke-ProbeA",
|
||
'Invoke-ProbeA "$(Invoke-ProbeB """")"',
|
||
"${a`u{0041}'}; Invoke-ProbeA; #'",
|
||
'"${a`u{0041}" # }; $(Invoke-ProbeA)"',
|
||
'@"\n${a`u{0041}\n"@; \' # } $(Invoke-ProbeA)\n"@#\'',
|
||
"$probe[1+$(Invoke-ProbeA)]",
|
||
"function else {}\nelse\nInvoke-ProbeA\n{}",
|
||
"function catch {}\ncatch\nInvoke-ProbeA\n{}",
|
||
"until (1)",
|
||
"else { Invoke-ProbeA }",
|
||
"catch { Invoke-ProbeA }",
|
||
"finally { Invoke-ProbeA }",
|
||
"param { Invoke-ProbeA }",
|
||
"switch ('x') {\n a#'\n ' {} default { Invoke-ProbeA } # '\n}",
|
||
"data -SupportedCommand a<#'#> ' {}; Invoke-ProbeA # ' {}",
|
||
"if ($false) { switch -File $global:probe[0#'\n] { default {} } }; Invoke-ProbeA # ' {} }",
|
||
"if ($false) { switch -File $.a[a<#'#>'] { default {} } }; Invoke-ProbeA # '] {} }",
|
||
"Invoke-ProbeA 'a'b[a<#'#>']; Invoke-ProbeB; # ']",
|
||
"try { throw 1 } catch [Exception<#'#>] { Invoke-ProbeA } # ' {}",
|
||
"Invoke-ProbeA --`%('| Invoke-ProbeB #')",
|
||
"if ($false) { Invoke-ProbeA --`%('\n} if ($true) { Invoke-ProbeB # ')\n}",
|
||
"if ($false) { $.a<#'#>' }; Invoke-ProbeA; #' }",
|
||
"function $.probe { Invoke-ProbeA }; $.probe",
|
||
"-$n#'\nInvoke-ProbeA\n#'",
|
||
"--$n<#'#>; Invoke-ProbeA; #'",
|
||
'Invoke-ProbeA "$( "a`"; Invoke-ProbeB; `"b" )"',
|
||
'Invoke-ProbeA "$( `"\'"; Invoke-ProbeB; "\'`" )"',
|
||
'Invoke-ProbeA "$( "a`""""; Invoke-ProbeB; "c`"""" )"',
|
||
'Invoke-ProbeA "$( Invoke-ProbeA @"\n""@; Invoke-ProbeB; \'\n"@#\'\n)"',
|
||
'Invoke-ProbeA "$( \'(\' )"#" )"; Invoke-ProbeB',
|
||
'Invoke-ProbeA "$( <# ( #> )"#" )"; Invoke-ProbeB',
|
||
'Invoke-ProbeA "$( # (\n)"#" )"; Invoke-ProbeB',
|
||
'Invoke-ProbeA "$( ${(} )"#" )"; Invoke-ProbeB',
|
||
'Invoke-ProbeA "$( "(" )"#" )"; Invoke-ProbeB',
|
||
"@\"\n$( '(' )\n\"@ ' )\n\"@\nInvoke-ProbeA\n#'",
|
||
"Invoke-ProbeA arg`\r\nInvoke-ProbeB",
|
||
"Invoke-ProbeA arg`\r\n#'\r\nInvoke-ProbeB\r\n#'",
|
||
'@"\n`\r\n"@; Invoke-ProbeA; \'\r\n"@#\'',
|
||
"switch ('x') {\n $a#'\n ' {} default { Invoke-ProbeA } # '\n}",
|
||
"switch ('x') {\n 1#'\n ' {} default { Invoke-ProbeA } # '\n}",
|
||
"switch ('x') {\n ${a}b#'\n ' {} default { Invoke-ProbeA } # '\n}",
|
||
"switch ('x') {\n $a<#'#>' {} default { Invoke-ProbeA } # '\n}",
|
||
"if ($false) { switch -File @{} { a {} --% {} } }; Invoke-ProbeA <#\n} } #>",
|
||
"- 1#'\nInvoke-ProbeA\n#'",
|
||
"- $n#'\nInvoke-ProbeA\n#'",
|
||
"-- $n#'\nInvoke-ProbeA\n#'",
|
||
"-[int]$n#'\nInvoke-ProbeA\n#'",
|
||
"--[int]$n#'\nInvoke-ProbeA\n#'",
|
||
"$:a = Invoke-ProbeA; Invoke-ProbeB",
|
||
"$α = Invoke-ProbeA; Invoke-ProbeB",
|
||
"foreach ($:a in (Invoke-ProbeA)) { Invoke-ProbeB }",
|
||
"foreach ($α in (Invoke-ProbeA)) { Invoke-ProbeB }",
|
||
"$:a#'\nInvoke-ProbeA\n#'",
|
||
"$α#'\nInvoke-ProbeA\n#'",
|
||
"$:a = @(1); Invoke-ProbeA $:a[0]#'\nInvoke-ProbeB\n#'",
|
||
"$α = @(1); Invoke-ProbeA $α[0]#'\nInvoke-ProbeB\n#'",
|
||
"Invoke-ProbeA @α; Invoke-ProbeB",
|
||
"Invoke-ProbeA $?[a<#'#>']; Invoke-ProbeB; # ']",
|
||
"Invoke-ProbeA $?a[a<#'#>']; Invoke-ProbeB; # ']",
|
||
"Invoke-ProbeA $?.a[a<#'#>']; Invoke-ProbeB; # ']",
|
||
"Invoke-ProbeA ${a}::b[a<#'#>']; Invoke-ProbeB; # ']",
|
||
"Invoke-ProbeA $a..b[a<#'#>']; Invoke-ProbeB; # ']",
|
||
"function f ([a#]$x) {}; Invoke-ProbeA <#\n]) {} #>",
|
||
"if ($false) { [a#]$x }; Invoke-ProbeA <#\n] } #>",
|
||
"if ($false) { $x.Foo[a#]() }; Invoke-ProbeA <#\n] } #>",
|
||
"try { } catch [a#] { Invoke-ProbeA } <#\n] {} #>",
|
||
"Invoke-ProbeA a,#'\nb; Invoke-ProbeB; #'",
|
||
"Invoke-ProbeA a,<#'#>b; Invoke-ProbeB; #'",
|
||
"Invoke-ProbeA a,@'\n'\n'@; Invoke-ProbeB; #'",
|
||
"Invoke-ProbeA a,@{k=1#'\n}; Invoke-ProbeB; #'}",
|
||
"Invoke-ProbeA a,$probe[0]#'\nInvoke-ProbeB\n#'",
|
||
'Invoke-ProbeA "$( "a``""; Invoke-ProbeB; `"c``"" )"',
|
||
'Invoke-ProbeA "$( "a``""; Invoke-ProbeB; ""c``"" )"',
|
||
'Invoke-ProbeA "$( `"a``""; Invoke-ProbeB; `"c``"" )"',
|
||
'Invoke-ProbeA "$( Invoke-ProbeA ``"a; Invoke-ProbeB; Invoke-ProbeA ``" )"',
|
||
'Invoke-ProbeA "$( Invoke-ProbeA --% ""`" | Invoke-ProbeB\n)"',
|
||
"Invoke-ProbeA a, --% ; Invoke-ProbeB",
|
||
"Invoke-ProbeA a,\n--% ; Invoke-ProbeB",
|
||
"Invoke-ProbeA a, --`% ; Invoke-ProbeB",
|
||
"Invoke-ProbeA a,<##>--% ; Invoke-ProbeB",
|
||
"Invoke-ProbeA -Param: --% ; Invoke-ProbeB",
|
||
"Invoke-ProbeA -Param:<##>--% ; Invoke-ProbeB",
|
||
"Invoke-ProbeA a,`\r\n1# ; Invoke-ProbeB",
|
||
"Invoke-ProbeA a,`\r\n$x# ; Invoke-ProbeB",
|
||
"Invoke-ProbeA a,`\n\n1# ; Invoke-ProbeB",
|
||
"Invoke-ProbeA a,` #'\nb; Invoke-ProbeB\n#'",
|
||
"Invoke-ProbeA a,` <#'#>b; Invoke-ProbeB; #'",
|
||
"Invoke-ProbeA a,` @'\n'\n'@; Invoke-ProbeB; #'",
|
||
"Invoke-ProbeA a,` @{k=1#'\n}; Invoke-ProbeB; #'}",
|
||
"Invoke-ProbeA x,'a'[0]#'\nInvoke-ProbeB\n#'",
|
||
"Invoke-ProbeA x,\"a\"[0]#'\nInvoke-ProbeB\n#'",
|
||
"Invoke-ProbeA x,'a'#'\nInvoke-ProbeB\n#'",
|
||
"Invoke-ProbeA x,'a'<#'#>; Invoke-ProbeB; #'",
|
||
"Invoke-ProbeA $probe.Length#'\nInvoke-ProbeB\n#'",
|
||
"Invoke-ProbeA $probe.Length<#'#>; Invoke-ProbeB; #'",
|
||
"Invoke-ProbeA $probe.<#'#>Length; Invoke-ProbeB; #'",
|
||
"Invoke-ProbeA $probe.#'\nLength; Invoke-ProbeB\n#'",
|
||
"Invoke-ProbeA (1).ToString#'\nInvoke-ProbeB\n#'",
|
||
"Invoke-ProbeA (1)?.ToString#'\nInvoke-ProbeB\n#'",
|
||
"Invoke-ProbeA 'a'.Length#'\nInvoke-ProbeB\n#'",
|
||
"Invoke-ProbeA ${probe}.Length#'\nInvoke-ProbeB\n#'",
|
||
"Invoke-ProbeA x,$probe.Length#'\nInvoke-ProbeB\n#'",
|
||
"Invoke-ProbeA x,${probe}.Length#'\nInvoke-ProbeB\n#'",
|
||
]
|
||
|
||
// Runtime execution is restricted to probes and $null/stream redirections; other cases are parser-only.
|
||
const oracle = String.raw`
|
||
$ErrorActionPreference = 'Stop'
|
||
$probe = 'Invoke-ProbeB'
|
||
$script:seen = [System.Collections.Generic.List[string]]::new()
|
||
function Invoke-ProbeA { [void]$script:seen.Add('Invoke-ProbeA'); 1 }
|
||
function Invoke-ProbeB { [void]$script:seen.Add('Invoke-ProbeB'); 1 }
|
||
function Invoke-ProbeC { [void]$script:seen.Add('Invoke-ProbeC'); 1 }
|
||
function Invoke-ProbeBlock {
|
||
[void]$script:seen.Add('Invoke-ProbeBlock')
|
||
foreach ($argument in $args) {
|
||
if ($argument -is [scriptblock]) { & $argument }
|
||
}
|
||
}
|
||
$results = foreach ($source in (ConvertFrom-Json -InputObject ([Console]::In.ReadToEnd()))) {
|
||
$tokens = $null
|
||
$errors = $null
|
||
$ast = [System.Management.Automation.Language.Parser]::ParseInput($source, [ref]$tokens, [ref]$errors)
|
||
$nodes = @($ast.FindAll({ param($node) $node -is [System.Management.Automation.Language.CommandAst] }, $true))
|
||
$commands = @($nodes | ForEach-Object {
|
||
@{
|
||
name = $_.GetCommandName()
|
||
text = $_.Extent.Text
|
||
start = $_.Extent.StartOffset
|
||
end = $_.Extent.EndOffset
|
||
}
|
||
})
|
||
$script:seen.Clear()
|
||
$runtimeError = $null
|
||
$unsafe = @($nodes | Where-Object {
|
||
$name = $_.GetCommandName()
|
||
($name -and $name -notin @('Invoke-ProbeA', 'Invoke-ProbeB', 'Invoke-ProbeC', 'Invoke-ProbeBlock', 'ForEach-Object')) -or
|
||
(!$name -and $_.CommandElements[0] -isnot [System.Management.Automation.Language.ScriptBlockExpressionAst])
|
||
})
|
||
$files = @($ast.FindAll({ param($node)
|
||
$node -is [System.Management.Automation.Language.FileRedirectionAst] -and $node.Location.Extent.Text -ne '$null'
|
||
}, $true))
|
||
$background = @($ast.FindAll({ param($node)
|
||
$node -is [System.Management.Automation.Language.PipelineAst] -and $node.Background
|
||
}, $true))
|
||
if ($errors.Count -eq 0 -and $unsafe.Count -eq 0 -and $files.Count -eq 0 -and $background.Count -eq 0) {
|
||
try { & ([scriptblock]::Create($source)) | Out-Null }
|
||
catch { $runtimeError = $_.Exception.Message }
|
||
}
|
||
@{
|
||
source = $source
|
||
commands = $commands
|
||
errors = @($errors | ForEach-Object { $_.ErrorId })
|
||
executed = @($script:seen.ToArray())
|
||
runtimeError = $runtimeError
|
||
}
|
||
}
|
||
ConvertTo-Json -InputObject @($results) -Depth 10 -Compress
|
||
`
|
||
|
||
// Run with SHELL_SCAN_PWSH=/path/to/pwsh bun run test test/shell-scan/powershell-runtime.test.ts.
|
||
test.skipIf(!pwsh)(
|
||
"successful PowerShell scans cover real parser boundaries and executed probe calls",
|
||
async () => {
|
||
const process = Bun.spawn([pwsh!, "-NoLogo", "-NoProfile", "-NonInteractive", "-Command", oracle], {
|
||
stdin: new Blob([JSON.stringify(fixtures)]),
|
||
stdout: "pipe",
|
||
stderr: "pipe",
|
||
timeout: 45_000,
|
||
})
|
||
const [output, error, code] = await Promise.all([
|
||
new Response(process.stdout).text(),
|
||
new Response(process.stderr).text(),
|
||
process.exited,
|
||
])
|
||
expect({ code, error }).toEqual({ code: 0, error: "" })
|
||
const results: Array<{
|
||
source: string
|
||
commands: Array<{ name: string | null; text: string; start: number; end: number }>
|
||
errors: string[]
|
||
executed: string[]
|
||
runtimeError: string | null
|
||
}> = JSON.parse(output)
|
||
expect(results).toHaveLength(fixtures.length)
|
||
const failures: string[] = []
|
||
let scanned = 0
|
||
let executed = 0
|
||
for (const result of results) {
|
||
const scan = ShellScan.scanPowerShell(result.source)
|
||
if (supported.includes(result.source)) {
|
||
expect(result.errors, result.source).toEqual([])
|
||
expect(scan.kind, result.source).toBe("scanned")
|
||
}
|
||
if (scan.kind === "opaque" || result.errors.length > 0) continue
|
||
scanned++
|
||
executed += result.executed.length
|
||
const missing = result.commands.filter(
|
||
(command) =>
|
||
command.name !== null &&
|
||
!scan.commands.some(
|
||
(candidate) =>
|
||
candidate.words[0]?.toLowerCase() === command.name?.toLowerCase() &&
|
||
candidate.resource === command.text.trim(),
|
||
),
|
||
)
|
||
const unobserved = result.executed.filter(
|
||
(name) => !scan.commands.some((command) => command.words[0]?.toLowerCase() === name.toLowerCase()),
|
||
)
|
||
if (missing.length || unobserved.length)
|
||
failures.push(
|
||
JSON.stringify({
|
||
source: result.source,
|
||
missing: missing.map((command) => command.text),
|
||
unobserved,
|
||
scanned: scan.commands.map((command) => command.words[0]),
|
||
}),
|
||
)
|
||
}
|
||
expect(scanned).toBeGreaterThan(100)
|
||
expect(executed).toBeGreaterThan(100)
|
||
expect(failures).toEqual([])
|
||
},
|
||
60_000,
|
||
)
|