@aikidosec/safe-chain 1.4.2 → 1.4.4
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 +66 -5
- package/bin/safe-chain.js +13 -13
- package/docs/troubleshooting.md +31 -16
- package/package.json +3 -1
- package/src/installation/downloadAgent.js +125 -0
- package/src/installation/installOnMacOS.js +155 -0
- package/src/installation/installOnWindows.js +203 -0
- package/src/installation/installUltimate.js +35 -0
- package/src/main.js +5 -5
- package/src/shell-integration/helpers.js +67 -2
- package/src/shell-integration/setup.js +16 -16
- package/src/shell-integration/shellDetection.js +2 -2
- package/src/shell-integration/startup-scripts/init-fish.fish +16 -2
- package/src/shell-integration/startup-scripts/init-posix.sh +8 -0
- package/src/shell-integration/supported-shells/powershell.js +14 -5
- package/src/shell-integration/supported-shells/windowsPowershell.js +14 -5
- package/src/ultimate/ultimateTroubleshooting.js +111 -0
- package/src/utils/safeSpawn.js +16 -0
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
|
-
-
|
|
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://
|
|
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
|
@@ -63,8 +63,8 @@ if (tool) {
|
|
|
63
63
|
} else if (command === "setup") {
|
|
64
64
|
setup();
|
|
65
65
|
} else if (command === "teardown") {
|
|
66
|
-
teardownDirectories();
|
|
67
66
|
teardown();
|
|
67
|
+
teardownDirectories();
|
|
68
68
|
} else if (command === "setup-ci") {
|
|
69
69
|
setupCi();
|
|
70
70
|
} else if (command === "--version" || command === "-v" || command === "-v") {
|
|
@@ -82,36 +82,36 @@ if (tool) {
|
|
|
82
82
|
|
|
83
83
|
function writeHelp() {
|
|
84
84
|
ui.writeInformation(
|
|
85
|
-
chalk.bold("Usage: ") + chalk.cyan("safe-chain <command>")
|
|
85
|
+
chalk.bold("Usage: ") + chalk.cyan("safe-chain <command>"),
|
|
86
86
|
);
|
|
87
87
|
ui.emptyLine();
|
|
88
88
|
ui.writeInformation(
|
|
89
89
|
`Available commands: ${chalk.cyan("setup")}, ${chalk.cyan(
|
|
90
|
-
"teardown"
|
|
90
|
+
"teardown",
|
|
91
91
|
)}, ${chalk.cyan("setup-ci")}, ${chalk.cyan("help")}, ${chalk.cyan(
|
|
92
|
-
"--version"
|
|
93
|
-
)}
|
|
92
|
+
"--version",
|
|
93
|
+
)}`,
|
|
94
94
|
);
|
|
95
95
|
ui.emptyLine();
|
|
96
96
|
ui.writeInformation(
|
|
97
97
|
`- ${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
|
|
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.`,
|
|
100
100
|
);
|
|
101
101
|
ui.writeInformation(
|
|
102
102
|
`- ${chalk.cyan(
|
|
103
|
-
"safe-chain teardown"
|
|
104
|
-
)}: This will remove safe-chain aliases from your shell configuration
|
|
103
|
+
"safe-chain teardown",
|
|
104
|
+
)}: This will remove safe-chain aliases from your shell configuration.`,
|
|
105
105
|
);
|
|
106
106
|
ui.writeInformation(
|
|
107
107
|
`- ${chalk.cyan(
|
|
108
|
-
"safe-chain setup-ci"
|
|
109
|
-
)}: This will setup safe-chain for CI environments by creating shims and modifying the PATH
|
|
108
|
+
"safe-chain setup-ci",
|
|
109
|
+
)}: This will setup safe-chain for CI environments by creating shims and modifying the PATH.`,
|
|
110
110
|
);
|
|
111
111
|
ui.writeInformation(
|
|
112
112
|
`- ${chalk.cyan("safe-chain --version")} (or ${chalk.cyan(
|
|
113
|
-
"-v"
|
|
114
|
-
)}): Display the current version of safe-chain
|
|
113
|
+
"-v",
|
|
114
|
+
)}): Display the current version of safe-chain.`,
|
|
115
115
|
);
|
|
116
116
|
ui.emptyLine();
|
|
117
117
|
}
|
package/docs/troubleshooting.md
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "1.4.4",
|
|
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
|
+
}
|
|
@@ -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
|
-
|
|
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 {
|
|
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
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { platform } from 'os';
|
|
2
|
+
import { ui } from "../environment/userInteraction.js";
|
|
3
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
4
|
+
import {randomUUID} from "node:crypto";
|
|
5
|
+
import {createWriteStream} from "fs";
|
|
6
|
+
import archiver from 'archiver';
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
|
|
9
|
+
export async function printUltimateLogs() {
|
|
10
|
+
const { proxyLogPath, ultimateLogPath, proxyErrLogPath, ultimateErrLogPath } = getPathsPerPlatform();
|
|
11
|
+
|
|
12
|
+
await printLogs(
|
|
13
|
+
"SafeChain Proxy",
|
|
14
|
+
proxyLogPath,
|
|
15
|
+
proxyErrLogPath
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
await printLogs(
|
|
19
|
+
"SafeChain Ultimate",
|
|
20
|
+
ultimateLogPath,
|
|
21
|
+
ultimateErrLogPath
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function troubleshootingExport() {
|
|
26
|
+
const { logDir } = getPathsPerPlatform();
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
if (!existsSync(logDir)) {
|
|
29
|
+
ui.writeError(`Log directory not found: ${logDir}`);
|
|
30
|
+
reject(new Error(`Log directory not found: ${logDir}`));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const date = new Date().toISOString().split('T')[0];
|
|
35
|
+
const uuid = randomUUID();
|
|
36
|
+
const zipFileName = `safechain-ultimate-${date}-${uuid}.zip`;
|
|
37
|
+
const output = createWriteStream(zipFileName);
|
|
38
|
+
const archive = archiver('zip', { zlib: { level: 9 } });
|
|
39
|
+
|
|
40
|
+
output.on('close', () => {
|
|
41
|
+
ui.writeInformation(`Logs collected and zipped as: ${path.resolve(zipFileName)}`);
|
|
42
|
+
resolve(zipFileName);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
archive.on('error', (err) => {
|
|
46
|
+
ui.writeError(`Failed to zip logs: ${err.message}`);
|
|
47
|
+
reject(err);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
archive.pipe(output);
|
|
51
|
+
archive.directory(logDir, false);
|
|
52
|
+
archive.finalize();
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
function getPathsPerPlatform() {
|
|
58
|
+
const os = platform();
|
|
59
|
+
if (os === 'win32') {
|
|
60
|
+
const logDir = `C:\\ProgramData\\AikidoSecurity\\SafeChainUltimate\\logs`;
|
|
61
|
+
return {
|
|
62
|
+
logDir,
|
|
63
|
+
proxyLogPath: `${logDir}\\SafeChainProxy.log`,
|
|
64
|
+
ultimateLogPath: `${logDir}\\SafeChainUltimate.log`,
|
|
65
|
+
proxyErrLogPath: `${logDir}\\SafeChainProxy.err`,
|
|
66
|
+
ultimateErrLogPath: `${logDir}\\SafeChainUltimate.err`,
|
|
67
|
+
};
|
|
68
|
+
} else if (os === 'darwin') {
|
|
69
|
+
const logDir = `/Library/Logs/AikidoSecurity/SafeChainUltimate`;
|
|
70
|
+
return {
|
|
71
|
+
logDir,
|
|
72
|
+
proxyLogPath: `${logDir}/safechain-proxy.log`,
|
|
73
|
+
ultimateLogPath: `${logDir}/safechain-ultimate.log`,
|
|
74
|
+
proxyErrLogPath: `${logDir}/safechain-proxy.error.log`,
|
|
75
|
+
ultimateErrLogPath: `${logDir}/safechain-ultimate.error.log`,
|
|
76
|
+
};
|
|
77
|
+
} else {
|
|
78
|
+
throw new Error('Unsupported platform for log printing.');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @param {string} appName
|
|
84
|
+
* @param {string} logPath
|
|
85
|
+
* @param {string} errLogPath
|
|
86
|
+
*/
|
|
87
|
+
async function printLogs(appName, logPath, errLogPath) {
|
|
88
|
+
ui.writeInformation(`=== ${appName} Logs ===`);
|
|
89
|
+
try {
|
|
90
|
+
if (existsSync(logPath)) {
|
|
91
|
+
const logs = readFileSync(logPath, "utf-8");
|
|
92
|
+
ui.writeInformation(logs);
|
|
93
|
+
} else {
|
|
94
|
+
ui.writeWarning(`${appName} log file not found: ${logPath}`);
|
|
95
|
+
}
|
|
96
|
+
} catch (error) {
|
|
97
|
+
ui.writeError(`Failed to read ${appName} logs: ${error}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
ui.writeInformation(`=== ${appName} Error Logs ===`);
|
|
101
|
+
try {
|
|
102
|
+
if (existsSync(errLogPath)) {
|
|
103
|
+
const errLogs = readFileSync(errLogPath, "utf-8");
|
|
104
|
+
ui.writeInformation(errLogs);
|
|
105
|
+
} else {
|
|
106
|
+
ui.writeInformation(`No error log file found for ${appName}.`);
|
|
107
|
+
}
|
|
108
|
+
} catch (error) {
|
|
109
|
+
ui.writeError(`Failed to read ${appName} error logs: ${error}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
package/src/utils/safeSpawn.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn, execSync } from "child_process";
|
|
2
2
|
import os from "os";
|
|
3
|
+
import { ui } from "../environment/userInteraction.js";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* @param {string} arg
|
|
@@ -135,3 +136,18 @@ export async function safeSpawn(command, args, options = {}) {
|
|
|
135
136
|
});
|
|
136
137
|
});
|
|
137
138
|
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* @param {string} command
|
|
142
|
+
* @param {string[]} args
|
|
143
|
+
* @param {import("child_process").SpawnOptions} options
|
|
144
|
+
*
|
|
145
|
+
* @returns {Promise<{status: number, stdout: string, stderr: string}>}
|
|
146
|
+
*/
|
|
147
|
+
export async function printVerboseAndSafeSpawn(command, args, options = {}) {
|
|
148
|
+
ui.writeVerbose(`Running: ${command} ${args.join(" ")}`);
|
|
149
|
+
|
|
150
|
+
const result = await safeSpawn(command, args, options);
|
|
151
|
+
|
|
152
|
+
return result;
|
|
153
|
+
}
|