From b590aa6e6932d7e7d4928d125d1577a5b5093ac4 Mon Sep 17 00:00:00 2001 From: Neoreo Date: Thu, 30 Jul 2026 16:14:06 +0200 Subject: [PATCH 1/3] Add PowerShell Encode/Decode operation --- src/core/config/Categories.json | 1 + .../operations/PowerShellEncodeDecode.mjs | 71 +++++++++++++++++++ .../tests/PowerShellEncodeDecode.mjs | 60 ++++++++++++++++ 3 files changed, 132 insertions(+) create mode 100644 src/core/operations/PowerShellEncodeDecode.mjs create mode 100644 tests/operations/tests/PowerShellEncodeDecode.mjs 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..58e64534 --- /dev/null +++ b/src/core/operations/PowerShellEncodeDecode.mjs @@ -0,0 +1,71 @@ +/** + * @author neoreo + * @copyright Crown Copyright 2026 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import OperationError from "../errors/OperationError.mjs"; +import cptable from "codepage"; +import {toBase64, fromBase64} from "../lib/Base64.mjs"; + +// PowerShell -EncodedCommand uses UTF-16LE (code page 1200) +const UTF16LE = 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(UTF16LE, input); + return toBase64(new Uint8Array(encoded).buffer); + } else if (mode === "Decode") { + const bytes = fromBase64(input, "A-Za-z0-9+/=", "byteArray"); + return cptable.utils.decode(UTF16LE, new Uint8Array(bytes)); + } else { + throw new OperationError("Invalid mode"); + } + } + +} + +export default PowerShellEncodeDecode; diff --git a/tests/operations/tests/PowerShellEncodeDecode.mjs b/tests/operations/tests/PowerShellEncodeDecode.mjs new file mode 100644 index 00000000..de2a05dd --- /dev/null +++ b/tests/operations/tests/PowerShellEncodeDecode.mjs @@ -0,0 +1,60 @@ +/** + * 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"], + }, + ], + }, +]); From 47b9ffb393cddfd9872c0b2f23481424347467b7 Mon Sep 17 00:00:00 2001 From: Neoreo Date: Thu, 30 Jul 2026 16:30:12 +0200 Subject: [PATCH 2/3] Fix: typo in test case --- tests/node/tests/nodeApi.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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", () => { From a65044004b27414506db0ec6067ed1355877fbd1 Mon Sep 17 00:00:00 2001 From: Neoreo Date: Thu, 30 Jul 2026 16:41:54 +0200 Subject: [PATCH 3/3] Address review: naming, arg validation, test coverage, help count --- .../operations/PowerShellEncodeDecode.mjs | 12 +++------ .../tests/PowerShellEncodeDecode.mjs | 26 +++++++++++++++++++ 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/core/operations/PowerShellEncodeDecode.mjs b/src/core/operations/PowerShellEncodeDecode.mjs index 58e64534..690d3cf2 100644 --- a/src/core/operations/PowerShellEncodeDecode.mjs +++ b/src/core/operations/PowerShellEncodeDecode.mjs @@ -5,12 +5,11 @@ */ import Operation from "../Operation.mjs"; -import OperationError from "../errors/OperationError.mjs"; import cptable from "codepage"; import {toBase64, fromBase64} from "../lib/Base64.mjs"; // PowerShell -EncodedCommand uses UTF-16LE (code page 1200) -const UTF16LE = 1200; +const UTF_16LE = 1200; /** * PowerShell -e Encode/Decode operation @@ -56,14 +55,11 @@ class PowerShellEncodeDecode extends Operation { const [mode] = args; if (mode === "Encode") { - const encoded = cptable.utils.encode(UTF16LE, input); + const encoded = cptable.utils.encode(UTF_16LE, input); return toBase64(new Uint8Array(encoded).buffer); - } else if (mode === "Decode") { - const bytes = fromBase64(input, "A-Za-z0-9+/=", "byteArray"); - return cptable.utils.decode(UTF16LE, new Uint8Array(bytes)); - } else { - throw new OperationError("Invalid mode"); } + const bytes = fromBase64(input, "A-Za-z0-9+/=", "byteArray"); + return cptable.utils.decode(UTF_16LE, new Uint8Array(bytes)); } } diff --git a/tests/operations/tests/PowerShellEncodeDecode.mjs b/tests/operations/tests/PowerShellEncodeDecode.mjs index de2a05dd..98922b85 100644 --- a/tests/operations/tests/PowerShellEncodeDecode.mjs +++ b/tests/operations/tests/PowerShellEncodeDecode.mjs @@ -57,4 +57,30 @@ TestRegister.addTests([ }, ], }, + { + 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"], + }, + ], + }, ]);