@aikidosec/safe-chain 1.0.19 → 1.0.21

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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Aikido Safe Chain
2
2
 
3
- The Aikido Safe Chain **prevents developers from installing malware** on their workstations through npm, npx, yarn, pnpm and pnpx.
3
+ The Aikido Safe Chain **prevents developers from installing malware** on their workstations through npm, npx, yarn, pnpm and pnpx. It's **free** to use and does not require any token.
4
4
 
5
5
  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.
6
6
 
@@ -67,6 +67,21 @@ To uninstall the Aikido Safe Chain, you can run the following command:
67
67
  ```
68
68
  3. **❗Restart your terminal** to remove the aliases.
69
69
 
70
+ # Configuration
71
+
72
+ ## Malware Action
73
+
74
+ You can control how Aikido Safe Chain responds when malware is detected using the `--safe-chain-malware-action` flag:
75
+
76
+ - `--safe-chain-malware-action=block` (**default**) - Automatically blocks installation and exits with an error when malware is detected
77
+ - `--safe-chain-malware-action=prompt` - Prompts the user to decide whether to continue despite the malware detection
78
+
79
+ Example usage:
80
+
81
+ ```shell
82
+ npm install suspicious-package --safe-chain-malware-action=prompt
83
+ ```
84
+
70
85
  # Usage in CI/CD
71
86
 
72
87
  🚧 Support for CI/CD environments is coming soon...
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aikidosec/safe-chain",
3
- "version": "1.0.19",
3
+ "version": "1.0.21",
4
4
  "scripts": {
5
5
  "test": "node --test --experimental-test-module-mocks 'src/**/*.spec.js'",
6
6
  "test:watch": "node --test --watch --experimental-test-module-mocks 'src/**/*.spec.js'",
@@ -0,0 +1,50 @@
1
+ const state = {
2
+ malwareAction: undefined,
3
+ };
4
+
5
+ const SAFE_CHAIN_ARG_PREFIX = "--safe-chain-";
6
+
7
+ export function initializeCliArguments(args) {
8
+ // Reset state on each call
9
+ state.malwareAction = undefined;
10
+
11
+ const safeChainArgs = [];
12
+ const remainingArgs = [];
13
+
14
+ for (const arg of args) {
15
+ if (arg.toLowerCase().startsWith(SAFE_CHAIN_ARG_PREFIX)) {
16
+ safeChainArgs.push(arg);
17
+ } else {
18
+ remainingArgs.push(arg);
19
+ }
20
+ }
21
+
22
+ setMalwareAction(safeChainArgs);
23
+
24
+ return remainingArgs;
25
+ }
26
+
27
+ function setMalwareAction(args) {
28
+ const safeChainMalwareActionArg = SAFE_CHAIN_ARG_PREFIX + "malware-action=";
29
+
30
+ const action = getLastArgEqualsValue(args, safeChainMalwareActionArg);
31
+ if (!action) {
32
+ return;
33
+ }
34
+ state.malwareAction = action.toLowerCase();
35
+ }
36
+
37
+ function getLastArgEqualsValue(args, prefix) {
38
+ for (var i = args.length - 1; i >= 0; i--) {
39
+ const arg = args[i];
40
+ if (arg.toLowerCase().startsWith(prefix)) {
41
+ return arg.substring(prefix.length);
42
+ }
43
+ }
44
+
45
+ return undefined;
46
+ }
47
+
48
+ export function getMalwareAction() {
49
+ return state.malwareAction;
50
+ }
@@ -0,0 +1,14 @@
1
+ import * as cliArguments from "./cliArguments.js";
2
+
3
+ export function getMalwareAction() {
4
+ const action = cliArguments.getMalwareAction();
5
+
6
+ if (action === MALWARE_ACTION_PROMPT) {
7
+ return MALWARE_ACTION_PROMPT;
8
+ }
9
+
10
+ return MALWARE_ACTION_BLOCK;
11
+ }
12
+
13
+ export const MALWARE_ACTION_BLOCK = "block";
14
+ export const MALWARE_ACTION_PROMPT = "prompt";
package/src/main.js CHANGED
@@ -3,9 +3,13 @@
3
3
  import { scanCommand, shouldScanCommand } from "./scanning/index.js";
4
4
  import { ui } from "./environment/userInteraction.js";
5
5
  import { getPackageManager } from "./packagemanager/currentPackageManager.js";
6
+ import { initializeCliArguments } from "./config/cliArguments.js";
6
7
 
7
8
  export async function main(args) {
8
9
  try {
10
+ // This parses all the --safe-chain arguments and removes them from the args array
11
+ args = initializeCliArguments(args);
12
+
9
13
  if (shouldScanCommand(args)) {
10
14
  await scanCommand(args);
11
15
  }
@@ -14,6 +14,7 @@ export function createPnpmPackageManager() {
14
14
  matchesCommand(args, "upgrade") ||
15
15
  matchesCommand(args, "up") ||
16
16
  matchesCommand(args, "install") ||
17
+ matchesCommand(args, "i") ||
17
18
  // dlx does not always come in the first position
18
19
  // eg: pnpm --package=yo --package=generator-webapp dlx yo webapp
19
20
  // documentation: https://pnpm.io/cli/dlx#--package-name
@@ -4,6 +4,7 @@ import { setTimeout } from "timers/promises";
4
4
  import chalk from "chalk";
5
5
  import { getPackageManager } from "../packagemanager/currentPackageManager.js";
6
6
  import { ui } from "../environment/userInteraction.js";
7
+ import { getMalwareAction, MALWARE_ACTION_PROMPT } from "../config/settings.js";
7
8
 
8
9
  export function shouldScanCommand(args) {
9
10
  if (!args || args.length === 0) {
@@ -59,10 +60,7 @@ export async function scanCommand(args) {
59
60
  spinner.succeed("No malicious packages detected.");
60
61
  } else {
61
62
  printMaliciousChanges(audit.disallowedChanges, spinner);
62
- await acceptRiskOrExit(
63
- "Do you want to continue with the installation despite the risks?",
64
- false
65
- );
63
+ await onMalwareFound();
66
64
  }
67
65
  }
68
66
 
@@ -74,19 +72,23 @@ function printMaliciousChanges(changes, spinner) {
74
72
  }
75
73
  }
76
74
 
77
- async function acceptRiskOrExit(message, defaultValue) {
75
+ async function onMalwareFound() {
78
76
  ui.emptyLine();
79
- const continueInstall = await ui.confirm({
80
- message: message,
81
- default: defaultValue,
82
- });
83
-
84
- if (continueInstall) {
85
- ui.writeInformation("Continuing with the installation...");
86
- return;
77
+
78
+ if (getMalwareAction() === MALWARE_ACTION_PROMPT) {
79
+ const continueInstall = await ui.confirm({
80
+ message:
81
+ "Malicious packages were found. Do you want to continue with the installation?",
82
+ default: false,
83
+ });
84
+
85
+ if (continueInstall) {
86
+ ui.writeWarning("Continuing with the installation despite the risks...");
87
+ return;
88
+ }
87
89
  }
88
90
 
89
- ui.writeInformation("Exiting without installing packages.");
91
+ ui.writeError("Exiting without installing malicious packages.");
90
92
  ui.emptyLine();
91
93
  process.exit(1);
92
94
  }
@@ -3,7 +3,8 @@ import {
3
3
  doesExecutableExistOnSystem,
4
4
  removeLinesMatchingPattern,
5
5
  } from "../helpers.js";
6
- import { execSync } from "child_process";
6
+ import { execSync, spawnSync } from "child_process";
7
+ import * as os from "os";
7
8
 
8
9
  const shellName = "Bash";
9
10
  const executableName = "bash";
@@ -43,10 +44,12 @@ function setup() {
43
44
 
44
45
  function getStartupFile() {
45
46
  try {
46
- return execSync(startupFileCommand, {
47
+ var path = execSync(startupFileCommand, {
47
48
  encoding: "utf8",
48
49
  shell: executableName,
49
50
  }).trim();
51
+
52
+ return windowsFixPath(path);
50
53
  } catch (error) {
51
54
  throw new Error(
52
55
  `Command failed: ${startupFileCommand}. Error: ${error.message}`
@@ -54,6 +57,50 @@ function getStartupFile() {
54
57
  }
55
58
  }
56
59
 
60
+ function windowsFixPath(path) {
61
+ try {
62
+ if (os.platform() !== "win32") {
63
+ return path;
64
+ }
65
+
66
+ // On windows cygwin bash, paths are returned in format /c/user/..., but we need it in format C:\user\...
67
+ // To convert, the cygpath -w command can be used to convert to the desired format.
68
+ // Cygpath only exists on Cygwin, so we first check if the command is available.
69
+ // If it is, we use it to convert the path.
70
+ if (hasCygpath()) {
71
+ return cygpathw(path);
72
+ }
73
+
74
+ return path;
75
+ } catch {
76
+ return path;
77
+ }
78
+ }
79
+
80
+ function hasCygpath() {
81
+ try {
82
+ var result = spawnSync("where", ["cygpath"], { shell: executableName });
83
+ return result.status === 0;
84
+ } catch {
85
+ return false;
86
+ }
87
+ }
88
+
89
+ function cygpathw(path) {
90
+ try {
91
+ var result = spawnSync("cygpath", ["-w", path], {
92
+ encoding: "utf8",
93
+ shell: executableName,
94
+ });
95
+ if (result.status === 0) {
96
+ return result.stdout.trim();
97
+ }
98
+ return path;
99
+ } catch {
100
+ return path;
101
+ }
102
+ }
103
+
57
104
  export default {
58
105
  name: shellName,
59
106
  isInstalled,