Add uninstallation process for ultimate

This commit is contained in:
Sander Declerck 2026-01-27 11:29:19 +01:00
parent dcebe734df
commit 53b7d9295c
No known key found for this signature in database
4 changed files with 167 additions and 12 deletions

View file

@ -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:
@ -67,6 +70,10 @@ if (tool) {
(async () => { (async () => {
await installUltimate(); await installUltimate();
})(); })();
} else if (command === "--uninstall-ultimate") {
(async () => {
await uninstallUltimate();
})();
} else if (command === "teardown") { } else if (command === "teardown") {
teardownDirectories(); teardownDirectories();
teardown(); teardown();
@ -108,6 +115,11 @@ function writeHelp() {
"safe-chain --ultimate", "safe-chain --ultimate",
)}: This installs the ultimate version of safe-chain, enabling protection for more eco-systems (vscode).`, )}: 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( ui.writeInformation(
`- ${chalk.cyan( `- ${chalk.cyan(
"safe-chain teardown", "safe-chain teardown",

View file

@ -1,11 +1,14 @@
import { tmpdir } from "os"; import { tmpdir } from "os";
import { unlinkSync } from "fs"; import { unlinkSync, rmSync } from "fs";
import { join } from "path"; import { join } from "path";
import { execSync } 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";
export async function installOnMacOS() { export async function installOnMacOS() {
if (!isRunningAsRoot()) { if (!isRunningAsRoot()) {
ui.writeError("Root privileges required."); 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() { function isRunningAsRoot() {
const rootUserUid = 0; const rootUserUid = 0;
return process.getuid?.() === rootUserUid; return process.getuid?.() === rootUserUid;

View file

@ -9,6 +9,34 @@ 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 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() { export async function installOnWindows() {
if (!(await isRunningAsAdmin())) { if (!(await isRunningAsAdmin())) {
ui.writeError("Administrator privileges required."); ui.writeError("Administrator privileges required.");
@ -64,7 +92,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 +108,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) { return productCode || null;
ui.writeVerbose("No existing installation found (fresh install).");
return;
} }
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 +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 * @param {string} msiPath
*/ */

View file

@ -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);