Merge pull request #295 from AikidoSec/ultimate-installer

Add safe-chain ultimate installer
This commit is contained in:
bitterpanda 2026-01-22 14:13:55 +01:00 committed by GitHub
commit 8e966b0609
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 434 additions and 17 deletions

View file

@ -16,6 +16,7 @@ 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";
/** @type {string} */
// This checks the current file's dirname in a way that's compatible with:
@ -62,6 +63,10 @@ if (tool) {
process.exit(0);
} else if (command === "setup") {
setup();
} else if (command === "--ultimate") {
(async () => {
await installUltimate();
})();
} else if (command === "teardown") {
teardownDirectories();
teardown();
@ -82,36 +87,41 @@ if (tool) {
function writeHelp() {
ui.writeInformation(
chalk.bold("Usage: ") + chalk.cyan("safe-chain <command>")
chalk.bold("Usage: ") + chalk.cyan("safe-chain <command>"),
);
ui.emptyLine();
ui.writeInformation(
`Available commands: ${chalk.cyan("setup")}, ${chalk.cyan(
"teardown"
"teardown",
)}, ${chalk.cyan("setup-ci")}, ${chalk.cyan("help")}, ${chalk.cyan(
"--version"
)}`
"--version",
)}`,
);
ui.emptyLine();
ui.writeInformation(
`- ${chalk.cyan(
"safe-chain setup"
)}: This will setup your shell to wrap safe-chain around npm, npx, yarn, pnpm, pnpx, bun, bunx, pip and pip3.`
"safe-chain setup",
)}: 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 teardown"
)}: This will remove safe-chain aliases from your shell configuration.`
"safe-chain --ultimate",
)}: This installs the ultimate version of safe-chain, enabling protection for more eco-systems (vscode).`,
);
ui.writeInformation(
`- ${chalk.cyan(
"safe-chain setup-ci"
)}: This will setup safe-chain for CI environments by creating shims and modifying the PATH.`
"safe-chain teardown",
)}: This will remove safe-chain aliases from your shell configuration.`,
);
ui.writeInformation(
`- ${chalk.cyan(
"safe-chain setup-ci",
)}: This will setup safe-chain for CI environments by creating shims and modifying the PATH.`,
);
ui.writeInformation(
`- ${chalk.cyan("safe-chain --version")} (or ${chalk.cyan(
"-v"
)}): Display the current version of safe-chain.`
"-v",
)}): Display the current version of safe-chain.`,
);
ui.emptyLine();
}

View file

@ -0,0 +1,125 @@
import { createWriteStream, createReadStream } from "fs";
import { createHash } from "crypto";
import { pipeline } from "stream/promises";
import fetch from "make-fetch-happen";
const ULTIMATE_VERSION = "v0.2.1";
const DOWNLOAD_URLS = {
win32: {
x64: {
url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-windows-amd64.msi`,
checksum:
"sha256:8d86a44d314746099ba50cfae0cc1eae6232522deb348b226da92aae12754eec",
},
arm64: {
url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-windows-arm64.msi`,
checksum:
"sha256:ab5b8335cc257d53424f73d6681920875083cd9b3f53e52d944bf867a415e027",
},
},
darwin: {
x64: {
url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-darwin-amd64.pkg`,
checksum:
"sha256:73f83d9352c4fd25f7693d9e53bbbb2b7ac70d16217d745495c9efb50dc4a3a6",
},
arm64: {
url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-darwin-arm64.pkg`,
checksum:
"sha256:bd419e9c82488539b629b04c97aa1d2dc90e54ff045bd7277a6b40d26f8ebc73",
},
},
};
/**
* 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>}
*/
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;
}

View file

@ -0,0 +1,92 @@
import { tmpdir } from "os";
import { unlinkSync } from "fs";
import { join } from "path";
import { ui } from "../environment/userInteraction.js";
import { printVerboseAndSafeSpawn } from "../utils/safeSpawn.js";
import { downloadAgentToFile, getAgentVersion } from "./downloadAgent.js";
import chalk from "chalk";
export async function installOnMacOS() {
if (!isRunningAsRoot()) {
ui.writeError("Root privileges required.");
ui.writeInformation("Please run this command with sudo:");
ui.writeInformation(" 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);
}
}
function isRunningAsRoot() {
const rootUserUid = 0;
return process.getuid?.() === rootUserUid;
}
/**
* @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.");
}
}

View file

@ -0,0 +1,153 @@
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 installOnWindows() {
if (!(await isRunningAsAdmin())) {
ui.writeError("Administrator privileges required.");
ui.writeInformation(
"Please run this command in an elevated terminal (Run as Administrator).",
);
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);
}
}
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";
}
async function uninstallIfInstalled() {
// 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 {
ui.writeVerbose("No existing installation found (fresh install).");
return;
}
if (!productCode) {
ui.writeVerbose("No existing installation found (fresh install).");
return;
}
ui.writeInformation("🗑️ Removing previous installation...");
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})`);
}
}
/**
* @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.");
}
}

View file

@ -0,0 +1,21 @@
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";
export async function installUltimate() {
initializeCliArguments(process.argv);
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.`,
);
}
}

View file

@ -73,20 +73,20 @@ export async function main(args) {
ui.writeVerbose(
`${chalk.green("✔")} Safe-chain: Scanned ${
auditStats.totalPackages
} packages, no malware found.`
} packages, no malware found.`,
);
}
if (proxy.hasSuppressedVersions()) {
ui.writeInformation(
`${chalk.yellow(
""
)} Safe-chain: Some package versions were suppressed due to minimum age requirement.`
"",
)} Safe-chain: Some package versions were suppressed due to minimum age requirement.`,
);
ui.writeInformation(
` To disable this check, use: ${chalk.cyan(
"--safe-chain-skip-minimum-package-age"
)}`
"--safe-chain-skip-minimum-package-age",
)}`,
);
}

View file

@ -1,5 +1,6 @@
import { spawn, execSync } from "child_process";
import os from "os";
import { ui } from "../environment/userInteraction.js";
/**
* @param {string} arg
@ -135,3 +136,18 @@ export async function safeSpawn(command, args, options = {}) {
});
});
}
/**
* @param {string} command
* @param {string[]} args
* @param {import("child_process").SpawnOptions} options
*
* @returns {Promise<{status: number, stdout: string, stderr: string}>}
*/
export async function printVerboseAndSafeSpawn(command, args, options = {}) {
ui.writeVerbose(`Running: ${command} ${args.join(" ")}`);
const result = await safeSpawn(command, args, options);
return result;
}