@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.
package/README.md CHANGED
@@ -66,7 +66,6 @@ You can find all available versions on the [releases page](https://github.com/Ai
66
66
  ### Verify the installation
67
67
 
68
68
  1. **❗Restart your terminal** to start using the Aikido Safe Chain.
69
-
70
69
  - 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.
71
70
 
72
71
  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
159
158
  You can set the logging level through multiple sources (in order of priority):
160
159
 
161
160
  1. **CLI Argument** (highest priority):
162
-
163
161
  - `--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.
164
162
 
165
163
  ```shell
@@ -288,6 +286,7 @@ iex "& { $(iwr 'https://github.com/AikidoSec/safe-chain/releases/latest/download
288
286
  - ✅ **CircleCI**
289
287
  - ✅ **Jenkins**
290
288
  - ✅ **Bitbucket Pipelines**
289
+ - ✅ **GitLab Pipelines**
291
290
 
292
291
  ## GitHub Actions Example
293
292
 
@@ -386,14 +385,76 @@ steps:
386
385
  - step:
387
386
  name: Install
388
387
  script:
389
- - npm install -g @aikidosec/safe-chain
390
- - safe-chain setup-ci
388
+ - curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh -s -- --ci
391
389
  - export PATH=~/.safe-chain/shims:$PATH
392
390
  - npm ci
393
391
  ```
394
392
 
395
393
  After setup, all subsequent package manager commands in your CI pipeline will automatically be protected by Aikido Safe Chain's malware detection.
396
394
 
395
+ ## GitLab Pipelines Example
396
+
397
+ To add safe-chain in GitLab pipelines, you need to install it in the image running the pipeline. This can be done by:
398
+
399
+ 1. Define a dockerfile to run your build
400
+
401
+ ```dockerfile
402
+ FROM node:lts
403
+
404
+ # Install safe-chain
405
+ RUN curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh -s -- --ci
406
+
407
+ # Add safe-chain to PATH
408
+ ENV PATH="/root/.safe-chain/shims:/root/.safe-chain/bin:${PATH}"
409
+ ```
410
+
411
+ 2. Build the Docker image in your CI pipeline
412
+
413
+ ```yaml
414
+ build-image:
415
+ stage: build-image
416
+ image: docker:latest
417
+ services:
418
+ - docker:dind
419
+ script:
420
+ - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
421
+ - docker build -t $CI_REGISTRY_IMAGE:latest .
422
+ - docker push $CI_REGISTRY_IMAGE:latest
423
+ ```
424
+
425
+ 3. Use the image in your pipeline:
426
+ ```yaml
427
+ npm-ci:
428
+ stage: install
429
+ image: $CI_REGISTRY_IMAGE:latest
430
+ script:
431
+ - npm ci
432
+ ```
433
+
434
+ The full pipeline for this example looks like this:
435
+
436
+ ```yaml
437
+ stages:
438
+ - build-image
439
+ - install
440
+
441
+ build-image:
442
+ stage: build-image
443
+ image: docker:latest
444
+ services:
445
+ - docker:dind
446
+ script:
447
+ - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
448
+ - docker build -t $CI_REGISTRY_IMAGE:latest .
449
+ - docker push $CI_REGISTRY_IMAGE:latest
450
+
451
+ npm-ci:
452
+ stage: install
453
+ image: $CI_REGISTRY_IMAGE:latest
454
+ script:
455
+ - npm ci
456
+ ```
457
+
397
458
  # Troubleshooting
398
459
 
399
- Having issues? See the [Troubleshooting Guide](https://github.com/AikidoSec/safe-chain/blob/main/docs/troubleshooting.md) for help with common problems.
460
+ Having issues? See the [Troubleshooting Guide](https://help.aikido.dev/code-scanning/aikido-malware-scanning/safe-chain-troubleshooting) for help with common problems.
package/bin/safe-chain.js CHANGED
@@ -16,6 +16,14 @@ import path from "path";
16
16
  import { fileURLToPath } from "url";
17
17
  import fs from "fs";
18
18
  import { knownAikidoTools } from "../src/shell-integration/helpers.js";
19
+ import {
20
+ installUltimate,
21
+ uninstallUltimate,
22
+ } from "../src/installation/installUltimate.js";
23
+ import {
24
+ printUltimateLogs,
25
+ troubleshootingExport,
26
+ } from "../src/ultimate/ultimateTroubleshooting.js";
19
27
 
20
28
  /** @type {string} */
21
29
  // This checks the current file's dirname in a way that's compatible with:
@@ -62,9 +70,42 @@ if (tool) {
62
70
  process.exit(0);
63
71
  } else if (command === "setup") {
64
72
  setup();
73
+ } else if (command === "ultimate") {
74
+ const cliArgs = initializeCliArguments(process.argv.slice(2));
75
+ const subCommand = cliArgs[1];
76
+ if (subCommand === "uninstall") {
77
+ guardCliArgsMaxLenght(2, cliArgs, "safe-chain ultimate uninstall");
78
+ (async () => {
79
+ await uninstallUltimate();
80
+ })();
81
+ } else if (subCommand === "troubleshooting-logs") {
82
+ guardCliArgsMaxLenght(
83
+ 2,
84
+ cliArgs,
85
+ "safe-chain ultimate troubleshooting-logs",
86
+ );
87
+ (async () => {
88
+ await printUltimateLogs();
89
+ })();
90
+ } else if (subCommand === "troubleshooting-export") {
91
+ guardCliArgsMaxLenght(
92
+ 2,
93
+ cliArgs,
94
+ "safe-chain ultimate troubleshooting-export",
95
+ );
96
+ (async () => {
97
+ await troubleshootingExport();
98
+ })();
99
+ } else {
100
+ guardCliArgsMaxLenght(1, cliArgs, "safe-chain ultimate");
101
+ // Install command = when no subcommand is provided (safe-chain ultimate)
102
+ (async () => {
103
+ await installUltimate();
104
+ })();
105
+ }
65
106
  } else if (command === "teardown") {
66
- teardownDirectories();
67
107
  teardown();
108
+ teardownDirectories();
68
109
  } else if (command === "setup-ci") {
69
110
  setupCi();
70
111
  } else if (command === "--version" || command === "-v" || command === "-v") {
@@ -80,38 +121,77 @@ if (tool) {
80
121
  process.exit(1);
81
122
  }
82
123
 
124
+ /**
125
+ * @param {Number} maxLength
126
+ * @param {String[]} args
127
+ * @param {String} command
128
+ */
129
+ function guardCliArgsMaxLenght(maxLength, args, command) {
130
+ if (args.length > maxLength) {
131
+ ui.writeError(`Unexpected number of arguments for command ${command}.`);
132
+ ui.emptyLine();
133
+
134
+ writeHelp();
135
+
136
+ process.exit(1);
137
+ }
138
+ }
139
+
83
140
  function writeHelp() {
84
141
  ui.writeInformation(
85
- chalk.bold("Usage: ") + chalk.cyan("safe-chain <command>")
142
+ chalk.bold("Usage: ") + chalk.cyan("safe-chain <command>"),
86
143
  );
87
144
  ui.emptyLine();
88
145
  ui.writeInformation(
89
146
  `Available commands: ${chalk.cyan("setup")}, ${chalk.cyan(
90
- "teardown"
91
- )}, ${chalk.cyan("setup-ci")}, ${chalk.cyan("help")}, ${chalk.cyan(
92
- "--version"
93
- )}`
147
+ "teardown",
148
+ )}, ${chalk.cyan("setup-ci")}, ${chalk.cyan("ultimate")}, ${chalk.cyan("help")}, ${chalk.cyan(
149
+ "--version",
150
+ )}`,
94
151
  );
