From dabb75810428a9806df5cbeab67e3d27ab073d11 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 5 Sep 2025 16:09:15 +0200 Subject: [PATCH] Implement teardown logic --- packages/safe-chain-bun/bin/safe-chain-bun.js | 8 +- packages/safe-chain-bun/src/setup.js | 48 +---- packages/safe-chain-bun/src/setup.spec.js | 3 +- packages/safe-chain-bun/src/teardown.js | 35 ++++ packages/safe-chain-bun/src/teardown.spec.js | 182 ++++++++++++++++++ packages/safe-chain-bun/src/toml-utils.js | 118 ++++++++++++ 6 files changed, 345 insertions(+), 49 deletions(-) create mode 100644 packages/safe-chain-bun/src/teardown.js create mode 100644 packages/safe-chain-bun/src/teardown.spec.js create mode 100644 packages/safe-chain-bun/src/toml-utils.js diff --git a/packages/safe-chain-bun/bin/safe-chain-bun.js b/packages/safe-chain-bun/bin/safe-chain-bun.js index 115d42b..e0b15d9 100755 --- a/packages/safe-chain-bun/bin/safe-chain-bun.js +++ b/packages/safe-chain-bun/bin/safe-chain-bun.js @@ -1,6 +1,7 @@ #!/usr/bin/env node import { setup } from "../src/setup.js"; +import { teardown } from "../src/teardown.js"; if (process.argv.length < 3) { console.error("No command provided. Please provide a command to execute."); @@ -19,6 +20,9 @@ if (command === "help" || command === "--help" || command === "-h") { if (command === "setup") { const configFile = process.argv[3]; setup(configFile); +} else if (command === "teardown") { + const configFile = process.argv[3]; + teardown(configFile); } else { console.error(`Unknown command: ${command}.`); console.log(); @@ -29,9 +33,11 @@ if (command === "setup") { function writeHelp() { console.log("Usage: safe-chain-bun "); console.log(); - console.log("Available commands: setup, help"); + console.log("Available commands: setup, teardown, help"); console.log(); console.log("- safe-chain-bun setup: Register Safe-Chain-Bun as a security scanner in ~/.bunfig.toml"); console.log("- safe-chain-bun setup : Register Safe-Chain-Bun as a security scanner in specified bunfig.toml file"); + console.log("- safe-chain-bun teardown: Remove Safe-Chain-Bun scanner from ~/.bunfig.toml"); + console.log("- safe-chain-bun teardown : Remove Safe-Chain-Bun scanner from specified bunfig.toml file"); console.log(); } \ No newline at end of file diff --git a/packages/safe-chain-bun/src/setup.js b/packages/safe-chain-bun/src/setup.js index ef31316..2eb9e50 100644 --- a/packages/safe-chain-bun/src/setup.js +++ b/packages/safe-chain-bun/src/setup.js @@ -1,6 +1,6 @@ import fs from "fs"; import path from "path"; -import os from "os"; +import { getGlobalConfigPath, addScannerToToml } from "./toml-utils.js"; /** * Main setup function that registers safe-chain-bun as a security scanner @@ -31,13 +31,6 @@ export function setup(configFile) { } } -/** - * Gets the global bunfig.toml path - * @returns {string} Path to global bunfig.toml - */ -function getGlobalConfigPath() { - return path.join(os.homedir(), ".bunfig.toml"); -} /** * Updates or creates a bunfig.toml file with safe-chain-bun scanner configuration @@ -86,42 +79,3 @@ function updateBunfigFile(filePath, isGlobal) { } } -/** - * Adds or updates the scanner configuration in TOML content - * @param {string} content - Existing TOML content - * @returns {{content: string, changed: boolean}} Updated content and change status - */ -export function addScannerToToml(content) { - const scannerLine = 'scanner = "@aikidosec/safe-chain-bun"'; - - if (content.includes(scannerLine)) { - return { content, changed: false }; - } - - const lines = content.split(/[\r\n\u2028\u2029]+/); - const installSecurityRegex = /^\[install\.security\]$/; - const scannerRegex = /^scanner\s*=.*$/; - - const securitySectionIndex = lines.findIndex(line => installSecurityRegex.test(line)); - - if (securitySectionIndex >= 0) { - const scannerLineIndex = lines.findIndex((line, index) => - index > securitySectionIndex && scannerRegex.test(line) - ); - - if (scannerLineIndex >= 0) { - lines[scannerLineIndex] = scannerLine; - } else { - lines.splice(securitySectionIndex + 1, 0, scannerLine); - } - } else { - if (lines[lines.length - 1] !== '') { - lines.push(''); - } - lines.push('[install.security]'); - lines.push(scannerLine); - lines.push(''); - } - - return { content: lines.join(os.EOL), changed: true }; -} \ No newline at end of file diff --git a/packages/safe-chain-bun/src/setup.spec.js b/packages/safe-chain-bun/src/setup.spec.js index ecd1d23..328d527 100644 --- a/packages/safe-chain-bun/src/setup.spec.js +++ b/packages/safe-chain-bun/src/setup.spec.js @@ -3,7 +3,8 @@ import assert from "node:assert"; import fs from "fs"; import path from "path"; import os from "os"; -import { addScannerToToml, setup } from "./setup.js"; +import { addScannerToToml } from "./toml-utils.js"; +import { setup } from "./setup.js"; describe("addScannerToToml", () => { it("should add scanner to empty content", () => { diff --git a/packages/safe-chain-bun/src/teardown.js b/packages/safe-chain-bun/src/teardown.js new file mode 100644 index 0000000..6f50ae5 --- /dev/null +++ b/packages/safe-chain-bun/src/teardown.js @@ -0,0 +1,35 @@ +import fs from "fs"; +import path from "path"; +import { getGlobalConfigPath, removeScannerFromToml } from "./toml-utils.js"; + +/** + * Main teardown function that removes safe-chain-bun as a security scanner + * @param {string|undefined} configFile - Optional path to specific bunfig.toml file + */ +export function teardown(configFile) { + try { + const targetFile = configFile ? path.resolve(configFile) : getGlobalConfigPath(); + const isGlobal = !configFile; + + if (!fs.existsSync(targetFile)) { + const displayPath = isGlobal ? "~/.bunfig.toml" : configFile; + console.log(`ℹ️ Config file not found: ${displayPath}`); + return; + } + + const content = fs.readFileSync(targetFile, "utf8"); + const result = removeScannerFromToml(content); + + if (result.changed) { + fs.writeFileSync(targetFile, result.content, "utf8"); + const displayPath = isGlobal ? "~/.bunfig.toml" : configFile; + console.log(`✅ Safe-Chain-Bun scanner removed from ${displayPath}`); + } else { + const displayPath = isGlobal ? "~/.bunfig.toml" : configFile; + console.log(`ℹ️ Safe-Chain-Bun scanner not found in ${displayPath}`); + } + } catch (error) { + console.error(`❌ Failed to remove Safe-Chain-Bun scanner: ${error.message}`); + process.exit(1); + } +} \ No newline at end of file diff --git a/packages/safe-chain-bun/src/teardown.spec.js b/packages/safe-chain-bun/src/teardown.spec.js new file mode 100644 index 0000000..604082a --- /dev/null +++ b/packages/safe-chain-bun/src/teardown.spec.js @@ -0,0 +1,182 @@ +import { describe, it, before, after, mock } from "node:test"; +import assert from "node:assert"; +import fs from "fs"; +import path from "path"; +import os from "os"; +import { removeScannerFromToml } from "./toml-utils.js"; +import { teardown } from "./teardown.js"; + +describe("removeScannerFromToml", () => { + it("should return unchanged if scanner not present", () => { + const input = `[build]\ntarget = "node"`; + const result = removeScannerFromToml(input); + assert.strictEqual(result.changed, false); + assert.strictEqual(result.content, input); + }); + + it("should remove scanner line only", () => { + const input = `[install.security]\nscanner = "@aikidosec/safe-chain-bun"\nother = "config"`; + const result = removeScannerFromToml(input); + assert.strictEqual(result.changed, true); + assert.ok(!result.content.includes('scanner = "@aikidosec/safe-chain-bun"')); + assert.ok(result.content.includes('other = "config"')); + assert.ok(result.content.includes('[install.security]')); + }); + + it("should remove entire [install.security] section if only scanner present", () => { + const input = `[build]\ntarget = "node"\n\n[install.security]\nscanner = "@aikidosec/safe-chain-bun"\n\n[test]\npreload = "./setup.ts"`; + const result = removeScannerFromToml(input); + assert.strictEqual(result.changed, true); + assert.ok(!result.content.includes('[install.security]')); + assert.ok(!result.content.includes('scanner = "@aikidosec/safe-chain-bun"')); + assert.ok(result.content.includes('[build]')); + assert.ok(result.content.includes('[test]')); + }); + + it("should handle section with comments and whitespace", () => { + const input = `[install.security]\n# Security configuration\nscanner = "@aikidosec/safe-chain-bun"\n\n# End of security`; + const result = removeScannerFromToml(input); + assert.strictEqual(result.changed, true); + assert.ok(!result.content.includes('[install.security]')); + assert.ok(!result.content.includes('scanner = "@aikidosec/safe-chain-bun"')); + }); + + it("should preserve other scanner configurations", () => { + const input = `[install.security]\nscanner = "@other/scanner"`; + const result = removeScannerFromToml(input); + assert.strictEqual(result.changed, false); + assert.strictEqual(result.content, input); + }); + + it("should handle mixed line endings", () => { + const input = `[install.security]\r\nscanner = "@aikidosec/safe-chain-bun"\r\n\r\n[build]\r\ntarget = "node"`; + const result = removeScannerFromToml(input); + assert.strictEqual(result.changed, true); + assert.ok(!result.content.includes('[install.security]')); + assert.ok(result.content.includes('[build]')); + assert.ok(result.content.includes('target = "node"')); + }); + + it("should handle complex TOML with multiple sections", () => { + const input = `[install] +registry = "https://registry.npmjs.org/" + +[install.security] +scanner = "@aikidosec/safe-chain-bun" + +[build] +target = "node"`; + + const result = removeScannerFromToml(input); + assert.strictEqual(result.changed, true); + assert.ok(!result.content.includes('[install.security]')); + assert.ok(!result.content.includes('scanner = "@aikidosec/safe-chain-bun"')); + assert.ok(result.content.includes('[install]')); + assert.ok(result.content.includes('[build]')); + }); +}); + +describe("teardown function", () => { + let tempDir; + let originalConsoleLog; + let originalConsoleError; + let consoleOutput; + let consoleErrors; + + before(() => { + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "safe-chain-bun-teardown-test-")); + + consoleOutput = []; + consoleErrors = []; + originalConsoleLog = console.log; + originalConsoleError = console.error; + console.log = (...args) => consoleOutput.push(args.join(" ")); + console.error = (...args) => consoleErrors.push(args.join(" ")); + }); + + after(() => { + console.log = originalConsoleLog; + console.error = originalConsoleError; + fs.rmSync(tempDir, { recursive: true, force: true }); + }); + + // Helper to reset console mocks before each test + const resetConsole = () => { + consoleOutput.length = 0; + consoleErrors.length = 0; + }; + + it("should remove scanner from global config", () => { + resetConsole(); + const mockHomedir = mock.method(os, "homedir", () => tempDir); + const globalConfigPath = path.join(tempDir, ".bunfig.toml"); + + fs.writeFileSync(globalConfigPath, `[install.security]\nscanner = "@aikidosec/safe-chain-bun"`); + + teardown(); + + const content = fs.readFileSync(globalConfigPath, "utf8"); + assert.ok(!content.includes('scanner = "@aikidosec/safe-chain-bun"')); + assert.ok(consoleOutput.some(msg => msg.includes("✅ Safe-Chain-Bun scanner removed"))); + + mockHomedir.mock.restore(); + }); + + it("should remove scanner from specific file", () => { + resetConsole(); + const specificConfigPath = path.join(tempDir, "project-bunfig.toml"); + fs.writeFileSync(specificConfigPath, `[build]\ntarget = "node"\n\n[install.security]\nscanner = "@aikidosec/safe-chain-bun"`); + + teardown(specificConfigPath); + + const content = fs.readFileSync(specificConfigPath, "utf8"); + assert.ok(!content.includes('scanner = "@aikidosec/safe-chain-bun"')); + assert.ok(!content.includes('[install.security]')); + assert.ok(content.includes('[build]')); + assert.ok(consoleOutput.some(msg => msg.includes("✅ Safe-Chain-Bun scanner removed"))); + }); + + it("should handle file not found gracefully", () => { + resetConsole(); + const nonExistentPath = path.join(tempDir, "does-not-exist.toml"); + + teardown(nonExistentPath); + + assert.ok(consoleOutput.some(msg => msg.includes("ℹ️ Config file not found"))); + }); + + it("should report when scanner not found", () => { + resetConsole(); + const specificConfigPath = path.join(tempDir, "no-scanner.toml"); + fs.writeFileSync(specificConfigPath, `[build]\ntarget = "node"`); + + teardown(specificConfigPath); + + assert.ok(consoleOutput.some(msg => msg.includes("ℹ️ Safe-Chain-Bun scanner not found"))); + }); + + it("should handle permission errors gracefully", () => { + resetConsole(); + const mockHomedir = mock.method(os, "homedir", () => tempDir); + const globalConfigPath = path.join(tempDir, ".bunfig.toml"); + + fs.writeFileSync(globalConfigPath, `[install.security]\nscanner = "@aikidosec/safe-chain-bun"`); + fs.chmodSync(globalConfigPath, 0o444); // Read-only + + let exitCode; + const mockExit = mock.method(process, "exit", (code) => { + exitCode = code; + }); + + teardown(); + + assert.strictEqual(exitCode, 1); + assert.ok(consoleErrors.some(msg => msg.includes("❌ Failed to remove Safe-Chain-Bun scanner"))); + + // Cleanup + fs.chmodSync(globalConfigPath, 0o644); + + mockExit.mock.restore(); + mockHomedir.mock.restore(); + }); +}); \ No newline at end of file diff --git a/packages/safe-chain-bun/src/toml-utils.js b/packages/safe-chain-bun/src/toml-utils.js new file mode 100644 index 0000000..0c9b9aa --- /dev/null +++ b/packages/safe-chain-bun/src/toml-utils.js @@ -0,0 +1,118 @@ +import os from "os"; +import path from "path"; + +/** + * Gets the global bunfig.toml path + * @returns {string} Path to global bunfig.toml + */ +export function getGlobalConfigPath() { + return path.join(os.homedir(), ".bunfig.toml"); +} + +/** + * Adds or updates the scanner configuration in TOML content + * @param {string} content - Existing TOML content + * @returns {{content: string, changed: boolean}} Updated content and change status + */ +export function addScannerToToml(content) { + const scannerLine = 'scanner = "@aikidosec/safe-chain-bun"'; + + if (content.includes(scannerLine)) { + return { content, changed: false }; + } + + const lines = content.split(/[\r\n\u2028\u2029]+/); + const installSecurityRegex = /^\[install\.security\]$/; + const scannerRegex = /^scanner\s*=.*$/; + + const securitySectionIndex = lines.findIndex(line => installSecurityRegex.test(line)); + + if (securitySectionIndex >= 0) { + const scannerLineIndex = lines.findIndex((line, index) => + index > securitySectionIndex && scannerRegex.test(line) + ); + + if (scannerLineIndex >= 0) { + lines[scannerLineIndex] = scannerLine; + } else { + lines.splice(securitySectionIndex + 1, 0, scannerLine); + } + } else { + if (lines[lines.length - 1] !== '') { + lines.push(''); + } + lines.push('[install.security]'); + lines.push(scannerLine); + lines.push(''); + } + + return { content: lines.join(os.EOL), changed: true }; +} + +/** + * Removes safe-chain-bun scanner configuration from TOML content + * @param {string} content - Existing TOML content + * @returns {{content: string, changed: boolean}} Updated content and change status + */ +export function removeScannerFromToml(content) { + const scannerLine = 'scanner = "@aikidosec/safe-chain-bun"'; + + if (!content.includes(scannerLine)) { + return { content, changed: false }; + } + + const lines = content.split(/[\r\n\u2028\u2029]+/); + const installSecurityRegex = /^\[install\.security\]$/; + const scannerRegex = /^scanner\s*=\s*"@aikidosec\/safe-chain-bun"$/; + + const securitySectionIndex = lines.findIndex(line => installSecurityRegex.test(line)); + + if (securitySectionIndex >= 0) { + const scannerLineIndex = lines.findIndex((line, index) => + index > securitySectionIndex && scannerRegex.test(line) + ); + + if (scannerLineIndex >= 0) { + lines.splice(scannerLineIndex, 1); + + // Check if [install.security] section is now empty + let isEmpty = true; + for (let i = securitySectionIndex + 1; i < lines.length; i++) { + const line = lines[i].trim(); + if (line === '') continue; + if (line.startsWith('#')) continue; + if (line.startsWith('[')) break; + isEmpty = false; + break; + } + + if (isEmpty) { + let sectionEnd = lines.length; + for (let i = securitySectionIndex + 1; i < lines.length; i++) { + const line = lines[i].trim(); + if (line.startsWith('[')) { + sectionEnd = i; + break; + } + } + + let removeStart = securitySectionIndex; + let removeEnd = sectionEnd; + + while (removeEnd > securitySectionIndex + 1 && lines[removeEnd - 1].trim() === '') { + removeEnd--; + } + + if (removeStart > 0 && lines[removeStart - 1].trim() === '') { + removeStart--; + } + + lines.splice(removeStart, removeEnd - removeStart); + } + + return { content: lines.join(os.EOL), changed: true }; + } + } + + return { content, changed: false }; +} \ No newline at end of file