test(shell-scan): add generated safety coverage

This commit is contained in:
Kit Langton
2026-08-12 22:15:59 -04:00
parent 761806578b
commit 4baa6b4a82
+182
View File
@@ -0,0 +1,182 @@
import { describe, expect, test } from "bun:test"
import { ShellScan } from "../src/index.js"
const staticCommands = [
["git status", ["git", "status"]],
["printf ok", ["printf", "ok"]],
["curl example.com", ["curl", "example.com"]],
] as const
describe("ShellScan generated properties", () => {
test("decomposes every combination of static commands and separators", () => {
const separators = [" ; ", " && ", " || ", " | ", " |& ", "\n"]
for (const [left, leftWords] of staticCommands) {
for (const separator of separators) {
for (const [right, rightWords] of staticCommands) {
expect(ShellScan.scan(left + separator + right)).toEqual({
kind: "scanned",
commands: [
{ resource: left, words: [...leftWords] },
{ resource: right, words: [...rightWords] },
],
})
}
}
}
})
test("keeps quoted and escaped separators in arguments", () => {
const literals = [";", "|", "&", "#", "<", ">"]
const forms = literals.flatMap((literal) => [
{ source: `'left${literal}right'`, word: `left${literal}right` },
{ source: `"left${literal}right"`, word: `left${literal}right` },
{ source: `left\\${literal}right`, word: `left${literal}right` },
])
for (const form of forms) {
expect(ShellScan.scan(`printf %s ${form.source}`)).toEqual({
kind: "scanned",
commands: [{ resource: `printf %s ${form.source}`, words: ["printf", "%s", form.word] }],
})
}
})
test("fails closed when valid commands are mutated with malformed syntax", () => {
const mutate = [
(command: string) => `${command} "unterminated`,
(command: string) => `${command} 'unterminated`,
(command: string) => `${command} \\`,
(command: string) => `${command} &&`,
(command: string) => `| ${command}`,
(command: string) => `${command} || || printf reached`,
(command: string) => `${command} >`,
(command: string) => `${command} > > output`,
]
for (const [command] of staticCommands) {
for (const mutation of mutate) expect(ShellScan.scan(mutation(command)).kind).toBe("opaque")
}
})
test("fails closed for generated dynamic command heads", () => {
const heads = ["$COMMAND", "${COMMAND}", "pre$COMMAND", '"$COMMAND"', "$(printf git)", "`printf git`"]
const tails = ["status", "--version", "-rf /"]
for (const head of heads) {
for (const tail of tails) expect(ShellScan.scan(`${head} ${tail}`).kind).toBe("opaque")
}
})
test("fails closed for wrappers and shell evaluators under static prefixes", () => {
const prefixes = ["", "FOO=bar ", "FOO=bar BAR=baz "]
const wrapped = [
"time git status",
"command git status",
"builtin printf ok",
"exec git status",
"env FOO=bar git status",
"sudo git status",
"nice git status",
"nohup git status",
"xargs rm",
"source ./script.sh",
". ./script.sh",
"trap 'git status' EXIT",
"eval 'git status'",
"bash -c 'git status'",
"/bin/sh ./script.sh",
]
for (const prefix of prefixes) {
for (const command of wrapped) expect(ShellScan.scan(prefix + command).kind).toBe("opaque")
}
})
})
describe("ShellScan generated PowerShell properties", () => {
test("decomposes every combination of static commands and separators", () => {
const commands = [
["Get-ChildItem", ["Get-ChildItem"]],
["Write-Output ok", ["Write-Output", "ok"]],
["Get-Content input.txt", ["Get-Content", "input.txt"]],
] as const
const separators = ["; ", " | ", "\n"]
for (const [left, leftWords] of commands) {
for (const separator of separators) {
for (const [right, rightWords] of commands) {
expect(ShellScan.scanPowerShell(left + separator + right)).toEqual({
kind: "scanned",
commands: [
{ resource: left, words: [...leftWords] },
{ resource: right, words: [...rightWords] },
],
})
}
}
}
})
test("keeps quoted and escaped separators in arguments", () => {
const literals = [";", "|", "&", "#", "<", ">"]
const forms = literals.flatMap((literal) => [
{ source: `'left${literal}right'`, word: `left${literal}right` },
{ source: `"left${literal}right"`, word: `left${literal}right` },
{ source: `left\`${literal}right`, word: `left${literal}right` },
])
for (const form of forms) {
expect(ShellScan.scanPowerShell(`Write-Output ${form.source}`)).toEqual({
kind: "scanned",
commands: [{ resource: `Write-Output ${form.source}`, words: ["Write-Output", form.word] }],
})
}
})
test("fails closed when valid commands are mutated with malformed syntax", () => {
const mutations = [
'Write-Output ok "unterminated',
"Write-Output ok 'unterminated",
"Write-Output ok`",
"Write-Output ok |",
"Write-Output ok || || Write-Output reached",
"Write-Output ok >",
]
for (const command of mutations) expect(ShellScan.scanPowerShell(command).kind).toBe("opaque")
})
test("fails closed for generated dynamic heads, evaluators, and script execution", () => {
const commands = [
"$Command status",
"${Command} status",
"& git status",
". ./script.ps1",
"Invoke-Expression 'git status'",
"iex 'git status'",
"Import-Module ./module.psm1",
"./script.ps1 -Force",
]
const shells = ["powershell", "powershell.exe", "pwsh", "pwsh.exe"]
const switches = ["-Command", "-c", "-EncodedCommand", "-e", "-File", "-f"]
for (const command of commands) expect(ShellScan.scanPowerShell(command).kind).toBe("opaque")
for (const shell of shells) {
for (const flag of switches) {
expect(ShellScan.scanPowerShell(`${shell} ${flag} 'git status'`).kind).toBe("opaque")
}
}
})
test("fails closed for dynamic location changes but accepts known directory variables", () => {
const locations = ["Set-Location", "cd", "chdir", "sl", "Push-Location"]
const dynamic = ["$target", "$(Resolve-Path ..)", "(Resolve-Path ..)"]
const known = ["$PWD/project", "$HOME/project", "$PSHOME/Modules", "$env:TEMP/project"]
for (const location of locations) {
for (const target of dynamic) expect(ShellScan.scanPowerShell(`${location} ${target}`).kind).toBe("opaque")
for (const target of known) expect(ShellScan.scanPowerShell(`${location} ${target}`).kind).toBe("scanned")
}
})
})