diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 6ad3adb7..87cab479 100644 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -64,6 +64,7 @@ "Change IP format", "Encode text", "Decode text", + "PowerShell -e Encode/Decode", "Text Encoding Brute Force", "Swap endianness", "To MessagePack", diff --git a/src/core/operations/PowerShellEncodeDecode.mjs b/src/core/operations/PowerShellEncodeDecode.mjs new file mode 100644 index 00000000..690d3cf2 --- /dev/null +++ b/src/core/operations/PowerShellEncodeDecode.mjs @@ -0,0 +1,67 @@ +/** + * @author neoreo + * @copyright Crown Copyright 2026 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import cptable from "codepage"; +import {toBase64, fromBase64} from "../lib/Base64.mjs"; + +// PowerShell -EncodedCommand uses UTF-16LE (code page 1200) +const UTF_16LE = 1200; + +/** + * PowerShell -e Encode/Decode operation + */ +class PowerShellEncodeDecode extends Operation { + + /** + * PowerShellEncodeDecode constructor + */ + constructor() { + super(); + + this.name = "PowerShell -e Encode/Decode"; + this.module = "Encodings"; + this.description = [ + "Encodes or decodes a PowerShell -EncodedCommand (-e) payload in a single operation.", + "

", + "PowerShell's -EncodedCommand parameter expects the command encoded as UTF-16LE and then Base64'd. ", + "This operation combines both steps so you don't have to chain 'Encode text' and 'To Base64' (or their decode equivalents) yourself.", + "

", + "Encode: whoami becomes dwBoAG8AYQBtAGkA, runnable as powershell -e dwBoAG8AYQBtAGkA.", + "
", + "Decode: dwBoAG8AYQBtAGkA becomes whoami." + ].join("\n"); + this.infoURL = "https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_powershell_exe#-encodedcommand-base64encodedcommand"; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { + name: "Mode", + type: "option", + value: ["Encode", "Decode"] + } + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + const [mode] = args; + + if (mode === "Encode") { + const encoded = cptable.utils.encode(UTF_16LE, input); + return toBase64(new Uint8Array(encoded).buffer); + } + const bytes = fromBase64(input, "A-Za-z0-9+/=", "byteArray"); + return cptable.utils.decode(UTF_16LE, new Uint8Array(bytes)); + } + +} + +export default PowerShellEncodeDecode; diff --git a/tests/node/tests/nodeApi.mjs b/tests/node/tests/nodeApi.mjs index b65b9abc..43d1605d 100644 --- a/tests/node/tests/nodeApi.mjs +++ b/tests/node/tests/nodeApi.mjs @@ -168,7 +168,7 @@ TestRegister.addApiTests([ it("chef.help: returns multiple results", () => { const result = chef.help("base 64"); - assert.strictEqual(result.length, 14); + assert.strictEqual(result.length, 15); }), it("chef.help: looks in description for matches too", () => { diff --git a/tests/operations/tests/PowerShellEncodeDecode.mjs b/tests/operations/tests/PowerShellEncodeDecode.mjs new file mode 100644 index 00000000..98922b85 --- /dev/null +++ b/tests/operations/tests/PowerShellEncodeDecode.mjs @@ -0,0 +1,86 @@ +/** + * PowerShell -e Encode/Decode tests. + * + * @author neoreo + * + * @copyright Crown Copyright 2026 + * @license Apache-2.0 + */ +import TestRegister from "../../lib/TestRegister.mjs"; + +TestRegister.addTests([ + { + name: "PowerShell -e Encode/Decode: encode nothing", + input: "", + expectedOutput: "", + recipeConfig: [ + { + op: "PowerShell -e Encode/Decode", + args: ["Encode"], + }, + ], + }, + { + name: "PowerShell -e Encode/Decode: encode whoami", + input: "whoami", + expectedOutput: "dwBoAG8AYQBtAGkA", + recipeConfig: [ + { + op: "PowerShell -e Encode/Decode", + args: ["Encode"], + }, + ], + }, + { + name: "PowerShell -e Encode/Decode: decode whoami", + input: "dwBoAG8AYQBtAGkA", + expectedOutput: "whoami", + recipeConfig: [ + { + op: "PowerShell -e Encode/Decode", + args: ["Decode"], + }, + ], + }, + { + name: "PowerShell -e Encode/Decode: round trip", + input: "Get-Process | Where-Object {$_.CPU -gt 10}", + expectedOutput: "Get-Process | Where-Object {$_.CPU -gt 10}", + recipeConfig: [ + { + op: "PowerShell -e Encode/Decode", + args: ["Encode"], + }, + { + op: "PowerShell -e Encode/Decode", + args: ["Decode"], + }, + ], + }, + { + name: "PowerShell -e Encode/Decode: round trip with non-ASCII characters", + input: "Write-Host \"café ☕\"", + expectedOutput: "Write-Host \"café ☕\"", + recipeConfig: [ + { + op: "PowerShell -e Encode/Decode", + args: ["Encode"], + }, + { + op: "PowerShell -e Encode/Decode", + args: ["Decode"], + }, + ], + }, + { + name: "PowerShell -e Encode/Decode: decode ignores characters outside the Base64 alphabet", + input: "!!!@@@###", + expectedOutput: "", + recipeConfig: [ + { + op: "PowerShell -e Encode/Decode", + args: ["Decode"], + }, + ], + }, +]);