diff --git a/packages/safe-chain/bin/safe-chain.js b/packages/safe-chain/bin/safe-chain.js index d048ce1..06add7e 100755 --- a/packages/safe-chain/bin/safe-chain.js +++ b/packages/safe-chain/bin/safe-chain.js @@ -16,7 +16,10 @@ import path from "path"; import { fileURLToPath } from "url"; import fs from "fs"; 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} */ // This checks the current file's dirname in a way that's compatible with: @@ -67,6 +70,10 @@ if (tool) { (async () => { await installUltimate(); })(); +} else if (command === "--uninstall-ultimate") { + (async () => { + await uninstallUltimate(); + })(); } else if (command === "teardown") { teardownDirectories(); teardown(); @@ -108,6 +115,11 @@ function writeHelp() { "safe-chain --ultimate", )}: This installs the ultimate version of safe-chain, enabling protection for more eco-systems (vscode).`, ); + ui.writeInformation( + `- ${chalk.cyan( + "safe-chain --uninstall-ultimate", + )}: This uninstalls the ultimate version of safe-chain.`, + ); ui.writeInformation( `- ${chalk.cyan( "safe-chain teardown", diff --git a/packages/safe-chain/src/installation/installOnMacOS.js b/packages/safe-chain/src/installation/installOnMacOS.js index 0d475b1..b2d39ce 100644 --- a/packages/safe-chain/src/installation/installOnMacOS.js +++ b/packages/safe-chain/src/installation/installOnMacOS.js @@ -1,11 +1,14 @@ import { tmpdir } from "os"; -import { unlinkSync } from "fs"; +import { unlinkSync, rmSync } from "fs"; import { join } from "path"; +import { execSync } 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"; + export async function installOnMacOS() { if (!isRunningAsRoot()) { ui.writeError("Root privileges required."); @@ -52,6 +55,87 @@ export async function installOnMacOS() { } } +export async function uninstallOnMacOS() { + if (!isRunningAsRoot()) { + ui.writeError("Root privileges required."); + ui.writeInformation("Please run this command with sudo:"); + ui.writeInformation(" sudo safe-chain --uninstall-ultimate"); + return; + } + + ui.emptyLine(); + + if (!isPackageInstalled()) { + ui.writeInformation("SafeChain Ultimate is not installed."); + return; + } + + ui.writeInformation("โน๏ธ Stopping service..."); + await stopService(); + + ui.writeInformation("๐Ÿ—‘๏ธ Removing installed files..."); + removeKnownFiles(); + + ui.writeInformation("๐Ÿงน Forgetting package receipt..."); + forgetPackage(); + + 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; + } +} + +async function stopService() { + const result = await printVerboseAndSafeSpawn( + "launchctl", + ["bootout", `system/${MACOS_PKG_IDENTIFIER}`], + { stdio: "pipe" }, + ); + + if (result.status !== 0) { + ui.writeVerbose("Service not running (will continue with uninstall)."); + } +} + +const MACOS_KNOWN_PATHS = [ + "/Library/Application Support/AikidoSecurity/SafeChainUltimate", + "/Library/Logs/AikidoSecurity/SafeChainUltimate", + `/Library/LaunchDaemons/${MACOS_PKG_IDENTIFIER}.plist`, +]; + +function removeKnownFiles() { + for (const filePath of MACOS_KNOWN_PATHS) { + try { + rmSync(filePath, { recursive: true, force: true }); + ui.writeVerbose(`Removed: ${filePath}`); + } catch { + ui.writeVerbose(`Failed to remove: ${filePath}`); + } + } +} + +function forgetPackage() { + try { + execSync(`pkgutil --forget ${MACOS_PKG_IDENTIFIER}`, { + encoding: "utf8", + stdio: "pipe", + }); + } catch { + ui.writeVerbose("Failed to forget package receipt."); + } +} + function isRunningAsRoot() { const rootUserUid = 0; return process.getuid?.() === rootUserUid; diff --git a/packages/safe-chain/src/installation/installOnWindows.js b/packages/safe-chain/src/installation/installOnWindows.js index 0741fb7..16bf2b7 100644 --- a/packages/safe-chain/src/installation/installOnWindows.js +++ b/packages/safe-chain/src/installation/installOnWindows.js @@ -9,6 +9,34 @@ import { downloadAgentToFile, getAgentVersion } from "./downloadAgent.js"; const WINDOWS_SERVICE_NAME = "SafeChainUltimate"; const WINDOWS_APP_NAME = "SafeChain Ultimate"; +export async function uninstallOnWindows() { + if (!(await isRunningAsAdmin())) { + ui.writeError("Administrator privileges required."); + ui.writeInformation( + "Please run this command in an elevated terminal (Run as Administrator).", + ); + return; + } + + ui.emptyLine(); + + const productCode = getInstalledProductCode(); + if (!productCode) { + ui.writeInformation("SafeChain Ultimate is not installed."); + return; + } + + ui.writeInformation("โน๏ธ Stopping running service..."); + 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 isRunningAsAdmin())) { ui.writeError("Administrator privileges required."); @@ -64,7 +92,11 @@ async function isRunningAsAdmin() { 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. // If found, outputs the product GUID (e.g., "{12345678-1234-...}") needed for msiexec uninstall. ui.writeVerbose(`Finding product code with PowerShell`); @@ -76,15 +108,15 @@ async function uninstallIfInstalled() { { encoding: "utf8" }, ).trim(); } catch { - ui.writeVerbose("No existing installation found (fresh install)."); - return; - } - if (!productCode) { - ui.writeVerbose("No existing installation found (fresh install)."); - return; + return null; } + return productCode || null; +} - ui.writeInformation("๐Ÿ—‘๏ธ Removing previous installation..."); +/** + * @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) @@ -103,6 +135,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 */ diff --git a/packages/safe-chain/src/installation/installUltimate.js b/packages/safe-chain/src/installation/installUltimate.js index a79a2b1..cfcdcca 100644 --- a/packages/safe-chain/src/installation/installUltimate.js +++ b/packages/safe-chain/src/installation/installUltimate.js @@ -1,8 +1,24 @@ import { platform } from "os"; import { ui } from "../environment/userInteraction.js"; import { initializeCliArguments } from "../config/cliArguments.js"; -import { installOnWindows } from "./installOnWindows.js"; -import { installOnMacOS } from "./installOnMacOS.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() { initializeCliArguments(process.argv);