@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.
- 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 +57 -0
- package/bin/aikido-npm.js +8 -0
- package/bin/aikido-npx.js +8 -0
- package/bin/aikido-yarn.js +8 -0
- package/bin/safe-chain.js +57 -0
- package/eslint.config.js +25 -0
- package/package.json +28 -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 +18 -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 +92 -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/helpers.js +44 -0
- package/src/shell-integration/removeShell.js +140 -0
- package/src/shell-integration/removeShell.spec.js +177 -0
- package/src/shell-integration/setupShell.js +151 -0
- package/src/shell-integration/setupShell.spec.js +304 -0
- package/src/shell-integration/shellDetection.js +75 -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,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
|
+

|
|
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
|
+
}
|
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,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aikidosec/safe-chain",
|
|
3
|
-
"version": "1.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
|
|
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,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
|
+
}
|