@aikidosec/safe-chain 1.0.0 → 1.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/.editorconfig +8 -0
  2. package/.github/workflows/build-and-release.yml +41 -0
  3. package/.github/workflows/test-on-pr.yml +28 -0
  4. package/README.md +57 -0
  5. package/bin/aikido-npm.js +8 -0
  6. package/bin/aikido-npx.js +8 -0
  7. package/bin/aikido-yarn.js +8 -0
  8. package/bin/safe-chain.js +57 -0
  9. package/eslint.config.js +25 -0
  10. package/package.json +28 -5
  11. package/safe-package-manager-demo.gif +0 -0
  12. package/src/api/aikido.js +31 -0
  13. package/src/api/npmApi.js +46 -0
  14. package/src/config/configFile.js +91 -0
  15. package/src/environment/environment.js +14 -0
  16. package/src/environment/userInteraction.js +79 -0
  17. package/src/main.js +18 -0
  18. package/src/packagemanager/currentPackageManager.js +28 -0
  19. package/src/packagemanager/npm/createPackageManager.js +83 -0
  20. package/src/packagemanager/npm/dependencyScanner/commandArgumentScanner.js +37 -0
  21. package/src/packagemanager/npm/dependencyScanner/dryRunScanner.js +50 -0
  22. package/src/packagemanager/npm/dependencyScanner/nullScanner.js +6 -0
  23. package/src/packagemanager/npm/parsing/parseNpmInstallDryRunOutput.js +57 -0
  24. package/src/packagemanager/npm/parsing/parseNpmInstallDryRunOutput.spec.js +134 -0
  25. package/src/packagemanager/npm/parsing/parsePackagesFromInstallArgs.js +109 -0
  26. package/src/packagemanager/npm/parsing/parsePackagesFromInstallArgs.spec.js +176 -0
  27. package/src/packagemanager/npm/runNpmCommand.js +33 -0
  28. package/src/packagemanager/npm/utils/cmd-list.js +171 -0
  29. package/src/packagemanager/npm/utils/npmCommands.js +26 -0
  30. package/src/packagemanager/npx/createPackageManager.js +13 -0
  31. package/src/packagemanager/npx/dependencyScanner/commandArgumentScanner.js +31 -0
  32. package/src/packagemanager/npx/parsing/parsePackagesFromArguments.js +106 -0
  33. package/src/packagemanager/npx/parsing/parsePackagesFromArguments.spec.js +147 -0
  34. package/src/packagemanager/npx/runNpxCommand.js +17 -0
  35. package/src/packagemanager/yarn/createPackageManager.js +34 -0
  36. package/src/packagemanager/yarn/dependencyScanner/commandArgumentScanner.js +28 -0
  37. package/src/packagemanager/yarn/parsing/parsePackagesFromArguments.js +102 -0
  38. package/src/packagemanager/yarn/parsing/parsePackagesFromArguments.spec.js +126 -0
  39. package/src/packagemanager/yarn/runYarnCommand.js +17 -0
  40. package/src/scanning/audit/index.js +56 -0
  41. package/src/scanning/index.js +92 -0
  42. package/src/scanning/index.scanCommand.spec.js +180 -0
  43. package/src/scanning/index.shouldScanCommand.spec.js +47 -0
  44. package/src/scanning/malwareDatabase.js +62 -0
  45. package/src/shell-integration/helpers.js +44 -0
  46. package/src/shell-integration/removeShell.js +140 -0
  47. package/src/shell-integration/removeShell.spec.js +177 -0
  48. package/src/shell-integration/setupShell.js +151 -0
  49. package/src/shell-integration/setupShell.spec.js +304 -0
  50. package/src/shell-integration/shellDetection.js +75 -0
