diff --git a/packages/safe-chain/src/shell-integration/helpers.js b/packages/safe-chain/src/shell-integration/helpers.js index 3e71d71..17b527c 100644 --- a/packages/safe-chain/src/shell-integration/helpers.js +++ b/packages/safe-chain/src/shell-integration/helpers.js @@ -1,4 +1,4 @@ -import { spawnSync } from "child_process"; +import { spawnSync, execSync } from "child_process"; import * as os from "os"; import fs from "fs"; import path from "path"; @@ -243,3 +243,34 @@ function createFileIfNotExists(filePath) { fs.writeFileSync(filePath, "", "utf-8"); } + +/** + * 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 + */ +export function validatePowerShellExecutionPolicy(shellExecutableName) { + // Security: Only allow known shell executables + const validShells = ["pwsh", "powershell"]; + if (!validShells.includes(shellExecutableName)) { + return { isValid: false, policy: "Unknown" }; + } + + try { + // Security: Use literal command string, no interpolation + const policy = execSync("Get-ExecutionPolicy", { + encoding: "utf8", + shell: shellExecutableName, + timeout: 5000, // 5 second timeout + }).trim(); + + const acceptablePolicies = ["RemoteSigned", "Unrestricted", "Bypass"]; + return { + isValid: acceptablePolicies.includes(policy), + policy: policy, + }; + } catch (/** @type {any} */ error) { + // If we can't check the policy, return false to be safe + return { isValid: false, policy: "Unknown" }; + } +} 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 8cec258..b26a3ff 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/powershell.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/powershell.js @@ -2,6 +2,7 @@ import { addLineToFile, doesExecutableExistOnSystem, removeLinesMatchingPattern, + validatePowerShellExecutionPolicy, } from "../helpers.js"; import { execSync } from "child_process"; @@ -39,6 +40,16 @@ function teardown(tools) { } function setup() { + // Check execution policy + const { isValid, policy } = 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` + ); + } + const startupFile = getStartupFile(); addLineToFile( 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 3a15376..5c93f45 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 @@ -8,14 +8,20 @@ import { knownAikidoTools } from "../helpers.js"; describe("PowerShell Core shell integration", () => { let mockStartupFile; let powershell; + let executionPolicyResult; beforeEach(async () => { // Create temporary startup file for testing mockStartupFile = path.join( tmpdir(), - `test-powershell-profile-${Date.now()}.ps1` + `test-powershell-profile-${Date.now()}.ps1`, ); + executionPolicyResult = { + isValid: true, + policy: "RemoteSigned", + }; + // Mock the helpers module mock.module("../helpers.js", { namedExports: { @@ -33,6 +39,7 @@ describe("PowerShell Core shell integration", () => { const filteredLines = lines.filter((line) => !pattern.test(line)); fs.writeFileSync(filePath, filteredLines.join("\n"), "utf-8"); }, + validatePowerShellExecutionPolicy: () => executionPolicyResult, }, }); @@ -76,8 +83,8 @@ describe("PowerShell Core shell integration", () => { const content = fs.readFileSync(mockStartupFile, "utf-8"); assert.ok( content.includes( - '. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1" # Safe-chain PowerShell initialization script' - ) + '. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1" # Safe-chain PowerShell initialization script', + ), ); }); }); @@ -98,7 +105,7 @@ describe("PowerShell Core shell integration", () => { const content = fs.readFileSync(mockStartupFile, "utf-8"); assert.ok( - !content.includes('. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"') + !content.includes('. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"'), ); assert.ok(content.includes("Set-Alias ls ")); assert.ok(content.includes("Set-Alias grep ")); @@ -173,14 +180,14 @@ describe("PowerShell Core shell integration", () => { powershell.setup(); let content = fs.readFileSync(mockStartupFile, "utf-8"); assert.ok( - content.includes('. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"') + content.includes('. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"'), ); // Teardown powershell.teardown(knownAikidoTools); content = fs.readFileSync(mockStartupFile, "utf-8"); assert.ok( - !content.includes('. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"') + !content.includes('. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"'), ); }); @@ -197,4 +204,21 @@ describe("PowerShell Core shell integration", () => { assert.strictEqual(sourceMatches, 1, "Should not duplicate source lines"); }); }); + + describe("execution policy", () => { + it(`should throw for restricted policies`, () => { + executionPolicyResult = { + isValid: false, + policy: "Restricted", + }; + + assert.throws( + () => powershell.setup(), + (err) => + err.message.startsWith( + "PowerShell execution policy is set to 'Restricted'", + ), + ); + }); + }); }); 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 e554a32..cb07e0f 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/windowsPowershell.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/windowsPowershell.js @@ -2,6 +2,7 @@ import { addLineToFile, doesExecutableExistOnSystem, removeLinesMatchingPattern, + validatePowerShellExecutionPolicy, } from "../helpers.js"; import { execSync } from "child_process"; @@ -39,6 +40,16 @@ function teardown(tools) { } function setup() { + // Check execution policy + const { isValid, policy } = 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` + ); + } + const startupFile = getStartupFile(); addLineToFile( 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 c201c60..9a3a696 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 @@ -8,14 +8,20 @@ import { knownAikidoTools } from "../helpers.js"; describe("Windows PowerShell shell integration", () => { let mockStartupFile; let windowsPowershell; + let executionPolicyResult; beforeEach(async () => { // Create temporary startup file for testing mockStartupFile = path.join( tmpdir(), - `test-windows-powershell-profile-${Date.now()}.ps1` + `test-windows-powershell-profile-${Date.now()}.ps1`, ); + executionPolicyResult = { + isValid: true, + policy: "RemoteSigned", + }; + // Mock the helpers module mock.module("../helpers.js", { namedExports: { @@ -33,6 +39,7 @@ describe("Windows PowerShell shell integration", () => { const filteredLines = lines.filter((line) => !pattern.test(line)); fs.writeFileSync(filePath, filteredLines.join("\n"), "utf-8"); }, + validatePowerShellExecutionPolicy: () => executionPolicyResult, }, }); @@ -76,8 +83,8 @@ describe("Windows PowerShell shell integration", () => { const content = fs.readFileSync(mockStartupFile, "utf-8"); assert.ok( content.includes( - '. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1" # Safe-chain PowerShell initialization script' - ) + '. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1" # Safe-chain PowerShell initialization script', + ), ); }); }); @@ -98,7 +105,7 @@ describe("Windows PowerShell shell integration", () => { const content = fs.readFileSync(mockStartupFile, "utf-8"); assert.ok( - !content.includes('. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"') + !content.includes('. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"'), ); assert.ok(content.includes("Set-Alias ls ")); assert.ok(content.includes("Set-Alias grep ")); @@ -173,14 +180,14 @@ describe("Windows PowerShell shell integration", () => { windowsPowershell.setup(); let content = fs.readFileSync(mockStartupFile, "utf-8"); assert.ok( - content.includes('. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"') + content.includes('. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"'), ); // Teardown windowsPowershell.teardown(knownAikidoTools); content = fs.readFileSync(mockStartupFile, "utf-8"); assert.ok( - !content.includes('. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"') + !content.includes('. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"'), ); }); @@ -197,4 +204,21 @@ describe("Windows PowerShell shell integration", () => { assert.strictEqual(sourceMatches, 1, "Should not duplicate source lines"); }); }); + + describe("execution policy", () => { + it(`should throw for restricted policies`, () => { + executionPolicyResult = { + isValid: false, + policy: "Restricted", + }; + + assert.throws( + () => windowsPowershell.setup(), + (err) => + err.message.startsWith( + "PowerShell execution policy is set to 'Restricted'", + ), + ); + }); + }); });