@aikidosec/safe-chain 1.4.4 â 1.4.8
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 +65 -8
- package/package.json +1 -1
- package/src/api/aikido.js +93 -18
- package/src/config/cliArguments.js +24 -1
- package/src/config/configFile.js +64 -6
- package/src/config/environmentVariables.js +13 -2
- package/src/config/settings.js +51 -4
- package/src/main.js +6 -2
- package/src/packagemanager/_shared/commandErrors.js +17 -0
- package/src/packagemanager/bun/createBunPackageManager.js +2 -7
- package/src/packagemanager/npm/runNpmCommand.js +2 -7
- package/src/packagemanager/npx/runNpxCommand.js +2 -7
- package/src/packagemanager/pip/runPipCommand.js +2 -7
- package/src/packagemanager/pipx/runPipXCommand.js +2 -7
- package/src/packagemanager/pnpm/runPnpmCommand.js +3 -7
- package/src/packagemanager/poetry/createPoetryPackageManager.js +2 -7
- package/src/packagemanager/uv/runUvCommand.js +2 -7
- package/src/packagemanager/yarn/runYarnCommand.js +2 -7
- package/src/registryProxy/certBundle.js +25 -3
- package/src/registryProxy/http-utils.js +63 -0
- package/src/registryProxy/interceptors/createInterceptorForEcoSystem.js +1 -1
- package/src/registryProxy/interceptors/interceptorBuilder.js +37 -4
- package/src/registryProxy/interceptors/minimumPackageAgeExclusions.js +33 -0
- package/src/registryProxy/interceptors/npm/modifyNpmInfo.js +18 -41
- package/src/registryProxy/interceptors/npm/npmInterceptor.js +47 -2
- package/src/registryProxy/interceptors/npm/parseNpmPackageUrl.js +20 -3
- package/src/registryProxy/interceptors/pip/modifyPipInfo.js +167 -0
- package/src/registryProxy/interceptors/pip/modifyPipJsonResponse.js +176 -0
- package/src/registryProxy/interceptors/pip/parsePipPackageUrl.js +162 -0
- package/src/registryProxy/interceptors/pip/pipInterceptor.js +122 -0
- package/src/registryProxy/interceptors/pip/pipMetadataResponseUtils.js +27 -0
- package/src/registryProxy/interceptors/pip/pipMetadataVersionUtils.js +131 -0
- package/src/registryProxy/interceptors/suppressedVersionsState.js +21 -0
- package/src/registryProxy/mitmRequestHandler.js +12 -6
- package/src/registryProxy/registryProxy.js +72 -9
- package/src/scanning/newPackagesDatabaseBuilder.js +71 -0
- package/src/scanning/newPackagesDatabaseWarnings.js +17 -0
- package/src/scanning/newPackagesListCache.js +126 -0
- package/src/scanning/packageNameVariants.js +29 -0
- package/src/shell-integration/setup.js +7 -3
- package/src/shell-integration/shellDetection.js +2 -0
- package/src/shell-integration/supported-shells/bash.js +19 -1
- package/src/shell-integration/supported-shells/fish.js +18 -0
- package/src/shell-integration/supported-shells/powershell.js +18 -0
- package/src/shell-integration/supported-shells/windowsPowershell.js +18 -0
- package/src/shell-integration/supported-shells/zsh.js +19 -1
- package/src/shell-integration/teardown.js +7 -1
- package/src/ultimate/ultimateTroubleshooting.js +1 -1
- package/src/installation/downloadAgent.js +0 -125
- package/src/installation/installOnMacOS.js +0 -155
- package/src/installation/installOnWindows.js +0 -203
- package/src/installation/installUltimate.js +0 -35
- package/src/registryProxy/interceptors/pipInterceptor.js +0 -132
|
@@ -71,6 +71,22 @@ function getStartupFile() {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
function getManualTeardownInstructions() {
|
|
75
|
+
return [
|
|
76
|
+
`Remove the following line from your PowerShell profile (run "echo $PROFILE" to find its location):`,
|
|
77
|
+
` . "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"`,
|
|
78
|
+
`Then restart your terminal or run: . $PROFILE`,
|
|
79
|
+
];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function getManualSetupInstructions() {
|
|
83
|
+
return [
|
|
84
|
+
`Add the following line to your PowerShell profile (run "echo $PROFILE" to find its location):`,
|
|
85
|
+
` . "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"`,
|
|
86
|
+
`Then restart your terminal or run: . $PROFILE`,
|
|
87
|
+
];
|
|
88
|
+
}
|
|
89
|
+
|
|
74
90
|
/**
|
|
75
91
|
* @type {import("../shellDetection.js").Shell}
|
|
76
92
|
*/
|
|
@@ -79,4 +95,6 @@ export default {
|
|
|
79
95
|
isInstalled,
|
|
80
96
|
setup,
|
|
81
97
|
teardown,
|
|
98
|
+
getManualSetupInstructions,
|
|
99
|
+
getManualTeardownInstructions,
|
|
82
100
|
};
|
|
@@ -31,7 +31,7 @@ function teardown(tools) {
|
|
|
31
31
|
);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
// Removes the line that sources the safe-chain zsh initialization script (~/.
|
|
34
|
+
// Removes the line that sources the safe-chain zsh initialization script (~/.safe-chain/scripts/init-posix.sh)
|
|
35
35
|
removeLinesMatchingPattern(
|
|
36
36
|
startupFile,
|
|
37
37
|
/^source\s+~\/\.safe-chain\/scripts\/init-posix\.sh/,
|
|
@@ -66,9 +66,27 @@ function getStartupFile() {
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
function getManualTeardownInstructions() {
|
|
70
|
+
return [
|
|
71
|
+
`Remove the following line from your ~/.zshrc file:`,
|
|
72
|
+
` source ~/.safe-chain/scripts/init-posix.sh`,
|
|
73
|
+
`Then restart your terminal or run: source ~/.zshrc`,
|
|
74
|
+
];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function getManualSetupInstructions() {
|
|
78
|
+
return [
|
|
79
|
+
`Add the following line to your ~/.zshrc file:`,
|
|
80
|
+
` source ~/.safe-chain/scripts/init-posix.sh`,
|
|
81
|
+
`Then restart your terminal or run: source ~/.zshrc`,
|
|
82
|
+
];
|
|
83
|
+
}
|
|
84
|
+
|
|
69
85
|
export default {
|
|
70
86
|
name: shellName,
|
|
71
87
|
isInstalled,
|
|
72
88
|
setup,
|
|
73
89
|
teardown,
|
|
90
|
+
getManualSetupInstructions,
|
|
91
|
+
getManualTeardownInstructions,
|
|
74
92
|
};
|
|
@@ -47,8 +47,14 @@ export async function teardown() {
|
|
|
47
47
|
ui.writeError(
|
|
48
48
|
`${chalk.bold("- " + shell.name + ":")} ${chalk.red(
|
|
49
49
|
"Teardown failed"
|
|
50
|
-
)}
|
|
50
|
+
)}`
|
|
51
51
|
);
|
|
52
|
+
ui.emptyLine();
|
|
53
|
+
ui.writeInformation(` ${chalk.bold("To tear down manually:")}`);
|
|
54
|
+
for (const instruction of shell.getManualTeardownInstructions()) {
|
|
55
|
+
ui.writeInformation(` ${instruction}`);
|
|
56
|
+
}
|
|
57
|
+
ui.emptyLine();
|
|
52
58
|
}
|
|
53
59
|
}
|
|
54
60
|
|
|
@@ -1,125 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,155 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,203 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
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
|
-
}
|