package/.editorconfig ADDED
@@ -0,0 +1,8 @@
1
+
2
+ [*]
3
+ charset = utf-8
4
+ insert_final_newline = true
5
+ end_of_line = lf
6
+ indent_style = space
7
+ indent_size = 2
8
+ max_line_length = 80
@@ -0,0 +1,41 @@
1
+ name: Create Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "*"
7
+
8
+ jobs:
9
+ build:
10
+ runs-on: ubuntu-latest
11
+
12
+ steps:
13
+ - name: Checkout code
14
+ uses: actions/checkout@v3
15
+
16
+ - name: Set up Node.js
17
+ uses: actions/setup-node@v3
18
+ with:
19
+ node-version: "lts/*"
20
+ registry-url: "https://registry.npmjs.org/"
21
+ env:
22
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
23
+
24
+ - name: Set version number
25
+ id: get_version
26
+ run: |
27
+ version="${{ github.ref_name }}"
28
+ echo "tag=$version" >> $GITHUB_OUTPUT
29
+
30
+ - name: Set the version
31
+ run: npm --no-git-tag-version version ${{ steps.get_version.outputs.tag }}
32
+
33
+ - name: Install dependencies
34
+ run: npm ci
35
+
36
+ - name: Publish to npm
37
+ run: |
38
+ echo "Publishing version ${{ steps.get_version.outputs.tag }} to NPM"
39
+ npm publish --access public
40
+ env:
41
+ NPM_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
@@ -0,0 +1,28 @@
1
+ name: Run Unit Tests
2
+
3
+ on:
4
+ pull_request:
5
+ branches:
6
+ - main
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+
12
+ steps:
13
+ - name: Checkout code
14
+ uses: actions/checkout@v3
15
+
16
+ - name: Set up Node.js
17
+ uses: actions/setup-node@v3
18
+ with:
19
+ node-version: "lts/*"
20
+
21
+ - name: Install dependencies
22
+ run: npm ci
23
+
24
+ - name: Run tests
25
+ run: npm test
26
+
27
+ - name: Run ESLint
28
+ run: npm run lint
package/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # Aikido Safe Chain
2
+
3
+ The Aikido Safe Chain wraps around the [npm cli](https://github.com/npm/cli), [npx](https://github.com/npm/cli/blob/latest/docs/content/commands/npx.md), and [yarn](https://yarnpkg.com/) to provide extra checks before installing new packages. This tool will detect when a package contains malware and prompt you to exit, preventing npm, npx, or yarn from downloading or running the malware.
4
+
5
+ ![demo](./safe-package-manager-demo.gif)
6
+
7
+ ## Installation
8
+
9
+ Installing the Aikido Safe Chain is easy. You just need 3 simple steps:
10
+
11
+ 1. **Install the Aikido Safe Chain package globally** using npm:
12
+ ```shell
13
+ npm install -g @aikidosec/safe-chain
14
+ ```
15
+ 2. **Setup the shell integration** by running:
16
+ ```shell
17
+ safe-chain setup-shell
18
+ ```
19
+ 3. **Restart your terminal** to start using the Aikido Safe Chain.
20
+
21
+ When running `npm`, `npx`, or `yarn` commands, the Aikido Safe Chain will automatically check for malware in the packages you are trying to install. If any malware is detected, it will prompt you to exit the command.
22
+
23
+ ## Aliases in shell
24
+
25
+ The Aikido Safe Chain will setup aliases in your shell for the commands **npm**, **npx**, and **yarn**. This means that when you run these commands, they will be replaced with the Aikido Safe Chain commands **aikido-npm**, **aikido-npx**, and **aikido-yarn** respectively. The Aikido Safe Chain will do a pre-install check for malware before running the original command.
26
+
27
+ ### Creating shell aliases
28
+
29
+ The `setup-shell` command will detect which shells are installed and add the aliases for **npm**, **npx**, and **yarn** to your shell startup scripts.
30
+
31
+ Supported shells include:
32
+
33
+ - ✅ **Bash**: `~/.bashrc`
34
+ - ✅ **Zsh**: `~/.zshrc`
35
+ - ✅ **Fish**: `~/.config/fish/config.fish`
36
+ - ✅ **Powershell**: `$PROFILE`
37
+ - ✅ **PowerShell Core**: `$PROFILE`
38
+
39
+ After adding the alias, **the shell needs to restart in order to load the alias**.
40
+
41
+ ### Removing shell aliases
42
+
43
+ The `remove-shell` command will remove the aliases for **npm**, **npx**, and **yarn** from your shell startup scripts. This is useful if you want to stop using the Aikido Safe Chain or if you want to switch to a different package manager.
44
+
45
+ ## Uninstallation
46
+
47
+ To uninstall the Aikido Safe Chain, you can run the following command:
48
+
49
+ 1. **Remove all aliases from your shell** by running:
50
+ ```shell
51
+ safe-chain remove-shell
52
+ ```
53
+ 2. **Uninstall the Aikido Safe Chain package** using npm:
54
+ ```shell
55
+ npm uninstall -g @aikidosec/safe-chain
56
+ ```
57
+ 3. **Restart your terminal** to remove the aliases.
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { main } from "../src/main.js";
4
+ import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
5
+
6
+ const packageManagerName = "npm";
7
+ initializePackageManager(packageManagerName, process.versions.node);
8
+ await main(process.argv.slice(2));
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { main } from "../src/main.js";
4
+ import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
5
+
6
+ const packageManagerName = "npx";
7
+ initializePackageManager(packageManagerName, process.versions.node);
8
+ await main(process.argv.slice(2));
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { main } from "../src/main.js";
4
+ import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
5
+
6
+ const packageManagerName = "yarn";
7
+ initializePackageManager(packageManagerName, process.versions.node);
8
+ await main(process.argv.slice(2));
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+
3
+ import chalk from "chalk";
4
+ import { ui } from "../src/environment/userInteraction.js";
5
+ import { setupShell } from "../src/shell-integration/setupShell.js";
6
+ import { removeShell } from "../src/shell-integration/removeShell.js";
7
+
8
+ if (process.argv.length < 3) {
9
+ ui.writeError("No command provided. Please provide a command to execute.");
10
+ ui.emptyLine();
11
+ writeHelp();
12
+ process.exit(1);
13
+ }
14
+
15
+ const command = process.argv[2];
16
+
17
+ if (command === "help" || command === "--help" || command === "-h") {
18
+ writeHelp();
19
+ process.exit(0);
20
+ }
21
+
22
+ if (command === "setup-shell") {
23
+ setupShell();
24
+ } else if (command === "remove-shell") {
25
+ removeShell();
26
+ } else {
27
+ ui.writeError(`Unknown command: ${command}.`);
28
+ ui.emptyLine();
29
+
30
+ writeHelp();
31
+
32
+ process.exit(1);
33
+ }
34
+
35
+ function writeHelp() {
36
+ ui.writeInformation(
37
+ chalk.bold("Usage: ") + chalk.cyan("safe-chain <command>")
38
+ );
39
+ ui.emptyLine();
40
+ ui.writeInformation(
41
+ `Available commands: ${chalk.cyan("setup-shell")}, ${chalk.cyan(
42
+ "remove-shell"
43
+ )}, ${chalk.cyan("help")}`
44
+ );
45
+ ui.emptyLine();
46
+ ui.writeInformation(
47
+ `- ${chalk.cyan(
48
+ "safe-chain setup-shell"
49
+ )}: This will setup your shell to wrap safe-chain around npm, npx and yarn.`
50
+ );
51
+ ui.writeInformation(
52
+ `- ${chalk.cyan(
53
+ "safe-chain remove-shell"
54
+ )}: This will remove safe-chain aliases from your shell configuration.`
55
+ );
56
+ ui.emptyLine();
57
+ }
@@ -0,0 +1,25 @@
1
+ import js from "@eslint/js";
2
+ import { defineConfig } from "@eslint/config-helpers";
3
+ import globals from "globals";
4
+ import importPlugin from "eslint-plugin-import";
5
+
6
+ export default defineConfig([
7
+ {
8
+ files: ["**/*.{js,mjs,cjs,ts}"],
9
+ plugins: { js },
10
+ extends: ["js/recommended"],
11
+ },
12
+ {
13
+ files: ["**/*.{js,mjs,cjs,ts}"],
14
+ languageOptions: { globals: globals.node },
15
+ },
16
+ importPlugin.flatConfigs.recommended,
17
+ {
18
+ files: ["**/*.{js,mjs,cjs}"],
19
+ languageOptions: {
20
+ ecmaVersion: "latest",
21
+ sourceType: "module",
22
+ },
23
+ rules: {},
24
+ },
25
+ ]);
package/package.json CHANGED
@@ -1,18 +1,41 @@
1
1
  {
2
2
  "name": "@aikidosec/safe-chain",
3
- "version": "1.0.0",
4
- "scripts": {},
3
+ "version": "1.0.11",
4
+ "scripts": {
5
+ "test": "node --test --experimental-test-module-mocks **/*.spec.js",
6
+ "test:watch": "node --test --watch --experimental-test-module-mocks **/*.spec.js",
7
+ "lint": "eslint ."
8
+ },
5
9
  "repository": {
6
10
  "type": "git",
7
11
  "url": "git+https://github.com/AikidoSec/safe-npm.git"
8
12
  },
9
- "bin": {},
13
+ "bin": {
14
+ "aikido-npm": "bin/aikido-npm.js",
15
+ "aikido-npx": "bin/aikido-npx.js",
16
+ "aikido-yarn": "bin/aikido-yarn.js",
17
+ "safe-chain": "bin/safe-chain.js"
18
+ },
10
19
  "type": "module",
11
20
  "keywords": [],
12
21
  "author": "Aikido Security",
13
22
  "license": "AGPL-3.0-or-later",
14
- "description": "The Aikido Safe Package Manager wraps around the [npm cli](https://github.com/npm/cli), [npx](https://github.com/npm/cli/blob/latest/docs/content/commands/npx.md), [yarn](https://yarnpkg.com/), [pnpm](https://pnpm.io/), and [pnpx](https://pnpm.io/cli/dlx) to provide extra checks before installing new packages. This tool will detect when a package contains malware and prompt you to exit, preventing npm, npx, yarn, pnpm, or pnpx from downloading or running the malware.",
15
- "dependencies": {},
23
+ "description": "The Aikido Safe Chain wraps around the [npm cli](https://github.com/npm/cli), [npx](https://github.com/npm/cli/blob/latest/docs/content/commands/npx.md), [yarn](https://yarnpkg.com/), [pnpm](https://pnpm.io/), and [pnpx](https://pnpm.io/cli/dlx) to provide extra checks before installing new packages. This tool will detect when a package contains malware and prompt you to exit, preventing npm, npx, yarn, pnpm, or pnpx from downloading or running the malware.",
24
+ "dependencies": {
25
+ "@inquirer/prompts": "^7.4.1",
26
+ "abbrev": "^3.0.1",
27
+ "chalk": "^5.4.1",
28
+ "npm-registry-fetch": "^18.0.2",
29
+ "ora": "^8.2.0",
30
+ "semver": "^7.7.2"
31
+ },
32
+ "devDependencies": {
33
+ "@eslint/js": "^9.26.0",
34
+ "eslint": "^9.26.0",
35
+ "eslint-plugin-import": "^2.31.0",
36
+ "globals": "^16.1.0",
37
+ "typescript-eslint": "^8.32.0"
38
+ },
16
39
  "main": "eslint.config.js",
17
40
  "bugs": {
18
41
  "url": "https://github.com/AikidoSec/safe-npm/issues"
Binary file
@@ -0,0 +1,31 @@
1
+ const malwareDatabaseUrl =
2
+ "https://malware-list.aikido.dev/malware_predictions.json";
3
+
4
+ export async function fetchMalwareDatabase() {
5
+ const response = await fetch(malwareDatabaseUrl);
6
+ if (!response.ok) {
7
+ throw new Error(`Error fetching malware database: ${response.statusText}`);
8
+ }
9
+
10
+ try {
11
+ let malwareDatabase = await response.json();
12
+ return {
13
+ malwareDatabase: malwareDatabase,
14
+ version: response.headers.get("etag") || undefined,
15
+ };
16
+ } catch (error) {
17
+ throw new Error(`Error parsing malware database: ${error.message}`);
18
+ }
19
+ }
20
+
21
+ export async function fetchMalwareDatabaseVersion() {
22
+ const response = await fetch(malwareDatabaseUrl, {
23
+ method: "HEAD",
24
+ });
25
+ if (!response.ok) {
26
+ throw new Error(
27
+ `Error fetching malware database version: ${response.statusText}`
28
+ );
29
+ }
30
+ return response.headers.get("etag") || undefined;
31
+ }
@@ -0,0 +1,46 @@
1
+ import * as semver from "semver";
2
+ import * as npmFetch from "npm-registry-fetch";
3
+
4
+ export async function resolvePackageVersion(packageName, versionRange) {
5
+ if (!versionRange) {
6
+ versionRange = "latest";
7
+ }
8
+
9
+ if (semver.valid(versionRange)) {
10
+ // The version is a fixed version, no need to resolve
11
+ return versionRange;
12
+ }
13
+
14
+ const packageInfo = await getPackageInfo(packageName);
15
+ if (!packageInfo) {
16
+ // It is possible that no version is found (could be a private package, or a package that doesn't exist)
17
+ // In this case, we return null to indicate that we couldn't resolve the version
18
+ return null;
19
+ }
20
+
21
+ const distTags = packageInfo["dist-tags"];
22
+ if (distTags && distTags[versionRange]) {
23
+ // If the version range is a dist-tag, return the version associated with that tag
24
+ // e.g., "latest", "next", etc.
25
+ return distTags[versionRange];
26
+ }
27
+
28
+ // If the version range is not a dist-tag, we need to resolve the highest version matching the range.
29
+ // This is useful for ranges like "^1.0.0" or "~2.3.4".
30
+ const availableVersions = Object.keys(packageInfo.versions);
31
+ const resolvedVersion = semver.maxSatisfying(availableVersions, versionRange);
32
+ if (resolvedVersion) {
33
+ return resolvedVersion;
34
+ }
35
+
36
+ // Nothing matched the range, return null
37
+ return null;
38
+ }
39
+
40
+ async function getPackageInfo(packageName) {
41
+ try {
42
+ return await npmFetch.json(packageName);
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
@@ -0,0 +1,91 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import os from "os";
4
+ import { ui } from "../environment/userInteraction.js";
5
+
6
+ export function getScanTimeout() {
7
+ const config = readConfigFile();
8
+ return (
9
+ parseInt(process.env.AIKIDO_SCAN_TIMEOUT_MS) || config.scanTimeout || 10000 // Default to 10 seconds
10
+ );
11
+ }
12
+
13
+ export function writeDatabaseToLocalCache(data, version) {
14
+ try {
15
+ const databasePath = getDatabasePath();
16
+ const versionPath = getDatabaseVersionPath();
17
+
18
+ fs.writeFileSync(databasePath, JSON.stringify(data));
19
+ fs.writeFileSync(versionPath, version.toString());
20
+ } catch {
21
+ ui.writeWarning(
22
+ "Failed to write malware database to local cache, next time the database will be fetched from the server again."
23
+ );
24
+ }
25
+ }
26
+
27
+ export function readDatabaseFromLocalCache() {
28
+ try {
29
+ const databasePath = getDatabasePath();
30
+ if (!fs.existsSync(databasePath)) {
31
+ return {
32
+ malwareDatabase: null,
33
+ version: null,
34
+ };
35
+ }
36
+ const data = fs.readFileSync(databasePath, "utf8");
37
+ const malwareDatabase = JSON.parse(data);
38
+ const versionPath = getDatabaseVersionPath();
39
+ let version = null;
40
+ if (fs.existsSync(versionPath)) {
41
+ version = fs.readFileSync(versionPath, "utf8").trim();
42
+ }
43
+ return {
44
+ malwareDatabase: malwareDatabase,
45
+ version: version,
46
+ };
47
+ } catch {
48
+ ui.writeWarning(
49
+ "Failed to read malware database from local cache. Continuing without local cache."
50
+ );
51
+ return {
52
+ malwareDatabase: null,
53
+ version: null,
54
+ };
55
+ }
56
+ }
57
+
58
+ function readConfigFile() {
59
+ const configFilePath = getConfigFilePath();
60
+
61
+ if (!fs.existsSync(configFilePath)) {
62
+ return {};
63
+ }
64
+
65
+ const data = fs.readFileSync(configFilePath, "utf8");
66
+ return JSON.parse(data);
67
+ }
68
+
69
+ function getDatabasePath() {
70
+ const aikidoDir = getAikidoDirectory();
71
+ return path.join(aikidoDir, "malwareDatabase.json");
72
+ }
73
+
74
+ function getDatabaseVersionPath() {
75
+ const aikidoDir = getAikidoDirectory();
76
+ return path.join(aikidoDir, "version.txt");
77
+ }
78
+
79
+ function getConfigFilePath() {
80
+ return path.join(getAikidoDirectory(), "config.json");
81
+ }
82
+
83
+ function getAikidoDirectory() {
84
+ const homeDir = os.homedir();
85
+ const aikidoDir = path.join(homeDir, ".aikido");
86
+
87
+ if (!fs.existsSync(aikidoDir)) {
88
+ fs.mkdirSync(aikidoDir, { recursive: true });
89
+ }
90
+ return aikidoDir;
91
+ }
@@ -0,0 +1,14 @@
1
+ export function isCi() {
2
+ const ciEnvironments = [
3
+ "CI",
4
+ "TF_BUILD", // Azure devops does not set CI, but TF_BUILD
5
+ ];
6
+
7
+ for (const env of ciEnvironments) {
8
+ if (process.env[env]) {
9
+ return true;
10
+ }
11
+ }
12
+
13
+ return false;
14
+ }
@@ -0,0 +1,79 @@
1
+ import chalk from "chalk";
2
+ import ora from "ora";
3
+ import { confirm as inquirerConfirm } from "@inquirer/prompts";
4
+ import { isCi } from "./environment.js";
5
+
6
+ function emptyLine() {
7
+ writeInformation("");
8
+ }
9
+
10
+ function writeInformation(message, ...optionalParams) {
11
+ console.log(message, ...optionalParams);
12
+ }
13
+
14
+ function writeWarning(message, ...optionalParams) {
15
+ if (!isCi()) {
16
+ message = chalk.yellow(message);
17
+ }
18
+ console.warn(message, ...optionalParams);
19
+ }
20
+
21
+ function writeError(message, ...optionalParams) {
22
+ if (!isCi()) {
23
+ message = chalk.red(message);
24
+ }
25
+ console.error(message, ...optionalParams);
26
+ }
27
+
28
+ function startProcess(message) {
29
+ if (isCi()) {
30
+ return {
31
+ succeed: (message) => {
32
+ writeInformation(message);
33
+ },
34
+ fail: (message) => {
35
+ writeError(message);
36
+ },
37
+ stop: () => {},
38
+ setText: (message) => {
39
+ writeInformation(message);
40
+ },
41
+ };
42
+ } else {
43
+ const spinner = ora(message).start();
44
+ return {
45
+ succeed: (message) => {
46
+ spinner.succeed(message);
47
+ },
48
+ fail: (message) => {
49
+ spinner.fail(message);
50
+ },
51
+ stop: () => {
52
+ spinner.stop();
53
+ },
54
+ setText: (message) => {
55
+ spinner.text = message;
56
+ },
57
+ };
58
+ }
59
+ }
60
+
61
+ async function confirm(config) {
62
+ if (isCi()) {
63
+ return Promise.resolve(config.default);
64
+ } else {
65
+ return inquirerConfirm({
66
+ message: config.message,
67
+ default: config.default,
68
+ });
69
+ }
70
+ }
71
+
72
+ export const ui = {
73
+ writeInformation,
74
+ writeWarning,
75
+ writeError,
76
+ emptyLine,
77
+ startProcess,
78
+ confirm,
79
+ };
package/src/main.js ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { scanCommand, shouldScanCommand } from "./scanning/index.js";
4
+ import { ui } from "./environment/userInteraction.js";
5
+ import { getPackageManager } from "./packagemanager/currentPackageManager.js";
6
+
7
+ export async function main(args) {
8
+ try {
9
+ if (shouldScanCommand(args)) {
10
+ await scanCommand(args);
11
+ }
12
+ } catch (error) {
13
+ ui.writeError("Failed to check for malicious packages:", error.message);
14
+ }
15
+
16
+ var result = getPackageManager().runCommand(args);
17
+ process.exit(result.status);
18
+ }
@@ -0,0 +1,28 @@
1
+ import { createNpmPackageManager } from "./npm/createPackageManager.js";
2
+ import { createNpxPackageManager } from "./npx/createPackageManager.js";
3
+ import { createYarnPackageManager } from "./yarn/createPackageManager.js";
4
+
5
+ const state = {
6
+ packageManagerName: null,
7
+ };
8
+
9
+ export function initializePackageManager(packageManagerName, version) {
10
+ if (packageManagerName === "npm") {
11
+ state.packageManagerName = createNpmPackageManager(version);
12
+ } else if (packageManagerName === "npx") {
13
+ state.packageManagerName = createNpxPackageManager();
14
+ } else if (packageManagerName === "yarn") {
15
+ state.packageManagerName = createYarnPackageManager();
16
+ } else {
17
+ throw new Error("Unsupported package manager: " + packageManagerName);
18
+ }
19
+
20
+ return state.packageManagerName;
21
+ }
22
+
23
+ export function getPackageManager() {
24
+ if (!state.packageManagerName) {
25
+ throw new Error("Package manager not initialized.");
26
+ }
27
+ return state.packageManagerName;
28
+ }