mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 12:10:49 +00:00
Merge remote-tracking branch 'origin/main' into feature/new-package-list-pypi
This commit is contained in:
commit
6f1299a29d
9 changed files with 25 additions and 599 deletions
14
install-scripts/install-endpoint-mac.sh
Normal file → Executable file
14
install-scripts/install-endpoint-mac.sh
Normal file → Executable file
|
|
@ -1,14 +1,14 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
# Downloads and installs SafeChain Ultimate endpoint on macOS
|
# Downloads and installs Aikido Endpoint Protection on macOS
|
||||||
#
|
#
|
||||||
# Usage: curl -fsSL <url> | sudo sh -s -- --token <TOKEN>
|
# Usage: curl -fsSL <url> | sudo sh -s -- --token <TOKEN>
|
||||||
|
|
||||||
set -e # Exit on error
|
set -e # Exit on error
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
INSTALL_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v1.2.5/SafeChainUltimate.pkg"
|
INSTALL_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v1.2.7/EndpointProtection.pkg"
|
||||||
DOWNLOAD_SHA256="abc2b0e6c6a4ca33cd893eeb16744f9f2da90013fb1abac301f5c00c2ad8bc30"
|
DOWNLOAD_SHA256="2c180c575b6fbeb1e33b69cf8357a2a7dbf6868b5f98cfb82b83243daccc0cf9"
|
||||||
TOKEN_FILE="/tmp/aikido_endpoint_token.txt"
|
TOKEN_FILE="/tmp/aikido_endpoint_token.txt"
|
||||||
|
|
||||||
# Colors for output
|
# Colors for output
|
||||||
|
|
@ -111,10 +111,10 @@ main() {
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# 2. Download and verify checksum
|
# 2. Download and verify checksum
|
||||||
PKG_FILE=$(mktemp /tmp/SafeChainUltimate.XXXXXX.pkg)
|
PKG_FILE=$(mktemp /tmp/AikidoEndpoint.XXXXXX.pkg)
|
||||||
trap cleanup EXIT
|
trap cleanup EXIT
|
||||||
|
|
||||||
info "Downloading SafeChain Ultimate..."
|
info "Downloading Aikido Endpoint Protection..."
|
||||||
download "$INSTALL_URL" "$PKG_FILE"
|
download "$INSTALL_URL" "$PKG_FILE"
|
||||||
|
|
||||||
info "Verifying checksum..."
|
info "Verifying checksum..."
|
||||||
|
|
@ -124,10 +124,10 @@ main() {
|
||||||
printf "%s" "$TOKEN" > "$TOKEN_FILE"
|
printf "%s" "$TOKEN" > "$TOKEN_FILE"
|
||||||
|
|
||||||
# 4. Install the package
|
# 4. Install the package
|
||||||
info "Installing SafeChain Ultimate..."
|
info "Installing Aikido Endpoint Protection..."
|
||||||
installer -pkg "$PKG_FILE" -target /
|
installer -pkg "$PKG_FILE" -target /
|
||||||
|
|
||||||
info "SafeChain Ultimate installed successfully!"
|
info "Aikido Endpoint Protection installed successfully!"
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# Downloads and installs SafeChain Ultimate endpoint on Windows
|
# Downloads and installs Aikido Endpoint Protection on Windows
|
||||||
#
|
#
|
||||||
# Usage: iex "& { $(iwr '<url>' -UseBasicParsing) } -token <TOKEN>"
|
# Usage: iex "& { $(iwr '<url>' -UseBasicParsing) } -token <TOKEN>"
|
||||||
|
|
||||||
|
|
@ -7,8 +7,8 @@ param(
|
||||||
)
|
)
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
$InstallUrl = "https://github.com/AikidoSec/safechain-internals/releases/download/v1.2.5/SafeChainUltimate.msi"
|
$InstallUrl = "https://github.com/AikidoSec/safechain-internals/releases/download/v1.2.7/EndpointProtection.msi"
|
||||||
$DownloadSha256 = "c4d1be7bb2128473b8e955244dc186b5d3f091f668b43cdd3d810cff9d38193c"
|
$DownloadSha256 = "7bad18d7df9e0654d2edd16a52aea34b0455c3c6d8fb407362d0a86a77cb7d4f"
|
||||||
|
|
||||||
# Ensure TLS 1.2 is enabled for downloads
|
# Ensure TLS 1.2 is enabled for downloads
|
||||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||||
|
|
@ -53,9 +53,9 @@ function Install-Endpoint {
|
||||||
}
|
}
|
||||||
|
|
||||||
# 2. Download the .msi
|
# 2. Download the .msi
|
||||||
$msiFile = Join-Path $env:TEMP "SafeChainUltimate-$([System.Guid]::NewGuid().ToString('N')).msi"
|
$msiFile = Join-Path $env:TEMP "AikidoEndpoint-$([System.Guid]::NewGuid().ToString('N')).msi"
|
||||||
|
|
||||||
Write-Info "Downloading SafeChain Ultimate..."
|
Write-Info "Downloading Aikido Endpoint Protection..."
|
||||||
try {
|
try {
|
||||||
$ProgressPreference = 'SilentlyContinue'
|
$ProgressPreference = 'SilentlyContinue'
|
||||||
Invoke-WebRequest -Uri $InstallUrl -OutFile $msiFile -UseBasicParsing
|
Invoke-WebRequest -Uri $InstallUrl -OutFile $msiFile -UseBasicParsing
|
||||||
|
|
@ -75,13 +75,13 @@ function Install-Endpoint {
|
||||||
Write-Info "Checksum verified successfully."
|
Write-Info "Checksum verified successfully."
|
||||||
|
|
||||||
# 3. Install the package with token passed as MSI property
|
# 3. Install the package with token passed as MSI property
|
||||||
Write-Info "Installing SafeChain Ultimate..."
|
Write-Info "Installing Aikido Endpoint Protection..."
|
||||||
$process = Start-Process -FilePath "msiexec" -ArgumentList "/i", "`"$msiFile`"", "/qn", "/norestart", "AIKIDO_TOKEN=$token" -Wait -PassThru
|
$process = Start-Process -FilePath "msiexec" -ArgumentList "/i", "`"$msiFile`"", "/qn", "/norestart", "AIKIDO_TOKEN=$token" -Wait -PassThru
|
||||||
if ($process.ExitCode -ne 0) {
|
if ($process.ExitCode -ne 0) {
|
||||||
Write-Error-Custom "MSI installer failed (exit code: $($process.ExitCode))."
|
Write-Error-Custom "MSI installer failed (exit code: $($process.ExitCode))."
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Info "SafeChain Ultimate installed successfully!"
|
Write-Info "Aikido Endpoint Protection installed successfully!"
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
# Cleanup
|
# Cleanup
|
||||||
|
|
|
||||||
10
install-scripts/uninstall-endpoint-mac.sh
Normal file → Executable file
10
install-scripts/uninstall-endpoint-mac.sh
Normal file → Executable file
|
|
@ -1,13 +1,13 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
# Uninstalls SafeChain Ultimate endpoint on macOS
|
# Uninstalls Aikido Endpoint Protection on macOS
|
||||||
#
|
#
|
||||||
# Usage: curl -fsSL <url> | sudo sh
|
# Usage: curl -fsSL <url> | sudo sh
|
||||||
|
|
||||||
set -e # Exit on error
|
set -e # Exit on error
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
UNINSTALL_SCRIPT="/Library/Application Support/AikidoSecurity/SafeChainUltimate/scripts/uninstall"
|
UNINSTALL_SCRIPT="/Library/Application Support/AikidoSecurity/EndpointProtection/scripts/uninstall"
|
||||||
|
|
||||||
# Colors for output
|
# Colors for output
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
|
|
@ -38,13 +38,13 @@ main() {
|
||||||
|
|
||||||
# Check if the uninstall script exists
|
# Check if the uninstall script exists
|
||||||
if [ ! -f "$UNINSTALL_SCRIPT" ]; then
|
if [ ! -f "$UNINSTALL_SCRIPT" ]; then
|
||||||
error "SafeChain Ultimate does not appear to be installed (uninstall script not found)."
|
error "Aikido Endpoint Protection does not appear to be installed (uninstall script not found)."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
info "Uninstalling SafeChain Ultimate..."
|
info "Uninstalling Aikido Endpoint Protection..."
|
||||||
"$UNINSTALL_SCRIPT"
|
"$UNINSTALL_SCRIPT"
|
||||||
|
|
||||||
info "SafeChain Ultimate uninstalled successfully!"
|
info "Aikido Endpoint Protection uninstalled successfully!"
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
# Uninstalls SafeChain Ultimate endpoint on Windows
|
# Uninstalls Aikido Endpoint Protection endpoint on Windows
|
||||||
#
|
#
|
||||||
# Usage: iex (iwr '<url>' -UseBasicParsing)
|
# Usage: iex (iwr '<url>' -UseBasicParsing)
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
$AppName = "SafeChain Ultimate"
|
$AppName = "Aikido Endpoint Protection"
|
||||||
|
|
||||||
# Helper functions
|
# Helper functions
|
||||||
function Write-Info {
|
function Write-Info {
|
||||||
|
|
@ -32,22 +32,22 @@ function Uninstall-Endpoint {
|
||||||
}
|
}
|
||||||
|
|
||||||
# Find the installed product
|
# Find the installed product
|
||||||
Write-Info "Looking for SafeChain Ultimate installation..."
|
Write-Info "Looking for Aikido Endpoint Protection installation..."
|
||||||
$app = Get-WmiObject -Class Win32_Product -Filter "Name='$AppName'"
|
$app = Get-WmiObject -Class Win32_Product -Filter "Name='$AppName'"
|
||||||
|
|
||||||
if (-not $app) {
|
if (-not $app) {
|
||||||
Write-Error-Custom "SafeChain Ultimate does not appear to be installed."
|
Write-Error-Custom "Aikido Endpoint Protection does not appear to be installed."
|
||||||
}
|
}
|
||||||
|
|
||||||
$productCode = $app.IdentifyingNumber
|
$productCode = $app.IdentifyingNumber
|
||||||
|
|
||||||
Write-Info "Uninstalling SafeChain Ultimate..."
|
Write-Info "Uninstalling Aikido Endpoint Protection..."
|
||||||
$process = Start-Process -FilePath "msiexec" -ArgumentList "/x", $productCode, "/qn", "/norestart" -Wait -PassThru
|
$process = Start-Process -FilePath "msiexec" -ArgumentList "/x", $productCode, "/qn", "/norestart" -Wait -PassThru
|
||||||
if ($process.ExitCode -ne 0) {
|
if ($process.ExitCode -ne 0) {
|
||||||
Write-Error-Custom "Uninstall failed (exit code: $($process.ExitCode))."
|
Write-Error-Custom "Uninstall failed (exit code: $($process.ExitCode))."
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Info "SafeChain Ultimate uninstalled successfully!"
|
Write-Info "Aikido Endpoint Protection uninstalled successfully!"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Run uninstallation
|
# Run uninstallation
|
||||||
|
|
|
||||||
|
|
@ -1,125 +0,0 @@
|
||||||
import { createWriteStream, createReadStream } from "fs";
|
|
||||||
import { createHash } from "crypto";
|
|
||||||
import { pipeline } from "stream/promises";
|
|
||||||
import fetch from "make-fetch-happen";
|
|
||||||
|
|
||||||
const ULTIMATE_VERSION = "v1.0.0";
|
|
||||||
|
|
||||||
export const DOWNLOAD_URLS = {
|
|
||||||
win32: {
|
|
||||||
x64: {
|
|
||||||
url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-windows-amd64.msi`,
|
|
||||||
checksum:
|
|
||||||
"sha256:c6a36f9b8e55ab6b7e8742cbabc4469d85809237c0f5e6c21af20b36c416ee1d",
|
|
||||||
},
|
|
||||||
arm64: {
|
|
||||||
url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-windows-arm64.msi`,
|
|
||||||
checksum:
|
|
||||||
"sha256:46acd1af6a9938ea194c8ee8b34ca9b47c8de22e088a0791f3c0751dd6239c90",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
darwin: {
|
|
||||||
x64: {
|
|
||||||
url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-darwin-amd64.pkg`,
|
|
||||||
checksum:
|
|
||||||
"sha256:bb1829e8ca422e885baf37bef08dcbe7df7a30f248e2e89c4071564f7d4f3396",
|
|
||||||
},
|
|
||||||
arm64: {
|
|
||||||
url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-darwin-arm64.pkg`,
|
|
||||||
checksum:
|
|
||||||
"sha256:7fe4a785709911cc366d8224b4c290677573b8c4833bd9054768299e55c5f0ed",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds the download URL for the SafeChain Agent installer.
|
|
||||||
* @param {string} fileName
|
|
||||||
*/
|
|
||||||
export function getAgentDownloadUrl(fileName) {
|
|
||||||
return `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/${fileName}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Downloads a file from a URL to a local path.
|
|
||||||
* @param {string} url
|
|
||||||
* @param {string} destPath
|
|
||||||
*/
|
|
||||||
export async function downloadFile(url, destPath) {
|
|
||||||
const response = await fetch(url);
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Download failed: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
await pipeline(response.body, createWriteStream(destPath));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the current agent version.
|
|
||||||
*/
|
|
||||||
export function getAgentVersion() {
|
|
||||||
return ULTIMATE_VERSION;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns download info (url, checksum) for the current OS and architecture.
|
|
||||||
* @returns {{ url: string, checksum: string } | null}
|
|
||||||
*/
|
|
||||||
export function getDownloadInfoForCurrentPlatform() {
|
|
||||||
const platform = process.platform;
|
|
||||||
const arch = process.arch;
|
|
||||||
|
|
||||||
if (!Object.hasOwn(DOWNLOAD_URLS, platform)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const platformUrls =
|
|
||||||
DOWNLOAD_URLS[/** @type {keyof typeof DOWNLOAD_URLS} */ (platform)];
|
|
||||||
|
|
||||||
if (!Object.hasOwn(platformUrls, arch)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return platformUrls[/** @type {keyof typeof platformUrls} */ (arch)];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verifies the checksum of a file.
|
|
||||||
* @param {string} filePath
|
|
||||||
* @param {string} expectedChecksum - Format: "algorithm:hash" (e.g., "sha256:abc123...")
|
|
||||||
* @returns {Promise<boolean>}
|
|
||||||
*/
|
|
||||||
export async function verifyChecksum(filePath, expectedChecksum) {
|
|
||||||
const [algorithm, expected] = expectedChecksum.split(":");
|
|
||||||
|
|
||||||
const hash = createHash(algorithm);
|
|
||||||
|
|
||||||
if (filePath.includes("..")) throw new Error("Invalid file path");
|
|
||||||
const stream = createReadStream(filePath);
|
|
||||||
|
|
||||||
for await (const chunk of stream) {
|
|
||||||
hash.update(chunk);
|
|
||||||
}
|
|
||||||
|
|
||||||
const actual = hash.digest("hex");
|
|
||||||
return actual === expected;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Downloads the SafeChain agent for the current OS/arch and verifies its checksum.
|
|
||||||
* @param {string} fileName - Destination file path
|
|
||||||
* @returns {Promise<string | null>} The file path if successful, null if no download URL for current platform
|
|
||||||
*/
|
|
||||||
export async function downloadAgentToFile(fileName) {
|
|
||||||
const info = getDownloadInfoForCurrentPlatform();
|
|
||||||
if (!info) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
await downloadFile(info.url, fileName);
|
|
||||||
|
|
||||||
const isValid = await verifyChecksum(fileName, info.checksum);
|
|
||||||
if (!isValid) {
|
|
||||||
throw new Error("Checksum verification failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
return fileName;
|
|
||||||
}
|
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
import { describe, it, after } from "node:test";
|
|
||||||
import assert from "node:assert";
|
|
||||||
import { tmpdir } from "node:os";
|
|
||||||
import { join } from "node:path";
|
|
||||||
import { unlinkSync, writeFileSync } from "node:fs";
|
|
||||||
import { createHash } from "node:crypto";
|
|
||||||
import {
|
|
||||||
DOWNLOAD_URLS,
|
|
||||||
verifyChecksum,
|
|
||||||
} from "./downloadAgent.js";
|
|
||||||
|
|
||||||
describe("downloadAgent", () => {
|
|
||||||
const tempFiles = [];
|
|
||||||
|
|
||||||
after(() => {
|
|
||||||
for (const file of tempFiles) {
|
|
||||||
try {
|
|
||||||
unlinkSync(file);
|
|
||||||
} catch {
|
|
||||||
// ignore cleanup errors
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const [platform, architectures] of Object.entries(DOWNLOAD_URLS)) {
|
|
||||||
for (const [arch, { url, checksum }] of Object.entries(architectures)) {
|
|
||||||
it(`${platform}/${arch} has a valid download definition`, () => {
|
|
||||||
assert.match(
|
|
||||||
url,
|
|
||||||
/^https:\/\/github\.com\/AikidoSec\/safechain-internals\/releases\/download\/v\d+\.\d+\.\d+\/.+/,
|
|
||||||
);
|
|
||||||
assert.match(checksum, /^sha256:[a-f0-9]{64}$/);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
it("verifies checksum for a local file", async () => {
|
|
||||||
const destPath = join(tmpdir(), `safe-chain-test-${Date.now()}`);
|
|
||||||
tempFiles.push(destPath);
|
|
||||||
|
|
||||||
writeFileSync(destPath, "safe-chain-test");
|
|
||||||
|
|
||||||
const expectedHash = createHash("sha256")
|
|
||||||
.update("safe-chain-test")
|
|
||||||
.digest("hex");
|
|
||||||
|
|
||||||
assert.equal(
|
|
||||||
await verifyChecksum(destPath, `sha256:${expectedHash}`),
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
assert.equal(
|
|
||||||
await verifyChecksum(destPath, `sha256:${"0".repeat(64)}`),
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,155 +0,0 @@
|
||||||
import { tmpdir } from "os";
|
|
||||||
import { unlinkSync } from "fs";
|
|
||||||
import { join } from "path";
|
|
||||||
import { execSync, spawnSync } from "child_process";
|
|
||||||
import { ui } from "../environment/userInteraction.js";
|
|
||||||
import { printVerboseAndSafeSpawn } from "../utils/safeSpawn.js";
|
|
||||||
import { downloadAgentToFile, getAgentVersion } from "./downloadAgent.js";
|
|
||||||
import chalk from "chalk";
|
|
||||||
|
|
||||||
const MACOS_PKG_IDENTIFIER = "com.aikidosecurity.safechainultimate";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if root privileges are available and displays error message if not.
|
|
||||||
* @param {string} command - The sudo command to show in the error message
|
|
||||||
* @returns {boolean} True if running as root, false otherwise.
|
|
||||||
*/
|
|
||||||
function requireRootPrivileges(command) {
|
|
||||||
if (isRunningAsRoot()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.writeError("Root privileges required.");
|
|
||||||
ui.writeInformation("Please run this command with sudo:");
|
|
||||||
ui.writeInformation(` ${command}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isRunningAsRoot() {
|
|
||||||
const rootUserUid = 0;
|
|
||||||
return process.getuid?.() === rootUserUid;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function installOnMacOS() {
|
|
||||||
if (!requireRootPrivileges("sudo safe-chain ultimate")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pkgPath = join(tmpdir(), `SafeChainUltimate-${Date.now()}.pkg`);
|
|
||||||
|
|
||||||
ui.emptyLine();
|
|
||||||
ui.writeInformation(`📥 Downloading SafeChain Ultimate ${getAgentVersion()}`);
|
|
||||||
ui.writeVerbose(`Destination: ${pkgPath}`);
|
|
||||||
|
|
||||||
const result = await downloadAgentToFile(pkgPath);
|
|
||||||
if (!result) {
|
|
||||||
ui.writeError("No download available for this platform/architecture.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
ui.writeInformation("⚙️ Installing SafeChain Ultimate...");
|
|
||||||
await runPkgInstaller(pkgPath);
|
|
||||||
|
|
||||||
ui.emptyLine();
|
|
||||||
ui.writeInformation(
|
|
||||||
"✅ SafeChain Ultimate installed and started successfully!",
|
|
||||||
);
|
|
||||||
ui.emptyLine();
|
|
||||||
ui.writeInformation(
|
|
||||||
chalk.cyan("🔐 ") +
|
|
||||||
chalk.bold("ACTION REQUIRED: ") +
|
|
||||||
"macOS will show a popup to install our certificate.",
|
|
||||||
);
|
|
||||||
ui.writeInformation(
|
|
||||||
" " +
|
|
||||||
chalk.bold("Please accept the certificate") +
|
|
||||||
" to complete the installation.",
|
|
||||||
);
|
|
||||||
ui.emptyLine();
|
|
||||||
} finally {
|
|
||||||
ui.writeVerbose(`Cleaning up temporary file: ${pkgPath}`);
|
|
||||||
cleanup(pkgPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const MACOS_UNINSTALL_SCRIPT =
|
|
||||||
"/Library/Application\\ Support/AikidoSecurity/SafeChainUltimate/scripts/uninstall";
|
|
||||||
|
|
||||||
export async function uninstallOnMacOS() {
|
|
||||||
if (!requireRootPrivileges("sudo safe-chain ultimate uninstall")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.emptyLine();
|
|
||||||
|
|
||||||
if (!isPackageInstalled()) {
|
|
||||||
ui.writeInformation("SafeChain Ultimate is not installed.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.writeInformation("🗑️ Uninstalling SafeChain Ultimate...");
|
|
||||||
ui.writeVerbose(`Running: ${MACOS_UNINSTALL_SCRIPT}`);
|
|
||||||
|
|
||||||
const result = spawnSync(MACOS_UNINSTALL_SCRIPT, {
|
|
||||||
stdio: "inherit",
|
|
||||||
shell: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.status !== 0) {
|
|
||||||
ui.writeError(
|
|
||||||
`Uninstall script failed (exit code: ${result.status}). Please try again or remove manually.`,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.emptyLine();
|
|
||||||
ui.writeInformation("✅ SafeChain Ultimate has been uninstalled.");
|
|
||||||
ui.emptyLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
function isPackageInstalled() {
|
|
||||||
try {
|
|
||||||
const output = execSync(`pkgutil --pkg-info ${MACOS_PKG_IDENTIFIER}`, {
|
|
||||||
encoding: "utf8",
|
|
||||||
stdio: "pipe",
|
|
||||||
});
|
|
||||||
return output.includes(MACOS_PKG_IDENTIFIER);
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} pkgPath
|
|
||||||
*/
|
|
||||||
async function runPkgInstaller(pkgPath) {
|
|
||||||
// Uses installer to install the package (https://ss64.com/mac/installer.html)
|
|
||||||
// Options:
|
|
||||||
// -pkg (required): The package to be installed.
|
|
||||||
// -target (required): The target volume is specified with the -target parameter.
|
|
||||||
// --> "-target /" installs to the current boot volume.
|
|
||||||
|
|
||||||
const result = await printVerboseAndSafeSpawn(
|
|
||||||
"installer",
|
|
||||||
["-pkg", pkgPath, "-target", "/"],
|
|
||||||
{
|
|
||||||
stdio: "inherit",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.status !== 0) {
|
|
||||||
throw new Error(`PKG installer failed (exit code: ${result.status})`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} pkgPath
|
|
||||||
*/
|
|
||||||
function cleanup(pkgPath) {
|
|
||||||
try {
|
|
||||||
unlinkSync(pkgPath);
|
|
||||||
} catch {
|
|
||||||
ui.writeVerbose("Failed to clean up temporary installer file.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,203 +0,0 @@
|
||||||
import { tmpdir } from "os";
|
|
||||||
import { unlinkSync } from "fs";
|
|
||||||
import { join } from "path";
|
|
||||||
import { execSync } from "child_process";
|
|
||||||
import { ui } from "../environment/userInteraction.js";
|
|
||||||
import { printVerboseAndSafeSpawn, safeSpawn } from "../utils/safeSpawn.js";
|
|
||||||
import { downloadAgentToFile, getAgentVersion } from "./downloadAgent.js";
|
|
||||||
|
|
||||||
const WINDOWS_SERVICE_NAME = "SafeChainUltimate";
|
|
||||||
const WINDOWS_APP_NAME = "SafeChain Ultimate";
|
|
||||||
|
|
||||||
export async function uninstallOnWindows() {
|
|
||||||
if (!(await requireAdminPrivileges())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.emptyLine();
|
|
||||||
|
|
||||||
const productCode = getInstalledProductCode();
|
|
||||||
if (!productCode) {
|
|
||||||
ui.writeInformation("SafeChain Ultimate is not installed.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await stopServiceIfRunning();
|
|
||||||
|
|
||||||
ui.writeInformation("🗑️ Uninstalling SafeChain Ultimate...");
|
|
||||||
await uninstallByProductCode(productCode);
|
|
||||||
|
|
||||||
ui.emptyLine();
|
|
||||||
ui.writeInformation("✅ SafeChain Ultimate has been uninstalled.");
|
|
||||||
ui.emptyLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function installOnWindows() {
|
|
||||||
if (!(await requireAdminPrivileges())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const msiPath = join(tmpdir(), `SafeChainUltimate-${Date.now()}.msi`);
|
|
||||||
|
|
||||||
ui.emptyLine();
|
|
||||||
ui.writeInformation(`📥 Downloading SafeChain Ultimate ${getAgentVersion()}`);
|
|
||||||
ui.writeVerbose(`Destination: ${msiPath}`);
|
|
||||||
|
|
||||||
const result = await downloadAgentToFile(msiPath);
|
|
||||||
if (!result) {
|
|
||||||
ui.writeError("No download available for this platform/architecture.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
ui.emptyLine();
|
|
||||||
await stopServiceIfRunning();
|
|
||||||
await uninstallIfInstalled();
|
|
||||||
|
|
||||||
ui.writeInformation("⚙️ Installing SafeChain Ultimate...");
|
|
||||||
await runMsiInstaller(msiPath);
|
|
||||||
|
|
||||||
ui.emptyLine();
|
|
||||||
ui.writeInformation(
|
|
||||||
"✅ SafeChain Ultimate installed and started successfully!",
|
|
||||||
);
|
|
||||||
ui.emptyLine();
|
|
||||||
} finally {
|
|
||||||
ui.writeVerbose(`Cleaning up temporary file: ${msiPath}`);
|
|
||||||
cleanup(msiPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if admin privileges are available and displays error message if not.
|
|
||||||
* @returns {Promise<boolean>} True if running as admin, false otherwise.
|
|
||||||
*/
|
|
||||||
async function requireAdminPrivileges() {
|
|
||||||
if (await isRunningAsAdmin()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.writeError("Administrator privileges required.");
|
|
||||||
ui.writeInformation(
|
|
||||||
"Please run this command in an elevated terminal (Run as Administrator).",
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function isRunningAsAdmin() {
|
|
||||||
// Uses Windows Security API to check if current process has admin privileges.
|
|
||||||
// Returns "True" or "False" as a string.
|
|
||||||
const result = await safeSpawn(
|
|
||||||
"powershell",
|
|
||||||
[
|
|
||||||
"-Command",
|
|
||||||
"([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)",
|
|
||||||
],
|
|
||||||
{ stdio: "pipe" },
|
|
||||||
);
|
|
||||||
|
|
||||||
return result.status === 0 && result.stdout.trim() === "True";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the MSI product code for SafeChain Ultimate, or null if not installed.
|
|
||||||
* @returns {string | null}
|
|
||||||
*/
|
|
||||||
function getInstalledProductCode() {
|
|
||||||
// Query Win32_Product via WMI to find the installed SafeChain Agent.
|
|
||||||
// If found, outputs the product GUID (e.g., "{12345678-1234-...}") needed for msiexec uninstall.
|
|
||||||
ui.writeVerbose(`Finding product code with PowerShell`);
|
|
||||||
|
|
||||||
let productCode;
|
|
||||||
try {
|
|
||||||
productCode = execSync(
|
|
||||||
`powershell -Command "$app = Get-WmiObject -Class Win32_Product -Filter \\"Name='${WINDOWS_APP_NAME}'\\"; if ($app) { Write-Output $app.IdentifyingNumber }"`,
|
|
||||||
{ encoding: "utf8" },
|
|
||||||
).trim();
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return productCode || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} productCode
|
|
||||||
*/
|
|
||||||
async function uninstallByProductCode(productCode) {
|
|
||||||
ui.writeVerbose(`Found product code: ${productCode}`);
|
|
||||||
|
|
||||||
// Use msiexec to run the msi installer quitely (https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/msiexec)
|
|
||||||
// Options:
|
|
||||||
// - /x: Uninstalls the package.
|
|
||||||
// - /qn: Specifies there's no UI during the installation process.
|
|
||||||
// - /norestart: Stops the device from restarting after the installation completes.
|
|
||||||
const uninstallResult = await printVerboseAndSafeSpawn(
|
|
||||||
"msiexec",
|
|
||||||
["/x", productCode, "/qn", "/norestart"],
|
|
||||||
{ stdio: "inherit" },
|
|
||||||
);
|
|
||||||
|
|
||||||
if (uninstallResult.status !== 0) {
|
|
||||||
throw new Error(`Uninstall failed (exit code: ${uninstallResult.status})`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function uninstallIfInstalled() {
|
|
||||||
const productCode = getInstalledProductCode();
|
|
||||||
if (!productCode) {
|
|
||||||
ui.writeVerbose("No existing installation found (fresh install).");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.writeInformation("🗑️ Removing previous installation...");
|
|
||||||
await uninstallByProductCode(productCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} msiPath
|
|
||||||
*/
|
|
||||||
async function runMsiInstaller(msiPath) {
|
|
||||||
// Use msiexec to run the msi installer quitely (https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/msiexec)
|
|
||||||
// Options:
|
|
||||||
// - /i: Specifies normal installation
|
|
||||||
// - /qn: Specifies there's no UI during the installation process.
|
|
||||||
|
|
||||||
const result = await printVerboseAndSafeSpawn(
|
|
||||||
"msiexec",
|
|
||||||
["/i", msiPath, "/qn"],
|
|
||||||
{
|
|
||||||
stdio: "inherit",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.status !== 0) {
|
|
||||||
throw new Error(`MSI installer failed (exit code: ${result.status})`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function stopServiceIfRunning() {
|
|
||||||
ui.writeInformation("⏹️ Stopping running service...");
|
|
||||||
|
|
||||||
const result = await printVerboseAndSafeSpawn(
|
|
||||||
"net",
|
|
||||||
["stop", WINDOWS_SERVICE_NAME],
|
|
||||||
{
|
|
||||||
stdio: "pipe",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.status !== 0) {
|
|
||||||
ui.writeVerbose("Service not running (will start after installation).");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} msiPath
|
|
||||||
*/
|
|
||||||
function cleanup(msiPath) {
|
|
||||||
try {
|
|
||||||
unlinkSync(msiPath);
|
|
||||||
} catch {
|
|
||||||
ui.writeVerbose("Failed to clean up temporary installer file.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
import { platform } from "os";
|
|
||||||
import { ui } from "../environment/userInteraction.js";
|
|
||||||
import { initializeCliArguments } from "../config/cliArguments.js";
|
|
||||||
import { installOnWindows, uninstallOnWindows } from "./installOnWindows.js";
|
|
||||||
import { installOnMacOS, uninstallOnMacOS } from "./installOnMacOS.js";
|
|
||||||
|
|
||||||
export async function uninstallUltimate() {
|
|
||||||
initializeCliArguments(process.argv);
|
|
||||||
|
|
||||||
const operatingSystem = platform();
|
|
||||||
|
|
||||||
if (operatingSystem === "win32") {
|
|
||||||
await uninstallOnWindows();
|
|
||||||
} else if (operatingSystem === "darwin") {
|
|
||||||
await uninstallOnMacOS();
|
|
||||||
} else {
|
|
||||||
ui.writeInformation(
|
|
||||||
`Uninstall is not yet supported on ${operatingSystem}.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function installUltimate() {
|
|
||||||
const operatingSystem = platform();
|
|
||||||
|
|
||||||
if (operatingSystem === "win32") {
|
|
||||||
await installOnWindows();
|
|
||||||
} else if (operatingSystem === "darwin") {
|
|
||||||
await installOnMacOS();
|
|
||||||
} else {
|
|
||||||
ui.writeInformation(
|
|
||||||
`${operatingSystem} is not supported yet by SafeChain's ultimate version.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue