diff --git a/packages/safe-chain/bin/aikido-bun.js b/packages/safe-chain/bin/aikido-bun.js new file mode 100755 index 0000000..01e3972 --- /dev/null +++ b/packages/safe-chain/bin/aikido-bun.js @@ -0,0 +1,10 @@ +#!/usr/bin/env node + +import { main } from "../src/main.js"; +import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js"; + +const packageManagerName = "bun"; +initializePackageManager(packageManagerName); +var exitCode = await main(process.argv.slice(2)); + +process.exit(exitCode); diff --git a/packages/safe-chain/bin/aikido-bunx.js b/packages/safe-chain/bin/aikido-bunx.js new file mode 100755 index 0000000..fb378e5 --- /dev/null +++ b/packages/safe-chain/bin/aikido-bunx.js @@ -0,0 +1,10 @@ +#!/usr/bin/env node + +import { main } from "../src/main.js"; +import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js"; + +const packageManagerName = "bunx"; +initializePackageManager(packageManagerName); +var exitCode = await main(process.argv.slice(2)); + +process.exit(exitCode); diff --git a/packages/safe-chain/package.json b/packages/safe-chain/package.json index d28fe73..c0d2115 100644 --- a/packages/safe-chain/package.json +++ b/packages/safe-chain/package.json @@ -12,6 +12,8 @@ "aikido-yarn": "bin/aikido-yarn.js", "aikido-pnpm": "bin/aikido-pnpm.js", "aikido-pnpx": "bin/aikido-pnpx.js", + "aikido-bun": "bin/aikido-bun.js", + "aikido-bunx": "bin/aikido-bunx.js", "safe-chain": "bin/safe-chain.js" }, "type": "module", diff --git a/packages/safe-chain/src/packagemanager/bun/createBunPackageManager.js b/packages/safe-chain/src/packagemanager/bun/createBunPackageManager.js new file mode 100644 index 0000000..14faa5f --- /dev/null +++ b/packages/safe-chain/src/packagemanager/bun/createBunPackageManager.js @@ -0,0 +1,42 @@ +import { ui } from "../../environment/userInteraction.js"; +import { safeSpawn } from "../../utils/safeSpawn.js"; +import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; + +export function createBunPackageManager() { + return { + runCommand: (args) => runBunCommand("bun", args), + + // For bun, we use the proxy-only approach to block package downloads, + // so we don't need to analyze commands. + isSupportedCommand: () => false, + getDependencyUpdatesForCommand: () => [], + }; +} + +export function createBunxPackageManager() { + return { + runCommand: (args) => runBunCommand("bunx", args), + + // For bunx, we use the proxy-only approach to block package downloads, + // so we don't need to analyze commands. + isSupportedCommand: () => false, + getDependencyUpdatesForCommand: () => [], + }; +} + +async function runBunCommand(command, args) { + try { + const result = await safeSpawn(command, args, { + stdio: "inherit", + env: mergeSafeChainProxyEnvironmentVariables(process.env), + }); + return { status: result.status }; + } catch (error) { + if (error.status) { + return { status: error.status }; + } else { + ui.writeError("Error executing command:", error.message); + return { status: 1 }; + } + } +} diff --git a/packages/safe-chain/src/packagemanager/currentPackageManager.js b/packages/safe-chain/src/packagemanager/currentPackageManager.js index 9497a20..2a10d86 100644 --- a/packages/safe-chain/src/packagemanager/currentPackageManager.js +++ b/packages/safe-chain/src/packagemanager/currentPackageManager.js @@ -1,3 +1,7 @@ +import { + createBunPackageManager, + createBunxPackageManager, +} from "./bun/createBunPackageManager.js"; import { createNpmPackageManager } from "./npm/createPackageManager.js"; import { createNpxPackageManager } from "./npx/createPackageManager.js"; import { @@ -21,6 +25,10 @@ export function initializePackageManager(packageManagerName, version) { state.packageManagerName = createPnpmPackageManager(); } else if (packageManagerName === "pnpx") { state.packageManagerName = createPnpxPackageManager(); + } else if (packageManagerName === "bun") { + state.packageManagerName = createBunPackageManager(); + } else if (packageManagerName === "bunx") { + state.packageManagerName = createBunxPackageManager(); } else { throw new Error("Unsupported package manager: " + packageManagerName); } diff --git a/packages/safe-chain/src/shell-integration/helpers.js b/packages/safe-chain/src/shell-integration/helpers.js index 4137471..b7fdc9c 100644 --- a/packages/safe-chain/src/shell-integration/helpers.js +++ b/packages/safe-chain/src/shell-integration/helpers.js @@ -9,8 +9,9 @@ export const knownAikidoTools = [ { tool: "yarn", aikidoCommand: "aikido-yarn" }, { tool: "pnpm", aikidoCommand: "aikido-pnpm" }, { tool: "pnpx", aikidoCommand: "aikido-pnpx" }, - // When adding a new tool here, also update the expected alias in the tests (setup.spec.js, teardown.spec.js) - // and add the documentation for the new tool in the README.md + { tool: "bun", aikidoCommand: "aikido-bun" }, + { tool: "bunx", aikidoCommand: "aikido-bunx" }, + // When adding a new tool here, also update the documentation for the new tool in the README.md ]; /** @@ -18,15 +19,15 @@ export const knownAikidoTools = [ * Example: "npm, npx, yarn, pnpm, and pnpx commands" */ export function getPackageManagerList() { - const tools = knownAikidoTools.map(t => t.tool); + const tools = knownAikidoTools.map((t) => t.tool); if (tools.length <= 1) { - return `${tools[0] || ''} commands`; + return `${tools[0] || ""} commands`; } if (tools.length === 2) { return `${tools[0]} and ${tools[1]} commands`; } const lastTool = tools.pop(); - return `${tools.join(', ')}, and ${lastTool} commands`; + return `${tools.join(", ")}, and ${lastTool} commands`; } export function doesExecutableExistOnSystem(executableName) { diff --git a/packages/safe-chain/src/shell-integration/startup-scripts/init-fish.fish b/packages/safe-chain/src/shell-integration/startup-scripts/init-fish.fish index 87f6a79..29d6bf3 100644 --- a/packages/safe-chain/src/shell-integration/startup-scripts/init-fish.fish +++ b/packages/safe-chain/src/shell-integration/startup-scripts/init-fish.fish @@ -46,6 +46,14 @@ function pnpx wrapSafeChainCommand "pnpx" "aikido-pnpx" $argv end +function bun + wrapSafeChainCommand "bun" "aikido-bun" $argv +end + +function bunx + wrapSafeChainCommand "bunx" "aikido-bunx" $argv +end + function npm # If args is just -v or --version and nothing else, just run the `npm -v` command # This is because nvm uses this to check the version of npm diff --git a/packages/safe-chain/src/shell-integration/startup-scripts/init-posix.sh b/packages/safe-chain/src/shell-integration/startup-scripts/init-posix.sh index 01b23c4..353c6c0 100644 --- a/packages/safe-chain/src/shell-integration/startup-scripts/init-posix.sh +++ b/packages/safe-chain/src/shell-integration/startup-scripts/init-posix.sh @@ -42,6 +42,14 @@ function pnpx() { wrapSafeChainCommand "pnpx" "aikido-pnpx" "$@" } +function bun() { + wrapSafeChainCommand "bun" "aikido-bun" "$@" +} + +function bunx() { + wrapSafeChainCommand "bunx" "aikido-bunx" "$@" +} + function npm() { if [[ "$1" == "-v" || "$1" == "--version" ]] && [[ $# -eq 1 ]]; then # If args is just -v or --version and nothing else, just run the npm version command diff --git a/packages/safe-chain/src/shell-integration/startup-scripts/init-pwsh.ps1 b/packages/safe-chain/src/shell-integration/startup-scripts/init-pwsh.ps1 index 7fb44d6..a449405 100644 --- a/packages/safe-chain/src/shell-integration/startup-scripts/init-pwsh.ps1 +++ b/packages/safe-chain/src/shell-integration/startup-scripts/init-pwsh.ps1 @@ -68,6 +68,14 @@ function pnpx { Invoke-WrappedCommand "pnpx" "aikido-pnpx" $args } +function bun { + Invoke-WrappedCommand "bun" "aikido-bun" $args +} + +function bunx { + Invoke-WrappedCommand "bunx" "aikido-bunx" $args +} + function npm { # If args is just -v or --version and nothing else, just run the npm version command # This is because nvm uses this to check the version of npm diff --git a/test/e2e/Dockerfile b/test/e2e/Dockerfile index 3c8ce73..484f5fe 100644 --- a/test/e2e/Dockerfile +++ b/test/e2e/Dockerfile @@ -46,6 +46,9 @@ RUN volta install npm@${NPM_VERSION} RUN volta install yarn@${YARN_VERSION} RUN volta install pnpm@${PNPM_VERSION} +# Install Bun +RUN curl -fsSL https://bun.sh/install | bash + # Copy and install Safe chain COPY --from=builder /app/*.tgz /pkgs/ RUN npm install -g /pkgs/*.tgz diff --git a/test/e2e/bun.e2e.spec.js b/test/e2e/bun.e2e.spec.js new file mode 100644 index 0000000..8dea93b --- /dev/null +++ b/test/e2e/bun.e2e.spec.js @@ -0,0 +1,79 @@ +import { describe, it, before, beforeEach, afterEach } from "node:test"; +import { DockerTestContainer } from "./DockerTestContainer.js"; +import assert from "node:assert"; + +describe("E2E: bun coverage", () => { + let container; + + before(async () => { + DockerTestContainer.buildImage(); + }); + + beforeEach(async () => { + // Run a new Docker container for each test + container = new DockerTestContainer(); + await container.start(); + + const installationShell = await container.openShell("zsh"); + await installationShell.runCommand("safe-chain setup"); + }); + + afterEach(async () => { + // Stop and clean up the container after each test + if (container) { + await container.stop(); + container = null; + } + }); + + it(`safe-chain succesfully installs safe packages`, async () => { + const shell = await container.openShell("bash"); + const result = await shell.runCommand("bun i axios"); + + assert.ok( + result.output.includes("no malicious packages found."), + `Output did not include expected text. Output was:\n${result.output}` + ); + }); + + it(`safe-chain blocks download of malicious packages already in package.json`, async () => { + const shell = await container.openShell("bash"); + await shell.runCommand( + 'echo \'{"name":"test-project","version":"1.0.0","dependencies":{"safe-chain-test":"0.0.1-security"}}\' > package.json' + ); + + var result = await shell.runCommand("bun install"); + + assert.ok( + result.output.includes("blocked 1 malicious package downloads"), + `Output did not include expected text. Output was:\n${result.output}` + ); + assert.ok( + result.output.includes("- safe-chain-test"), + `Output did not include expected text. Output was:\n${result.output}` + ); + assert.ok( + result.output.includes("Exiting without installing malicious packages."), + `Output did not include expected text. Output was:\n${result.output}` + ); + }); + + it("safe-chain blocks bunx from downloading malicious packages", async () => { + const shell = await container.openShell("bash"); + + const result = await shell.runCommand("bunx safe-chain-test"); + + assert.ok( + result.output.includes("blocked 1 malicious package downloads"), + `Output did not include expected text. Output was:\n${result.output}` + ); + assert.ok( + result.output.includes("- safe-chain-test"), + `Output did not include expected text. Output was:\n${result.output}` + ); + assert.ok( + result.output.includes("Exiting without installing malicious packages."), + `Output did not include expected text. Output was:\n${result.output}` + ); + }); +});