@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
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import { parsePackagesFromArguments } from "./parsePackagesFromArguments.js";
|
|
4
|
+
|
|
5
|
+
describe("standardYarnArgumentParser", () => {
|
|
6
|
+
it("should return an empty array for no changes", () => {
|
|
7
|
+
const args = ["add"];
|
|
8
|
+
|
|
9
|
+
const result = parsePackagesFromArguments(args);
|
|
10
|
+
|
|
11
|
+
assert.deepEqual(result, []);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("should return an array of changes for one package", () => {
|
|
15
|
+
const args = ["add", "axios@1.9.0"];
|
|
16
|
+
|
|
17
|
+
const result = parsePackagesFromArguments(args);
|
|
18
|
+
|
|
19
|
+
assert.deepEqual(result, [{ name: "axios", version: "1.9.0" }]);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("should return the package with latest tag if absent", () => {
|
|
23
|
+
const args = ["add", "axios"];
|
|
24
|
+
|
|
25
|
+
const result = parsePackagesFromArguments(args);
|
|
26
|
+
|
|
27
|
+
assert.deepEqual(result, [{ name: "axios", version: "latest" }]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("should only return all packages", () => {
|
|
31
|
+
const args = ["add", "axios", "jest"];
|
|
32
|
+
|
|
33
|
+
const result = parsePackagesFromArguments(args);
|
|
34
|
+
|
|
35
|
+
assert.deepEqual(result, [
|
|
36
|
+
{ name: "axios", version: "latest" },
|
|
37
|
+
{ name: "jest", version: "latest" },
|
|
38
|
+
]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("should ignore options with parameters and return an array of changes", () => {
|
|
42
|
+
const args = ["add", "--proxy", "http://localhost", "axios@1.9.0"];
|
|
43
|
+
|
|
44
|
+
const result = parsePackagesFromArguments(args);
|
|
45
|
+
|
|
46
|
+
assert.deepEqual(result, [{ name: "axios", version: "1.9.0" }]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("should parse version even for aliased packages", () => {
|
|
50
|
+
const args = ["add", "server@npm:axios@1.9.0"];
|
|
51
|
+
|
|
52
|
+
const result = parsePackagesFromArguments(args);
|
|
53
|
+
|
|
54
|
+
assert.deepEqual(result, [{ name: "axios", version: "1.9.0" }]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should parse scoped packages", () => {
|
|
58
|
+
const args = ["add", "@scope/package@1.0.0"];
|
|
59
|
+
|
|
60
|
+
const result = parsePackagesFromArguments(args);
|
|
61
|
+
|
|
62
|
+
assert.deepEqual(result, [{ name: "@scope/package", version: "1.0.0" }]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("should parse packages with version ranges", () => {
|
|
66
|
+
const args = ["add", "axios@^1.9.0"];
|
|
67
|
+
|
|
68
|
+
const result = parsePackagesFromArguments(args);
|
|
69
|
+
|
|
70
|
+
assert.deepEqual(result, [{ name: "axios", version: "^1.9.0" }]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("should parse package folders", () => {
|
|
74
|
+
const args = ["add", "./local-package"];
|
|
75
|
+
|
|
76
|
+
const result = parsePackagesFromArguments(args);
|
|
77
|
+
|
|
78
|
+
assert.deepEqual(result, [{ name: "./local-package", version: "latest" }]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("should parse tarballs", () => {
|
|
82
|
+
const args = ["add", "file:./local-package.tgz"];
|
|
83
|
+
|
|
84
|
+
const result = parsePackagesFromArguments(args);
|
|
85
|
+
|
|
86
|
+
assert.deepEqual(result, [
|
|
87
|
+
{ name: "file:./local-package.tgz", version: "latest" },
|
|
88
|
+
]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("should parse tarball URLs", () => {
|
|
92
|
+
const args = ["add", "https://example.com/local-package.tgz"];
|
|
93
|
+
|
|
94
|
+
const result = parsePackagesFromArguments(args);
|
|
95
|
+
|
|
96
|
+
assert.deepEqual(result, [
|
|
97
|
+
{ name: "https://example.com/local-package.tgz", version: "latest" },
|
|
98
|
+
]);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("should parse git URLs", () => {
|
|
102
|
+
const args = ["add", "git://github.com/http-party/http-server"];
|
|
103
|
+
|
|
104
|
+
const result = parsePackagesFromArguments(args);
|
|
105
|
+
|
|
106
|
+
assert.deepEqual(result, [
|
|
107
|
+
{ name: "git://github.com/http-party/http-server", version: "latest" },
|
|
108
|
+
]);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("should parse packages with -p {packageName}", () => {
|
|
112
|
+
const args = ["dlx", "-p", "axios@1.9.0"];
|
|
113
|
+
|
|
114
|
+
const result = parsePackagesFromArguments(args);
|
|
115
|
+
|
|
116
|
+
assert.deepEqual(result, [{ name: "axios", version: "1.9.0" }]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("should parse packages with --package {packageName}", () => {
|
|
120
|
+
const args = ["dlx", "--package", "axios@1.9.0"];
|
|
121
|
+
|
|
122
|
+
const result = parsePackagesFromArguments(args);
|
|
123
|
+
|
|
124
|
+
assert.deepEqual(result, [{ name: "axios", version: "1.9.0" }]);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import { ui } from "../../environment/userInteraction.js";
|
|
3
|
+
|
|
4
|
+
export function runYarnCommand(args) {
|
|
5
|
+
try {
|
|
6
|
+
const npxCommand = `yarn ${args.join(" ")}`;
|
|
7
|
+
execSync(npxCommand, { stdio: "inherit" });
|
|
8
|
+
} catch (error) {
|
|
9
|
+
if (error.status) {
|
|
10
|
+
return { status: error.status };
|
|
11
|
+
} else {
|
|
12
|
+
ui.writeError("Error executing command:", error.message);
|
|
13
|
+
return { status: 1 };
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return { status: 0 };
|
|
17
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MALWARE_STATUS_MALWARE,
|
|
3
|
+
openMalwareDatabase,
|
|
4
|
+
} from "../malwareDatabase.js";
|
|
5
|
+
|
|
6
|
+
export async function auditChanges(changes) {
|
|
7
|
+
const allowedChanges = [];
|
|
8
|
+
const disallowedChanges = [];
|
|
9
|
+
|
|
10
|
+
var malwarePackages = await getPackagesWithMalware(
|
|
11
|
+
changes.filter(
|
|
12
|
+
(change) => change.type === "add" || change.type === "change"
|
|
13
|
+
)
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
for (const change of changes) {
|
|
17
|
+
const malwarePackage = malwarePackages.find(
|
|
18
|
+
(pkg) => pkg.name === change.name && pkg.version === change.version
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
if (malwarePackage) {
|
|
22
|
+
disallowedChanges.push({ ...change, reason: malwarePackage.status });
|
|
23
|
+
} else {
|
|
24
|
+
allowedChanges.push(change);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const auditResults = {
|
|
29
|
+
allowedChanges,
|
|
30
|
+
disallowedChanges,
|
|
31
|
+
isAllowed: disallowedChanges.length === 0,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return auditResults;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function getPackagesWithMalware(changes) {
|
|
38
|
+
if (changes.length === 0) {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const malwareDb = await openMalwareDatabase();
|
|
43
|
+
let allVulnerablePackages = [];
|
|
44
|
+
|
|
45
|
+
for (const change of changes) {
|
|
46
|
+
if (malwareDb.isMalware(change.name, change.version)) {
|
|
47
|
+
allVulnerablePackages.push({
|
|
48
|
+
name: change.name,
|
|
49
|
+
version: change.version,
|
|
50
|
+
status: MALWARE_STATUS_MALWARE,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return allVulnerablePackages;
|
|
56
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { auditChanges } from "./audit/index.js";
|
|
2
|
+
import { getScanTimeout } from "../config/configFile.js";
|
|
3
|
+
import { setTimeout } from "timers/promises";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { getPackageManager } from "../packagemanager/currentPackageManager.js";
|
|
6
|
+
import { ui } from "../environment/userInteraction.js";
|
|
7
|
+
|
|
8
|
+
export function shouldScanCommand(args) {
|
|
9
|
+
if (!args || args.length === 0) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return getPackageManager().isSupportedCommand(args);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function scanCommand(args) {
|
|
17
|
+
if (!shouldScanCommand(args)) {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let timedOut = false;
|
|
22
|
+
|
|
23
|
+
const spinner = ui.startProcess("Scanning for malicious packages...");
|
|
24
|
+
let audit;
|
|
25
|
+
|
|
26
|
+
await Promise.race([
|
|
27
|
+
(async () => {
|
|
28
|
+
try {
|
|
29
|
+
const packageManager = getPackageManager();
|
|
30
|
+
const changes = await packageManager.getDependencyUpdatesForCommand(
|
|
31
|
+
args
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
if (timedOut) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (changes.length > 0) {
|
|
39
|
+
spinner.setText(`Scanning ${changes.length} package(s)...`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
audit = await auditChanges(changes);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
spinner.fail(`Error while scanning: ${error.message}`);
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
})(),
|
|
48
|
+
setTimeout(getScanTimeout()).then(() => {
|
|
49
|
+
timedOut = true;
|
|
50
|
+
}),
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
if (timedOut) {
|
|
54
|
+
spinner.fail("Timeout exceeded while scanning.");
|
|
55
|
+
throw new Error("Timeout exceeded while scanning npm install command.");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!audit || audit.isAllowed) {
|
|
59
|
+
spinner.succeed("No malicious packages detected.");
|
|
60
|
+
} else {
|
|
61
|
+
printMaliciousChanges(audit.disallowedChanges, spinner);
|
|
62
|
+
await acceptRiskOrExit(
|
|
63
|
+
"Do you want to continue with the installation despite the risks?",
|
|
64
|
+
false
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function printMaliciousChanges(changes, spinner) {
|
|
70
|
+
spinner.fail(chalk.bold("Malicious changes detected:"));
|
|
71
|
+
|
|
72
|
+
for (const change of changes) {
|
|
73
|
+
ui.writeInformation(` - ${change.name}@${change.version}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function acceptRiskOrExit(message, defaultValue) {
|
|
78
|
+
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;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
ui.writeInformation(
|
|
90
|
+
"Exiting without installing packages. Please check the output."
|
|
91
|
+
);
|
|
92
|
+
ui.emptyLine();
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it, mock } from "node:test";
|
|
3
|
+
import { setTimeout } from "node:timers/promises";
|
|
4
|
+
|
|
5
|
+
describe("scanCommand", async () => {
|
|
6
|
+
const getScanTimeoutMock = mock.fn(() => 1000);
|
|
7
|
+
const mockGetDependencyUpdatesForCommand = mock.fn();
|
|
8
|
+
const mockStartProcess = mock.fn(() => ({
|
|
9
|
+
setText: () => {},
|
|
10
|
+
succeed: () => {},
|
|
11
|
+
fail: () => {},
|
|
12
|
+
}));
|
|
13
|
+
const mockConfirm = mock.fn(() => true);
|
|
14
|
+
|
|
15
|
+
// import { getPackageManager } from "../packagemanager/currentPackageManager.js";
|
|
16
|
+
mock.module("../packagemanager/currentPackageManager.js", {
|
|
17
|
+
namedExports: {
|
|
18
|
+
getPackageManager: () => {
|
|
19
|
+
return {
|
|
20
|
+
isSupportedCommand: () => true,
|
|
21
|
+
getDependencyUpdatesForCommand: mockGetDependencyUpdatesForCommand,
|
|
22
|
+
};
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// import { getScanTimeout } from "../config/configFile.js";
|
|
28
|
+
mock.module("../config/configFile.js", {
|
|
29
|
+
namedExports: {
|
|
30
|
+
getScanTimeout: getScanTimeoutMock,
|
|
31
|
+
getBaseUrl: () => undefined,
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// import { ui } from "../environment/userInteraction.js";
|
|
36
|
+
mock.module("../environment/userInteraction.js", {
|
|
37
|
+
namedExports: {
|
|
38
|
+
ui: {
|
|
39
|
+
startProcess: mockStartProcess,
|
|
40
|
+
writeError: () => {},
|
|
41
|
+
writeInformation: () => {},
|
|
42
|
+
writeWarning: () => {},
|
|
43
|
+
emptyLine: () => {},
|
|
44
|
+
confirm: mockConfirm,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// import { auditChanges, MAX_LENGTH_EXCEEDED } from "./audit/index.js";
|
|
50
|
+
mock.module("./audit/index.js", {
|
|
51
|
+
namedExports: {
|
|
52
|
+
auditChanges: (changes) => {
|
|
53
|
+
const malisciousChangeName = "malicious";
|
|
54
|
+
const allowedChanges = changes.filter(
|
|
55
|
+
(change) => change.name !== malisciousChangeName
|
|
56
|
+
);
|
|
57
|
+
const disallowedChanges = changes
|
|
58
|
+
.filter((change) => change.name === malisciousChangeName)
|
|
59
|
+
.map((change) => ({
|
|
60
|
+
...change,
|
|
61
|
+
reason: "malicious",
|
|
62
|
+
}));
|
|
63
|
+
const auditResults = {
|
|
64
|
+
allowedChanges,
|
|
65
|
+
disallowedChanges,
|
|
66
|
+
isAllowed: disallowedChanges.length === 0,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return auditResults;
|
|
70
|
+
},
|
|
71
|
+
MAX_LENGTH_EXCEEDED: "MAX_LENGTH_EXCEEDED",
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const { scanCommand } = await import("./index.js");
|
|
76
|
+
|
|
77
|
+
it("should succeed when there are no changes", async () => {
|
|
78
|
+
let successMessageWasSet = false;
|
|
79
|
+
mockStartProcess.mock.mockImplementationOnce(() => ({
|
|
80
|
+
setText: () => {},
|
|
81
|
+
succeed: () => {
|
|
82
|
+
successMessageWasSet = true;
|
|
83
|
+
},
|
|
84
|
+
fail: () => {},
|
|
85
|
+
}));
|
|
86
|
+
mockGetDependencyUpdatesForCommand.mock.mockImplementation(() => []);
|
|
87
|
+
|
|
88
|
+
await scanCommand(["install", "lodash"]);
|
|
89
|
+
|
|
90
|
+
assert.equal(successMessageWasSet, true);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("should succeed when changes are not malicious", async () => {
|
|
94
|
+
let successMessageWasSet = false;
|
|
95
|
+
mockStartProcess.mock.mockImplementationOnce(() => ({
|
|
96
|
+
setText: () => {},
|
|
97
|
+
succeed: () => {
|
|
98
|
+
successMessageWasSet = true;
|
|
99
|
+
},
|
|
100
|
+
fail: () => {},
|
|
101
|
+
}));
|
|
102
|
+
mockGetDependencyUpdatesForCommand.mock.mockImplementation(() => [
|
|
103
|
+
{ name: "lodash", version: "4.17.21" },
|
|
104
|
+
]);
|
|
105
|
+
|
|
106
|
+
await scanCommand(["install", "lodash"]);
|
|
107
|
+
|
|
108
|
+
assert.equal(successMessageWasSet, true);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("should throw an error when timing out", async () => {
|
|
112
|
+
let failureMessageWasSet = false;
|
|
113
|
+
mockStartProcess.mock.mockImplementationOnce(() => ({
|
|
114
|
+
setText: () => {},
|
|
115
|
+
succeed: () => {},
|
|
116
|
+
fail: () => {
|
|
117
|
+
failureMessageWasSet = true;
|
|
118
|
+
},
|
|
119
|
+
}));
|
|
120
|
+
getScanTimeoutMock.mock.mockImplementationOnce(() => 100);
|
|
121
|
+
mockGetDependencyUpdatesForCommand.mock.mockImplementation(async () => {
|
|
122
|
+
await setTimeout(150);
|
|
123
|
+
return [{ name: "lodash", version: "4.17.21" }];
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
await assert.rejects(scanCommand(["install", "lodash"]));
|
|
127
|
+
|
|
128
|
+
assert.equal(failureMessageWasSet, true);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("should fail and prompt the user when malicious changes are detected", async () => {
|
|
132
|
+
let failureMessageWasSet = false;
|
|
133
|
+
mockStartProcess.mock.mockImplementationOnce(() => ({
|
|
134
|
+
setText: () => {},
|
|
135
|
+
succeed: () => {},
|
|
136
|
+
fail: () => {
|
|
137
|
+
failureMessageWasSet = true;
|
|
138
|
+
},
|
|
139
|
+
}));
|
|
140
|
+
mockGetDependencyUpdatesForCommand.mock.mockImplementation(() => [
|
|
141
|
+
{ name: "malicious", version: "1.0.0" },
|
|
142
|
+
]);
|
|
143
|
+
let userWasPrompted = false;
|
|
144
|
+
mockConfirm.mock.mockImplementationOnce(() => {
|
|
145
|
+
userWasPrompted = true;
|
|
146
|
+
return true; // Simulate user accepting the risk, otherwise the process would exit
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
await scanCommand(["install", "malicious"]);
|
|
150
|
+
|
|
151
|
+
assert.equal(failureMessageWasSet, true);
|
|
152
|
+
assert.equal(userWasPrompted, true);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("should not report a timeout when the user takes a long time to respond (it should not affect the timeout)", async () => {
|
|
156
|
+
let failureMessages = [];
|
|
157
|
+
mockStartProcess.mock.mockImplementationOnce(() => ({
|
|
158
|
+
setText: () => {},
|
|
159
|
+
succeed: () => {},
|
|
160
|
+
fail: (message) => {
|
|
161
|
+
failureMessages.push(message);
|
|
162
|
+
},
|
|
163
|
+
}));
|
|
164
|
+
getScanTimeoutMock.mock.mockImplementationOnce(() => 100);
|
|
165
|
+
mockGetDependencyUpdatesForCommand.mock.mockImplementation(async () => {
|
|
166
|
+
return [{ name: "malicious", version: "4.17.21" }];
|
|
167
|
+
});
|
|
168
|
+
mockConfirm.mock.mockImplementationOnce(async () => {
|
|
169
|
+
await setTimeout(200);
|
|
170
|
+
return true; // Simulate user accepting the risk, otherwise the process would exit
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
await scanCommand(["install", "malicious"]);
|
|
174
|
+
|
|
175
|
+
assert.equal(failureMessages.length, 1);
|
|
176
|
+
const failureMessage = failureMessages[0];
|
|
177
|
+
assert.equal(failureMessage.toLowerCase().includes("timeout"), false);
|
|
178
|
+
assert.equal(failureMessage.toLowerCase().includes("malicious"), true);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it, mock } from "node:test";
|
|
3
|
+
|
|
4
|
+
describe("shouldScanCommand", async () => {
|
|
5
|
+
const isSupportedCommandMock = mock.fn(() => undefined);
|
|
6
|
+
|
|
7
|
+
mock.module("../packagemanager/currentPackageManager.js", {
|
|
8
|
+
namedExports: {
|
|
9
|
+
getPackageManager: () => {
|
|
10
|
+
return {
|
|
11
|
+
isSupportedCommand: isSupportedCommandMock,
|
|
12
|
+
getDependencyUpdatesForCommand: () => [],
|
|
13
|
+
};
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const { shouldScanCommand } = await import("./index.js");
|
|
19
|
+
|
|
20
|
+
it("should return false if the argument is an empty array", () => {
|
|
21
|
+
const result = shouldScanCommand([]);
|
|
22
|
+
|
|
23
|
+
assert.strictEqual(result, false);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("should return false if the argument is undefined", () => {
|
|
27
|
+
const result = shouldScanCommand(undefined);
|
|
28
|
+
|
|
29
|
+
assert.strictEqual(result, false);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("should return true if the package manager supports the command", () => {
|
|
33
|
+
isSupportedCommandMock.mock.mockImplementation(() => true);
|
|
34
|
+
|
|
35
|
+
const result = shouldScanCommand(["install", "lodash"]);
|
|
36
|
+
|
|
37
|
+
assert.strictEqual(result, true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("should return false if the package manager does not support the command", () => {
|
|
41
|
+
isSupportedCommandMock.mock.mockImplementation(() => false);
|
|
42
|
+
|
|
43
|
+
const result = shouldScanCommand(["unknown", "command"]);
|
|
44
|
+
|
|
45
|
+
assert.strictEqual(result, false);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {
|
|
2
|
+
fetchMalwareDatabase,
|
|
3
|
+
fetchMalwareDatabaseVersion,
|
|
4
|
+
} from "../api/aikido.js";
|
|
5
|
+
import {
|
|
6
|
+
readDatabaseFromLocalCache,
|
|
7
|
+
writeDatabaseToLocalCache,
|
|
8
|
+
} from "../config/configFile.js";
|
|
9
|
+
|
|
10
|
+
export async function openMalwareDatabase() {
|
|
11
|
+
const malwareDatabase = await getMalwareDatabase();
|
|
12
|
+
|
|
13
|
+
function getPackageStatus(name, version) {
|
|
14
|
+
const packageData = malwareDatabase.find(
|
|
15
|
+
(pkg) =>
|
|
16
|
+
pkg.package_name === name &&
|
|
17
|
+
(pkg.version === version || pkg.version === "*")
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
if (!packageData) {
|
|
21
|
+
return MALWARE_STATUS_OK;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return packageData.reason;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
getPackageStatus,
|
|
29
|
+
isMalware: (name, version) => {
|
|
30
|
+
const status = getPackageStatus(name, version);
|
|
31
|
+
return isMalwareStatus(status);
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function getMalwareDatabase() {
|
|
37
|
+
const { malwareDatabase: cachedDatabase, version: cachedVersion } =
|
|
38
|
+
readDatabaseFromLocalCache();
|
|
39
|
+
|
|
40
|
+
if (cachedDatabase) {
|
|
41
|
+
const currentVersion = await fetchMalwareDatabaseVersion();
|
|
42
|
+
|
|
43
|
+
if (cachedVersion === currentVersion) {
|
|
44
|
+
return cachedDatabase;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const { malwareDatabase, version } = await fetchMalwareDatabase();
|
|
49
|
+
writeDatabaseToLocalCache(malwareDatabase, version);
|
|
50
|
+
|
|
51
|
+
return malwareDatabase;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isMalwareStatus(status) {
|
|
55
|
+
let malwareStatus = status.toUpperCase();
|
|
56
|
+
return malwareStatus === MALWARE_STATUS_MALWARE;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const MALWARE_STATUS_OK = "OK";
|
|
60
|
+
export const MALWARE_STATUS_MALWARE = "MALWARE";
|
|
61
|
+
export const MALWARE_STATUS_TELEMETRY = "TELEMETRY";
|
|
62
|
+
export const MALWARE_STATUS_PROTESTWARE = "PROTESTWARE";
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { EOL } from "os";
|
|
4
|
+
import { getAliases } from "./helpers.js";
|
|
5
|
+
import { ui } from "../environment/userInteraction.js";
|
|
6
|
+
|
|
7
|
+
export function isAddAliasCommand(args) {
|
|
8
|
+
if (args[0] === "add-aikido-aliases") {
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (args[0] === "add-aikido-npm-alias") {
|
|
13
|
+
// not in the documenation anymore, but still here for backwards compatibility
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function addAlias(args) {
|
|
21
|
+
if (!isAddAliasCommand(args)) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (args.length < 2) {
|
|
26
|
+
ui.writeError("Please specify the file to add the alias to.");
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const filePath = path.resolve(args[1]);
|
|
31
|
+
const aliases = getAliases(filePath);
|
|
32
|
+
|
|
33
|
+
if (!fs.existsSync(filePath)) {
|
|
34
|
+
fs.writeFileSync(filePath, "", "utf-8");
|
|
35
|
+
ui.writeInformation(`File ${filePath} created.`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const fileContent = fs.readFileSync(filePath, "utf-8");
|
|
39
|
+
|
|
40
|
+
let missingAliases = [];
|
|
41
|
+
for (const alias of aliases) {
|
|
42
|
+
if (fileContent.includes(alias)) {
|
|
43
|
+
ui.writeInformation(`Alias "${alias}" already exists in ${filePath}`);
|
|
44
|
+
} else {
|
|
45
|
+
missingAliases.push(alias);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (missingAliases.length === 0) {
|
|
50
|
+
ui.writeInformation(`The aliases are already present in ${filePath}`);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const aliasLines = missingAliases.join(EOL);
|
|
55
|
+
ui.writeInformation(`Adding alias "${aliasLines}" to ${filePath}...`);
|
|
56
|
+
|
|
57
|
+
// Append the alias to the file
|
|
58
|
+
fs.appendFileSync(filePath, `${EOL}${aliasLines}${EOL}`, "utf-8");
|
|
59
|
+
ui.writeInformation(`Alias added to ${filePath}`);
|
|
60
|
+
ui.writeInformation(
|
|
61
|
+
`Please restart your terminal for the changes to take effect.`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const knownAikidoTools = [
|
|
2
|
+
{ tool: "npm", aikidoCommand: "aikido-npm" },
|
|
3
|
+
{ tool: "npx", aikidoCommand: "aikido-npx" },
|
|
4
|
+
{ tool: "yarn", aikidoCommand: "aikido-yarn" },
|
|
5
|
+
// When adding a new tool here, also update the expected alias in the tests (shellIntegration.spec.js)
|
|
6
|
+
// and add the documentation for the new tool in the README.md
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
export function getAliases(fileName) {
|
|
10
|
+
const fileExtension = fileName.split(".").pop().toLowerCase();
|
|
11
|
+
|
|
12
|
+
let createAlias = pickCreateAliasFunction(fileExtension);
|
|
13
|
+
|
|
14
|
+
const aliases = knownAikidoTools.map(({ tool, aikidoCommand }) =>
|
|
15
|
+
createAlias(tool, aikidoCommand)
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
return aliases;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function pickCreateAliasFunction(fileExtension) {
|
|
22
|
+
let createAlias;
|
|
23
|
+
switch (fileExtension) {
|
|
24
|
+
case "ps1":
|
|
25
|
+
createAlias = createGeneralPowershellAlias;
|
|
26
|
+
break;
|
|
27
|
+
case "fish":
|
|
28
|
+
createAlias = createGeneralFishAlias;
|
|
29
|
+
break;
|
|
30
|
+
default:
|
|
31
|
+
createAlias = createGeneralPosixAlias;
|
|
32
|
+
}
|
|
33
|
+
return createAlias;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function createGeneralPosixAlias(tool, aikidoCommand) {
|
|
37
|
+
return `alias ${tool}='${aikidoCommand}'`;
|
|
38
|
+
}
|
|
39
|
+
function createGeneralPowershellAlias(tool, aikidoCommand) {
|
|
40
|
+
return `Set-Alias ${tool} ${aikidoCommand}`;
|
|
41
|
+
}
|
|
42
|
+
function createGeneralFishAlias(tool, aikidoCommand) {
|
|
43
|
+
return `alias ${tool} "${aikidoCommand}"`;
|
|
44
|
+
}
|