@aikidosec/safe-chain 1.0.0 → 1.0.10
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/.editorconfig +8 -0
- package/.github/workflows/build-and-release.yml +41 -0
- package/.github/workflows/test-on-pr.yml +28 -0
- package/README.md +55 -0
- package/bin/aikido-npm.js +8 -0
- package/bin/aikido-npx.js +8 -0
- package/bin/aikido-yarn.js +8 -0
- package/eslint.config.js +25 -0
- package/package.json +27 -5
- package/safe-package-manager-demo.gif +0 -0
- package/src/api/aikido.js +31 -0
- package/src/api/npmApi.js +46 -0
- package/src/config/configFile.js +91 -0
- package/src/environment/environment.js +14 -0
- package/src/environment/userInteraction.js +79 -0
- package/src/main.js +31 -0
- package/src/packagemanager/currentPackageManager.js +28 -0
- package/src/packagemanager/npm/createPackageManager.js +83 -0
- package/src/packagemanager/npm/dependencyScanner/commandArgumentScanner.js +37 -0
- package/src/packagemanager/npm/dependencyScanner/dryRunScanner.js +50 -0
- package/src/packagemanager/npm/dependencyScanner/nullScanner.js +6 -0
- package/src/packagemanager/npm/parsing/parseNpmInstallDryRunOutput.js +57 -0
- package/src/packagemanager/npm/parsing/parseNpmInstallDryRunOutput.spec.js +134 -0
- package/src/packagemanager/npm/parsing/parsePackagesFromInstallArgs.js +109 -0
- package/src/packagemanager/npm/parsing/parsePackagesFromInstallArgs.spec.js +176 -0
- package/src/packagemanager/npm/runNpmCommand.js +33 -0
- package/src/packagemanager/npm/utils/cmd-list.js +171 -0
- package/src/packagemanager/npm/utils/npmCommands.js +26 -0
- package/src/packagemanager/npx/createPackageManager.js +13 -0
- package/src/packagemanager/npx/dependencyScanner/commandArgumentScanner.js +31 -0
- package/src/packagemanager/npx/parsing/parsePackagesFromArguments.js +106 -0
- package/src/packagemanager/npx/parsing/parsePackagesFromArguments.spec.js +147 -0
- package/src/packagemanager/npx/runNpxCommand.js +17 -0
- package/src/packagemanager/yarn/createPackageManager.js +34 -0
- package/src/packagemanager/yarn/dependencyScanner/commandArgumentScanner.js +28 -0
- package/src/packagemanager/yarn/parsing/parsePackagesFromArguments.js +102 -0
- package/src/packagemanager/yarn/parsing/parsePackagesFromArguments.spec.js +126 -0
- package/src/packagemanager/yarn/runYarnCommand.js +17 -0
- package/src/scanning/audit/index.js +56 -0
- package/src/scanning/index.js +94 -0
- package/src/scanning/index.scanCommand.spec.js +180 -0
- package/src/scanning/index.shouldScanCommand.spec.js +47 -0
- package/src/scanning/malwareDatabase.js +62 -0
- package/src/shell-integration/addAlias.js +63 -0
- package/src/shell-integration/helpers.js +44 -0
- package/src/shell-integration/removeAlias.js +61 -0
- package/src/shell-integration/shellIntegration.spec.js +172 -0
package/.editorconfig
ADDED
|
@@ -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,55 @@
|
|
|
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
|
+

