@aikidosec/safe-chain 1.4.2 → 1.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,203 @@
1
+ import { tmpdir } from "os";
2
+ import { unlinkSync } from "fs";
3
+ import { join } from "path";
4
+ import { execSync } from "child_process";
5
+ import { ui } from "../environment/userInteraction.js";
6
+ import { printVerboseAndSafeSpawn, safeSpawn } from "../utils/safeSpawn.js";
7
+ import { downloadAgentToFile, getAgentVersion } from "./downloadAgent.js";
8
+
9
+ const WINDOWS_SERVICE_NAME = "SafeChainUltimate";
10
+ const WINDOWS_APP_NAME = "SafeChain Ultimate";
11
+
12
+ export async function uninstallOnWindows() {
13
+ if (!(await requireAdminPrivileges())) {
14
+ return;
15
+ }
16
+
17
+ ui.emptyLine();
18
+
19
+ const productCode = getInstalledProductCode();
20
+ if (!productCode) {
21
+ ui.writeInformation("SafeChain Ultimate is not installed.");
22
+ return;
23
+ }
24
+
25
+ await stopServiceIfRunning();
26
+
27
+ ui.writeInformation("🗑️ Uninstalling SafeChain Ultimate...");
28
+ await uninstallByProductCode(productCode);
29
+
30
+ ui.emptyLine();
31
+ ui.writeInformation("✅ SafeChain Ultimate has been uninstalled.");
32
+ ui.emptyLine();
33
+ }
34
+
35
+ export async function installOnWindows() {
36
+ if (!(await requireAdminPrivileges())) {
37
+ return;
38
+ }
39
+
40
+ const msiPath = join(tmpdir(), `SafeChainUltimate-${Date.now()}.msi`);
41
+
42
+ ui.emptyLine();
43
+ ui.writeInformation(`📥 Downloading SafeChain Ultimate ${getAgentVersion()}`);
44
+ ui.writeVerbose(`Destination: ${msiPath}`);
45
+
46
+ const result = await downloadAgentToFile(msiPath);
47
+ if (!result) {
48
+ ui.writeError("No download available for this platform/architecture.");
49
+ return;
50
+ }
51
+
52
+ try {
53
+ ui.emptyLine();
54
+ await stopServiceIfRunning();
55
+ await uninstallIfInstalled();
56
+
57
+ ui.writeInformation("⚙️ Installing SafeChain Ultimate...");
58
+ await runMsiInstaller(msiPath);
59
+
60
+ ui.emptyLine();
61
+ ui.writeInformation(
62
+ "✅ SafeChain Ultimate installed and started successfully!",
63
+ );
64
+ ui.emptyLine();
65
+ } finally {
66
+ ui.writeVerbose(`Cleaning up temporary file: ${msiPath}`);
67
+ cleanup(msiPath);
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Checks if admin privileges are available and displays error message if not.
73
+ * @returns {Promise<boolean>} True if running as admin, false otherwise.
74
+ */
75
+ async function requireAdminPrivileges() {
76
+ if (await isRunningAsAdmin()) {
77
+ return true;
78
+ }
79
+
80
+ ui.writeError("Administrator privileges required.");
81
+ ui.writeInformation(
82
+ "Please run this command in an elevated terminal (Run as Administrator).",
83
+ );
84
+ return false;
85
+ }
86
+
87
+ async function isRunningAsAdmin() {
88
+ // Uses Windows Security API to check if current process has admin privileges.
89
+ // Returns "True" or "False" as a string.
90
+ const result = await safeSpawn(
91
+ "powershell",
92
+ [
93
+ "-Command",
94
+ "([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)",
95
+ ],
96
+ { stdio: "pipe" },
97
+ );
98
+
99
+ return result.status === 0 && result.stdout.trim() === "True";
100
+ }
101
+
102
+ /**
103
+ * Returns the MSI product code for SafeChain Ultimate, or null if not installed.
104
+ * @returns {string | null}
105
+ */
106
+ function getInstalledProductCode() {
107
+ // Query Win32_Product via WMI to find the installed SafeChain Agent.
108
+ // If found, outputs the product GUID (e.g., "{12345678-1234-...}") needed for msiexec uninstall.
109
+ ui.writeVerbose(`Finding product code with PowerShell`);
110
+
111
+ let productCode;
112
+ try {
113
+ productCode = execSync(
114
+ `powershell -Command "$app = Get-WmiObject -Class Win32_Product -Filter \\"Name='${WINDOWS_APP_NAME}'\\"; if ($app) { Write-Output $app.IdentifyingNumber }"`,
115
+ { encoding: "utf8" },
116
+ ).trim();
117
+ } catch {
118
+ return null;
119
+ }
120
+ return productCode || null;
121
+ }
122
+
123
+ /**
124
+ * @param {string} productCode
125
+ */
126
+ async function uninstallByProductCode(productCode) {
127
+ ui.writeVerbose(`Found product code: ${productCode}`);
128
+
129
+ // Use msiexec to run the msi installer quitely (https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/msiexec)
130
+ // Options:
131
+ // - /x: Uninstalls the package.
132
+ // - /qn: Specifies there's no UI during the installation process.
133
+ // - /norestart: Stops the device from restarting after the installation completes.
134
+ const uninstallResult = await printVerboseAndSafeSpawn(
135
+ "msiexec",
136
+ ["/x", productCode, "/qn", "/norestart"],
137
+ { stdio: "inherit" },
138
+ );
139
+
140
+ if (uninstallResult.status !== 0) {
141
+ throw new Error(`Uninstall failed (exit code: ${uninstallResult.status})`);
142
+ }
143
+ }
144
+
145
+ async function uninstallIfInstalled() {
146
+ const productCode = getInstalledProductCode();
147
+ if (!productCode) {
148
+ ui.writeVerbose("No existing installation found (fresh install).");
149
+ return;
150
+ }
151
+
152
+ ui.writeInformation("🗑️ Removing previous installation...");
153
+ await uninstallByProductCode(productCode);
154
+ }
155
+
156
+ /**
157
+ * @param {string} msiPath
158
+ */
159
+ async function runMsiInstaller(msiPath) {
160
+ // Use msiexec to run the msi installer quitely (https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/msiexec)
161
+ // Options:
162
+ // - /i: Specifies normal installation
163
+ // - /qn: Specifies there's no UI during the installation process.
164
+
165
+ const result = await printVerboseAndSafeSpawn(
166
+ "msiexec",
167
+ ["/i", msiPath, "/qn"],
168
+ {
169
+ stdio: "inherit",
170
+ },
171
+ );
172
+
173
+ if (result.status !== 0) {
174
+ throw new Error(`MSI installer failed (exit code: ${result.status})`);
175
+ }
176
+ }
177
+
178
+ async function stopServiceIfRunning() {
179
+ ui.writeInformation("⏹️ Stopping running service...");
180
+
181
+ const result = await printVerboseAndSafeSpawn(
182
+ "net",
183
+ ["stop", WINDOWS_SERVICE_NAME],
184
+ {
185
+ stdio: "pipe",
186
+ },
187
+ );
188
+
189
+ if (result.status !== 0) {
190
+ ui.writeVerbose("Service not running (will start after installation).");
191
+ }
192
+ }
193
+
194
+ /**
195
+ * @param {string} msiPath
196
+ */
197
+ function cleanup(msiPath) {
198
+ try {
199
+ unlinkSync(msiPath);
200
+ } catch {
201
+ ui.writeVerbose("Failed to clean up temporary installer file.");
202
+ }
203
+ }
@@ -0,0 +1,35 @@
1
+ import { platform } from "os";
2
+ import { ui } from "../environment/userInteraction.js";
3
+ import { initializeCliArguments } from "../config/cliArguments.js";
4
+ import { installOnWindows, uninstallOnWindows } from "./installOnWindows.js";
5
+ import { installOnMacOS, uninstallOnMacOS } from "./installOnMacOS.js";
6
+
7
+ export async function uninstallUltimate() {
8
+ initializeCliArguments(process.argv);
9
+
10
+ const operatingSystem = platform();
11
+
12
+ if (operatingSystem === "win32") {
13
+ await uninstallOnWindows();
14
+ } else if (operatingSystem === "darwin") {
15
+ await uninstallOnMacOS();
16
+ } else {
17
+ ui.writeInformation(
18
+ `Uninstall is not yet supported on ${operatingSystem}.`,
19
+ );
20
+ }
21
+ }
22
+
23
+ export async function installUltimate() {
24
+ const operatingSystem = platform();
25
+
26
+ if (operatingSystem === "win32") {
27
+ await installOnWindows();
28
+ } else if (operatingSystem === "darwin") {
29
+ await installOnMacOS();
30
+ } else {
31
+ ui.writeInformation(
32
+ `${operatingSystem} is not supported yet by SafeChain's ultimate version.`,
33
+ );
34
+ }
35
+ }
package/src/main.js CHANGED
@@ -73,20 +73,20 @@ export async function main(args) {
73
73
  ui.writeVerbose(
74
74
  `${chalk.green("✔")} Safe-chain: Scanned ${
75
75
  auditStats.totalPackages
76
- } packages, no malware found.`
76
+ } packages, no malware found.`,
77
77
  );
78
78
  }
79
79
 
80
80
  if (proxy.hasSuppressedVersions()) {
81
81
  ui.writeInformation(
82
82
  `${chalk.yellow(
83
- "ℹ"
84
- )} Safe-chain: Some package versions were suppressed due to minimum age requirement.`
83
+ "ℹ",
84
+ )} Safe-chain: Some package versions were suppressed due to minimum age requirement.`,
85
85
  );
86
86
  ui.writeInformation(
87
87
  ` To disable this check, use: ${chalk.cyan(
88
- "--safe-chain-skip-minimum-package-age"
89
- )}`
88
+ "--safe-chain-skip-minimum-package-age",
89
+ )}`,
90
90
  );
91
91
  }
92
92
 
@@ -3,6 +3,8 @@ import * as os from "os";
3
3
  import fs from "fs";
4
4
  import path from "path";
5
5
  import { ECOSYSTEM_JS, ECOSYSTEM_PY } from "../config/settings.js";
6
+ import { safeSpawn } from "../utils/safeSpawn.js";
7
+ import { ui } from "../environment/userInteraction.js";
6
8
 
7
9
  /**
8
10
  * @typedef {Object} AikidoTool
@@ -99,7 +101,7 @@ export const knownAikidoTools = [
99
101
  aikidoCommand: "aikido-pipx",
100
102
  ecoSystem: ECOSYSTEM_PY,
101
103
  internalPackageManagerName: "pipx",
102
- }
104
+ },
103
105
  // When adding a new tool here, also update the documentation for the new tool in the README.md
104
106
  ];
105
107
 
@@ -216,7 +218,13 @@ export function addLineToFile(filePath, line, eol) {
216
218
  eol = eol || os.EOL;
217
219
 
218
220
  const fileContent = fs.readFileSync(filePath, "utf-8");
219
- const updatedContent = fileContent + eol + line + eol;
221
+ let updatedContent = fileContent;
222
+
223
+ if (!fileContent.endsWith(eol)) {
224
+ updatedContent += eol;
225
+ }
226
+
227
+ updatedContent += line + eol;
220
228
  fs.writeFileSync(filePath, updatedContent, "utf-8");
221
229
  }
222
230
 
@@ -237,3 +245,60 @@ function createFileIfNotExists(filePath) {
237
245
 
238
246
  fs.writeFileSync(filePath, "", "utf-8");
239
247
  }
248
+
249
+ /**
250
+ * Checks if PowerShell execution policy allows script execution
251
+ * @param {string} shellExecutableName - The name of the PowerShell executable ("pwsh" or "powershell")
252
+ * @returns {Promise<{isValid: boolean, policy: string}>} validation result
253
+ */
254
+ export async function validatePowerShellExecutionPolicy(shellExecutableName) {
255
+ // Security: Only allow known shell executables
256
+ const validShells = ["pwsh", "powershell"];
257
+ if (!validShells.includes(shellExecutableName)) {
258
+ return { isValid: false, policy: "Unknown" };
259
+ }
260
+
261
+ try {
262
+ // For Windows PowerShell (5.1), clean PSModulePath to avoid conflicts with PowerShell 7 modules
263
+ // When safe-chain is invoked from PowerShell 7, it sets its module paths to PSModulePath, causing
264
+ // Windows PowerShell to try loading incompatible PowerShell 7 modules.
265
+ // Setting the environment to Windows PowerShell's modules fixes this.
266
+ let spawnOptions;
267
+ if (shellExecutableName === "powershell") {
268
+ const userProfile = process.env.USERPROFILE || "";
269
+ const cleanPSModulePath = [
270
+ path.join(userProfile, "Documents", "WindowsPowerShell", "Modules"),
271
+ "C:\\Program Files\\WindowsPowerShell\\Modules",
272
+ "C:\\WINDOWS\\system32\\WindowsPowerShell\\v1.0\\Modules",
273
+ ].join(";");
274
+
275
+ spawnOptions = {
276
+ env: {
277
+ ...process.env,
278
+ PSModulePath: cleanPSModulePath,
279
+ },
280
+ };
281
+ } else {
282
+ spawnOptions = {};
283
+ }
284
+
285
+ const commandResult = await safeSpawn(
286
+ shellExecutableName,
287
+ ["-Command", "Get-ExecutionPolicy"],
288
+ spawnOptions,
289
+ );
290
+
291
+ const policy = commandResult.stdout.trim();
292
+
293
+ const acceptablePolicies = ["RemoteSigned", "Unrestricted", "Bypass"];
294
+ return {
295
+ isValid: acceptablePolicies.includes(policy),
296
+ policy: policy,
297
+ };
298
+ } catch (err) {
299
+ ui.writeWarning(
300
+ `An error happened while trying to find the current executionpolicy in powershell: ${err}`,
301
+ );
302
+ return { isValid: false, policy: "Unknown" };
303
+ }
304
+ }
@@ -1,7 +1,11 @@
1
1
  import chalk from "chalk";
2
2
  import { ui } from "../environment/userInteraction.js";
3
3
  import { detectShells } from "./shellDetection.js";
4
- import { knownAikidoTools, getPackageManagerList, getScriptsDir } from "./helpers.js";
4
+ import {
5
+ knownAikidoTools,
6
+ getPackageManagerList,
7
+ getScriptsDir,
8
+ } from "./helpers.js";
5
9
  import fs from "fs";
6
10
  import path from "path";
7
11
  import { fileURLToPath } from "url";
@@ -26,7 +30,7 @@ if (import.meta.url) {
26
30
  export async function setup() {
27
31
  ui.writeInformation(
28
32
  chalk.bold("Setting up shell aliases.") +
29
- ` This will wrap safe-chain around ${getPackageManagerList()}.`
33
+ ` This will wrap safe-chain around ${getPackageManagerList()}.`,
30
34
  );
31
35
  ui.emptyLine();
32
36
 
@@ -42,12 +46,12 @@ export async function setup() {
42
46
  ui.writeInformation(
43
47
  `Detected ${shells.length} supported shell(s): ${shells
44
48
  .map((shell) => chalk.bold(shell.name))
45
- .join(", ")}.`
49
+ .join(", ")}.`,
46
50
  );