95
152
  ui.emptyLine();
96
153
  ui.writeInformation(
97
154
  `- ${chalk.cyan(
98
- "safe-chain setup"
99
- )}: This will setup your shell to wrap safe-chain around npm, npx, yarn, pnpm, pnpx, bun, bunx, pip and pip3.`
155
+ "safe-chain setup",
156
+ )}: This will setup your shell to wrap safe-chain around npm, npx, yarn, pnpm, pnpx, bun, bunx, pip and pip3.`,
100
157
  );
101
158
  ui.writeInformation(
102
159
  `- ${chalk.cyan(
103
- "safe-chain teardown"
104
- )}: This will remove safe-chain aliases from your shell configuration.`
160
+ "safe-chain teardown",
161
+ )}: This will remove safe-chain aliases from your shell configuration.`,
105
162
  );
106
163
  ui.writeInformation(
107
164
  `- ${chalk.cyan(
108
- "safe-chain setup-ci"
109
- )}: This will setup safe-chain for CI environments by creating shims and modifying the PATH.`
165
+ "safe-chain setup-ci",
166
+ )}: This will setup safe-chain for CI environments by creating shims and modifying the PATH.`,
110
167
  );
111
168
  ui.writeInformation(
112
169
  `- ${chalk.cyan("safe-chain --version")} (or ${chalk.cyan(
113
- "-v"
114
- )}): Display the current version of safe-chain.`
170
+ "-v",
171
+ )}): Display the current version of safe-chain.`,
172
+ );
173
+ ui.emptyLine();
174
+ ui.writeInformation(chalk.bold("Ultimate commands:"));
175
+ ui.emptyLine();
176
+ ui.writeInformation(
177
+ `- ${chalk.cyan(
178
+ "safe-chain ultimate",
179
+ )}: Install the ultimate version of safe-chain, enabling protection for more eco-systems.`,
180
+ );
181
+ ui.writeInformation(
182
+ `- ${chalk.cyan(
183
+ "safe-chain ultimate troubleshooting-logs",
184
+ )}: Prints standard and error logs for safe-chain ultimate and it's proxy.`,
185
+ );
186
+ ui.writeInformation(
187
+ `- ${chalk.cyan(
188
+ "safe-chain ultimate troubleshooting-export",
189
+ )}: Creates a zip archive of useful data for troubleshooting safe-chain ultimate, that can be shared with our support team.`,
190
+ );
191
+ ui.writeInformation(
192
+ `- ${chalk.cyan(
193
+ "safe-chain ultimate uninstall",
194
+ )}: Uninstall the ultimate version of safe-chain.`,
115
195
  );
