diff --git a/.github/workflows/test-on-pr.yml b/.github/workflows/test-on-pr.yml index 6aa1d85..9a40824 100644 --- a/.github/workflows/test-on-pr.yml +++ b/.github/workflows/test-on-pr.yml @@ -1,9 +1,6 @@ name: Run tests -on: - pull_request: - branches: - - main +on: pull_request jobs: unit-test: diff --git a/package-lock.json b/package-lock.json index 59057a2..8910066 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,10 @@ "resolved": "packages/safe-chain", "link": true }, + "node_modules/@aikidosec/safe-chain-bun": { + "resolved": "packages/safe-chain-bun", + "link": true + }, "node_modules/@aikidosec/safe-chain-e2e-tests": { "resolved": "test/e2e", "link": true @@ -738,6 +742,160 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/@oven/bun-darwin-aarch64": { + "version": "1.2.21", + "resolved": "https://registry.npmjs.org/@oven/bun-darwin-aarch64/-/bun-darwin-aarch64-1.2.21.tgz", + "integrity": "sha512-SihfZ3czKeWz6Z3m5rUDrMlarwOXjnkUg+7tIiSB9VZCFSvWEItMfdAF170eCXxZmEh7A1dw20a3lW37lkmlrA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true + }, + "node_modules/@oven/bun-darwin-x64": { + "version": "1.2.21", + "resolved": "https://registry.npmjs.org/@oven/bun-darwin-x64/-/bun-darwin-x64-1.2.21.tgz", + "integrity": "sha512-iXr4y2ap6EmME7/EDoLMxSRKAh9yswKfrHDb9sF+ExHbk1C+XsNGxMY73ckQe2w0SIH6NXz2cRMTORbZ8LNjig==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true + }, + "node_modules/@oven/bun-darwin-x64-baseline": { + "version": "1.2.21", + "resolved": "https://registry.npmjs.org/@oven/bun-darwin-x64-baseline/-/bun-darwin-x64-baseline-1.2.21.tgz", + "integrity": "sha512-3KeslC5z3vpXxluYBqh6EDwojxTSyWJQeYPJFf7y/Z5QJuAN7g33l8jrx072X8P/G8CBzU1lJky14vhhnqWd7A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true + }, + "node_modules/@oven/bun-linux-aarch64": { + "version": "1.2.21", + "resolved": "https://registry.npmjs.org/@oven/bun-linux-aarch64/-/bun-linux-aarch64-1.2.21.tgz", + "integrity": "sha512-jpUFKGUpim4h4KOqI1VYYgvifZVrWNQZFrmVPfSqGb0ZzF/p5L2qc9Hy2aUL3Lo+zHMPylwbe0iLKElPYk0xoQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@oven/bun-linux-aarch64-musl": { + "version": "1.2.21", + "resolved": "https://registry.npmjs.org/@oven/bun-linux-aarch64-musl/-/bun-linux-aarch64-musl-1.2.21.tgz", + "integrity": "sha512-7UoUHKACYDin3iR6kdqUrF1AOCCjTHPTv1xmzlX4rzwNQvFYSAR83AMrY7hkatKGzLYkI8EjXDAvFJpwF+ZxoA==", + "cpu": [ + "aarch64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@oven/bun-linux-x64": { + "version": "1.2.21", + "resolved": "https://registry.npmjs.org/@oven/bun-linux-x64/-/bun-linux-x64-1.2.21.tgz", + "integrity": "sha512-6RuXFaVU2ve0TVw1vfFo7ix/jh9IX7mMAEhwE2odX8EdX/ea55upiivYQ/EKeXt+Ij3STc2bCeV4vvRoEJAHdg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@oven/bun-linux-x64-baseline": { + "version": "1.2.21", + "resolved": "https://registry.npmjs.org/@oven/bun-linux-x64-baseline/-/bun-linux-x64-baseline-1.2.21.tgz", + "integrity": "sha512-oZ5FUMfeghwbQcL9oxajsKjwVI+1GnVvxcJ3z+pifuXaLMZr25NCr5h0q2j+ZxEFL3RtL/Pyj8/HLfzGEIVAVg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@oven/bun-linux-x64-musl": { + "version": "1.2.21", + "resolved": "https://registry.npmjs.org/@oven/bun-linux-x64-musl/-/bun-linux-x64-musl-1.2.21.tgz", + "integrity": "sha512-ioZjU+2yyLJXaDA8FKoy+tj/fuZKovG9EMp+n9+EG7g3MULbe5nU8gdsS/dET28WzuPlDlSkqF8EUocvg4HajQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@oven/bun-linux-x64-musl-baseline": { + "version": "1.2.21", + "resolved": "https://registry.npmjs.org/@oven/bun-linux-x64-musl-baseline/-/bun-linux-x64-musl-baseline-1.2.21.tgz", + "integrity": "sha512-0NzMg4XdXgujDM2jZogiV6MgACXW0a0NfB+o6fxwmUzdmMBUk1ZMRzypUi4XKjGUe89mYcPJcVFQRRnNwzTK/Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@oven/bun-windows-x64": { + "version": "1.2.21", + "resolved": "https://registry.npmjs.org/@oven/bun-windows-x64/-/bun-windows-x64-1.2.21.tgz", + "integrity": "sha512-DZVCXrZGN/B4JnVnieZin1Kxse1wOkf+Fm2hDGpZHzs27ECbw5xPMFIc0r/oCpxTc/InxuvYO9UGoOmvhFaHsQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, + "node_modules/@oven/bun-windows-x64-baseline": { + "version": "1.2.21", + "resolved": "https://registry.npmjs.org/@oven/bun-windows-x64-baseline/-/bun-windows-x64-baseline-1.2.21.tgz", + "integrity": "sha512-sTnkLdThgsa6X8ib6eb3+zgy+CGJOibK6Th4wV2wmZFi5af6TM+digEi9i+q/X3nabGwPXm0V4vBiVpvcFilsA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -1308,6 +1466,41 @@ "node": ">=8" } }, + "node_modules/bun": { + "version": "1.2.21", + "resolved": "https://registry.npmjs.org/bun/-/bun-1.2.21.tgz", + "integrity": "sha512-y0lJ02dS90U3PJm+7KAKY8Se95AQvP5Xm77LouUwrpNOHpv59kBG4SK1+9iE1cAhpUaFipq+0EJ56S6MmE3row==", + "cpu": [ + "arm64", + "x64", + "aarch64" + ], + "hasInstallScript": true, + "license": "MIT", + "os": [ + "darwin", + "linux", + "win32" + ], + "peer": true, + "bin": { + "bun": "bin/bun.exe", + "bunx": "bin/bunx.exe" + }, + "optionalDependencies": { + "@oven/bun-darwin-aarch64": "1.2.21", + "@oven/bun-darwin-x64": "1.2.21", + "@oven/bun-darwin-x64-baseline": "1.2.21", + "@oven/bun-linux-aarch64": "1.2.21", + "@oven/bun-linux-aarch64-musl": "1.2.21", + "@oven/bun-linux-x64": "1.2.21", + "@oven/bun-linux-x64-baseline": "1.2.21", + "@oven/bun-linux-x64-musl": "1.2.21", + "@oven/bun-linux-x64-musl-baseline": "1.2.21", + "@oven/bun-windows-x64": "1.2.21", + "@oven/bun-windows-x64-baseline": "1.2.21" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -5797,6 +5990,17 @@ "safe-chain": "bin/safe-chain.js" } }, + "packages/safe-chain-bun": { + "name": "@aikidosec/safe-chain-bun", + "version": "1.0.0", + "license": "AGPL-3.0-or-later", + "dependencies": { + "@aikidosec/safe-chain": "file:../safe-chain" + }, + "peerDependencies": { + "bun": ">=1.2.21" + } + }, "test/e2e": { "name": "@aikidosec/safe-chain-e2e-tests", "version": "1.0.0", diff --git a/package.json b/package.json index d3ca8e1..8575897 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "test/e2e" ], "scripts": { - "test": "npm run test --workspace=packages/safe-chain", + "test": "npm run test --workspace=packages/safe-chain --workspace=packages/safe-chain-bun", "test:e2e": "npm run test --workspace=test/e2e", "lint": "npm run lint --workspace=packages/safe-chain" }, diff --git a/packages/safe-chain-bun/package.json b/packages/safe-chain-bun/package.json new file mode 100644 index 0000000..b5a9e3e --- /dev/null +++ b/packages/safe-chain-bun/package.json @@ -0,0 +1,30 @@ +{ + "name": "@aikidosec/safe-chain-bun", + "version": "1.0.0", + "type": "module", + "main": "src/index.js", + "scripts": { + "test": "node --test --experimental-test-module-mocks 'src/**/*.spec.js'" + }, + "exports": { + ".": { + "bun": "./src/index.js", + "default": "./src/index.js" + } + }, + "keywords": ["bun", "security", "scanner", "malware", "aikido"], + "author": "Aikido Security", + "license": "AGPL-3.0-or-later", + "description": "Aikido Security Scanner for Bun package manager - detects malware and security threats during package installation", + "repository": { + "type": "git", + "url": "git+https://github.com/AikidoSec/safe-chain.git", + "directory": "packages/safe-chain-bun" + }, + "dependencies": { + "@aikidosec/safe-chain": "file:../safe-chain" + }, + "peerDependencies": { + "bun": ">=1.2.21" + } +} \ No newline at end of file diff --git a/packages/safe-chain-bun/src/index.js b/packages/safe-chain-bun/src/index.js new file mode 100644 index 0000000..fbd0f65 --- /dev/null +++ b/packages/safe-chain-bun/src/index.js @@ -0,0 +1,37 @@ +import { auditChanges } from "@aikidosec/safe-chain/scanning"; + +// Bun Security Scanner for Safe-Chain +// This is the entry point for Bun's native security scanner integration + +export const scanner = { + version: "1", // Our scanner is using version 1 of the bun security scanner API. + + async scan({ packages }) { + const advisories = []; + + try { + const changes = packages.map((pkg) => ({ + name: pkg.name, + version: pkg.version, + type: "add", + })); + + const audit = await auditChanges(changes); + + if (!audit.isAllowed) { + for (const change of audit.disallowedChanges) { + advisories.push({ + level: "fatal", // Fatal will block the installation process, this is what we want for packages that contain malware. + package: change.name, + url: null, + description: `Package ${change.name}@${change.version} contains known security threats (${change.reason}). Installation blocked by Safe-Chain.`, + }); + } + } + } catch (error) { + console.warn(`Safe-Chain security scan failed: ${error.message}`); + } + + return advisories; + }, +}; diff --git a/packages/safe-chain-bun/src/index.spec.js b/packages/safe-chain-bun/src/index.spec.js new file mode 100644 index 0000000..3293b56 --- /dev/null +++ b/packages/safe-chain-bun/src/index.spec.js @@ -0,0 +1,140 @@ +import assert from "node:assert/strict"; +import { describe, it, mock } from "node:test"; + +describe("Bun Scanner", async () => { + const mockAuditChanges = mock.fn(); + + // Mock the scanning module + mock.module("@aikidosec/safe-chain/scanning", { + namedExports: { + auditChanges: mockAuditChanges, + }, + }); + + const { scanner } = await import("./index.js"); + + it("should export scanner object with version", () => { + assert.strictEqual(scanner.version, "1"); + assert.strictEqual(typeof scanner.scan, "function"); + }); + + it("should return empty advisories for clean packages", async () => { + mockAuditChanges.mock.mockImplementation(() => ({ + allowedChanges: [{ name: "express", version: "4.18.2", type: "add" }], + disallowedChanges: [], + isAllowed: true, + })); + + const packages = [{ name: "express", version: "4.18.2" }]; + const result = await scanner.scan({ packages }); + + assert.deepEqual(result, []); + assert.strictEqual(mockAuditChanges.mock.callCount(), 1); + assert.deepEqual(mockAuditChanges.mock.calls[0].arguments[0], [ + { name: "express", version: "4.18.2", type: "add" }, + ]); + }); + + it("should return fatal advisory for malware packages", async () => { + mockAuditChanges.mock.mockImplementation(() => ({ + allowedChanges: [], + disallowedChanges: [ + { + name: "malicious-pkg", + version: "1.0.0", + type: "add", + reason: "MALWARE", + }, + ], + isAllowed: false, + })); + + const packages = [{ name: "malicious-pkg", version: "1.0.0" }]; + const result = await scanner.scan({ packages }); + + assert.strictEqual(result.length, 1); + assert.deepEqual(result[0], { + level: "fatal", + package: "malicious-pkg", + url: null, + description: "Package malicious-pkg@1.0.0 contains known security threats (MALWARE). Installation blocked by Safe-Chain.", + }); + }); + + it("should handle multiple packages with mixed results", async () => { + mockAuditChanges.mock.mockImplementation(() => ({ + allowedChanges: [{ name: "express", version: "4.18.2", type: "add" }], + disallowedChanges: [ + { + name: "malicious-pkg", + version: "1.0.0", + type: "add", + reason: "MALWARE", + }, + { + name: "another-bad-pkg", + version: "2.1.0", + type: "add", + reason: "MALWARE", + }, + ], + isAllowed: false, + })); + + const packages = [ + { name: "express", version: "4.18.2" }, + { name: "malicious-pkg", version: "1.0.0" }, + { name: "another-bad-pkg", version: "2.1.0" }, + ]; + const result = await scanner.scan({ packages }); + + assert.strictEqual(result.length, 2); + assert.strictEqual(result[0].package, "malicious-pkg"); + assert.strictEqual(result[0].level, "fatal"); + assert.strictEqual(result[1].package, "another-bad-pkg"); + assert.strictEqual(result[1].level, "fatal"); + }); + + it("should handle empty package list", async () => { + mockAuditChanges.mock.mockImplementation(() => ({ + allowedChanges: [], + disallowedChanges: [], + isAllowed: true, + })); + + const result = await scanner.scan({ packages: [] }); + + assert.deepEqual(result, []); + assert.deepEqual( + mockAuditChanges.mock.calls[mockAuditChanges.mock.callCount() - 1] + .arguments[0], + [] + ); + }); + + it("should convert Bun package format to safe-chain format correctly", async () => { + mockAuditChanges.mock.mockImplementation(() => ({ + allowedChanges: [], + disallowedChanges: [], + isAllowed: true, + })); + + const bunPackages = [ + { name: "lodash", version: "4.17.21" }, + { name: "@types/node", version: "20.0.0" }, + ]; + + await scanner.scan({ packages: bunPackages }); + + const expectedChanges = [ + { name: "lodash", version: "4.17.21", type: "add" }, + { name: "@types/node", version: "20.0.0", type: "add" }, + ]; + + assert.deepEqual( + mockAuditChanges.mock.calls[mockAuditChanges.mock.callCount() - 1] + .arguments[0], + expectedChanges + ); + }); +}); diff --git a/packages/safe-chain/package.json b/packages/safe-chain/package.json index a59d4ac..f303856 100644 --- a/packages/safe-chain/package.json +++ b/packages/safe-chain/package.json @@ -15,6 +15,14 @@ "safe-chain": "bin/safe-chain.js" }, "type": "module", + "exports": { + ".": { + "default": "./src/main.js" + }, + "./scanning": { + "default": "./src/scanning/audit/index.js" + } + }, "keywords": [], "author": "Aikido Security", "license": "AGPL-3.0-or-later",