47
51
 
48
52
  let updatedCount = 0;
49
53
  for (const shell of shells) {
50
- if (setupShell(shell)) {
54
+ if (await setupShell(shell)) {
51
55
  updatedCount++;
52
56
  }
53
57
  }
@@ -58,7 +62,7 @@ export async function setup() {
58
62
  }
59
63
  } catch (/** @type {any} */ error) {
60
64
  ui.writeError(
61
- `Failed to set up shell aliases: ${error.message}. Please check your shell configuration.`
65
+ `Failed to set up shell aliases: ${error.message}. Please check your shell configuration.`,
62
66
  );
63
67
  return;
64
68
  }
@@ -68,12 +72,12 @@ export async function setup() {
68
72
  * Calls the setup function for the given shell and reports the result.
69
73
  * @param {import("./shellDetection.js").Shell} shell
70
74
  */
71
- function setupShell(shell) {
75
+ async function setupShell(shell) {
72
76
  let success = false;
73
77
  let error;
74
78
  try {
75
79
  shell.teardown(knownAikidoTools); // First, tear down to prevent duplicate aliases
76
- success = shell.setup(knownAikidoTools);
80
+ success = await shell.setup(knownAikidoTools);
77
81
  } catch (/** @type {any} */ err) {
78
82
  success = false;
79
83
  error = err;
@@ -82,14 +86,14 @@ function setupShell(shell) {
82
86
  if (success) {
83
87
  ui.writeInformation(
84
88
  `${chalk.bold("- " + shell.name + ":")} ${chalk.green(
85
- "Setup successful"
86
- )}`
89
+ "Setup successful",
90
+ )}`,
87
91
  );
88
92
  } else {
89
93
  ui.writeError(
90
94
  `${chalk.bold("- " + shell.name + ":")} ${chalk.red(
91
- "Setup failed"
92
- )}. Please check your ${shell.name} configuration.`
95
+ "Setup failed",
96
+ )}. Please check your ${shell.name} configuration.`,
93
97
  );
94
98
  if (error) {
95
99
  let message = ` Error: ${error.message}`;
@@ -115,11 +119,7 @@ function copyStartupFiles() {
115
119
  }
116
120
 
117
121
  // Use absolute path for source
118
- const sourcePath = path.join(
119
- dirname,
120
- "startup-scripts",
121
- file
122
- );
122
+ const sourcePath = path.join(dirname, "startup-scripts", file);
123
123
  fs.copyFileSync(sourcePath, targetPath);
124
124
  }
125
125
  }
@@ -9,7 +9,7 @@ import { ui } from "../environment/userInteraction.js";
9
9
  * @typedef {Object} Shell
10
10
  * @property {string} name
11
11
  * @property {() => boolean} isInstalled
12
- * @property {(tools: import("./helpers.js").AikidoTool[]) => boolean} setup
12
+ * @property {(tools: import("./helpers.js").AikidoTool[]) => boolean|Promise<boolean>} setup
13
13
  * @property {(tools: import("./helpers.js").AikidoTool[]) => boolean} teardown
14
14
  */
15
15
 
@@ -28,7 +28,7 @@ export function detectShells() {
28
28
  }
29
29
  } catch (/** @type {any} */ error) {
30
30
  ui.writeError(
31
- `We were not able to detect which shells are installed on your system. Please check your shell configuration. Error: ${error.message}`
31
+ `We were not able to detect which shells are installed on your system. Please check your shell configuration. Error: ${error.message}`,
32
32
  );
33
33
  return [];
34
34
  }
@@ -71,13 +71,13 @@ end
71
71
 
72
72
  function printSafeChainWarning
73
73
  set original_cmd $argv[1]
74
-
74
+
75
75
  # Fish equivalent of ANSI color codes: yellow background, black text for "Warning:"
76
76
  set_color -b yellow black
77
77
  printf "Warning:"
78
78
  set_color normal
79
79
  printf " safe-chain is not available to protect you from installing malware. %s will run without it.\n" $original_cmd
80
-
80
+
81
81
  # Cyan text for the install command
82
82
  printf "Install safe-chain by using "
83
83
  set_color cyan
@@ -90,6 +90,20 @@ function wrapSafeChainCommand
90
90
  set original_cmd $argv[1]
91
91
  set cmd_args $argv[2..-1]
92
92
 
93
+ if not type -fq $original_cmd
94
+ # If the original command is not available, don't try to wrap it: invoke
95
+ # it transparently, so the shell can report errors as if this wrapper
96
+ # didn't exist. fish always adds extra debug information when executing
97
+ # missing commands from within a function, so after the "command not
98
+ # found" handler, there will be information about how the
99
+ # wrapSafeChainCommand function errored out. To avoid users assuming this
100
+ # is a safe-chain bug, display an explicit error message afterwards.
101
+ command $original_cmd $cmd_args
102
+ set oldstatus $status
103
+ echo "safe-chain tried to run $original_cmd but it doesn't seem to be installed in your \$PATH." >&2
104
+ return $oldstatus
105
+ end
106
+
93
107
  if type -q safe-chain
94
108
  # If the safe-chain command is available, just run it with the provided arguments
95
109
  safe-chain $original_cmd $cmd_args
@@ -76,6 +76,14 @@ function printSafeChainWarning() {
76
76
  function wrapSafeChainCommand() {
77
77
  local original_cmd="$1"
78
78
 
79
+ if ! type -f "${original_cmd}" > /dev/null 2>&1; then
80
+ # If the original command is not available, don't try to wrap it: invoke it
81
+ # transparently, so the shell can report errors as if this wrapper didn't
82
+ # exist.
83
+ command "$@"
84
+ return $?
85
+ fi
86
+
79
87
  if command -v safe-chain > /dev/null 2>&1; then
80
88
  # If the aikido command is available, just run it with the provided arguments
81
89
  safe-chain "$@"
@@ -2,6 +2,7 @@ import {
2
2
  addLineToFile,
3
3
  doesExecutableExistOnSystem,
4
4
  removeLinesMatchingPattern,
5
+ validatePowerShellExecutionPolicy,
5
6
  } from "../helpers.js";
6
7
  import { execSync } from "child_process";
7
8
 
@@ -25,25 +26,33 @@ function teardown(tools) {
25
26
  // Remove any existing alias for the tool
26
27
  removeLinesMatchingPattern(
27
28
  startupFile,
28
- new RegExp(`^Set-Alias\\s+${tool}\\s+`)
29
+ new RegExp(`^Set-Alias\\s+${tool}\\s+`),
29
30
  );
30
31
  }
31
32
 
32
33
  // Remove the line that sources the safe-chain PowerShell initialization script
33
34
  removeLinesMatchingPattern(
34
35
  startupFile,
35
- /^\.\s+["']?\$HOME[/\\].safe-chain[/\\]scripts[/\\]init-pwsh\.ps1["']?/
36
+ /^\.\s+["']?\$HOME[/\\].safe-chain[/\\]scripts[/\\]init-pwsh\.ps1["']?/,
36
37
  );
37
38
 
38
39
  return true;
39
40
  }
40
41
 
41
- function setup() {
42
+ async function setup() {
43
+ const { isValid, policy } =
44
+ await validatePowerShellExecutionPolicy(executableName);
45
+ if (!isValid) {
46
+ throw new Error(
47
+ `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`,
48
+ );
49
+ }
50
+
42
51
  const startupFile = getStartupFile();
43
52
 
44
53
  addLineToFile(
45
54
  startupFile,
46
- `. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1" # Safe-chain PowerShell initialization script`
55
+ `. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1" # Safe-chain PowerShell initialization script`,
47
56
  );
48
57
 
49
58
  return true;
@@ -57,7 +66,7 @@ function getStartupFile() {
57
66
  }).trim();
58
67
  } catch (/** @type {any} */ error) {
59
68
  throw new Error(
60
- `Command failed: ${startupFileCommand}. Error: ${error.message}`
69
+ `Command failed: ${startupFileCommand}. Error: ${error.message}`,
61
70
  );
62
71
  }
63
72
  }
@@ -2,6 +2,7 @@ import {
2
2
  addLineToFile,
3
3
  doesExecutableExistOnSystem,
4
4
  removeLinesMatchingPattern,
5
+ validatePowerShellExecutionPolicy,
5
6
  } from "../helpers.js";
6
7
  import { execSync } from "child_process";
7
8
 
@@ -25,25 +26,33 @@ function teardown(tools) {
25
26
  // Remove any existing alias for the tool
26
27
  removeLinesMatchingPattern(
27
28
  startupFile,
28
- new RegExp(`^Set-Alias\\s+${tool}\\s+`)
29
+ new RegExp(`^Set-Alias\\s+${tool}\\s+`),
29
30
  );
30
31
  }
31
32
 
32
33
  // Remove the line that sources the safe-chain PowerShell initialization script
33
34
  removeLinesMatchingPattern(
34
35
  startupFile,
35
- /^\.\s+["']?\$HOME[/\\].safe-chain[/\\]scripts[/\\]init-pwsh\.ps1["']?/
36
+ /^\.\s+["']?\$HOME[/\\].safe-chain[/\\]scripts[/\\]init-pwsh\.ps1["']?/,
36
37
  );
37
38
 
38
39
  return true;
39
40
  }
40
41
 
41
- function setup() {
42
+ async function setup() {
43
+ const { isValid, policy } =
44
+ await validatePowerShellExecutionPolicy(executableName);
45
+ if (!isValid) {
46
+ throw new Error(
47
+ `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`,
48
+ );
49
+ }
50
+
42
51
  const startupFile = getStartupFile();
43
52
 
44
53
  addLineToFile(
45
54
  startupFile,
46
- `. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1" # Safe-chain PowerShell initialization script`
55
+ `. "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1" # Safe-chain PowerShell initialization script`,
47
56
  );
48
57
 
49
58
  return true;
@@ -57,7 +66,7 @@ function getStartupFile() {
57
66
  }).trim();
58
67
  } catch (/** @type {any} */ error) {
59
68
  throw new Error(
60
- `Command failed: ${startupFileCommand}. Error: ${error.message}`
69
+ `Command failed: ${startupFileCommand}. Error: ${error.message}`,
61
70
  );
62
71
  }
63
72
  }