@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
@@ -0,0 +1,304 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert";
3
+ import { EOL, tmpdir } from "node:os";
4
+ import fs from "node:fs";
5
+ import { getAliases } from "./helpers.js";
6
+ import { readOrCreateStartupFile, appendAliasesToFile } from "./setupShell.js";
7
+
8
+ describe("setupShell", () => {
9
+ function runSetupTestsForEnvironment(shell, startupExtension, expectedAliases) {
10
+ describe(`${shell} shell setup`, () => {
11
+ it(`should add aliases to ${shell} file`, () => {
12
+ const lines = [`#!/usr/bin/env ${shell}`, "", "alias cls='clear'"];
13
+ const filePath = createShellStartupScript(lines, startupExtension);
14
+
15
+ const aliases = getAliases(filePath);
16
+ const fileContent = fs.readFileSync(filePath, "utf-8");
17
+
18
+ const result = appendAliasesToFile(aliases, fileContent, filePath);
19
+
20
+ assert.strictEqual(result.addedCount, 3, "Should add 3 aliases");
21
+ assert.strictEqual(result.existingCount, 0, "Should find no existing aliases");
22
+ assert.strictEqual(result.failedCount, 0, "Should have no failed aliases");
23
+
24
+ const updatedContent = readAndDeleteFile(filePath);
25
+ for (const alias of expectedAliases) {
26
+ assert.ok(updatedContent.includes(alias), `Alias "${alias}" should be added`);
27
+ }
28
+ assert.ok(updatedContent.includes("alias cls='clear'"), "Original aliases should remain");
29
+ });
30
+
31
+ it(`should not add aliases if they already exist in ${shell} file`, () => {
32
+ const lines = [`#!/usr/bin/env ${shell}`, "", ...expectedAliases];
33
+ const filePath = createShellStartupScript(lines, startupExtension);
34
+
35
+ const aliases = getAliases(filePath);
36
+ const fileContent = fs.readFileSync(filePath, "utf-8");
37
+
38
+ const result = appendAliasesToFile(aliases, fileContent, filePath);
39
+
40
+ assert.strictEqual(result.addedCount, 0, "Should add 0 aliases");
41
+ assert.strictEqual(result.existingCount, 3, "Should find 3 existing aliases");
42
+ assert.strictEqual(result.failedCount, 0, "Should have no failed aliases");
43
+
44
+ const updatedContent = readAndDeleteFile(filePath);
45
+ // Count occurrences to ensure no duplicates were added
46
+ for (const alias of expectedAliases) {
47
+ assert.strictEqual(countOccurrences(updatedContent, alias), 1, `Alias "${alias}" should appear exactly once`);
48
+ }
49
+ });
50
+
51
+ it(`should create file and add aliases if file does not exist for ${shell}`, () => {
52
+ const randomName = Math.random().toString(36).substring(2, 15);
53
+ const filePath = `${tmpdir()}/nonexistent-${randomName}${startupExtension}`;
54
+ if (fs.existsSync(filePath)) {
55
+ fs.rmSync(filePath, { force: true });
56
+ }
57
+
58
+ // Test readOrCreateStartupFile function
59
+ const fileContent = readOrCreateStartupFile(filePath);
60
+ assert.strictEqual(fileContent, "", "Should return empty string for new file");
61
+ assert.ok(fs.existsSync(filePath), "File should be created");
62
+
63
+ // Test adding aliases to the newly created file
64
+ const aliases = getAliases(filePath);
65
+ const result = appendAliasesToFile(aliases, fileContent, filePath);
66
+
67
+ assert.strictEqual(result.addedCount, 3, "Should add 3 aliases");
68
+ assert.strictEqual(result.existingCount, 0, "Should find no existing aliases");
69
+ assert.strictEqual(result.failedCount, 0, "Should have no failed aliases");
70
+
71
+ const updatedContent = readAndDeleteFile(filePath);
72
+ for (const alias of expectedAliases) {
73
+ assert.ok(updatedContent.includes(alias), `Alias "${alias}" should be added`);
74
+ }
75
+ });
76
+
77
+ it(`should add aliases only once when called multiple times for ${shell}`, () => {
78
+ const lines = [`#!/usr/bin/env ${shell}`, ""];
79
+ const filePath = createShellStartupScript(lines, startupExtension);
80
+
81
+ const aliases = getAliases(filePath);
82
+
83
+ // First call - should add aliases
84
+ let fileContent = fs.readFileSync(filePath, "utf-8");
85
+ const result1 = appendAliasesToFile(aliases, fileContent, filePath);
86
+ assert.strictEqual(result1.addedCount, 3, "First call should add 3 aliases");
87
+
88
+ // Second call - should detect existing aliases
89
+ fileContent = fs.readFileSync(filePath, "utf-8");
90
+ const result2 = appendAliasesToFile(aliases, fileContent, filePath);
91
+ assert.strictEqual(result2.addedCount, 0, "Second call should add 0 aliases");
92
+ assert.strictEqual(result2.existingCount, 3, "Second call should find 3 existing aliases");
93
+
94
+ const updatedContent = readAndDeleteFile(filePath);
95
+ for (const alias of expectedAliases) {
96
+ assert.strictEqual(countOccurrences(updatedContent, alias), 1, `Alias "${alias}" should appear exactly once`);
97
+ }
98
+ });
99
+
100
+ it(`should use real getAliases() for ${shell} file`, () => {
101
+ const filePath = `${tmpdir()}/test${startupExtension}`;
102
+ const aliases = getAliases(filePath);
103
+
104
+ // Verify we get the expected aliases for this shell type
105
+ assert.strictEqual(aliases.length, 3, "Should get 3 aliases (npm, npx, yarn)");
106
+ for (let i = 0; i < aliases.length; i++) {
107
+ assert.strictEqual(aliases[i], expectedAliases[i], `Alias ${i} should match expected format`);
108
+ }
109
+ });
110
+
111
+ it(`should handle mixed scenario - some existing, some new for ${shell}`, () => {
112
+ const lines = [`#!/usr/bin/env ${shell}`, "", expectedAliases[0], "alias other='command'"];
113
+ const filePath = createShellStartupScript(lines, startupExtension);
114
+
115
+ const aliases = getAliases(filePath);
116
+ const fileContent = fs.readFileSync(filePath, "utf-8");
117
+
118
+ const result = appendAliasesToFile(aliases, fileContent, filePath);
119
+
120
+ assert.strictEqual(result.addedCount, 2, "Should add 2 new aliases");
121
+ assert.strictEqual(result.existingCount, 1, "Should find 1 existing alias");
122
+ assert.strictEqual(result.failedCount, 0, "Should have no failed aliases");
123
+
124
+ const updatedContent = readAndDeleteFile(filePath);
125
+ for (const alias of expectedAliases) {
126
+ assert.ok(updatedContent.includes(alias), `Alias "${alias}" should be present`);
127
+ }
128
+ assert.ok(updatedContent.includes("alias other='command'"), "Other aliases should remain");
129
+ });
130
+ });
131
+ }
132
+
133
+ // Test for each shell type using real getAliases() output
134
+ runSetupTestsForEnvironment("bash", ".bashrc", [
135
+ "alias npm='aikido-npm'",
136
+ "alias npx='aikido-npx'",
137
+ "alias yarn='aikido-yarn'"
138
+ ]);
139
+
140
+ runSetupTestsForEnvironment("zsh", ".zshrc", [
141
+ "alias npm='aikido-npm'",
142
+ "alias npx='aikido-npx'",
143
+ "alias yarn='aikido-yarn'"
144
+ ]);
145
+
146
+ runSetupTestsForEnvironment("fish", ".fish", [
147
+ 'alias npm "aikido-npm"',
148
+ 'alias npx "aikido-npx"',
149
+ 'alias yarn "aikido-yarn"'
150
+ ]);
151
+
152
+ runSetupTestsForEnvironment("pwsh", ".ps1", [
153
+ "Set-Alias npm aikido-npm",
154
+ "Set-Alias npx aikido-npx",
155
+ "Set-Alias yarn aikido-yarn"
156
+ ]);
157
+
158
+ describe("readOrCreateStartupFile", () => {
159
+ it("should read existing file content", () => {
160
+ const lines = ["#!/usr/bin/env bash", "", "alias test='echo test'"];
161
+ const filePath = createShellStartupScript(lines, ".bashrc");
162
+
163
+ const content = readOrCreateStartupFile(filePath);
164
+
165
+ assert.ok(content.includes("#!/usr/bin/env bash"), "Should contain shebang");
166
+ assert.ok(content.includes("alias test='echo test'"), "Should contain existing aliases");
167
+
168
+ // Cleanup
169
+ fs.rmSync(filePath, { force: true });
170
+ });
171
+
172
+ it("should create file if it doesn't exist", () => {
173
+ const filePath = `${tmpdir()}/test-${Math.random().toString(36).substring(2, 15)}.bashrc`;
174
+ if (fs.existsSync(filePath)) {
175
+ fs.rmSync(filePath, { force: true });
176
+ }
177
+
178
+ const content = readOrCreateStartupFile(filePath);
179
+
180
+ assert.strictEqual(content, "", "Should return empty string for new file");
181
+ assert.ok(fs.existsSync(filePath), "File should be created");
182
+
183
+ // Cleanup
184
+ fs.rmSync(filePath, { force: true });
185
+ });
186
+
187
+ it("should handle empty existing file", () => {
188
+ const filePath = `${tmpdir()}/test-${Math.random().toString(36).substring(2, 15)}.bashrc`;
189
+ fs.writeFileSync(filePath, "", "utf-8");
190
+
191
+ const content = readOrCreateStartupFile(filePath);
192
+
193
+ assert.strictEqual(content, "", "Should return empty string for empty file");
194
+ assert.ok(fs.existsSync(filePath), "File should still exist");
195
+
196
+ // Cleanup
197
+ fs.rmSync(filePath, { force: true });
198
+ });
199
+ });
200
+
201
+ describe("appendAliasesToFile edge cases", () => {
202
+ it("should handle empty aliases array", () => {
203
+ const lines = ["#!/usr/bin/env bash", "", "alias test='echo test'"];
204
+ const filePath = createShellStartupScript(lines, ".bashrc");
205
+ const fileContent = fs.readFileSync(filePath, "utf-8");
206
+
207
+ const result = appendAliasesToFile([], fileContent, filePath);
208
+
209
+ assert.strictEqual(result.addedCount, 0, "Should add 0 aliases");
210
+ assert.strictEqual(result.existingCount, 0, "Should find 0 existing aliases");
211
+ assert.strictEqual(result.failedCount, 0, "Should have 0 failed aliases");
212
+
213
+ const updatedContent = readAndDeleteFile(filePath);
214
+ assert.ok(updatedContent.includes("alias test='echo test'"), "Original content should remain");
215
+ });
216
+
217
+ it("should handle partial substring matches correctly", () => {
218
+ const lines = [
219
+ "#!/usr/bin/env bash",
220
+ "",
221
+ "alias npmx='some-other-command'", // Contains 'npm' but shouldn't match 'alias npm='
222
+ "alias test='echo test'"
223
+ ];
224
+ const filePath = createShellStartupScript(lines, ".bashrc");
225
+ const fileContent = fs.readFileSync(filePath, "utf-8");
226
+
227
+ const aliases = ["alias npm='aikido-npm'"];
228
+ const result = appendAliasesToFile(aliases, fileContent, filePath);
229
+
230
+ assert.strictEqual(result.addedCount, 1, "Should add 1 alias (npm)");
231
+ assert.strictEqual(result.existingCount, 0, "Should find 0 existing aliases");
232
+ assert.strictEqual(result.failedCount, 0, "Should have 0 failed aliases");
233
+
234
+ const updatedContent = readAndDeleteFile(filePath);
235
+ assert.ok(updatedContent.includes("alias npm='aikido-npm'"), "npm alias should be added");
236
+ assert.ok(updatedContent.includes("alias npmx='some-other-command'"), "npmx alias should remain");
237
+ });
238
+
239
+ it("should handle file with only whitespace", () => {
240
+ const filePath = `${tmpdir()}/test-${Math.random().toString(36).substring(2, 15)}.bashrc`;
241
+ const fileContent = `${EOL}${EOL} ${EOL}`;
242
+ fs.writeFileSync(filePath, fileContent, "utf-8");
243
+
244
+ const aliases = ["alias npm='aikido-npm'"];
245
+ const result = appendAliasesToFile(aliases, fileContent, filePath);
246
+
247
+ assert.strictEqual(result.addedCount, 1, "Should add 1 alias");
248
+ assert.strictEqual(result.existingCount, 0, "Should find 0 existing aliases");
249
+ assert.strictEqual(result.failedCount, 0, "Should have 0 failed aliases");
250
+
251
+ const updatedContent = fs.readFileSync(filePath, "utf-8");
252
+ assert.ok(updatedContent.includes("alias npm='aikido-npm'"), "Alias should be added");
253
+
254
+ // Cleanup
255
+ fs.rmSync(filePath, { force: true });
256
+ });
257
+ });
258
+
259
+ describe("appendAliasesToFile error handling", () => {
260
+ it("should handle file permission errors gracefully", () => {
261
+ const filePath = `${tmpdir()}/test-${Math.random().toString(36).substring(2, 15)}.bashrc`;
262
+ fs.writeFileSync(filePath, "#!/usr/bin/env bash", "utf-8");
263
+
264
+ // Make file read-only to simulate permission error
265
+ fs.chmodSync(filePath, 0o444);
266
+
267
+ const aliases = ["alias npm='aikido-npm'"];
268
+ const fileContent = fs.readFileSync(filePath, "utf-8");
269
+
270
+ const result = appendAliasesToFile(aliases, fileContent, filePath);
271
+
272
+ assert.strictEqual(result.addedCount, 0, "Should add 0 aliases due to permission error");
273
+ assert.strictEqual(result.existingCount, 0, "Should find 0 existing aliases");
274
+ assert.strictEqual(result.failedCount, 1, "Should have 1 failed alias");
275
+
276
+ // Restore permissions and cleanup
277
+ fs.chmodSync(filePath, 0o644);
278
+ fs.rmSync(filePath, { force: true });
279
+ });
280
+ });
281
+ });
282
+
283
+ function createShellStartupScript(lines, fileExtension) {
284
+ const randomFileName = Math.random().toString(36).substring(2, 15);
285
+ const filePath = `${tmpdir()}/${randomFileName}${fileExtension}`;
286
+ fs.writeFileSync(filePath, lines.join(EOL), "utf-8");
287
+ return filePath;
288
+ }
289
+
290
+ function readAndDeleteFile(filePath) {
291
+ const fileContent = fs.readFileSync(filePath, "utf-8");
292
+ fs.rmSync(filePath, { force: true });
293
+ return fileContent.split(EOL);
294
+ }
295
+
296
+ function countOccurrences(lines, searchString) {
297
+ let count = 0;
298
+ for (const line of lines) {
299
+ if (line.includes(searchString)) {
300
+ count++;
301
+ }
302
+ }
303
+ return count;
304
+ }
@@ -0,0 +1,75 @@
1
+ import * as os from "os";
2
+ import { execSync } from "child_process";
3
+
4
+ const shellList = {
5
+ bash: {
6
+ name: "Bash",
7
+ executable: "bash",
8
+ getStartupFileCommand: "echo ~/.bashrc",
9
+ },
10
+ zsh: {
11
+ name: "Zsh",
12
+ executable: "zsh",
13
+ getStartupFileCommand: "echo ${ZDOTDIR:-$HOME}/.zshrc",
14
+ },
15
+ fish: {
16
+ name: "Fish",
17
+ executable: "fish",
18
+ getStartupFileCommand: "echo ~/.config/fish/config.fish",
19
+ },
20
+ powershell: {
21
+ name: "PowerShell Core",
22
+ executable: "pwsh",
23
+ getStartupFileCommand: "echo $PROFILE",
24
+ },
25
+ windowsPowerShell: {
26
+ name: "Windows PowerShell",
27
+ executable: "powershell",
28
+ getStartupFileCommand: "echo $PROFILE",
29
+ },
30
+ };
31
+
32
+ export function detectShells() {
33
+ let availableShells = [];
34
+
35
+ for (const shellName of Object.keys(shellList)) {
36
+ const shell = shellList[shellName];
37
+
38
+ if (isShellAvailable(shell)) {
39
+ const startupFile = getShellStartupFile(shell);
40
+ availableShells.push({
41
+ name: shell.name,
42
+ executable: shell.executable,
43
+ startupFile: startupFile || null,
44
+ });
45
+ }
46
+ }
47
+
48
+ return availableShells;
49
+ }
50
+
51
+ function isShellAvailable(shell) {
52
+ try {
53
+ if (os.platform() === "win32") {
54
+ execSync(`where ${shell.executable}`, { stdio: "ignore" });
55
+ } else {
56
+ execSync(`which ${shell.executable}`, { stdio: "ignore" });
57
+ }
58
+ return true;
59
+ } catch {
60
+ return false;
61
+ }
62
+ }
63
+
64
+ function getShellStartupFile(shell) {
65
+ try {
66
+ const command = shell.getStartupFileCommand;
67
+ const output = execSync(command, {
68
+ encoding: "utf8",
69
+ shell: shell.executable,
70
+ }).trim();
71
+ return output;
72
+ } catch {
73
+ return null;
74
+ }
75
+ }