116
196
  ui.emptyLine();
117
197
  }
@@ -149,6 +149,37 @@ Should include `~/.safe-chain/bin`
149
149
 
150
150
  **If persists:** Re-run the installation script
151
151
 
152
+ ### PowerShell Execution Policy Blocks Scripts (Windows)
153
+
154
+ **Symptom:** When opening PowerShell, you see an error like:
155
+
156
+ ```
157
+ . : File C:\Users\<username>\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1 cannot be loaded because
158
+ running scripts is disabled on this system.
159
+ CategoryInfo : SecurityError: (:) [], PSSecurityException
160
+ FullyQualifiedErrorId : UnauthorizedAccess
161
+ ```
162
+
163
+ **Cause:** Windows PowerShell's default execution policy (`Restricted`) blocks all script execution, including safe-chain's initialization script that's sourced from your PowerShell profile.
164
+
165
+ **Resolution:**
166
+
167
+ 1. **Set the execution policy to allow local scripts:**
168
+
169
+ Open PowerShell as Administrator and run:
170
+
171
+ ```powershell
172
+ Set-ExecutionPolicy -ExecutionPolicy RemoteSigned
173
+ ```
174
+
175
+ This allows:
176
+ - Local scripts (like safe-chain's) to run without signing
177
+ - Downloaded scripts to run only if signed by a trusted publisher
178
+
179
+ 2. **Restart PowerShell** and verify the error is resolved.
180
+
181
+ > **Note:** `RemoteSigned` is Microsoft's recommended execution policy for client computers. It provides a good balance between security and usability.
182
+
152
183
  ### Shell Aliases Persist After Uninstallation
153
184
 
154
185
  **Symptom:** safe-chain commands still active after running uninstall script
@@ -277,22 +308,6 @@ Look for and remove:
277
308
  rm -rf ~/.safe-chain
278
309
  ```
279
310
 
280
- ## Getting More Information
281
-
282
- ### Enable Verbose Logging
283
-
284
- Get detailed diagnostic output using a CLI flag or environment variable:
285
-
286
- ```bash
287
- # Using CLI flag
288
- npm install express --safe-chain-logging=verbose
289
- pip install requests --safe-chain-logging=verbose
290
-
291
- # Using environment variable (applies to all commands)
292
- export SAFE_CHAIN_LOGGING=verbose
293
- npm install express
294
- ```
295
-
296
311
  ### Report Issues
297
312
 
298
313
  If you encounter problems:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aikidosec/safe-chain",
3
- "version": "1.4.2",
3
+ "version": "1.4.3",
4
4
  "scripts": {
5
5
  "test": "node --test --experimental-test-module-mocks 'src/**/*.spec.js'",
6
6
  "test:watch": "node --test --watch --experimental-test-module-mocks 'src/**/*.spec.js'",
@@ -38,6 +38,7 @@
38
38
  "license": "AGPL-3.0-or-later",
39
39
  "description": "The Aikido Safe Chain wraps around the [npm cli](https://github.com/npm/cli), [npx](https://github.com/npm/cli/blob/latest/docs/content/commands/npx.md), [yarn](https://yarnpkg.com/), [pnpm](https://pnpm.io/), [pnpx](https://pnpm.io/cli/dlx), [bun](https://bun.sh/), [bunx](https://bun.sh/docs/cli/bunx), [uv](https://docs.astral.sh/uv/) (Python), and [pip](https://pip.pypa.io/) to provide extra checks before installing new packages. This tool will detect when a package contains malware and prompt you to exit, preventing npm, npx, yarn, pnpm, pnpx, bun, bunx, uv, or pip/pip3 from downloading or running the malware.",
40
40
  "dependencies": {
41
+ "archiver": "^7.0.1",
41
42
  "certifi": "14.5.15",
42
43
  "chalk": "5.4.1",
43
44
  "https-proxy-agent": "7.0.6",
@@ -48,6 +49,7 @@
48
49
  "semver": "7.7.2"
49
50
  },
50
51
  "devDependencies": {
52
+ "@types/archiver": "^7.0.0",
51
53
  "@types/ini": "^4.1.1",
52
54
  "@types/make-fetch-happen": "^10.0.4",
53
55
  "@types/node": "^18.19.130",
@@ -0,0 +1,125 @@
1
+ import { createWriteStream, createReadStream } from "fs";
2
+ import { createHash } from "crypto";
3
+ import { pipeline } from "stream/promises";
4
+ import fetch from "make-fetch-happen";
5
+
6
+ const ULTIMATE_VERSION = "v1.0.0";
7
+
8
+ export const DOWNLOAD_URLS = {
9
+ win32: {
10
+ x64: {
11
+ url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-windows-amd64.msi`,
12
+ checksum:
13
+ "sha256:c6a36f9b8e55ab6b7e8742cbabc4469d85809237c0f5e6c21af20b36c416ee1d",
14
+ },
15
+ arm64: {
16
+ url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-windows-arm64.msi`,
17
+ checksum:
18
+ "sha256:46acd1af6a9938ea194c8ee8b34ca9b47c8de22e088a0791f3c0751dd6239c90",
19
+ },
20
+ },
21
+ darwin: {
22
+ x64: {
23
+ url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-darwin-amd64.pkg`,
24
+ checksum:
25
+ "sha256:bb1829e8ca422e885baf37bef08dcbe7df7a30f248e2e89c4071564f7d4f3396",
26
+ },
27
+ arm64: {
28
+ url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-darwin-arm64.pkg`,
29
+ checksum:
30
+ "sha256:7fe4a785709911cc366d8224b4c290677573b8c4833bd9054768299e55c5f0ed",
31
+ },
32
+ },
33
+ };
34
+
35
+ /**
36
+ * Builds the download URL for the SafeChain Agent installer.
37
+ * @param {string} fileName
38
+ */
39
+ export function getAgentDownloadUrl(fileName) {
40
+ return `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/${fileName}`;
41
+ }
42
+
43
+ /**
44
+ * Downloads a file from a URL to a local path.
45
+ * @param {string} url
46
+ * @param {string} destPath
47
+ */
48
+ export async function downloadFile(url, destPath) {
49
+ const response = await fetch(url);
50
+ if (!response.ok) {
51
+ throw new Error(`Download failed: ${response.statusText}`);
52
+ }
53
+ await pipeline(response.body, createWriteStream(destPath));
54
+ }
55
+
56
+ /**
57
+ * Returns the current agent version.
58
+ */
59
+ export function getAgentVersion() {
60
+ return ULTIMATE_VERSION;
61
+ }
62
+
63
+ /**
64
+ * Returns download info (url, checksum) for the current OS and architecture.
65
+ * @returns {{ url: string, checksum: string } | null}
66
+ */
67
+ export function getDownloadInfoForCurrentPlatform() {
68
+ const platform = process.platform;
69
+ const arch = process.arch;
70
+
71
+ if (!Object.hasOwn(DOWNLOAD_URLS, platform)) {
72
+ return null;
73
+ }
74
+ const platformUrls =
75
+ DOWNLOAD_URLS[/** @type {keyof typeof DOWNLOAD_URLS} */ (platform)];
76
+
77
+ if (!Object.hasOwn(platformUrls, arch)) {
78
+ return null;
79
+ }
80
+
81
+ return platformUrls[/** @type {keyof typeof platformUrls} */ (arch)];
82
+ }
83
+
84
+ /**
85
+ * Verifies the checksum of a file.
86
+ * @param {string} filePath
87
+ * @param {string} expectedChecksum - Format: "algorithm:hash" (e.g., "sha256:abc123...")
88
+ * @returns {Promise<boolean>}
89
+ */
90
+ export async function verifyChecksum(filePath, expectedChecksum) {
91
+ const [algorithm, expected] = expectedChecksum.split(":");
92
+
93
+ const hash = createHash(algorithm);
94
+
95
+ if (filePath.includes("..")) throw new Error("Invalid file path");
96
+ const stream = createReadStream(filePath);
97
+
98
+ for await (const chunk of stream) {
99
+ hash.update(chunk);
100
+ }
101
+
102
+ const actual = hash.digest("hex");
103
+ return actual === expected;
104
+ }
105
+
106
+ /**
107
+ * Downloads the SafeChain agent for the current OS/arch and verifies its checksum.
108
+ * @param {string} fileName - Destination file path
109
+ * @returns {Promise<string | null>} The file path if successful, null if no download URL for current platform
110
+ */
111
+ export async function downloadAgentToFile(fileName) {
112
+ const info = getDownloadInfoForCurrentPlatform();
113
+ if (!info) {
114
+ return null;
115
+ }
116
+
117
+ await downloadFile(info.url, fileName);
118
+
119
+ const isValid = await verifyChecksum(fileName, info.checksum);
120
+ if (!isValid) {
121
+ throw new Error("Checksum verification failed");
122
+ }
123
+
124
+ return fileName;
125
+ }
@@ -0,0 +1,155 @@
1
+ import { tmpdir } from "os";
2
+ import { unlinkSync } from "fs";
3
+ import { join } from "path";
4
+ import { execSync, spawnSync } from "child_process";
5
+ import { ui } from "../environment/userInteraction.js";
6
+ import { printVerboseAndSafeSpawn } from "../utils/safeSpawn.js";
7
+ import { downloadAgentToFile, getAgentVersion } from "./downloadAgent.js";
8
+ import chalk from "chalk";
9
+
10
+ const MACOS_PKG_IDENTIFIER = "com.aikidosecurity.safechainultimate";
11
+
12
+ /**
13
+ * Checks if root privileges are available and displays error message if not.
14
+ * @param {string} command - The sudo command to show in the error message
15
+ * @returns {boolean} True if running as root, false otherwise.
16
+ */
17
+ function requireRootPrivileges(command) {
18
+ if (isRunningAsRoot()) {
19
+ return true;
20
+ }
21
+
22
+ ui.writeError("Root privileges required.");
23
+ ui.writeInformation("Please run this command with sudo:");
24
+ ui.writeInformation(` ${command}`);
25
+ return false;
26
+ }
27
+
28
+ function isRunningAsRoot() {
29
+ const rootUserUid = 0;
30
+ return process.getuid?.() === rootUserUid;
31
+ }
32
+
33
+ export async function installOnMacOS() {
34
+ if (!requireRootPrivileges("sudo safe-chain ultimate")) {
35
+ return;
36
+ }
37
+
38
+ const pkgPath = join(tmpdir(), `SafeChainUltimate-${Date.now()}.pkg`);
39
+
40
+ ui.emptyLine();
41
+ ui.writeInformation(`📥 Downloading SafeChain Ultimate ${getAgentVersion()}`);
42
+ ui.writeVerbose(`Destination: ${pkgPath}`);
43
+
44
+ const result = await downloadAgentToFile(pkgPath);
45
+ if (!result) {
46
+ ui.writeError("No download available for this platform/architecture.");
47
+ return;
48
+ }
49
+
50
+ try {
51
+ ui.writeInformation("⚙️ Installing SafeChain Ultimate...");
52
+ await runPkgInstaller(pkgPath);
53
+
54
+ ui.emptyLine();
55
+ ui.writeInformation(
56
+ "✅ SafeChain Ultimate installed and started successfully!",
57
+ );
58
+ ui.emptyLine();
59
+ ui.writeInformation(
60
+ chalk.cyan("🔐 ") +
61
+ chalk.bold("ACTION REQUIRED: ") +
62
+ "macOS will show a popup to install our certificate.",
63
+ );
64
+ ui.writeInformation(
65
+ " " +
66
+ chalk.bold("Please accept the certificate") +
67
+ " to complete the installation.",
68
+ );
69
+ ui.emptyLine();
70
+ } finally {
71
+ ui.writeVerbose(`Cleaning up temporary file: ${pkgPath}`);
72
+ cleanup(pkgPath);
73
+ }
74
+ }
75
+
76
+ const MACOS_UNINSTALL_SCRIPT =
77
+ "/Library/Application\\ Support/AikidoSecurity/SafeChainUltimate/scripts/uninstall";
78
+
79
+ export async function uninstallOnMacOS() {
80
+ if (!requireRootPrivileges("sudo safe-chain ultimate uninstall")) {
81
+ return;
82
+ }
83
+
84
+ ui.emptyLine();
85
+
86
+ if (!isPackageInstalled()) {
87
+ ui.writeInformation("SafeChain Ultimate is not installed.");
88
+ return;
89
+ }
90
+
91
+ ui.writeInformation("🗑️ Uninstalling SafeChain Ultimate...");
92
+ ui.writeVerbose(`Running: ${MACOS_UNINSTALL_SCRIPT}`);
93
+
94
+ const result = spawnSync(MACOS_UNINSTALL_SCRIPT, {
95
+ stdio: "inherit",
96
+ shell: true,
97
+ });
98
+
99
+ if (result.status !== 0) {
100
+ ui.writeError(
101
+ `Uninstall script failed (exit code: ${result.status}). Please try again or remove manually.`,
102
+ );
103
+ return;
104
+ }
105
+
106
+ ui.emptyLine();
107
+ ui.writeInformation("✅ SafeChain Ultimate has been uninstalled.");
108
+ ui.emptyLine();
109
+ }
110
+
111
+ function isPackageInstalled() {
112
+ try {
113
+ const output = execSync(`pkgutil --pkg-info ${MACOS_PKG_IDENTIFIER}`, {
114
+ encoding: "utf8",
115
+ stdio: "pipe",
116
+ });
117
+ return output.includes(MACOS_PKG_IDENTIFIER);
118
+ } catch {
119
+ return false;
120
+ }
121
+ }
122
+
123
+ /**
124
+ * @param {string} pkgPath
125
+ */
126
+ async function runPkgInstaller(pkgPath) {
127
+ // Uses installer to install the package (https://ss64.com/mac/installer.html)
128
+ // Options:
129
+ // -pkg (required): The package to be installed.
130
+ // -target (required): The target volume is specified with the -target parameter.
131
+ // --> "-target /" installs to the current boot volume.
132
+
133
+ const result = await printVerboseAndSafeSpawn(
134
+ "installer",
135
+ ["-pkg", pkgPath, "-target", "/"],
136
+ {
137
+ stdio: "inherit",
138
+ },
139
+ );
140
+
141
+ if (result.status !== 0) {
142
+ throw new Error(`PKG installer failed (exit code: ${result.status})`);
143
+ }
144
+ }
145
+
146
+ /**
147
+ * @param {string} pkgPath
148
+ */
149
+ function cleanup(pkgPath) {
150
+ try {
151
+ unlinkSync(pkgPath);
152
+ } catch {
153
+ ui.writeVerbose("Failed to clean up temporary installer file.");
154
+ }
155
+ }