mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 12:10:49 +00:00
Merge pull request #316 from AikidoSec/powershell-executionpolicy-check-beta
Powershell: check if the executionpolicy allows to run safe-chain
This commit is contained in:
commit
5796f12fa8
9 changed files with 181 additions and 58 deletions
|
|
@ -66,7 +66,6 @@ You can find all available versions on the [releases page](https://github.com/Ai
|
||||||
### Verify the installation
|
### Verify the installation
|
||||||
|
|
||||||
1. **❗Restart your terminal** to start using the Aikido Safe Chain.
|
1. **❗Restart your terminal** to start using the Aikido Safe Chain.
|
||||||
|
|
||||||
- This step is crucial as it ensures that the shell aliases for npm, npx, yarn, pnpm, pnpx, bun, bunx, pip, pip3, poetry, uv and pipx are loaded correctly. If you do not restart your terminal, the aliases will not be available.
|
- This step is crucial as it ensures that the shell aliases for npm, npx, yarn, pnpm, pnpx, bun, bunx, pip, pip3, poetry, uv and pipx are loaded correctly. If you do not restart your terminal, the aliases will not be available.
|
||||||
|
|
||||||
2. **Verify the installation** by running the verification command:
|
2. **Verify the installation** by running the verification command:
|
||||||
|
|
@ -159,7 +158,6 @@ You can control the output from Aikido Safe Chain using the `--safe-chain-loggin
|
||||||
You can set the logging level through multiple sources (in order of priority):
|
You can set the logging level through multiple sources (in order of priority):
|
||||||
|
|
||||||
1. **CLI Argument** (highest priority):
|
1. **CLI Argument** (highest priority):
|
||||||
|
|
||||||
- `--safe-chain-logging=silent` - Suppresses all Aikido Safe Chain output except when malware is blocked. The package manager output is written to stdout as normal, and Safe Chain only writes a short message if it has blocked malware and causes the process to exit.
|
- `--safe-chain-logging=silent` - Suppresses all Aikido Safe Chain output except when malware is blocked. The package manager output is written to stdout as normal, and Safe Chain only writes a short message if it has blocked malware and causes the process to exit.
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
|
|
@ -396,4 +394,4 @@ After setup, all subsequent package manager commands in your CI pipeline will au
|
||||||
|
|
||||||
# Troubleshooting
|
# Troubleshooting
|
||||||
|
|
||||||
Having issues? See the [Troubleshooting Guide](https://github.com/AikidoSec/safe-chain/blob/main/docs/troubleshooting.md) for help with common problems.
|
Having issues? See the [Troubleshooting Guide](https://help.aikido.dev/code-scanning/aikido-malware-scanning/safe-chain-troubleshooting) for help with common problems.
|
||||||
|
|
|
||||||
|
|
@ -104,8 +104,8 @@ if (tool) {
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
} else if (command === "teardown") {
|
} else if (command === "teardown") {
|
||||||
teardownDirectories();
|
|
||||||
teardown();
|
teardown();
|
||||||
|
teardownDirectories();
|
||||||
} else if (command === "setup-ci") {
|
} else if (command === "setup-ci") {
|
||||||
setupCi();
|
setupCi();
|
||||||
} else if (command === "--version" || command === "-v" || command === "-v") {
|
} else if (command === "--version" || command === "-v" || command === "-v") {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ import * as os from "os";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { ECOSYSTEM_JS, ECOSYSTEM_PY } from "../config/settings.js";
|
import { ECOSYSTEM_JS, ECOSYSTEM_PY } from "../config/settings.js";
|
||||||
|
import { safeSpawn } from "../utils/safeSpawn.js";
|
||||||
|
import { ui } from "../environment/userInteraction.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} AikidoTool
|
* @typedef {Object} AikidoTool
|
||||||
|
|
@ -243,3 +245,60 @@ function createFileIfNotExists(filePath) {
|
||||||
|
|
||||||
fs.writeFileSync(filePath, "", "utf-8");
|
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 {Promise<{isValid: boolean, policy: string}>} validation result
|
||||||
|
*/
|
||||||
|
export async function validatePowerShellExecutionPolicy(shellExecutableName) {
|
||||||
|
// Security: Only allow known shell executables
|
||||||
|
const validShells = ["pwsh", "powershell"];
|
||||||
|
if (!validShells.includes(shellExecutableName)) {
|
||||||
|
return { isValid: false, policy: "Unknown" };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// For Windows PowerShell (5.1), clean PSModulePath to avoid conflicts with PowerShell 7 modules
|
||||||
|
// When safe-chain is invoked from PowerShell 7, it sets its module paths to PSModulePath, causing
|
||||||
|
// Windows PowerShell to try loading incompatible PowerShell 7 modules.
|
||||||
|
// Setting the environment to Windows PowerShell's modules fixes this.
|
||||||
|
let spawnOptions;
|
||||||
|
if (shellExecutableName === "powershell") {
|
||||||
|
const userProfile = process.env.USERPROFILE || "";
|
||||||
|
const cleanPSModulePath = [
|
||||||
|
path.join(userProfile, "Documents", "WindowsPowerShell", "Modules"),
|
||||||
|
"C:\\Program Files\\WindowsPowerShell\\Modules",
|
||||||
|
"C:\\WINDOWS\\system32\\WindowsPowerShell\\v1.0\\Modules",
|
||||||
|
].join(";");
|
||||||
|
|
||||||
|
spawnOptions = {
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
PSModulePath: cleanPSModulePath,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
spawnOptions = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const commandResult = await safeSpawn(
|
||||||
|
shellExecutableName,
|
||||||
|
["-Command", "Get-ExecutionPolicy"],
|
||||||
|
spawnOptions,
|
||||||
|
);
|
||||||
|
|
||||||
|
const policy = commandResult.stdout.trim();
|
||||||
|
|
||||||
|
const acceptablePolicies = ["RemoteSigned", "Unrestricted", "Bypass"];
|
||||||
|
return {
|
||||||
|
isValid: acceptablePolicies.includes(policy),
|
||||||
|
policy: policy,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
ui.writeWarning(
|
||||||
|
`An error happened while trying to find the current executionpolicy in powershell: ${err}`,
|
||||||
|
);
|
||||||
|
return { isValid: false, policy: "Unknown" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import { ui } from "../environment/userInteraction.js";
|
import { ui } from "../environment/userInteraction.js";
|
||||||
import { detectShells } from "./shellDetection.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 fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
|
|
@ -26,7 +30,7 @@ if (import.meta.url) {
|
||||||
export async function setup() {
|
export async function setup() {
|
||||||
ui.writeInformation(
|
ui.writeInformation(
|
||||||
chalk.bold("Setting up shell aliases.") +
|
chalk.bold("Setting up shell aliases.") +
|
||||||
` This will wrap safe-chain around ${getPackageManagerList()}.`
|
` This will wrap safe-chain around ${getPackageManagerList()}.`,
|
||||||
);
|
);
|
||||||
ui.emptyLine();
|
ui.emptyLine();
|
||||||
|
|
||||||
|
|
@ -42,12 +46,12 @@ export async function setup() {
|
||||||
ui.writeInformation(
|
ui.writeInformation(
|
||||||
`Detected ${shells.length} supported shell(s): ${shells
|
`Detected ${shells.length} supported shell(s): ${shells
|
||||||
.map((shell) => chalk.bold(shell.name))
|
.map((shell) => chalk.bold(shell.name))
|
||||||
.join(", ")}.`
|
.join(", ")}.`,
|
||||||
);
|
);
|
||||||
|
|
||||||
let updatedCount = 0;
|
let updatedCount = 0;
|
||||||
for (const shell of shells) {
|
for (const shell of shells) {
|
||||||
if (setupShell(shell)) {
|
if (await setupShell(shell)) {
|
||||||
updatedCount++;
|
updatedCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -58,7 +62,7 @@ export async function setup() {
|
||||||
}
|
}
|
||||||
} catch (/** @type {any} */ error) {
|
} catch (/** @type {any} */ error) {
|
||||||
ui.writeError(
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -68,12 +72,12 @@ export async function setup() {
|
||||||
* Calls the setup function for the given shell and reports the result.
|
* Calls the setup function for the given shell and reports the result.
|
||||||
* @param {import("./shellDetection.js").Shell} shell
|
* @param {import("./shellDetection.js").Shell} shell
|
||||||
*/
|
*/
|
||||||
function setupShell(shell) {
|
async function setupShell(shell) {
|
||||||
let success = false;
|
let success = false;
|
||||||
let error;
|
let error;
|
||||||
try {
|
try {
|
||||||
shell.teardown(knownAikidoTools); // First, tear down to prevent duplicate aliases
|
shell.teardown(knownAikidoTools); // First, tear down to prevent duplicate aliases
|
||||||
success = shell.setup(knownAikidoTools);
|
success = await shell.setup(knownAikidoTools);
|
||||||
} catch (/** @type {any} */ err) {
|
} catch (/** @type {any} */ err) {
|
||||||
success = false;
|
success = false;
|
||||||
error = err;
|
error = err;
|
||||||
|
|
@ -82,14 +86,14 @@ function setupShell(shell) {
|
||||||
if (success) {
|
if (success) {
|
||||||
ui.writeInformation(
|
ui.writeInformation(
|
||||||
`${chalk.bold("- " + shell.name + ":")} ${chalk.green(
|
`${chalk.bold("- " + shell.name + ":")} ${chalk.green(
|
||||||
"Setup successful"
|
"Setup successful",
|
||||||
)}`
|
)}`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
ui.writeError(
|
ui.writeError(
|
||||||
`${chalk.bold("- " + shell.name + ":")} ${chalk.red(
|
`${chalk.bold("- " + shell.name + ":")} ${chalk.red(
|
||||||
"Setup failed"
|
"Setup failed",
|
||||||
)}. Please check your ${shell.name} configuration.`
|
)}. Please check your ${shell.name} configuration.`,
|
||||||
);
|
);
|
||||||
if (error) {
|
if (error) {
|
||||||
let message = ` Error: ${error.message}`;
|
let message = ` Error: ${error.message}`;
|
||||||
|
|
@ -115,11 +119,7 @@ function copyStartupFiles() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use absolute path for source
|
// Use absolute path for source
|
||||||
const sourcePath = path.join(
|
const sourcePath = path.join(dirname, "startup-scripts", file);
|
||||||
dirname,
|
|
||||||
"startup-scripts",
|
|
||||||
file
|
|
||||||
);
|
|
||||||
fs.copyFileSync(sourcePath, targetPath);
|
fs.copyFileSync(sourcePath, targetPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import { ui } from "../environment/userInteraction.js";
|
||||||
* @typedef {Object} Shell
|
* @typedef {Object} Shell
|
||||||
* @property {string} name
|
* @property {string} name
|
||||||
* @property {() => boolean} isInstalled
|
* @property {() => boolean} isInstalled
|
||||||
* @property {(tools: import("./helpers.js").AikidoTool[]) => boolean} setup
|
* @property {(tools: import("./helpers.js").AikidoTool[]) => boolean|Promise<boolean>} setup
|
||||||
* @property {(tools: import("./helpers.js").AikidoTool[]) => boolean} teardown
|
* @property {(tools: import("./helpers.js").AikidoTool[]) => boolean} teardown
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
@ -28,7 +28,7 @@ export function detectShells() {
|
||||||
}
|
}
|
||||||
} catch (/** @type {any} */ error) {
|
} catch (/** @type {any} */ error) {
|
||||||
ui.writeError(
|
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 [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import {
|
||||||
addLineToFile,
|
addLineToFile,
|
||||||
doesExecutableExistOnSystem,
|
doesExecutableExistOnSystem,
|
||||||
removeLinesMatchingPattern,
|
removeLinesMatchingPattern,
|
||||||
|
validatePowerShellExecutionPolicy,
|
||||||
} from "../helpers.js";
|
} from "../helpers.js";
|
||||||
import { execSync } from "child_process";
|
import { execSync } from "child_process";
|
||||||
|
|
||||||
|
|
@ -25,25 +26,33 @@ function teardown(tools) {
|
||||||
// Remove any existing alias for the tool
|
// Remove any existing alias for the tool
|
||||||
removeLinesMatchingPattern(
|
removeLinesMatchingPattern(
|
||||||
startupFile,
|
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
|
// Remove the line that sources the safe-chain PowerShell initialization script
|
||||||
removeLinesMatchingPattern(
|
removeLinesMatchingPattern(
|
||||||
startupFile,
|
startupFile,
|
||||||
/^\.\s+["']?\$HOME[/\\].safe-chain[/\\]scripts[/\\]init-pwsh\.ps1["']?/
|
/^\.\s+["']?\$HOME[/\\].safe-chain[/\\]scripts[/\\]init-pwsh\.ps1["']?/,
|
||||||
);
|
);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setup() {
|
async function setup() {
|
||||||
|
const { isValid, policy } =
|
||||||
|
await validatePowerShellExecutionPolicy(executableName);
|
||||||
|
if (!isValid) {
|
||||||
|
throw new Error(
|
||||||
|
`PowerShell execution policy is set to '${policy}', which prevents safe-chain from running.\n -> To fix this, open PowerShell as Administrator and run: Set-ExecutionPolicy -ExecutionPolicy RemoteSigned.\n For more information, see: https://help.aikido.dev/code-scanning/aikido-malware-scanning/safe-chain-troubleshooting#powershell-execution-policy-blocks-scripts-windows`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const startupFile = getStartupFile();
|
const startupFile = getStartupFile();
|
||||||
|
|
||||||
addLineToFile(
|
addLineToFile(
|
||||||
startupFile,
|
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;
|
return true;
|
||||||
|
|
@ -57,7 +66,7 @@ function getStartupFile() {
|
||||||
}).trim();
|
}).trim();
|
||||||
} catch (/** @type {any} */ error) {
|
} catch (/** @type {any} */ error) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Command failed: ${startupFileCommand}. Error: ${error.message}`
|
`Command failed: ${startupFileCommand}. Error: ${error.message}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,20 @@ import { knownAikidoTools } from "../helpers.js";
|
||||||
describe("PowerShell Core shell integration", () => {
|
describe("PowerShell Core shell integration", () => {
|
||||||
let mockStartupFile;
|
let mockStartupFile;
|
||||||
let powershell;
|
let powershell;
|
||||||
|
let executionPolicyResult;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
// Create temporary startup file for testing
|
// Create temporary startup file for testing
|
||||||
mockStartupFile = path.join(
|
mockStartupFile = path.join(
|
||||||
tmpdir(),
|
tmpdir(),
|
||||||
`test-powershell-profile-${Date.now()}.ps1`
|
`test-powershell-profile-${Date.now()}.ps1`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
executionPolicyResult = {
|
||||||
|
isValid: true,
|
||||||
|
policy: "RemoteSigned",
|
||||||
|
};
|
||||||
|
|
||||||
// Mock the helpers module
|
// Mock the helpers module
|
||||||
mock.module("../helpers.js", {
|
mock.module("../helpers.js", {
|
||||||
namedExports: {
|
namedExports: {
|
||||||
|
|
@ -33,6 +39,7 @@ describe("PowerShell Core shell integration", () => {
|
||||||
const filteredLines = lines.filter((line) => !pattern.test(line));
|
const filteredLines = lines.filter((line) => !pattern.test(line));
|
||||||
fs.writeFileSync(filePath, filteredLines.join("\n"), "utf-8");
|
fs.writeFileSync(filePath, filteredLines.join("\n"), "utf-8");
|
||||||
},
|
},
|
||||||
|
validatePowerShellExecutionPolicy: () => executionPolicyResult,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -69,15 +76,15 @@ describe("PowerShell Core shell integration", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("setup", () => {
|
describe("setup", () => {
|
||||||
it("should add init-pwsh.ps1 source line", () => {
|
it("should add init-pwsh.ps1 source line", async () => {
|
||||||
const result = powershell.setup();
|
const result = await powershell.setup();
|
||||||
assert.strictEqual(result, true);
|
assert.strictEqual(result, true);
|
||||||
|
|
||||||
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
assert.ok(
|
assert.ok(
|
||||||
content.includes(
|
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");
|
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
assert.ok(
|
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 ls "));
|
||||||
assert.ok(content.includes("Set-Alias grep "));
|
assert.ok(content.includes("Set-Alias grep "));
|
||||||
|
|
@ -168,26 +175,26 @@ describe("PowerShell Core shell integration", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("integration tests", () => {
|
describe("integration tests", () => {
|
||||||
it("should handle complete setup and teardown cycle", () => {
|
it("should handle complete setup and teardown cycle", async () => {
|
||||||
// Setup
|
// Setup
|
||||||
powershell.setup();
|
await powershell.setup();
|
||||||
let content = fs.readFileSync(mockStartupFile, "utf-8");
|
let content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
assert.ok(
|
assert.ok(
|
||||||
content.includes('. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"')
|
content.includes('. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"'),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Teardown
|
// Teardown
|
||||||
powershell.teardown(knownAikidoTools);
|
powershell.teardown(knownAikidoTools);
|
||||||
content = fs.readFileSync(mockStartupFile, "utf-8");
|
content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
assert.ok(
|
assert.ok(
|
||||||
!content.includes('. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"')
|
!content.includes('. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"'),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle multiple setup calls", () => {
|
it("should handle multiple setup calls", async () => {
|
||||||
powershell.setup();
|
await powershell.setup();
|
||||||
powershell.teardown(knownAikidoTools);
|
powershell.teardown(knownAikidoTools);
|
||||||
powershell.setup();
|
await powershell.setup();
|
||||||
|
|
||||||
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
const sourceMatches = (
|
const sourceMatches = (
|
||||||
|
|
@ -197,4 +204,21 @@ describe("PowerShell Core shell integration", () => {
|
||||||
assert.strictEqual(sourceMatches, 1, "Should not duplicate source lines");
|
assert.strictEqual(sourceMatches, 1, "Should not duplicate source lines");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("execution policy", () => {
|
||||||
|
it(`should throw for restricted policies`, async () => {
|
||||||
|
executionPolicyResult = {
|
||||||
|
isValid: false,
|
||||||
|
policy: "Restricted",
|
||||||
|
};
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
() => powershell.setup(),
|
||||||
|
(err) =>
|
||||||
|
err.message.startsWith(
|
||||||
|
"PowerShell execution policy is set to 'Restricted'",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import {
|
||||||
addLineToFile,
|
addLineToFile,
|
||||||
doesExecutableExistOnSystem,
|
doesExecutableExistOnSystem,
|
||||||
removeLinesMatchingPattern,
|
removeLinesMatchingPattern,
|
||||||
|
validatePowerShellExecutionPolicy,
|
||||||
} from "../helpers.js";
|
} from "../helpers.js";
|
||||||
import { execSync } from "child_process";
|
import { execSync } from "child_process";
|
||||||
|
|
||||||
|
|
@ -25,25 +26,33 @@ function teardown(tools) {
|
||||||
// Remove any existing alias for the tool
|
// Remove any existing alias for the tool
|
||||||
removeLinesMatchingPattern(
|
removeLinesMatchingPattern(
|
||||||
startupFile,
|
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
|
// Remove the line that sources the safe-chain PowerShell initialization script
|
||||||
removeLinesMatchingPattern(
|
removeLinesMatchingPattern(
|
||||||
startupFile,
|
startupFile,
|
||||||
/^\.\s+["']?\$HOME[/\\].safe-chain[/\\]scripts[/\\]init-pwsh\.ps1["']?/
|
/^\.\s+["']?\$HOME[/\\].safe-chain[/\\]scripts[/\\]init-pwsh\.ps1["']?/,
|
||||||
);
|
);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setup() {
|
async function setup() {
|
||||||
|
const { isValid, policy } =
|
||||||
|
await validatePowerShellExecutionPolicy(executableName);
|
||||||
|
if (!isValid) {
|
||||||
|
throw new Error(
|
||||||
|
`PowerShell execution policy is set to '${policy}', which prevents safe-chain from running.\n -> To fix this, open PowerShell as Administrator and run: Set-ExecutionPolicy -ExecutionPolicy RemoteSigned.\n For more information, see: https://help.aikido.dev/code-scanning/aikido-malware-scanning/safe-chain-troubleshooting#powershell-execution-policy-blocks-scripts-windows`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const startupFile = getStartupFile();
|
const startupFile = getStartupFile();
|
||||||
|
|
||||||
addLineToFile(
|
addLineToFile(
|
||||||
startupFile,
|
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;
|
return true;
|
||||||
|
|
@ -57,7 +66,7 @@ function getStartupFile() {
|
||||||
}).trim();
|
}).trim();
|
||||||
} catch (/** @type {any} */ error) {
|
} catch (/** @type {any} */ error) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Command failed: ${startupFileCommand}. Error: ${error.message}`
|
`Command failed: ${startupFileCommand}. Error: ${error.message}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,20 @@ import { knownAikidoTools } from "../helpers.js";
|
||||||
describe("Windows PowerShell shell integration", () => {
|
describe("Windows PowerShell shell integration", () => {
|
||||||
let mockStartupFile;
|
let mockStartupFile;
|
||||||
let windowsPowershell;
|
let windowsPowershell;
|
||||||
|
let executionPolicyResult;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
// Create temporary startup file for testing
|
// Create temporary startup file for testing
|
||||||
mockStartupFile = path.join(
|
mockStartupFile = path.join(
|
||||||
tmpdir(),
|
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 the helpers module
|
||||||
mock.module("../helpers.js", {
|
mock.module("../helpers.js", {
|
||||||
namedExports: {
|
namedExports: {
|
||||||
|
|
@ -33,6 +39,7 @@ describe("Windows PowerShell shell integration", () => {
|
||||||
const filteredLines = lines.filter((line) => !pattern.test(line));
|
const filteredLines = lines.filter((line) => !pattern.test(line));
|
||||||
fs.writeFileSync(filePath, filteredLines.join("\n"), "utf-8");
|
fs.writeFileSync(filePath, filteredLines.join("\n"), "utf-8");
|
||||||
},
|
},
|
||||||
|
validatePowerShellExecutionPolicy: () => executionPolicyResult,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -69,15 +76,15 @@ describe("Windows PowerShell shell integration", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("setup", () => {
|
describe("setup", () => {
|
||||||
it("should add init-pwsh.ps1 source line", () => {
|
it("should add init-pwsh.ps1 source line", async () => {
|
||||||
const result = windowsPowershell.setup();
|
const result = await windowsPowershell.setup();
|
||||||
assert.strictEqual(result, true);
|
assert.strictEqual(result, true);
|
||||||
|
|
||||||
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
assert.ok(
|
assert.ok(
|
||||||
content.includes(
|
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");
|
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
assert.ok(
|
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 ls "));
|
||||||
assert.ok(content.includes("Set-Alias grep "));
|
assert.ok(content.includes("Set-Alias grep "));
|
||||||
|
|
@ -168,26 +175,26 @@ describe("Windows PowerShell shell integration", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("integration tests", () => {
|
describe("integration tests", () => {
|
||||||
it("should handle complete setup and teardown cycle", () => {
|
it("should handle complete setup and teardown cycle", async () => {
|
||||||
// Setup
|
// Setup
|
||||||
windowsPowershell.setup();
|
await windowsPowershell.setup();
|
||||||
let content = fs.readFileSync(mockStartupFile, "utf-8");
|
let content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
assert.ok(
|
assert.ok(
|
||||||
content.includes('. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"')
|
content.includes('. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"'),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Teardown
|
// Teardown
|
||||||
windowsPowershell.teardown(knownAikidoTools);
|
windowsPowershell.teardown(knownAikidoTools);
|
||||||
content = fs.readFileSync(mockStartupFile, "utf-8");
|
content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
assert.ok(
|
assert.ok(
|
||||||
!content.includes('. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"')
|
!content.includes('. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"'),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle multiple setup calls", () => {
|
it("should handle multiple setup calls", async () => {
|
||||||
windowsPowershell.setup();
|
await windowsPowershell.setup();
|
||||||
windowsPowershell.teardown(knownAikidoTools);
|
windowsPowershell.teardown(knownAikidoTools);
|
||||||
windowsPowershell.setup();
|
await windowsPowershell.setup();
|
||||||
|
|
||||||
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
||||||
const sourceMatches = (
|
const sourceMatches = (
|
||||||
|
|
@ -197,4 +204,21 @@ describe("Windows PowerShell shell integration", () => {
|
||||||
assert.strictEqual(sourceMatches, 1, "Should not duplicate source lines");
|
assert.strictEqual(sourceMatches, 1, "Should not duplicate source lines");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("execution policy", () => {
|
||||||
|
it(`should throw for restricted policies`, async () => {
|
||||||
|
executionPolicyResult = {
|
||||||
|
isValid: false,
|
||||||
|
policy: "Restricted",
|
||||||
|
};
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
() => windowsPowershell.setup(),
|
||||||
|
(err) =>
|
||||||
|
err.message.startsWith(
|
||||||
|
"PowerShell execution policy is set to 'Restricted'",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue