mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 12:10:49 +00:00
Merge pull request #304 from AikidoSec/ultimate-uninstaller
Add uninstallation process for ultimate
This commit is contained in:
commit
ca101270cc
6 changed files with 232 additions and 40 deletions
|
|
@ -16,7 +16,10 @@ import path from "path";
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import { knownAikidoTools } from "../src/shell-integration/helpers.js";
|
import { knownAikidoTools } from "../src/shell-integration/helpers.js";
|
||||||
import { installUltimate } from "../src/installation/installUltimate.js";
|
import {
|
||||||
|
installUltimate,
|
||||||
|
uninstallUltimate,
|
||||||
|
} from "../src/installation/installUltimate.js";
|
||||||
|
|
||||||
/** @type {string} */
|
/** @type {string} */
|
||||||
// This checks the current file's dirname in a way that's compatible with:
|
// This checks the current file's dirname in a way that's compatible with:
|
||||||
|
|
@ -63,10 +66,17 @@ if (tool) {
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
} else if (command === "setup") {
|
} else if (command === "setup") {
|
||||||
setup();
|
setup();
|
||||||
} else if (command === "--ultimate") {
|
} else if (command === "ultimate") {
|
||||||
(async () => {
|
const subCommand = process.argv[3];
|
||||||
await installUltimate();
|
if (subCommand === "uninstall") {
|
||||||
})();
|
(async () => {
|
||||||
|
await uninstallUltimate();
|
||||||
|
})();
|
||||||
|
} else {
|
||||||
|
(async () => {
|
||||||
|
await installUltimate();
|
||||||
|
})();
|
||||||
|
}
|
||||||
} else if (command === "teardown") {
|
} else if (command === "teardown") {
|
||||||
teardownDirectories();
|
teardownDirectories();
|
||||||
teardown();
|
teardown();
|
||||||
|
|
@ -93,7 +103,7 @@ function writeHelp() {
|
||||||
ui.writeInformation(
|
ui.writeInformation(
|
||||||
`Available commands: ${chalk.cyan("setup")}, ${chalk.cyan(
|
`Available commands: ${chalk.cyan("setup")}, ${chalk.cyan(
|
||||||
"teardown",
|
"teardown",
|
||||||
)}, ${chalk.cyan("setup-ci")}, ${chalk.cyan("help")}, ${chalk.cyan(
|
)}, ${chalk.cyan("setup-ci")}, ${chalk.cyan("ultimate")}, ${chalk.cyan("help")}, ${chalk.cyan(
|
||||||
"--version",
|
"--version",
|
||||||
)}`,
|
)}`,
|
||||||
);
|
);
|
||||||
|
|
@ -103,11 +113,6 @@ function writeHelp() {
|
||||||
"safe-chain setup",
|
"safe-chain setup",
|
||||||
)}: This will setup your shell to wrap safe-chain around npm, npx, yarn, pnpm, pnpx, bun, bunx, pip and pip3.`,
|
)}: This will setup your shell to wrap safe-chain around npm, npx, yarn, pnpm, pnpx, bun, bunx, pip and pip3.`,
|
||||||
);
|
);
|
||||||
ui.writeInformation(
|
|
||||||
`- ${chalk.cyan(
|
|
||||||
"safe-chain --ultimate",
|
|
||||||
)}: This installs the ultimate version of safe-chain, enabling protection for more eco-systems (vscode).`,
|
|
||||||
);
|
|
||||||
ui.writeInformation(
|
ui.writeInformation(
|
||||||
`- ${chalk.cyan(
|
`- ${chalk.cyan(
|
||||||
"safe-chain teardown",
|
"safe-chain teardown",
|
||||||
|
|
@ -124,6 +129,19 @@ function writeHelp() {
|
||||||
)}): Display the current version of safe-chain.`,
|
)}): Display the current version of safe-chain.`,
|
||||||
);
|
);
|
||||||
ui.emptyLine();
|
ui.emptyLine();
|
||||||
|
ui.writeInformation(chalk.bold("Ultimate commands:"));
|
||||||
|
ui.emptyLine();
|
||||||
|
ui.writeInformation(
|
||||||
|
`- ${chalk.cyan(
|
||||||
|
"safe-chain ultimate",
|
||||||
|
)}: Install the ultimate version of safe-chain, enabling protection for more eco-systems.`,
|
||||||
|
);
|
||||||
|
ui.writeInformation(
|
||||||
|
`- ${chalk.cyan(
|
||||||
|
"safe-chain ultimate uninstall",
|
||||||
|
)}: Uninstall the ultimate version of safe-chain.`,
|
||||||
|
);
|
||||||
|
ui.emptyLine();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getVersion() {
|
async function getVersion() {
|
||||||
|
|
|
||||||
|
|
@ -3,31 +3,31 @@ import { createHash } from "crypto";
|
||||||
import { pipeline } from "stream/promises";
|
import { pipeline } from "stream/promises";
|
||||||
import fetch from "make-fetch-happen";
|
import fetch from "make-fetch-happen";
|
||||||
|
|
||||||
const ULTIMATE_VERSION = "v0.2.1";
|
const ULTIMATE_VERSION = "v0.2.3";
|
||||||
|
|
||||||
const DOWNLOAD_URLS = {
|
export const DOWNLOAD_URLS = {
|
||||||
win32: {
|
win32: {
|
||||||
x64: {
|
x64: {
|
||||||
url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-windows-amd64.msi`,
|
url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-windows-amd64.msi`,
|
||||||
checksum:
|
checksum:
|
||||||
"sha256:8d86a44d314746099ba50cfae0cc1eae6232522deb348b226da92aae12754eec",
|
"sha256:bd196ae05b876588f828a57c4d19b3e7ad96ba40007cf2b36693dc6e792d28cc",
|
||||||
},
|
},
|
||||||
arm64: {
|
arm64: {
|
||||||
url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-windows-arm64.msi`,
|
url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-windows-arm64.msi`,
|
||||||
checksum:
|
checksum:
|
||||||
"sha256:ab5b8335cc257d53424f73d6681920875083cd9b3f53e52d944bf867a415e027",
|
"sha256:79e046f24405e869494291e77c6d8640c8dc58d2ac1db87d3038e9eb8afbdc8b",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
darwin: {
|
darwin: {
|
||||||
x64: {
|
x64: {
|
||||||
url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-darwin-amd64.pkg`,
|
url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-darwin-amd64.pkg`,
|
||||||
checksum:
|
checksum:
|
||||||
"sha256:73f83d9352c4fd25f7693d9e53bbbb2b7ac70d16217d745495c9efb50dc4a3a6",
|
"sha256:99868cb663eef44d063d995d2dcc063f55b10eb719ee945d05fe8cf5fef5e2a5",
|
||||||
},
|
},
|
||||||
arm64: {
|
arm64: {
|
||||||
url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-darwin-arm64.pkg`,
|
url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-darwin-arm64.pkg`,
|
||||||
checksum:
|
checksum:
|
||||||
"sha256:bd419e9c82488539b629b04c97aa1d2dc90e54ff045bd7277a6b40d26f8ebc73",
|
"sha256:000b334c2eb85d8692be5d23af73f8b9fb686c9db726992223187b341ea79306",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -87,7 +87,7 @@ export function getDownloadInfoForCurrentPlatform() {
|
||||||
* @param {string} expectedChecksum - Format: "algorithm:hash" (e.g., "sha256:abc123...")
|
* @param {string} expectedChecksum - Format: "algorithm:hash" (e.g., "sha256:abc123...")
|
||||||
* @returns {Promise<boolean>}
|
* @returns {Promise<boolean>}
|
||||||
*/
|
*/
|
||||||
async function verifyChecksum(filePath, expectedChecksum) {
|
export async function verifyChecksum(filePath, expectedChecksum) {
|
||||||
const [algorithm, expected] = expectedChecksum.split(":");
|
const [algorithm, expected] = expectedChecksum.split(":");
|
||||||
|
|
||||||
const hash = createHash(algorithm);
|
const hash = createHash(algorithm);
|
||||||
|
|
|
||||||
45
packages/safe-chain/src/installation/downloadAgent.spec.js
Normal file
45
packages/safe-chain/src/installation/downloadAgent.spec.js
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { describe, it, after } from "node:test";
|
||||||
|
import assert from "node:assert";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { unlinkSync } from "node:fs";
|
||||||
|
import {
|
||||||
|
DOWNLOAD_URLS,
|
||||||
|
downloadFile,
|
||||||
|
verifyChecksum,
|
||||||
|
} from "./downloadAgent.js";
|
||||||
|
|
||||||
|
describe("downloadAgent checksums", { timeout: 120_000 }, () => {
|
||||||
|
const downloadedFiles = [];
|
||||||
|
|
||||||
|
after(() => {
|
||||||
|
for (const file of downloadedFiles) {
|
||||||
|
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} checksum matches`, async () => {
|
||||||
|
const destPath = join(
|
||||||
|
tmpdir(),
|
||||||
|
`safe-chain-test-${platform}-${arch}-${Date.now()}`
|
||||||
|
);
|
||||||
|
downloadedFiles.push(destPath);
|
||||||
|
|
||||||
|
await downloadFile(url, destPath);
|
||||||
|
|
||||||
|
const isValid = await verifyChecksum(destPath, checksum);
|
||||||
|
assert.strictEqual(
|
||||||
|
isValid,
|
||||||
|
true,
|
||||||
|
`Checksum mismatch for ${platform}/${arch} (${url})`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -1,16 +1,37 @@
|
||||||
import { tmpdir } from "os";
|
import { tmpdir } from "os";
|
||||||
import { unlinkSync } from "fs";
|
import { unlinkSync } from "fs";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
|
import { execSync, spawnSync } from "child_process";
|
||||||
import { ui } from "../environment/userInteraction.js";
|
import { ui } from "../environment/userInteraction.js";
|
||||||
import { printVerboseAndSafeSpawn } from "../utils/safeSpawn.js";
|
import { printVerboseAndSafeSpawn } from "../utils/safeSpawn.js";
|
||||||
import { downloadAgentToFile, getAgentVersion } from "./downloadAgent.js";
|
import { downloadAgentToFile, getAgentVersion } from "./downloadAgent.js";
|
||||||
import chalk from "chalk";
|
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() {
|
export async function installOnMacOS() {
|
||||||
if (!isRunningAsRoot()) {
|
if (!requireRootPrivileges("sudo safe-chain ultimate")) {
|
||||||
ui.writeError("Root privileges required.");
|
|
||||||
ui.writeInformation("Please run this command with sudo:");
|
|
||||||
ui.writeInformation(" sudo safe-chain --ultimate");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -52,9 +73,51 @@ export async function installOnMacOS() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isRunningAsRoot() {
|
const MACOS_UNINSTALL_SCRIPT =
|
||||||
const rootUserUid = 0;
|
"/Library/Application\\ Support/AikidoSecurity/SafeChainUltimate/scripts/uninstall";
|
||||||
return process.getuid?.() === rootUserUid;
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,31 @@ import { downloadAgentToFile, getAgentVersion } from "./downloadAgent.js";
|
||||||
const WINDOWS_SERVICE_NAME = "SafeChainUltimate";
|
const WINDOWS_SERVICE_NAME = "SafeChainUltimate";
|
||||||
const WINDOWS_APP_NAME = "SafeChain Ultimate";
|
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() {
|
export async function installOnWindows() {
|
||||||
if (!(await isRunningAsAdmin())) {
|
if (!(await requireAdminPrivileges())) {
|
||||||
ui.writeError("Administrator privileges required.");
|
|
||||||
ui.writeInformation(
|
|
||||||
"Please run this command in an elevated terminal (Run as Administrator).",
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -49,6 +68,22 @@ export async function installOnWindows() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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() {
|
async function isRunningAsAdmin() {
|
||||||
// Uses Windows Security API to check if current process has admin privileges.
|
// Uses Windows Security API to check if current process has admin privileges.
|
||||||
// Returns "True" or "False" as a string.
|
// Returns "True" or "False" as a string.
|
||||||
|
|
@ -64,7 +99,11 @@ async function isRunningAsAdmin() {
|
||||||
return result.status === 0 && result.stdout.trim() === "True";
|
return result.status === 0 && result.stdout.trim() === "True";
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uninstallIfInstalled() {
|
/**
|
||||||
|
* 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.
|
// 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.
|
// If found, outputs the product GUID (e.g., "{12345678-1234-...}") needed for msiexec uninstall.
|
||||||
ui.writeVerbose(`Finding product code with PowerShell`);
|
ui.writeVerbose(`Finding product code with PowerShell`);
|
||||||
|
|
@ -76,15 +115,15 @@ async function uninstallIfInstalled() {
|
||||||
{ encoding: "utf8" },
|
{ encoding: "utf8" },
|
||||||
).trim();
|
).trim();
|
||||||
} catch {
|
} catch {
|
||||||
ui.writeVerbose("No existing installation found (fresh install).");
|
return null;
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!productCode) {
|
|
||||||
ui.writeVerbose("No existing installation found (fresh install).");
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
return productCode || null;
|
||||||
|
}
|
||||||
|
|
||||||
ui.writeInformation("🗑️ Removing previous installation...");
|
/**
|
||||||
|
* @param {string} productCode
|
||||||
|
*/
|
||||||
|
async function uninstallByProductCode(productCode) {
|
||||||
ui.writeVerbose(`Found product code: ${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)
|
// Use msiexec to run the msi installer quitely (https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/msiexec)
|
||||||
|
|
@ -103,6 +142,17 @@ async function uninstallIfInstalled() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
* @param {string} msiPath
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,24 @@
|
||||||
import { platform } from "os";
|
import { platform } from "os";
|
||||||
import { ui } from "../environment/userInteraction.js";
|
import { ui } from "../environment/userInteraction.js";
|
||||||
import { initializeCliArguments } from "../config/cliArguments.js";
|
import { initializeCliArguments } from "../config/cliArguments.js";
|
||||||
import { installOnWindows } from "./installOnWindows.js";
|
import { installOnWindows, uninstallOnWindows } from "./installOnWindows.js";
|
||||||
import { installOnMacOS } from "./installOnMacOS.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() {
|
export async function installUltimate() {
|
||||||
initializeCliArguments(process.argv);
|
initializeCliArguments(process.argv);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue