From aa461b27c36b1bbcb13e177abafde2740b6bbd0e Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Thu, 5 Feb 2026 10:24:28 +0100 Subject: [PATCH] Use safeSpawn --- .../src/shell-integration/helpers.js | 23 ++++++------- .../safe-chain/src/shell-integration/setup.js | 32 +++++++++---------- .../src/shell-integration/shellDetection.js | 4 +-- .../supported-shells/powershell.js | 15 +++++---- .../supported-shells/powershell.spec.js | 18 +++++------ .../supported-shells/windowsPowershell.js | 15 +++++---- .../windowsPowershell.spec.js | 18 +++++------ 7 files changed, 62 insertions(+), 63 deletions(-) diff --git a/packages/safe-chain/src/shell-integration/helpers.js b/packages/safe-chain/src/shell-integration/helpers.js index d243123..a3d2f5e 100644 --- a/packages/safe-chain/src/shell-integration/helpers.js +++ b/packages/safe-chain/src/shell-integration/helpers.js @@ -1,8 +1,9 @@ -import { spawnSync, execSync } from "child_process"; +import { spawnSync } from "child_process"; import * as os from "os"; import fs from "fs"; import path from "path"; import { ECOSYSTEM_JS, ECOSYSTEM_PY } from "../config/settings.js"; +import { safeSpawn } from "../utils/safeSpawn.js"; /** * @typedef {Object} AikidoTool @@ -247,9 +248,9 @@ function createFileIfNotExists(filePath) { /** * Checks if PowerShell execution policy allows script execution * @param {string} shellExecutableName - The name of the PowerShell executable ("pwsh" or "powershell") - * @returns {{isValid: boolean, policy: string}} validation result + * @returns {Promise<{isValid: boolean, policy: string}>} validation result */ -export function validatePowerShellExecutionPolicy(shellExecutableName) { +export async function validatePowerShellExecutionPolicy(shellExecutableName) { // Security: Only allow known shell executables const validShells = ["pwsh", "powershell"]; if (!validShells.includes(shellExecutableName)) { @@ -257,16 +258,12 @@ export function validatePowerShellExecutionPolicy(shellExecutableName) { } try { - // Security: Use literal command string, no interpolation - // Import the Security module first - works for both powershell.exe and pwsh.exe - const policy = execSync( - "Import-Module Microsoft.PowerShell.Security; Get-ExecutionPolicy", - { - encoding: "utf8", - shell: shellExecutableName, - timeout: 5000, // 5 second timeout - } - ).trim(); + const commandResult = await safeSpawn(shellExecutableName, [ + "-Command", + "Get-ExecutionPolicy", + ]); + + const policy = commandResult.stdout.trim(); const acceptablePolicies = ["RemoteSigned", "Unrestricted", "Bypass"]; return { diff --git a/packages/safe-chain/src/shell-integration/setup.js b/packages/safe-chain/src/shell-integration/setup.js index 7e64c0b..4138db6 100644 --- a/packages/safe-chain/src/shell-integration/setup.js +++ b/packages/safe-chain/src/shell-integration/setup.js @@ -1,7 +1,11 @@ import chalk from "chalk"; import { ui } from "../environment/userInteraction.js"; import { detectShells } from "./shellDetection.js"; -import { knownAikidoTools, getPackageManagerList, getScriptsDir } from "./helpers.js"; +import { + knownAikidoTools, + getPackageManagerList, + getScriptsDir, +} from "./helpers.js"; import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; @@ -26,7 +30,7 @@ if (import.meta.url) { export async function setup() { ui.writeInformation( chalk.bold("Setting up shell aliases.") + - ` This will wrap safe-chain around ${getPackageManagerList()}.` + ` This will wrap safe-chain around ${getPackageManagerList()}.`, ); ui.emptyLine(); @@ -42,12 +46,12 @@ export async function setup() { ui.writeInformation( `Detected ${shells.length} supported shell(s): ${shells .map((shell) => chalk.bold(shell.name)) - .join(", ")}.` + .join(", ")}.`, ); let updatedCount = 0; for (const shell of shells) { - if (setupShell(shell)) { + if (await setupShell(shell)) { updatedCount++; } } @@ -58,7 +62,7 @@ export async function setup() { } } catch (/** @type {any} */ error) { ui.writeError( - `Failed to set up shell aliases: ${error.message}. Please check your shell configuration.` + `Failed to set up shell aliases: ${error.message}. Please check your shell configuration.`, ); return; } @@ -68,12 +72,12 @@ export async function setup() { * Calls the setup function for the given shell and reports the result. * @param {import("./shellDetection.js").Shell} shell */ -function setupShell(shell) { +async function setupShell(shell) { let success = false; let error; try { shell.teardown(knownAikidoTools); // First, tear down to prevent duplicate aliases - success = shell.setup(knownAikidoTools); + success = await shell.setup(knownAikidoTools); } catch (/** @type {any} */ err) { success = false; error = err; @@ -82,14 +86,14 @@ function setupShell(shell) { if (success) { ui.writeInformation( `${chalk.bold("- " + shell.name + ":")} ${chalk.green( - "Setup successful" - )}` + "Setup successful", + )}`, ); } else { ui.writeError( `${chalk.bold("- " + shell.name + ":")} ${chalk.red( - "Setup failed" - )}. Please check your ${shell.name} configuration.` + "Setup failed", + )}. Please check your ${shell.name} configuration.`, ); if (error) { let message = ` Error: ${error.message}`; @@ -115,11 +119,7 @@ function copyStartupFiles() { } // Use absolute path for source - const sourcePath = path.join( - dirname, - "startup-scripts", - file - ); + const sourcePath = path.join(dirname, "startup-scripts", file); fs.copyFileSync(sourcePath, targetPath); } } diff --git a/packages/safe-chain/src/shell-integration/shellDetection.js b/packages/safe-chain/src/shell-integration/shellDetection.js index 9e0f110..996125c 100644 --- a/packages/safe-chain/src/shell-integration/shellDetection.js +++ b/packages/safe-chain/src/shell-integration/shellDetection.js @@ -9,7 +9,7 @@ import { ui } from "../environment/userInteraction.js"; * @typedef {Object} Shell * @property {string} name * @property {() => boolean} isInstalled - * @property {(tools: import("./helpers.js").AikidoTool[]) => boolean} setup + * @property {(tools: import("./helpers.js").AikidoTool[]) => boolean|Promise} setup * @property {(tools: import("./helpers.js").AikidoTool[]) => boolean} teardown */ @@ -28,7 +28,7 @@ export function detectShells() { } } catch (/** @type {any} */ error) { ui.writeError( - `We were not able to detect which shells are installed on your system. Please check your shell configuration. Error: ${error.message}` + `We were not able to detect which shells are installed on your system. Please check your shell configuration. Error: ${error.message}`, ); return []; } diff --git a/packages/safe-chain/src/shell-integration/supported-shells/powershell.js b/packages/safe-chain/src/shell-integration/supported-shells/powershell.js index b26a3ff..a169915 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/powershell.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/powershell.js @@ -26,27 +26,28 @@ function teardown(tools) { // Remove any existing alias for the tool removeLinesMatchingPattern( startupFile, - new RegExp(`^Set-Alias\\s+${tool}\\s+`) + new RegExp(`^Set-Alias\\s+${tool}\\s+`), ); } // Remove the line that sources the safe-chain PowerShell initialization script removeLinesMatchingPattern( startupFile, - /^\.\s+["']?\$HOME[/\\].safe-chain[/\\]scripts[/\\]init-pwsh\.ps1["']?/ + /^\.\s+["']?\$HOME[/\\].safe-chain[/\\]scripts[/\\]init-pwsh\.ps1["']?/, ); return true; } -function setup() { +async function setup() { // Check execution policy - const { isValid, policy } = validatePowerShellExecutionPolicy(executableName); + const { isValid, policy } = + await validatePowerShellExecutionPolicy(executableName); if (!isValid) { throw new Error( `PowerShell execution policy is set to '${policy}', which prevents safe-chain from running. ` + `To fix this, open PowerShell as Administrator and run: Set-ExecutionPolicy -ExecutionPolicy RemoteSigned. ` + - `For more information, see: https://github.com/AikidoSec/safe-chain/blob/main/docs/troubleshooting.md#powershell-execution-policy-blocks-scripts-windows` + `For more information, see: https://github.com/AikidoSec/safe-chain/blob/main/docs/troubleshooting.md#powershell-execution-policy-blocks-scripts-windows`, ); } @@ -54,7 +55,7 @@ function setup() { addLineToFile( startupFile, - `. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1" # Safe-chain PowerShell initialization script` + `. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1" # Safe-chain PowerShell initialization script`, ); return true; @@ -68,7 +69,7 @@ function getStartupFile() { }).trim(); } catch (/** @type {any} */ error) { throw new Error( - `Command failed: ${startupFileCommand}. Error: ${error.message}` + `Command failed: ${startupFileCommand}. Error: ${error.message}`, ); } } diff --git a/packages/safe-chain/src/shell-integration/supported-shells/powershell.spec.js b/packages/safe-chain/src/shell-integration/supported-shells/powershell.spec.js index 5c93f45..de2c14b 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/powershell.spec.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/powershell.spec.js @@ -76,8 +76,8 @@ describe("PowerShell Core shell integration", () => { }); describe("setup", () => { - it("should add init-pwsh.ps1 source line", () => { - const result = powershell.setup(); + it("should add init-pwsh.ps1 source line", async () => { + const result = await powershell.setup(); assert.strictEqual(result, true); const content = fs.readFileSync(mockStartupFile, "utf-8"); @@ -175,9 +175,9 @@ describe("PowerShell Core shell integration", () => { }); describe("integration tests", () => { - it("should handle complete setup and teardown cycle", () => { + it("should handle complete setup and teardown cycle", async () => { // Setup - powershell.setup(); + await powershell.setup(); let content = fs.readFileSync(mockStartupFile, "utf-8"); assert.ok( content.includes('. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"'), @@ -191,10 +191,10 @@ describe("PowerShell Core shell integration", () => { ); }); - it("should handle multiple setup calls", () => { - powershell.setup(); + it("should handle multiple setup calls", async () => { + await powershell.setup(); powershell.teardown(knownAikidoTools); - powershell.setup(); + await powershell.setup(); const content = fs.readFileSync(mockStartupFile, "utf-8"); const sourceMatches = ( @@ -206,13 +206,13 @@ describe("PowerShell Core shell integration", () => { }); describe("execution policy", () => { - it(`should throw for restricted policies`, () => { + it(`should throw for restricted policies`, async () => { executionPolicyResult = { isValid: false, policy: "Restricted", }; - assert.throws( + await assert.rejects( () => powershell.setup(), (err) => err.message.startsWith( diff --git a/packages/safe-chain/src/shell-integration/supported-shells/windowsPowershell.js b/packages/safe-chain/src/shell-integration/supported-shells/windowsPowershell.js index cb07e0f..acf0830 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/windowsPowershell.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/windowsPowershell.js @@ -26,27 +26,28 @@ function teardown(tools) { // Remove any existing alias for the tool removeLinesMatchingPattern( startupFile, - new RegExp(`^Set-Alias\\s+${tool}\\s+`) + new RegExp(`^Set-Alias\\s+${tool}\\s+`), ); } // Remove the line that sources the safe-chain PowerShell initialization script removeLinesMatchingPattern( startupFile, - /^\.\s+["']?\$HOME[/\\].safe-chain[/\\]scripts[/\\]init-pwsh\.ps1["']?/ + /^\.\s+["']?\$HOME[/\\].safe-chain[/\\]scripts[/\\]init-pwsh\.ps1["']?/, ); return true; } -function setup() { +async function setup() { // Check execution policy - const { isValid, policy } = validatePowerShellExecutionPolicy(executableName); + const { isValid, policy } = + await validatePowerShellExecutionPolicy(executableName); if (!isValid) { throw new Error( `PowerShell execution policy is set to '${policy}', which prevents safe-chain from running. ` + `To fix this, open PowerShell as Administrator and run: Set-ExecutionPolicy -ExecutionPolicy RemoteSigned. ` + - `For more information, see: https://github.com/AikidoSec/safe-chain/blob/main/docs/troubleshooting.md#powershell-execution-policy-blocks-scripts-windows` + `For more information, see: https://github.com/AikidoSec/safe-chain/blob/main/docs/troubleshooting.md#powershell-execution-policy-blocks-scripts-windows`, ); } @@ -54,7 +55,7 @@ function setup() { addLineToFile( startupFile, - `. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1" # Safe-chain PowerShell initialization script` + `. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1" # Safe-chain PowerShell initialization script`, ); return true; @@ -68,7 +69,7 @@ function getStartupFile() { }).trim(); } catch (/** @type {any} */ error) { throw new Error( - `Command failed: ${startupFileCommand}. Error: ${error.message}` + `Command failed: ${startupFileCommand}. Error: ${error.message}`, ); } } diff --git a/packages/safe-chain/src/shell-integration/supported-shells/windowsPowershell.spec.js b/packages/safe-chain/src/shell-integration/supported-shells/windowsPowershell.spec.js index 9a3a696..561d0d4 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/windowsPowershell.spec.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/windowsPowershell.spec.js @@ -76,8 +76,8 @@ describe("Windows PowerShell shell integration", () => { }); describe("setup", () => { - it("should add init-pwsh.ps1 source line", () => { - const result = windowsPowershell.setup(); + it("should add init-pwsh.ps1 source line", async () => { + const result = await windowsPowershell.setup(); assert.strictEqual(result, true); const content = fs.readFileSync(mockStartupFile, "utf-8"); @@ -175,9 +175,9 @@ describe("Windows PowerShell shell integration", () => { }); describe("integration tests", () => { - it("should handle complete setup and teardown cycle", () => { + it("should handle complete setup and teardown cycle", async () => { // Setup - windowsPowershell.setup(); + await windowsPowershell.setup(); let content = fs.readFileSync(mockStartupFile, "utf-8"); assert.ok( content.includes('. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"'), @@ -191,10 +191,10 @@ describe("Windows PowerShell shell integration", () => { ); }); - it("should handle multiple setup calls", () => { - windowsPowershell.setup(); + it("should handle multiple setup calls", async () => { + await windowsPowershell.setup(); windowsPowershell.teardown(knownAikidoTools); - windowsPowershell.setup(); + await windowsPowershell.setup(); const content = fs.readFileSync(mockStartupFile, "utf-8"); const sourceMatches = ( @@ -206,13 +206,13 @@ describe("Windows PowerShell shell integration", () => { }); describe("execution policy", () => { - it(`should throw for restricted policies`, () => { + it(`should throw for restricted policies`, async () => { executionPolicyResult = { isValid: false, policy: "Restricted", }; - assert.throws( + await assert.rejects( () => windowsPowershell.setup(), (err) => err.message.startsWith(