Merge pull request #337 from AikidoSec/remove-dotaikido-in-uninstall

Remove the .aikido directory when uninstalling
This commit is contained in:
bitterpanda 2026-03-19 18:39:08 +01:00 committed by GitHub
commit 5864b09bde
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 129 additions and 76 deletions

View file

@ -202,7 +202,7 @@ You can set the minimum package age through multiple sources (in order of priori
npm install express npm install express
``` ```
3. **Config File** (`~/.aikido/config.json`): 3. **Config File** (`~/.safe-chain/config.json`):
```json ```json
{ {
@ -246,7 +246,7 @@ You can set custom registries through environment variable or config file. Both
export SAFE_CHAIN_PIP_CUSTOM_REGISTRIES="pip.company.com,registry.internal.net" export SAFE_CHAIN_PIP_CUSTOM_REGISTRIES="pip.company.com,registry.internal.net"
``` ```
2. **Config File** (`~/.aikido/config.json`): 2. **Config File** (`~/.safe-chain/config.json`):
```json ```json
{ {

View file

@ -4,7 +4,8 @@
# Use HOME on Unix, USERPROFILE on Windows (PowerShell Core is cross-platform) # Use HOME on Unix, USERPROFILE on Windows (PowerShell Core is cross-platform)
$HomeDir = if ($env:HOME) { $env:HOME } else { $env:USERPROFILE } $HomeDir = if ($env:HOME) { $env:HOME } else { $env:USERPROFILE }
$InstallDir = Join-Path $HomeDir ".safe-chain/bin" $DotSafeChain = Join-Path $HomeDir ".safe-chain"
$InstallDir = Join-Path $DotSafeChain "bin"
# Helper functions # Helper functions
function Write-Info { function Write-Info {
@ -123,34 +124,19 @@ function Uninstall-SafeChain {
Remove-NpmInstallation Remove-NpmInstallation
Remove-VoltaInstallation Remove-VoltaInstallation
# Remove installation directory # Remove .safe-chain directory
if (Test-Path $InstallDir) { if (Test-Path $DotSafeChain) {
Write-Info "Removing installation directory: $InstallDir" Write-Info "Removing installation directory: $DotSafeChain"
try { try {
Remove-Item -Path $InstallDir -Recurse -Force Remove-Item -Path $DotSafeChain -Recurse -Force
Write-Info "Successfully removed installation directory" Write-Info "Successfully removed installation directory"
} }
catch { catch {
Write-Error-Custom "Failed to remove $InstallDir : $_" Write-Error-Custom "Failed to remove $DotSafeChain : $_"
} }
} }
else { else {
Write-Info "Installation directory $InstallDir does not exist. Nothing to remove." Write-Info "Installation directory $DotSafeChain does not exist. Nothing to remove."
}
# Also try to remove the parent .safe-chain directory if it's empty
$parentDir = Split-Path $InstallDir -Parent
if (Test-Path $parentDir) {
$items = Get-ChildItem -Path $parentDir -Force
if ($items.Count -eq 0) {
Write-Info "Removing empty parent directory: $parentDir"
try {
Remove-Item -Path $parentDir -Force
}
catch {
Write-Warn "Could not remove empty parent directory: $_"
}
}
} }
Write-Info "safe-chain has been uninstalled successfully!" Write-Info "safe-chain has been uninstalled successfully!"

View file

@ -7,7 +7,7 @@
set -e # Exit on error set -e # Exit on error
# Configuration # Configuration
INSTALL_DIR="${HOME}/.safe-chain/bin" DOT_SAFE_CHAIN="${HOME}/.safe-chain"
# Colors for output # Colors for output
RED='\033[0;31m' RED='\033[0;31m'
@ -139,7 +139,7 @@ remove_nvm_installation() {
# Main uninstallation # Main uninstallation
main() { main() {
SAFE_CHAIN_LOCATION="$INSTALL_DIR/safe-chain" SAFE_CHAIN_LOCATION="$DOT_SAFE_CHAIN/bin/safe-chain"
if [ -x "$SAFE_CHAIN_LOCATION" ]; then if [ -x "$SAFE_CHAIN_LOCATION" ]; then
info "Running safe-chain teardown..." info "Running safe-chain teardown..."
@ -157,11 +157,11 @@ main() {
remove_nvm_installation remove_nvm_installation
# Remove install dir recursively if it exists # Remove install dir recursively if it exists
if [ -d "$INSTALL_DIR" ]; then if [ -d "$DOT_SAFE_CHAIN" ]; then
info "Removing installation directory $INSTALL_DIR" info "Removing installation directory $DOT_SAFE_CHAIN"
rm -rf "$INSTALL_DIR" || error "Failed to remove $INSTALL_DIR" rm -rf "$DOT_SAFE_CHAIN" || error "Failed to remove $DOT_SAFE_CHAIN"
else else
info "Installation directory $INSTALL_DIR does not exist. Nothing to remove." info "Installation directory $DOT_SAFE_CHAIN does not exist. Nothing to remove."
fi fi
} }

View file

@ -252,7 +252,30 @@ function getDatabaseVersionPath() {
* @returns {string} * @returns {string}
*/ */
function getConfigFilePath() { function getConfigFilePath() {
return path.join(getAikidoDirectory(), "config.json"); const primaryPath = path.join(getSafeChainDirectory(), "config.json");
if (fs.existsSync(primaryPath)) {
return primaryPath;
}
const legacyPath = path.join(getAikidoDirectory(), "config.json");
if (fs.existsSync(legacyPath)) {
return legacyPath;
}
return primaryPath;
}
/**
* @returns {string}
*/
function getSafeChainDirectory() {
const homeDir = os.homedir();
const safeChainDir = path.join(homeDir, ".safe-chain");
if (!fs.existsSync(safeChainDir)) {
fs.mkdirSync(safeChainDir, { recursive: true });
}
return safeChainDir;
} }
/** /**

View file

@ -1,16 +1,35 @@
import { describe, it, beforeEach, afterEach, mock } from "node:test"; import { describe, it, beforeEach, afterEach, mock } from "node:test";
import assert from "node:assert"; import assert from "node:assert";
import os from "os";
import path from "path";
let configFileContent = undefined; const safeChainConfigPath = path.join(os.homedir(), ".safe-chain", "config.json");
const aikidoConfigPath = path.join(os.homedir(), ".aikido", "config.json");
/** @type {Map<string, string>} */
let mockFiles = new Map();
mock.module("fs", { mock.module("fs", {
namedExports: { namedExports: {
existsSync: () => configFileContent !== undefined, existsSync: (filePath) => mockFiles.has(filePath),
readFileSync: () => configFileContent, readFileSync: (filePath) => {
writeFileSync: (content) => (configFileContent = content), if (!mockFiles.has(filePath)) {
throw new Error(`ENOENT: no such file: ${filePath}`);
}
return mockFiles.get(filePath);
},
writeFileSync: (filePath, content) => mockFiles.set(filePath, content),
mkdirSync: () => {}, mkdirSync: () => {},
}, },
}); });
/**
* Helper to set config content at the primary (~/.safe-chain/) location.
* @param {string} content
*/
function setConfigContent(content) {
mockFiles.set(safeChainConfigPath, content);
}
describe("getScanTimeout", async () => { describe("getScanTimeout", async () => {
let originalEnv; let originalEnv;
@ -29,12 +48,11 @@ describe("getScanTimeout", async () => {
delete process.env.AIKIDO_SCAN_TIMEOUT_MS; delete process.env.AIKIDO_SCAN_TIMEOUT_MS;
} }
configFileContent = undefined; mockFiles.clear();
}); });
it("should return default timeout of 10000ms when no config or env var is set", () => { it("should return default timeout of 10000ms when no config or env var is set", () => {
delete process.env.AIKIDO_SCAN_TIMEOUT_MS; delete process.env.AIKIDO_SCAN_TIMEOUT_MS;
configFileContent = undefined;
const timeout = getScanTimeout(); const timeout = getScanTimeout();
@ -43,7 +61,7 @@ describe("getScanTimeout", async () => {
it("should return timeout from config file when set", () => { it("should return timeout from config file when set", () => {
delete process.env.AIKIDO_SCAN_TIMEOUT_MS; delete process.env.AIKIDO_SCAN_TIMEOUT_MS;
configFileContent = JSON.stringify({ scanTimeout: 5000 }); setConfigContent(JSON.stringify({ scanTimeout: 5000 }));
const timeout = getScanTimeout(); const timeout = getScanTimeout();
@ -52,7 +70,7 @@ describe("getScanTimeout", async () => {
it("should prioritize environment variable over config file", () => { it("should prioritize environment variable over config file", () => {
process.env.AIKIDO_SCAN_TIMEOUT_MS = "20000"; process.env.AIKIDO_SCAN_TIMEOUT_MS = "20000";
configFileContent = JSON.stringify({ scanTimeout: 5000 }); setConfigContent(JSON.stringify({ scanTimeout: 5000 }));
const timeout = getScanTimeout(); const timeout = getScanTimeout();
@ -61,7 +79,7 @@ describe("getScanTimeout", async () => {
it("should handle invalid environment variable and fall back to config", () => { it("should handle invalid environment variable and fall back to config", () => {
process.env.AIKIDO_SCAN_TIMEOUT_MS = "invalid"; process.env.AIKIDO_SCAN_TIMEOUT_MS = "invalid";
configFileContent = JSON.stringify({ scanTimeout: 7000 }); setConfigContent(JSON.stringify({ scanTimeout: 7000 }));
const timeout = getScanTimeout(); const timeout = getScanTimeout();
@ -69,8 +87,6 @@ describe("getScanTimeout", async () => {
}); });
it("should ignore zero and negative values and fall back to default", () => { it("should ignore zero and negative values and fall back to default", () => {
configFileContent = undefined;
process.env.AIKIDO_SCAN_TIMEOUT_MS = "0"; process.env.AIKIDO_SCAN_TIMEOUT_MS = "0";
let timeout = getScanTimeout(); let timeout = getScanTimeout();
@ -84,7 +100,7 @@ describe("getScanTimeout", async () => {
it("should ignore textual non-numeric values in environment variable and fall back to config", () => { it("should ignore textual non-numeric values in environment variable and fall back to config", () => {
process.env.AIKIDO_SCAN_TIMEOUT_MS = "fast"; process.env.AIKIDO_SCAN_TIMEOUT_MS = "fast";
configFileContent = JSON.stringify({ scanTimeout: 8000 }); setConfigContent(JSON.stringify({ scanTimeout: 8000 }));
const timeout = getScanTimeout(); const timeout = getScanTimeout();
@ -93,7 +109,7 @@ describe("getScanTimeout", async () => {
it("should ignore textual non-numeric values in config file and fall back to default", () => { it("should ignore textual non-numeric values in config file and fall back to default", () => {
delete process.env.AIKIDO_SCAN_TIMEOUT_MS; delete process.env.AIKIDO_SCAN_TIMEOUT_MS;
configFileContent = JSON.stringify({ scanTimeout: "slow" }); setConfigContent(JSON.stringify({ scanTimeout: "slow" }));
const timeout = getScanTimeout(); const timeout = getScanTimeout();
@ -102,7 +118,7 @@ describe("getScanTimeout", async () => {
it("should ignore textual non-numeric values in both env and config, fall back to default", () => { it("should ignore textual non-numeric values in both env and config, fall back to default", () => {
process.env.AIKIDO_SCAN_TIMEOUT_MS = "quick"; process.env.AIKIDO_SCAN_TIMEOUT_MS = "quick";
configFileContent = JSON.stringify({ scanTimeout: "medium" }); setConfigContent(JSON.stringify({ scanTimeout: "medium" }));
const timeout = getScanTimeout(); const timeout = getScanTimeout();
@ -111,7 +127,7 @@ describe("getScanTimeout", async () => {
it("should ignore mixed alphanumeric strings in environment variable", () => { it("should ignore mixed alphanumeric strings in environment variable", () => {
process.env.AIKIDO_SCAN_TIMEOUT_MS = "5000ms"; process.env.AIKIDO_SCAN_TIMEOUT_MS = "5000ms";
configFileContent = JSON.stringify({ scanTimeout: 6000 }); setConfigContent(JSON.stringify({ scanTimeout: 6000 }));
const timeout = getScanTimeout(); const timeout = getScanTimeout();
@ -120,7 +136,7 @@ describe("getScanTimeout", async () => {
it("should ignore mixed alphanumeric strings in config file", () => { it("should ignore mixed alphanumeric strings in config file", () => {
delete process.env.AIKIDO_SCAN_TIMEOUT_MS; delete process.env.AIKIDO_SCAN_TIMEOUT_MS;
configFileContent = JSON.stringify({ scanTimeout: "3000ms" }); setConfigContent(JSON.stringify({ scanTimeout: "3000ms" }));
const timeout = getScanTimeout(); const timeout = getScanTimeout();
@ -132,19 +148,17 @@ describe("getMinimumPackageAgeHours", async () => {
const { getMinimumPackageAgeHours } = await import("./configFile.js"); const { getMinimumPackageAgeHours } = await import("./configFile.js");
afterEach(() => { afterEach(() => {
configFileContent = undefined; mockFiles.clear();
}); });
it("should return null when config file doesn't exist", () => { it("should return null when config file doesn't exist", () => {
configFileContent = undefined;
const hours = getMinimumPackageAgeHours(); const hours = getMinimumPackageAgeHours();
assert.strictEqual(hours, undefined); assert.strictEqual(hours, undefined);
}); });
it("should return null when config file exists but minimumPackageAgeHours is not set", () => { it("should return null when config file exists but minimumPackageAgeHours is not set", () => {
configFileContent = JSON.stringify({ scanTimeout: 5000 }); setConfigContent(JSON.stringify({ scanTimeout: 5000 }));
const hours = getMinimumPackageAgeHours(); const hours = getMinimumPackageAgeHours();
@ -152,7 +166,7 @@ describe("getMinimumPackageAgeHours", async () => {
}); });
it("should return value from config file when set to valid number", () => { it("should return value from config file when set to valid number", () => {
configFileContent = JSON.stringify({ minimumPackageAgeHours: 48 }); setConfigContent(JSON.stringify({ minimumPackageAgeHours: 48 }));
const hours = getMinimumPackageAgeHours(); const hours = getMinimumPackageAgeHours();
@ -160,7 +174,7 @@ describe("getMinimumPackageAgeHours", async () => {
}); });
it("should handle string numbers in config file", () => { it("should handle string numbers in config file", () => {
configFileContent = JSON.stringify({ minimumPackageAgeHours: "72" }); setConfigContent(JSON.stringify({ minimumPackageAgeHours: "72" }));
const hours = getMinimumPackageAgeHours(); const hours = getMinimumPackageAgeHours();
@ -168,7 +182,7 @@ describe("getMinimumPackageAgeHours", async () => {
}); });
it("should handle decimal values", () => { it("should handle decimal values", () => {
configFileContent = JSON.stringify({ minimumPackageAgeHours: 1.5 }); setConfigContent(JSON.stringify({ minimumPackageAgeHours: 1.5 }));
const hours = getMinimumPackageAgeHours(); const hours = getMinimumPackageAgeHours();
@ -176,7 +190,7 @@ describe("getMinimumPackageAgeHours", async () => {
}); });
it("should return null for non-numeric strings", () => { it("should return null for non-numeric strings", () => {
configFileContent = JSON.stringify({ minimumPackageAgeHours: "invalid" }); setConfigContent(JSON.stringify({ minimumPackageAgeHours: "invalid" }));
const hours = getMinimumPackageAgeHours(); const hours = getMinimumPackageAgeHours();
@ -184,7 +198,7 @@ describe("getMinimumPackageAgeHours", async () => {
}); });
it("should return undefined for values with units suffix", () => { it("should return undefined for values with units suffix", () => {
configFileContent = JSON.stringify({ minimumPackageAgeHours: "48h" }); setConfigContent(JSON.stringify({ minimumPackageAgeHours: "48h" }));
const hours = getMinimumPackageAgeHours(); const hours = getMinimumPackageAgeHours();
@ -192,7 +206,7 @@ describe("getMinimumPackageAgeHours", async () => {
}); });
it("should handle malformed JSON and return null", () => { it("should handle malformed JSON and return null", () => {
configFileContent = "{ invalid json"; setConfigContent("{ invalid json");
const hours = getMinimumPackageAgeHours(); const hours = getMinimumPackageAgeHours();
@ -200,7 +214,7 @@ describe("getMinimumPackageAgeHours", async () => {
}); });
it("should return 0 when minimumPackageAgeHours is set to 0", () => { it("should return 0 when minimumPackageAgeHours is set to 0", () => {
configFileContent = JSON.stringify({ minimumPackageAgeHours: 0 }); setConfigContent(JSON.stringify({ minimumPackageAgeHours: 0 }));
const hours = getMinimumPackageAgeHours(); const hours = getMinimumPackageAgeHours();
@ -208,7 +222,7 @@ describe("getMinimumPackageAgeHours", async () => {
}); });
it("should return 0 when minimumPackageAgeHours is set to string '0'", () => { it("should return 0 when minimumPackageAgeHours is set to string '0'", () => {
configFileContent = JSON.stringify({ minimumPackageAgeHours: "0" }); setConfigContent(JSON.stringify({ minimumPackageAgeHours: "0" }));
const hours = getMinimumPackageAgeHours(); const hours = getMinimumPackageAgeHours();
@ -216,7 +230,7 @@ describe("getMinimumPackageAgeHours", async () => {
}); });
it("should handle negative numeric values", () => { it("should handle negative numeric values", () => {
configFileContent = JSON.stringify({ minimumPackageAgeHours: -24 }); setConfigContent(JSON.stringify({ minimumPackageAgeHours: -24 }));
const hours = getMinimumPackageAgeHours(); const hours = getMinimumPackageAgeHours();
@ -224,7 +238,7 @@ describe("getMinimumPackageAgeHours", async () => {
}); });
it("should handle negative string values", () => { it("should handle negative string values", () => {
configFileContent = JSON.stringify({ minimumPackageAgeHours: "-48" }); setConfigContent(JSON.stringify({ minimumPackageAgeHours: "-48" }));
const hours = getMinimumPackageAgeHours(); const hours = getMinimumPackageAgeHours();
@ -249,19 +263,17 @@ for (const { packageManager, getCustomRegistries } of [
{ {
describe(getCustomRegistries.name, async () => { describe(getCustomRegistries.name, async () => {
afterEach(() => { afterEach(() => {
configFileContent = undefined; mockFiles.clear();
}); });
it("should return empty array when config file doesn't exist", () => { it("should return empty array when config file doesn't exist", () => {
configFileContent = undefined;
const registries = getCustomRegistries(); const registries = getCustomRegistries();
assert.deepStrictEqual(registries, []); assert.deepStrictEqual(registries, []);
}); });
it(`should return empty array when ${packageManager} config is not set`, () => { it(`should return empty array when ${packageManager} config is not set`, () => {
configFileContent = JSON.stringify({ scanTimeout: 5000 }); setConfigContent(JSON.stringify({ scanTimeout: 5000 }));
const registries = getCustomRegistries(); const registries = getCustomRegistries();
@ -269,9 +281,9 @@ for (const { packageManager, getCustomRegistries } of [
}); });
it("should return empty array when customRegistries is not an array", () => { it("should return empty array when customRegistries is not an array", () => {
configFileContent = JSON.stringify({ setConfigContent(JSON.stringify({
[packageManager]: { customRegistries: "not-an-array" }, [packageManager]: { customRegistries: "not-an-array" },
}); }));
const registries = getCustomRegistries(); const registries = getCustomRegistries();
@ -279,11 +291,11 @@ for (const { packageManager, getCustomRegistries } of [
}); });
it("should return array of custom registries when set", () => { it("should return array of custom registries when set", () => {
configFileContent = JSON.stringify({ setConfigContent(JSON.stringify({
[packageManager]: { [packageManager]: {
customRegistries: [`${packageManager}.company.com`, "registry.internal.net"], customRegistries: [`${packageManager}.company.com`, "registry.internal.net"],
}, },
}); }));
const registries = getCustomRegistries(); const registries = getCustomRegistries();
@ -294,7 +306,7 @@ for (const { packageManager, getCustomRegistries } of [
}); });
it("should filter out non-string values", () => { it("should filter out non-string values", () => {
configFileContent = JSON.stringify({ setConfigContent(JSON.stringify({
[packageManager]: { [packageManager]: {
customRegistries: [ customRegistries: [
`${packageManager}.company.com`, `${packageManager}.company.com`,
@ -305,7 +317,7 @@ for (const { packageManager, getCustomRegistries } of [
{}, {},
], ],
}, },
}); }));
const registries = getCustomRegistries(); const registries = getCustomRegistries();
@ -316,9 +328,9 @@ for (const { packageManager, getCustomRegistries } of [
}); });
it("should return empty array for empty customRegistries array", () => { it("should return empty array for empty customRegistries array", () => {
configFileContent = JSON.stringify({ setConfigContent(JSON.stringify({
[packageManager]: { customRegistries: [] }, [packageManager]: { customRegistries: [] },
}); }));
const registries = getCustomRegistries(); const registries = getCustomRegistries();
@ -326,7 +338,7 @@ for (const { packageManager, getCustomRegistries } of [
}); });
it("should handle malformed JSON and return empty array", () => { it("should handle malformed JSON and return empty array", () => {
configFileContent = "{ invalid json"; setConfigContent("{ invalid json");
const registries = getCustomRegistries(); const registries = getCustomRegistries();
@ -334,3 +346,35 @@ for (const { packageManager, getCustomRegistries } of [
}); });
}); });
} }
describe("config file location fallback", async () => {
const { getScanTimeout } = await import("./configFile.js");
afterEach(() => {
mockFiles.clear();
delete process.env.AIKIDO_SCAN_TIMEOUT_MS;
});
it("should read config from ~/.safe-chain/config.json when it exists", () => {
mockFiles.set(safeChainConfigPath, JSON.stringify({ scanTimeout: 3000 }));
assert.strictEqual(getScanTimeout(), 3000);
});
it("should fall back to ~/.aikido/config.json when primary does not exist", () => {
mockFiles.set(aikidoConfigPath, JSON.stringify({ scanTimeout: 4000 }));
assert.strictEqual(getScanTimeout(), 4000);
});
it("should prefer ~/.safe-chain/config.json when both exist", () => {
mockFiles.set(safeChainConfigPath, JSON.stringify({ scanTimeout: 3000 }));
mockFiles.set(aikidoConfigPath, JSON.stringify({ scanTimeout: 4000 }));
assert.strictEqual(getScanTimeout(), 3000);
});
it("should return default when neither config file exists", () => {
assert.strictEqual(getScanTimeout(), 10000);
});
});

View file

@ -32,7 +32,7 @@ function teardown(tools) {
); );
} }
// Removes the line that sources the safe-chain bash initialization script (~/.aikido/scripts/init-posix.sh) // Removes the line that sources the safe-chain bash initialization script (~/.safe-chain/scripts/init-posix.sh)
removeLinesMatchingPattern( removeLinesMatchingPattern(
startupFile, startupFile,
/^source\s+~\/\.safe-chain\/scripts\/init-posix\.sh/, /^source\s+~\/\.safe-chain\/scripts\/init-posix\.sh/,

View file

@ -31,7 +31,7 @@ function teardown(tools) {
); );
} }
// Removes the line that sources the safe-chain zsh initialization script (~/.aikido/scripts/init-posix.sh) // Removes the line that sources the safe-chain zsh initialization script (~/.safe-chain/scripts/init-posix.sh)
removeLinesMatchingPattern( removeLinesMatchingPattern(
startupFile, startupFile,
/^source\s+~\/\.safe-chain\/scripts\/init-posix\.sh/, /^source\s+~\/\.safe-chain\/scripts\/init-posix\.sh/,