@aikidosec/safe-chain 1.0.16 → 1.0.18
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/eslint.config.js +2 -1
- package/package.json +7 -4
- package/src/packagemanager/npm/parsing/parsePackagesFromInstallArgs.js +3 -1
- package/src/packagemanager/npm/parsing/parsePackagesFromInstallArgs.spec.js +8 -0
- package/src/packagemanager/npx/parsing/parsePackagesFromArguments.js +3 -1
- package/src/packagemanager/npx/parsing/parsePackagesFromArguments.spec.js +8 -0
- package/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.spec.js +8 -0
- package/src/packagemanager/yarn/parsing/parsePackagesFromArguments.js +3 -1
- package/src/packagemanager/yarn/parsing/parsePackagesFromArguments.spec.js +8 -0
- package/src/shell-integration/helpers.js +26 -2
- package/src/shell-integration/helpers.spec.js +113 -0
- package/src/shell-integration/setup.js +25 -0
- package/src/shell-integration/startup-scripts/init-fish.fish +58 -0
- package/src/shell-integration/startup-scripts/init-posix.sh +54 -0
- package/src/shell-integration/startup-scripts/init-pwsh.ps1 +80 -0
- package/src/shell-integration/supported-shells/bash.js +11 -7
- package/src/shell-integration/supported-shells/bash.spec.js +32 -32
- package/src/shell-integration/supported-shells/fish.js +11 -7
- package/src/shell-integration/supported-shells/fish.spec.js +21 -37
- package/src/shell-integration/supported-shells/powershell.js +11 -7
- package/src/shell-integration/supported-shells/powershell.spec.js +43 -47
- package/src/shell-integration/supported-shells/windowsPowershell.js +11 -7
- package/src/shell-integration/supported-shells/windowsPowershell.spec.js +43 -47
- package/src/shell-integration/supported-shells/zsh.js +11 -7
- package/src/shell-integration/supported-shells/zsh.spec.js +57 -30
- package/.github/workflows/build-and-release.yml +0 -41
- package/.github/workflows/test-on-pr.yml +0 -28
package/eslint.config.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import js from "@eslint/js";
|
|
2
|
-
import { defineConfig } from "@eslint/config-helpers";
|
|
2
|
+
import { defineConfig, globalIgnores } from "@eslint/config-helpers";
|
|
3
3
|
import globals from "globals";
|
|
4
4
|
import importPlugin from "eslint-plugin-import";
|
|
5
5
|
|
|
@@ -22,4 +22,5 @@ export default defineConfig([
|
|
|
22
22
|
},
|
|
23
23
|
rules: {},
|
|
24
24
|
},
|
|
25
|
+
globalIgnores(['test/e2e']),
|
|
25
26
|
]);
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aikidosec/safe-chain",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.18",
|
|
4
4
|
"scripts": {
|
|
5
|
-
"test": "node --test --experimental-test-module-mocks
|
|
6
|
-
"test:watch": "node --test --watch --experimental-test-module-mocks
|
|
5
|
+
"test": "node --test --experimental-test-module-mocks 'src/**/*.spec.js'",
|
|
6
|
+
"test:watch": "node --test --watch --experimental-test-module-mocks 'src/**/*.spec.js'",
|
|
7
7
|
"lint": "eslint ."
|
|
8
8
|
},
|
|
9
9
|
"repository": {
|
|
@@ -42,5 +42,8 @@
|
|
|
42
42
|
"bugs": {
|
|
43
43
|
"url": "https://github.com/AikidoSec/safe-chain/issues"
|
|
44
44
|
},
|
|
45
|
-
"homepage": "https://github.com/AikidoSec/safe-chain#readme"
|
|
45
|
+
"homepage": "https://github.com/AikidoSec/safe-chain#readme",
|
|
46
|
+
"overrides": {
|
|
47
|
+
"brace-expansion@<=2.0.2": "2.0.2"
|
|
48
|
+
}
|
|
46
49
|
}
|
|
@@ -86,7 +86,9 @@ function parsePackagename(arg) {
|
|
|
86
86
|
const lastAtIndex = arg.lastIndexOf("@");
|
|
87
87
|
|
|
88
88
|
let name, version;
|
|
89
|
-
|
|
89
|
+
// The index of the last "@" should be greater than 0
|
|
90
|
+
// If the index is 0, it means the package name starts with "@" (eg: "@vercel/otel")
|
|
91
|
+
if (lastAtIndex > 0) {
|
|
90
92
|
name = arg.slice(0, lastAtIndex);
|
|
91
93
|
version = arg.slice(lastAtIndex + 1);
|
|
92
94
|
} else {
|
|
@@ -19,6 +19,14 @@ describe("parsePackagesFromInstallArgs", () => {
|
|
|
19
19
|
assert.deepEqual(result, [{ name: "@jest/transform", version: "29.7.0" }]);
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
+
it("should return the package in the format @vercel/otel", () => {
|
|
23
|
+
const args = ["install", "@vercel/otel"];
|
|
24
|
+
|
|
25
|
+
const result = parsePackagesFromInstallArgs(args);
|
|
26
|
+
|
|
27
|
+
assert.deepEqual(result, [{ name: "@vercel/otel", version: "latest" }]);
|
|
28
|
+
});
|
|
29
|
+
|
|
22
30
|
it("should return an array of changes for multiple packages", () => {
|
|
23
31
|
const args = ["install", "express@4.17.1", "lodash@4.17.21"];
|
|
24
32
|
|
|
@@ -81,7 +81,9 @@ function parsePackagename(arg, defaultTag) {
|
|
|
81
81
|
const lastAtIndex = arg.lastIndexOf("@");
|
|
82
82
|
|
|
83
83
|
let name, version;
|
|
84
|
-
|
|
84
|
+
// The index of the last "@" should be greater than 0
|
|
85
|
+
// If the index is 0, it means the package name starts with "@" (eg: "@vercel/otel")
|
|
86
|
+
if (lastAtIndex > 0) {
|
|
85
87
|
name = arg.slice(0, lastAtIndex);
|
|
86
88
|
version = arg.slice(lastAtIndex + 1);
|
|
87
89
|
} else {
|
|
@@ -19,6 +19,14 @@ describe("parsePackagesFromArguments", () => {
|
|
|
19
19
|
assert.deepEqual(result, [{ name: "http-server", version: "14.1.1" }]);
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
+
it("should return the package in the format @vercel/otel", () => {
|
|
23
|
+
const args = ["@vercel/otel"];
|
|
24
|
+
|
|
25
|
+
const result = parsePackagesFromArguments(args);
|
|
26
|
+
|
|
27
|
+
assert.deepEqual(result, [{ name: "@vercel/otel", version: "latest" }]);
|
|
28
|
+
});
|
|
29
|
+
|
|
22
30
|
it("should return the package with latest tag if absent", () => {
|
|
23
31
|
const args = ["http-server"];
|
|
24
32
|
|
|
@@ -27,6 +27,14 @@ describe("standardPnpmArgumentParser", () => {
|
|
|
27
27
|
assert.deepEqual(result, [{ name: "axios", version: "latest" }]);
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
+
it("should return the package in the format @vercel/otel", () => {
|
|
31
|
+
const args = ["@vercel/otel"];
|
|
32
|
+
|
|
33
|
+
const result = parsePackagesFromArguments(args);
|
|
34
|
+
|
|
35
|
+
assert.deepEqual(result, [{ name: "@vercel/otel", version: "latest" }]);
|
|
36
|
+
});
|
|
37
|
+
|
|
30
38
|
it("should return the package with latest tag if the version is absent and package starts with @", () => {
|
|
31
39
|
const args = ["@aikidosec/package-name"];
|
|
32
40
|
|
|
@@ -77,7 +77,9 @@ function parsePackagename(arg, defaultTag) {
|
|
|
77
77
|
const lastAtIndex = arg.lastIndexOf("@");
|
|
78
78
|
|
|
79
79
|
let name, version;
|
|
80
|
-
|
|
80
|
+
// The index of the last "@" should be greater than 0
|
|
81
|
+
// If the index is 0, it means the package name starts with "@" (eg: "@vercel/otel")
|
|
82
|
+
if (lastAtIndex > 0) {
|
|
81
83
|
name = arg.slice(0, lastAtIndex);
|
|
82
84
|
version = arg.slice(lastAtIndex + 1);
|
|
83
85
|
} else {
|
|
@@ -38,6 +38,14 @@ describe("standardYarnArgumentParser", () => {
|
|
|
38
38
|
]);
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
+
it("should return the package in the format @vercel/otel", () => {
|
|
42
|
+
const args = ["add", "@vercel/otel"];
|
|
43
|
+
|
|
44
|
+
const result = parsePackagesFromArguments(args);
|
|
45
|
+
|
|
46
|
+
assert.deepEqual(result, [{ name: "@vercel/otel", version: "latest" }]);
|
|
47
|
+
});
|
|
48
|
+
|
|
41
49
|
it("should ignore options with parameters and return an array of changes", () => {
|
|
42
50
|
const args = ["add", "--proxy", "http://localhost", "axios@1.9.0"];
|
|
43
51
|
|
|
@@ -28,11 +28,35 @@ export function removeLinesMatchingPattern(filePath, pattern) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
const fileContent = fs.readFileSync(filePath, "utf-8");
|
|
31
|
-
const lines = fileContent.split(
|
|
32
|
-
const updatedLines = lines.filter((line) => !
|
|
31
|
+
const lines = fileContent.split(/[\r\n\u2028\u2029]+/);
|
|
32
|
+
const updatedLines = lines.filter((line) => !shouldRemoveLine(line, pattern));
|
|
33
33
|
fs.writeFileSync(filePath, updatedLines.join(os.EOL), "utf-8");
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
const maxLineLength = 100;
|
|
37
|
+
function shouldRemoveLine(line, pattern) {
|
|
38
|
+
const isPatternMatch = pattern.test(line);
|
|
39
|
+
|
|
40
|
+
if (!isPatternMatch) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (line.length > maxLineLength) {
|
|
45
|
+
// safe-chain only adds lines shorter than maxLineLength
|
|
46
|
+
// so if the line is longer, it must be from a different
|
|
47
|
+
// source and could be dangerous to remove
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (line.includes("\n") || line.includes("\r") || line.includes("\u2028") || line.includes("\u2029")) {
|
|
52
|
+
// If the line contains newlines, something has gone wrong in splitting
|
|
53
|
+
// \u2028 and \u2029 are Unicode line separator characters (line and paragraph separators)
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
36
60
|
export function addLineToFile(filePath, line) {
|
|
37
61
|
if (!fs.existsSync(filePath)) {
|
|
38
62
|
fs.writeFileSync(filePath, "", "utf-8");
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach, mock } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
|
|
7
|
+
describe("removeLinesMatchingPatternTests", () => {
|
|
8
|
+
let testFile;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
// Create temporary test file
|
|
12
|
+
testFile = path.join(tmpdir(), `test-helpers-${Date.now()}.txt`);
|
|
13
|
+
|
|
14
|
+
// Mock the os module to override EOL
|
|
15
|
+
mock.module("node:os", {
|
|
16
|
+
namedExports: {
|
|
17
|
+
EOL: "\r\n", // Simulate Windows line endings
|
|
18
|
+
tmpdir: tmpdir,
|
|
19
|
+
platform: () => "linux"
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
// Clean up test files
|
|
26
|
+
if (fs.existsSync(testFile)) {
|
|
27
|
+
fs.unlinkSync(testFile);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Reset mocks
|
|
31
|
+
mock.reset();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
it("should handle mixed line endings without wiping entire file", async () => {
|
|
36
|
+
// Import helpers after setting up the mock
|
|
37
|
+
const { removeLinesMatchingPattern } = await import("./helpers.js");
|
|
38
|
+
|
|
39
|
+
// Create a file with Unix line endings but os.EOL expects Windows
|
|
40
|
+
const fileContent = [
|
|
41
|
+
"# keep this line",
|
|
42
|
+
"alias npm='remove-this'",
|
|
43
|
+
"# keep this line too",
|
|
44
|
+
"alias yarn='remove-this-too'",
|
|
45
|
+
"# final line to keep"
|
|
46
|
+
].join("\n"); // File has Unix line endings
|
|
47
|
+
|
|
48
|
+
fs.writeFileSync(testFile, fileContent, "utf-8");
|
|
49
|
+
|
|
50
|
+
// Try to remove lines containing 'alias'
|
|
51
|
+
const pattern = /alias.*=/;
|
|
52
|
+
removeLinesMatchingPattern(testFile, pattern);
|
|
53
|
+
|
|
54
|
+
const result = fs.readFileSync(testFile, "utf-8");
|
|
55
|
+
|
|
56
|
+
// This test will fail because the function splits on '\r\n' but file uses '\n'
|
|
57
|
+
// So it treats the entire content as one line and if any part matches, removes everything
|
|
58
|
+
assert.ok(result.includes("keep this line"), "Should preserve non-matching lines");
|
|
59
|
+
assert.ok(result.includes("final line to keep"), "Should preserve final line");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("should handle mixed line endings with short matching content", async () => {
|
|
63
|
+
// Import helpers after setting up the mock
|
|
64
|
+
const { removeLinesMatchingPattern } = await import("./helpers.js");
|
|
65
|
+
|
|
66
|
+
// Create a file with Unix line endings, but make the entire content short
|
|
67
|
+
// to bypass the maxLineLength protection
|
|
68
|
+
const fileContent = [
|
|
69
|
+
"# keep1",
|
|
70
|
+
"alias x=y", // Short alias line that should be removed
|
|
71
|
+
"# keep2"
|
|
72
|
+
].join("\n"); // File has Unix line endings, total length < 100 chars
|
|
73
|
+
|
|
74
|
+
fs.writeFileSync(testFile, fileContent, "utf-8");
|
|
75
|
+
|
|
76
|
+
// Try to remove lines containing 'alias'
|
|
77
|
+
const pattern = /alias/;
|
|
78
|
+
removeLinesMatchingPattern(testFile, pattern);
|
|
79
|
+
|
|
80
|
+
const result = fs.readFileSync(testFile, "utf-8");
|
|
81
|
+
|
|
82
|
+
// This should now be protected by the newline detection
|
|
83
|
+
assert.ok(result.includes("keep1"), "Should preserve first line");
|
|
84
|
+
assert.ok(result.includes("keep2"), "Should preserve third line");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("should handle Unicode line separators that bypass newline detection", async () => {
|
|
88
|
+
// Import helpers after setting up the mock
|
|
89
|
+
const { removeLinesMatchingPattern } = await import("./helpers.js");
|
|
90
|
+
|
|
91
|
+
// Use Unicode line separator (U+2028) and paragraph separator (U+2029)
|
|
92
|
+
// These are considered line breaks but aren't \n or \r
|
|
93
|
+
const fileContent = [
|
|
94
|
+
"keep this",
|
|
95
|
+
"alias test=value",
|
|
96
|
+
"keep that"
|
|
97
|
+
].join("\u2028"); // Unicode line separator
|
|
98
|
+
|
|
99
|
+
fs.writeFileSync(testFile, fileContent, "utf-8");
|
|
100
|
+
|
|
101
|
+
// Try to remove lines containing 'alias'
|
|
102
|
+
const pattern = /alias/;
|
|
103
|
+
removeLinesMatchingPattern(testFile, pattern);
|
|
104
|
+
|
|
105
|
+
const result = fs.readFileSync(testFile, "utf-8");
|
|
106
|
+
|
|
107
|
+
// This could still wipe everything if split() treats it as one line
|
|
108
|
+
// but the content doesn't contain \n or \r so passes the newline check
|
|
109
|
+
assert.ok(result.includes("keep this"), "Should preserve first part");
|
|
110
|
+
assert.ok(result.includes("keep that"), "Should preserve last part");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
});
|
|
@@ -2,6 +2,10 @@ import chalk from "chalk";
|
|
|
2
2
|
import { ui } from "../environment/userInteraction.js";
|
|
3
3
|
import { detectShells } from "./shellDetection.js";
|
|
4
4
|
import { knownAikidoTools } from "./helpers.js";
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import os from "os";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
5
9
|
|
|
6
10
|
/**
|
|
7
11
|
* Loops over the detected shells and calls the setup function for each.
|
|
@@ -13,6 +17,8 @@ export async function setup() {
|
|
|
13
17
|
);
|
|
14
18
|
ui.emptyLine();
|
|
15
19
|
|
|
20
|
+
copyStartupFiles();
|
|
21
|
+
|
|
16
22
|
try {
|
|
17
23
|
const shells = detectShells();
|
|
18
24
|
if (shells.length === 0) {
|
|
@@ -73,3 +79,22 @@ function setupShell(shell) {
|
|
|
73
79
|
|
|
74
80
|
return success;
|
|
75
81
|
}
|
|
82
|
+
|
|
83
|
+
function copyStartupFiles() {
|
|
84
|
+
const startupFiles = ["init-posix.sh", "init-pwsh.ps1", "init-fish.fish"];
|
|
85
|
+
|
|
86
|
+
for (const file of startupFiles) {
|
|
87
|
+
const targetDir = path.join(os.homedir(), ".safe-chain", "scripts");
|
|
88
|
+
const targetPath = path.join(os.homedir(), ".safe-chain", "scripts", file);
|
|
89
|
+
|
|
90
|
+
if (!fs.existsSync(targetDir)) {
|
|
91
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Use absolute path for source
|
|
95
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
96
|
+
const __dirname = path.dirname(__filename);
|
|
97
|
+
const sourcePath = path.resolve(__dirname, "startup-scripts", file);
|
|
98
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
function printSafeChainWarning
|
|
2
|
+
set original_cmd $argv[1]
|
|
3
|
+
|
|
4
|
+
# Fish equivalent of ANSI color codes: yellow background, black text for "Warning:"
|
|
5
|
+
set_color -b yellow black
|
|
6
|
+
printf "Warning:"
|
|
7
|
+
set_color normal
|
|
8
|
+
printf " safe-chain is not available to protect you from installing malware. %s will run without it.\n" $original_cmd
|
|
9
|
+
|
|
10
|
+
# Cyan text for the install command
|
|
11
|
+
printf "Install safe-chain by using "
|
|
12
|
+
set_color cyan
|
|
13
|
+
printf "npm install -g @aikidosec/safe-chain"
|
|
14
|
+
set_color normal
|
|
15
|
+
printf ".\n"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
function wrapSafeChainCommand
|
|
19
|
+
set original_cmd $argv[1]
|
|
20
|
+
set aikido_cmd $argv[2]
|
|
21
|
+
set cmd_args $argv[3..-1]
|
|
22
|
+
|
|
23
|
+
if type -q $aikido_cmd
|
|
24
|
+
# If the aikido command is available, just run it with the provided arguments
|
|
25
|
+
$aikido_cmd $cmd_args
|
|
26
|
+
else
|
|
27
|
+
# If the aikido command is not available, print a warning and run the original command
|
|
28
|
+
printSafeChainWarning $original_cmd
|
|
29
|
+
command $original_cmd $cmd_args
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
function npx
|
|
34
|
+
wrapSafeChainCommand "npx" "aikido-npx" $argv
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
function yarn
|
|
38
|
+
wrapSafeChainCommand "yarn" "aikido-yarn" $argv
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
function pnpm
|
|
42
|
+
wrapSafeChainCommand "pnpm" "aikido-pnpm" $argv
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
function pnpx
|
|
46
|
+
wrapSafeChainCommand "pnpx" "aikido-pnpx" $argv
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
function npm
|
|
50
|
+
if test (count $argv) -eq 1 -a \( "$argv[1]" = "-v" -o "$argv[1]" = "--version" \)
|
|
51
|
+
# If args is just -v or --version and nothing else, just run the npm version command
|
|
52
|
+
# This is because nvm uses this to check the version of npm
|
|
53
|
+
command npm $argv
|
|
54
|
+
return
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
wrapSafeChainCommand "npm" "aikido-npm" $argv
|
|
58
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
|
|
2
|
+
function printSafeChainWarning() {
|
|
3
|
+
# \033[43;30m is used to set the background color to yellow and text color to black
|
|
4
|
+
# \033[0m is used to reset the text formatting
|
|
5
|
+
printf "\033[43;30mWarning:\033[0m safe-chain is not available to protect you from installing malware. %s will run without it.\n" "$1"
|
|
6
|
+
# \033[36m is used to set the text color to cyan
|
|
7
|
+
printf "Install safe-chain by using \033[36mnpm install -g @aikidosec/safe-chain\033[0m.\n"
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function wrapSafeChainCommand() {
|
|
11
|
+
local original_cmd="$1"
|
|
12
|
+
local aikido_cmd="$2"
|
|
13
|
+
|
|
14
|
+
# Remove the first 2 arguments (original_cmd and aikido_cmd) from $@
|
|
15
|
+
# so that "$@" now contains only the arguments passed to the original command
|
|
16
|
+
shift 2
|
|
17
|
+
|
|
18
|
+
if command -v "$aikido_cmd" > /dev/null 2>&1; then
|
|
19
|
+
# If the aikido command is available, just run it with the provided arguments
|
|
20
|
+
"$aikido_cmd" "$@"
|
|
21
|
+
else
|
|
22
|
+
# If the aikido command is not available, print a warning and run the original command
|
|
23
|
+
printSafeChainWarning "$original_cmd"
|
|
24
|
+
|
|
25
|
+
command "$original_cmd" "$@"
|
|
26
|
+
fi
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function npx() {
|
|
30
|
+
wrapSafeChainCommand "npx" "aikido-npx" "$@"
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function yarn() {
|
|
34
|
+
wrapSafeChainCommand "yarn" "aikido-yarn" "$@"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function pnpm() {
|
|
38
|
+
wrapSafeChainCommand "pnpm" "aikido-pnpm" "$@"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function pnpx() {
|
|
42
|
+
wrapSafeChainCommand "pnpx" "aikido-pnpx" "$@"
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function npm() {
|
|
46
|
+
if [[ "$1" == "-v" || "$1" == "--version" ]] && [[ $# -eq 1 ]]; then
|
|
47
|
+
# If args is just -v or --version and nothing else, just run the npm version command
|
|
48
|
+
# This is because nvm uses this to check the version of npm
|
|
49
|
+
command npm "$@"
|
|
50
|
+
return
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
wrapSafeChainCommand "npm" "aikido-npm" "$@"
|
|
54
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
function Write-SafeChainWarning {
|
|
2
|
+
param([string]$Command)
|
|
3
|
+
|
|
4
|
+
# PowerShell equivalent of ANSI color codes: yellow background, black text for "Warning:"
|
|
5
|
+
Write-Host "Warning:" -BackgroundColor Yellow -ForegroundColor Black -NoNewline
|
|
6
|
+
Write-Host " safe-chain is not available to protect you from installing malware. $Command will run without it."
|
|
7
|
+
|
|
8
|
+
# Cyan text for the install command
|
|
9
|
+
Write-Host "Install safe-chain by using " -NoNewline
|
|
10
|
+
Write-Host "npm install -g @aikidosec/safe-chain" -ForegroundColor Cyan -NoNewline
|
|
11
|
+
Write-Host "."
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function Test-CommandAvailable {
|
|
15
|
+
param([string]$Command)
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
Get-Command $Command -ErrorAction Stop | Out-Null
|
|
19
|
+
return $true
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return $false
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function Invoke-RealCommand {
|
|
27
|
+
param(
|
|
28
|
+
[string]$Command,
|
|
29
|
+
[string[]]$Arguments
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Find the real executable to avoid calling our wrapped functions
|
|
33
|
+
$realCommand = Get-Command -Name $Command -CommandType Application | Select-Object -First 1
|
|
34
|
+
if ($realCommand) {
|
|
35
|
+
& $realCommand.Source @Arguments
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function Invoke-WrappedCommand {
|
|
40
|
+
param(
|
|
41
|
+
[string]$OriginalCmd,
|
|
42
|
+
[string]$AikidoCmd,
|
|
43
|
+
[string[]]$Arguments
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
if (Test-CommandAvailable $AikidoCmd) {
|
|
47
|
+
& $AikidoCmd @Arguments
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
Write-SafeChainWarning $OriginalCmd
|
|
51
|
+
Invoke-RealCommand $OriginalCmd $Arguments
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function npx {
|
|
56
|
+
Invoke-WrappedCommand "npx" "aikido-npx" $args
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function yarn {
|
|
60
|
+
Invoke-WrappedCommand "yarn" "aikido-yarn" $args
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function pnpm {
|
|
64
|
+
Invoke-WrappedCommand "pnpm" "aikido-pnpm" $args
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function pnpx {
|
|
68
|
+
Invoke-WrappedCommand "pnpx" "aikido-pnpx" $args
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function npm {
|
|
72
|
+
# If args is just -v or --version and nothing else, just run the npm version command
|
|
73
|
+
# This is because nvm uses this to check the version of npm
|
|
74
|
+
if (($args.Length -eq 1) -and (($args[0] -eq "-v") -or ($args[0] -eq "--version"))) {
|
|
75
|
+
Invoke-RealCommand "npm" $args
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
Invoke-WrappedCommand "npm" "aikido-npm" $args
|
|
80
|
+
}
|
|
@@ -21,18 +21,22 @@ function teardown(tools) {
|
|
|
21
21
|
removeLinesMatchingPattern(startupFile, new RegExp(`^alias\\s+${tool}=`));
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
// Removes the line that sources the safe-chain bash initialization script (~/.aikido/scripts/init-posix.sh)
|
|
25
|
+
removeLinesMatchingPattern(
|
|
26
|
+
startupFile,
|
|
27
|
+
/^source\s+~\/\.safe-chain\/scripts\/init-posix\.sh/
|
|
28
|
+
);
|
|
29
|
+
|
|
24
30
|
return true;
|
|
25
31
|
}
|
|
26
32
|
|
|
27
|
-
function setup(
|
|
33
|
+
function setup() {
|
|
28
34
|
const startupFile = getStartupFile();
|
|
29
35
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
+
addLineToFile(
|
|
37
|
+
startupFile,
|
|
38
|
+
`source ~/.safe-chain/scripts/init-posix.sh # Safe-chain bash initialization script`
|
|
39
|
+
);
|
|
36
40
|
|
|
37
41
|
return true;
|
|
38
42
|
}
|
|
@@ -66,38 +66,17 @@ describe("Bash shell integration", () => {
|
|
|
66
66
|
});
|
|
67
67
|
|
|
68
68
|
describe("setup", () => {
|
|
69
|
-
it("should add
|
|
70
|
-
const
|
|
71
|
-
{ tool: "npm", aikidoCommand: "aikido-npm" },
|
|
72
|
-
{ tool: "npx", aikidoCommand: "aikido-npx" },
|
|
73
|
-
{ tool: "yarn", aikidoCommand: "aikido-yarn" },
|
|
74
|
-
];
|
|
75
|
-
|
|
76
|
-
const result = bash.setup(tools);
|
|
69
|
+
it("should add source line for bash initialization script", () => {
|
|
70
|
+
const result = bash.setup();
|
|
77
71
|
assert.strictEqual(result, true);
|
|
78
72
|
|
|
79
73
|
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
|
80
74
|
assert.ok(
|
|
81
|
-
content.includes(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
content.includes('alias npx="aikido-npx" # Safe-chain alias for npx')
|
|
85
|
-
);
|
|
86
|
-
assert.ok(
|
|
87
|
-
content.includes('alias yarn="aikido-yarn" # Safe-chain alias for yarn')
|
|
75
|
+
content.includes(
|
|
76
|
+
"source ~/.safe-chain/scripts/init-posix.sh # Safe-chain bash initialization script"
|
|
77
|
+
)
|
|
88
78
|
);
|
|
89
79
|
});
|
|
90
|
-
|
|
91
|
-
it("should handle empty tools array", () => {
|
|
92
|
-
const result = bash.setup([]);
|
|
93
|
-
assert.strictEqual(result, true);
|
|
94
|
-
|
|
95
|
-
// File should be created during teardown call even if no tools are provided
|
|
96
|
-
if (fs.existsSync(mockStartupFile)) {
|
|
97
|
-
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
|
98
|
-
assert.strictEqual(content.trim(), "");
|
|
99
|
-
}
|
|
100
|
-
});
|
|
101
80
|
});
|
|
102
81
|
|
|
103
82
|
describe("teardown", () => {
|
|
@@ -174,14 +153,14 @@ describe("Bash shell integration", () => {
|
|
|
174
153
|
// Setup
|
|
175
154
|
bash.setup(tools);
|
|
176
155
|
let content = fs.readFileSync(mockStartupFile, "utf-8");
|
|
177
|
-
assert.ok(content.includes(
|
|
178
|
-
assert.ok(content.includes('alias yarn="aikido-yarn"'));
|
|
156
|
+
assert.ok(content.includes("source ~/.safe-chain/scripts/init-posix.sh"));
|
|
179
157
|
|
|
180
158
|
// Teardown
|
|
181
159
|
bash.teardown(tools);
|
|
182
160
|
content = fs.readFileSync(mockStartupFile, "utf-8");
|
|
183
|
-
assert.ok(
|
|
184
|
-
|
|
161
|
+
assert.ok(
|
|
162
|
+
!content.includes("source ~/.safe-chain/scripts/init-posix.sh")
|
|
163
|
+
);
|
|
185
164
|
});
|
|
186
165
|
|
|
187
166
|
it("should handle multiple setup calls", () => {
|
|
@@ -192,8 +171,29 @@ describe("Bash shell integration", () => {
|
|
|
192
171
|
bash.setup(tools);
|
|
193
172
|
|
|
194
173
|
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
|
195
|
-
const
|
|
196
|
-
|
|
174
|
+
const sourceMatches = (content.match(/source.*init-posix\.sh/g) || [])
|
|
175
|
+
.length;
|
|
176
|
+
assert.strictEqual(sourceMatches, 1, "Should not duplicate source lines");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("should handle mixed content with aliases and source lines", () => {
|
|
180
|
+
const initialContent = [
|
|
181
|
+
"#!/bin/bash",
|
|
182
|
+
"alias npm='old-npm'",
|
|
183
|
+
"source ~/.safe-chain/scripts/init-posix.sh",
|
|
184
|
+
"alias ls='ls --color=auto'",
|
|
185
|
+
].join("\n");
|
|
186
|
+
|
|
187
|
+
fs.writeFileSync(mockStartupFile, initialContent, "utf-8");
|
|
188
|
+
|
|
189
|
+
// Teardown should remove both aliases and source line
|
|
190
|
+
bash.teardown(knownAikidoTools);
|
|
191
|
+
const content = fs.readFileSync(mockStartupFile, "utf-8");
|
|
192
|
+
assert.ok(!content.includes("alias npm="));
|
|
193
|
+
assert.ok(
|
|
194
|
+
!content.includes("source ~/.safe-chain/scripts/init-posix.sh")
|
|
195
|
+
);
|
|
196
|
+
assert.ok(content.includes("alias ls="));
|
|
197
197
|
});
|
|
198
198
|
});
|
|
199
199
|
});
|