|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
To install the Aikido Safe Chain, you can use the following command:
|
|
10
|
+
|
|
11
|
+
```shell
|
|
12
|
+
npm i -g @aikidosec/safe-chain
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Now you should be able to use the `aikido-npm`, `aikido-npx`, or `aikido-yarn` command instead of `npm`, `npx`, or `yarn`. Example: `aikido-npm install axios`, `aikido-yarn add lodash`.
|
|
16
|
+
|
|
17
|
+
## Aliases in shell
|
|
18
|
+
|
|
19
|
+
It is possible to create aliases in your shell startup script to make it easier to use the Aikido Safe Chain. This is useful if you want to use the Aikido Safe Chain as a drop-in replacement for **npm**, **npx**, or **yarn**. The aikido-npm, aikido-npx, and aikido-yarn commands will scan for malware and prompt you to exit if any is found. If not, they will run the original **npm**, **npx**, or **yarn** command.
|
|
20
|
+
|
|
21
|
+
### Creating an alias
|
|
22
|
+
|
|
23
|
+
The `add-aikido-aliases` command will add the aliases for **npm**, **npx**, and **yarn** to your shell startup script.
|
|
24
|
+
|
|
25
|
+
To add aliases to your shell startup script, you can use the built-in command `aikido-npm add-aikido-aliases`:
|
|
26
|
+
|
|
27
|
+
```shell
|
|
28
|
+
# Example for bash
|
|
29
|
+
aikido-npm add-aikido-aliases ~/.bashrc
|
|
30
|
+
|
|
31
|
+
# Example for zsh
|
|
32
|
+
aikido-npm add-aikido-aliases ~/.zshrc
|
|
33
|
+
|
|
34
|
+
# Example for powershell
|
|
35
|
+
aikido-npm add-aikido-aliases $PROFILE
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
This will create the aliases. The following table shows the aliases that will be created in the shell startup script:
|
|
39
|
+
|
|
40
|
+
| Shell | Startup script | Npm Alias | Npx Alias | Yarn Alias |
|
|
41
|
+
| -------------- | -------------------------- | --------------------------------------- | --------------------------------------- | ----------------------------------------- |
|
|
42
|
+
| **Bash** | ~/.bashrc | `alias npm='aikido-npm'` | `alias npx='aikido-npx'` | `alias yarn='aikido-yarn'` |
|
|
43
|
+
| **Zsh** | ~/.zshrc | `alias npm='aikido-npm'` | `alias npx='aikido-npx'` | `alias yarn='aikido-yarn'` |
|
|
44
|
+
| **Ash** | ~/.profile, ~/.ashrc | `alias npm='aikido-npm'` | `alias npx='aikido-npx'` | `alias yarn='aikido-yarn'` |
|
|
45
|
+
| **Fish** | ~/.config/fish/config.fish | `alias npm "aikido-npm"` | `alias npx "aikido-npx"` | `alias yarn "aikido-yarn"` |
|
|
46
|
+
| **Powershell** | $PROFILE | `Set-Alias -Name npm -Value aikido-npm` | `Set-Alias -Name npx -Value aikido-npx` | `Set-Alias -Name yarn -Value aikido-yarn` |
|
|
47
|
+
|
|
48
|
+
After adding the alias, **the shell needs to restart in order to load the alias**.
|
|
49
|
+
|
|
50
|
+
### Removing the alias
|
|
51
|
+
|
|
52
|
+
To remove the added aliases, you can use the built-in commands of `aikido-npm`:
|
|
53
|
+
|
|
54
|
+
- `aikido-npm remove-aikido-aliases file_name` (eg `~/.bashrc`, `~/.zshrc`, etc.)
|
|
55
|
+
This will remove the aliases if they are present in the file.
|
|
@@ -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));
|
package/eslint.config.js
ADDED
|
@@ -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,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aikidosec/safe-chain",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"scripts": {
|
|
3
|
+
"version": "1.0.10",
|
|
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
|
+
},
|
|
10
18
|
"type": "module",
|
|
11
19
|
"keywords": [],
|
|
12
20
|
"author": "Aikido Security",
|
|
13
21
|
"license": "AGPL-3.0-or-later",
|
|
14
|
-
"description": "The Aikido Safe
|
|
15
|
-
"dependencies": {
|
|
22
|
+
"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.",
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@inquirer/prompts": "^7.4.1",
|
|
25
|
+
"abbrev": "^3.0.1",
|
|
26
|
+
"chalk": "^5.4.1",
|
|
27
|
+
"npm-registry-fetch": "^18.0.2",
|
|
28
|
+
"ora": "^8.2.0",
|
|
29
|
+
"semver": "^7.7.2"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@eslint/js": "^9.26.0",
|
|
33
|
+
"eslint": "^9.26.0",
|
|
34
|
+
"eslint-plugin-import": "^2.31.0",
|
|
35
|
+
"globals": "^16.1.0",
|
|
36
|
+
"typescript-eslint": "^8.32.0"
|
|
37
|
+
},
|
|
16
38
|
"main": "eslint.config.js",
|
|
17
39
|
"bugs": {
|
|
18
40
|
"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,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,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { scanCommand, shouldScanCommand } from "./scanning/index.js";
|
|
4
|
+
import { isAddAliasCommand, addAlias } from "./shell-integration/addAlias.js";
|
|
5
|
+
import {
|
|
6
|
+
removeAlias,
|
|
7
|
+
isRemoveAliasCommand,
|
|
8
|
+
} from "./shell-integration/removeAlias.js";
|
|
9
|
+
import { ui } from "./environment/userInteraction.js";
|
|
10
|
+
import { getPackageManager } from "./packagemanager/currentPackageManager.js";
|
|
11
|
+
|
|
12
|
+
export async function main(args) {
|
|
13
|
+
try {
|
|
14
|
+
if (isAddAliasCommand(args)) {
|
|
15
|
+
addAlias(args);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (isRemoveAliasCommand(args)) {
|
|
19
|
+
removeAlias(args);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (shouldScanCommand(args)) {
|
|
23
|
+
await scanCommand(args);
|
|
24
|
+
}
|
|
25
|
+
} catch (error) {
|
|
26
|
+
ui.writeError("Failed to check for malicious packages:", error.message);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
var result = getPackageManager().runCommand(args);
|
|
30
|
+
process.exit(result.status);
|
|
31
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { commandArgumentScanner } from "./dependencyScanner/commandArgumentScanner.js";
|
|
2
|
+
import { dryRunScanner } from "./dependencyScanner/dryRunScanner.js";
|
|
3
|
+
import { nullScanner } from "./dependencyScanner/nullScanner.js";
|
|
4
|
+
import { runNpm } from "./runNpmCommand.js";
|
|
5
|
+
import {
|
|
6
|
+
getNpmCommandForArgs,
|
|
7
|
+
npmInstallCommand,
|
|
8
|
+
npmCiCommand,
|
|
9
|
+
npmInstallTestCommand,
|
|
10
|
+
npmInstallCiTestCommand,
|
|
11
|
+
npmUpdateCommand,
|
|
12
|
+
npmAuditCommand,
|
|
13
|
+
npmExecCommand,
|
|
14
|
+
} from "./utils/npmCommands.js";
|
|
15
|
+
|
|
16
|
+
export function createNpmPackageManager(version) {
|
|
17
|
+
const supportedScanners =
|
|
18
|
+
getMajorVersion(version) >= 22
|
|
19
|
+
? npm22AndAboveSupportedScanners
|
|
20
|
+
: npm21AndBelowSupportedScanners;
|
|
21
|
+
|
|
22
|
+
function isSupportedCommand(args) {
|
|
23
|
+
const scanner = findDependencyScannerForCommand(supportedScanners, args);
|
|
24
|
+
return scanner.shouldScan(args);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getDependencyUpdatesForCommand(args) {
|
|
28
|
+
const scanner = findDependencyScannerForCommand(supportedScanners, args);
|
|
29
|
+
return scanner.scan(args);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
getWarningMessage: () => warnForLimitedSupport(version),
|
|
34
|
+
runCommand: runNpm,
|
|
35
|
+
isSupportedCommand,
|
|
36
|
+
getDependencyUpdatesForCommand,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const npm22AndAboveSupportedScanners = {
|
|
41
|
+
[npmInstallCommand]: dryRunScanner(),
|
|
42
|
+
[npmUpdateCommand]: dryRunScanner(),
|
|
43
|
+
[npmCiCommand]: dryRunScanner(),
|
|
44
|
+
[npmAuditCommand]: dryRunScanner({
|
|
45
|
+
skipScanWhen: (args) => !args.includes("fix"),
|
|
46
|
+
}),
|
|
47
|
+
[npmExecCommand]: commandArgumentScanner({ ignoreDryRun: true }), // exec command doesn't support dry-run
|
|
48
|
+
|
|
49
|
+
// Running dry-run on install-test and install-ci-test will install & run tests.
|
|
50
|
+
// We only want to know if there are changes in the dependencies.
|
|
51
|
+
// So we run change the dry-run command to only check the install.
|
|
52
|
+
[npmInstallTestCommand]: dryRunScanner({ dryRunCommand: npmInstallCommand }),
|
|
53
|
+
[npmInstallCiTestCommand]: dryRunScanner({ dryRunCommand: npmCiCommand }),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const npm21AndBelowSupportedScanners = {
|
|
57
|
+
[npmInstallCommand]: commandArgumentScanner(),
|
|
58
|
+
[npmUpdateCommand]: commandArgumentScanner(),
|
|
59
|
+
[npmExecCommand]: commandArgumentScanner({ ignoreDryRun: true }), // exec command doesn't support dry-run
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
function warnForLimitedSupport(version) {
|
|
63
|
+
if (getMajorVersion(version) >= 22) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return `Aikido-npm will only scan the arguments of the install command for Node.js version prior to version 22.
|
|
68
|
+
Please update your Node.js version to 22 or higher for full coverage. Current version: v${version}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getMajorVersion(version) {
|
|
72
|
+
return parseInt(version.split(".")[0]);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function findDependencyScannerForCommand(scanners, args) {
|
|
76
|
+
const command = getNpmCommandForArgs(args);
|
|
77
|
+
if (!command) {
|
|
78
|
+
return nullScanner();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const scanner = scanners[command];
|
|
82
|
+
return scanner ? scanner : nullScanner();
|
|
83
|
+
}
|