@aikidosec/safe-chain 0.0.1-immutable-releases-beta

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 (112) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +517 -0
  3. package/bin/aikido-bun.js +14 -0
  4. package/bin/aikido-bunx.js +14 -0
  5. package/bin/aikido-npm.js +14 -0
  6. package/bin/aikido-npx.js +14 -0
  7. package/bin/aikido-pip.js +17 -0
  8. package/bin/aikido-pip3.js +17 -0
  9. package/bin/aikido-pipx.js +16 -0
  10. package/bin/aikido-pnpm.js +14 -0
  11. package/bin/aikido-pnpx.js +14 -0
  12. package/bin/aikido-poetry.js +13 -0
  13. package/bin/aikido-python.js +19 -0
  14. package/bin/aikido-python3.js +19 -0
  15. package/bin/aikido-uv.js +16 -0
  16. package/bin/aikido-yarn.js +14 -0
  17. package/bin/safe-chain.js +130 -0
  18. package/docs/banner.svg +151 -0
  19. package/docs/safe-package-manager-demo.gif +0 -0
  20. package/docs/safe-package-manager-demo.png +0 -0
  21. package/docs/shell-integration.md +149 -0
  22. package/docs/troubleshooting.md +321 -0
  23. package/npm-shrinkwrap.json +4069 -0
  24. package/package.json +72 -0
  25. package/src/api/aikido.js +187 -0
  26. package/src/api/npmApi.js +71 -0
  27. package/src/config/cliArguments.js +161 -0
  28. package/src/config/configFile.js +327 -0
  29. package/src/config/environmentVariables.js +57 -0
  30. package/src/config/settings.js +247 -0
  31. package/src/environment/environment.js +14 -0
  32. package/src/environment/userInteraction.js +122 -0
  33. package/src/main.js +123 -0
  34. package/src/packagemanager/_shared/commandErrors.js +17 -0
  35. package/src/packagemanager/_shared/matchesCommand.js +18 -0
  36. package/src/packagemanager/bun/createBunPackageManager.js +48 -0
  37. package/src/packagemanager/currentPackageManager.js +79 -0
  38. package/src/packagemanager/npm/createPackageManager.js +72 -0
  39. package/src/packagemanager/npm/dependencyScanner/commandArgumentScanner.js +74 -0
  40. package/src/packagemanager/npm/dependencyScanner/nullScanner.js +9 -0
  41. package/src/packagemanager/npm/parsing/parsePackagesFromInstallArgs.js +144 -0
  42. package/src/packagemanager/npm/runNpmCommand.js +20 -0
  43. package/src/packagemanager/npm/utils/abbrevs-generated.js +359 -0
  44. package/src/packagemanager/npm/utils/cmd-list.js +174 -0
  45. package/src/packagemanager/npm/utils/npmCommands.js +34 -0
  46. package/src/packagemanager/npx/createPackageManager.js +15 -0
  47. package/src/packagemanager/npx/dependencyScanner/commandArgumentScanner.js +43 -0
  48. package/src/packagemanager/npx/parsing/parsePackagesFromArguments.js +130 -0
  49. package/src/packagemanager/npx/runNpxCommand.js +20 -0
  50. package/src/packagemanager/pip/createPackageManager.js +25 -0
  51. package/src/packagemanager/pip/pipSettings.js +6 -0
  52. package/src/packagemanager/pip/runPipCommand.js +209 -0
  53. package/src/packagemanager/pipx/createPipXPackageManager.js +18 -0
  54. package/src/packagemanager/pipx/runPipXCommand.js +60 -0
  55. package/src/packagemanager/pnpm/createPackageManager.js +57 -0
  56. package/src/packagemanager/pnpm/dependencyScanner/commandArgumentScanner.js +35 -0
  57. package/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.js +109 -0
  58. package/src/packagemanager/pnpm/runPnpmCommand.js +32 -0
  59. package/src/packagemanager/poetry/createPoetryPackageManager.js +72 -0
  60. package/src/packagemanager/uv/createUvPackageManager.js +18 -0
  61. package/src/packagemanager/uv/runUvCommand.js +66 -0
  62. package/src/packagemanager/yarn/createPackageManager.js +41 -0
  63. package/src/packagemanager/yarn/dependencyScanner/commandArgumentScanner.js +35 -0
  64. package/src/packagemanager/yarn/parsing/parsePackagesFromArguments.js +128 -0
  65. package/src/packagemanager/yarn/runYarnCommand.js +36 -0
  66. package/src/registryProxy/certBundle.js +203 -0
  67. package/src/registryProxy/certUtils.js +178 -0
  68. package/src/registryProxy/getConnectTimeout.js +13 -0
  69. package/src/registryProxy/http-utils.js +80 -0
  70. package/src/registryProxy/interceptors/createInterceptorForEcoSystem.js +25 -0
  71. package/src/registryProxy/interceptors/interceptorBuilder.js +179 -0
  72. package/src/registryProxy/interceptors/minimumPackageAgeExclusions.js +33 -0
  73. package/src/registryProxy/interceptors/npm/modifyNpmInfo.js +180 -0
  74. package/src/registryProxy/interceptors/npm/npmInterceptor.js +101 -0
  75. package/src/registryProxy/interceptors/npm/parseNpmPackageUrl.js +60 -0
  76. package/src/registryProxy/interceptors/pip/modifyPipInfo.js +167 -0
  77. package/src/registryProxy/interceptors/pip/modifyPipJsonResponse.js +176 -0
  78. package/src/registryProxy/interceptors/pip/parsePipPackageUrl.js +162 -0
  79. package/src/registryProxy/interceptors/pip/pipInterceptor.js +122 -0
  80. package/src/registryProxy/interceptors/pip/pipMetadataResponseUtils.js +27 -0
  81. package/src/registryProxy/interceptors/pip/pipMetadataVersionUtils.js +131 -0
  82. package/src/registryProxy/interceptors/suppressedVersionsState.js +21 -0
  83. package/src/registryProxy/isImdsEndpoint.js +13 -0
  84. package/src/registryProxy/mitmRequestHandler.js +240 -0
  85. package/src/registryProxy/plainHttpProxy.js +95 -0
  86. package/src/registryProxy/registryProxy.js +255 -0
  87. package/src/registryProxy/tunnelRequestHandler.js +213 -0
  88. package/src/scanning/audit/index.js +129 -0
  89. package/src/scanning/index.js +82 -0
  90. package/src/scanning/malwareDatabase.js +131 -0
  91. package/src/scanning/newPackagesDatabaseBuilder.js +71 -0
  92. package/src/scanning/newPackagesDatabaseWarnings.js +17 -0
  93. package/src/scanning/newPackagesListCache.js +126 -0
  94. package/src/scanning/packageNameVariants.js +29 -0
  95. package/src/shell-integration/helpers.js +304 -0
  96. package/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh +22 -0
  97. package/src/shell-integration/path-wrappers/templates/windows-wrapper.template.cmd +24 -0
  98. package/src/shell-integration/setup-ci.js +172 -0
  99. package/src/shell-integration/setup.js +129 -0
  100. package/src/shell-integration/shellDetection.js +39 -0
  101. package/src/shell-integration/startup-scripts/init-fish.fish +115 -0
  102. package/src/shell-integration/startup-scripts/init-posix.sh +96 -0
  103. package/src/shell-integration/startup-scripts/init-pwsh.ps1 +171 -0
  104. package/src/shell-integration/supported-shells/bash.js +152 -0
  105. package/src/shell-integration/supported-shells/fish.js +95 -0
  106. package/src/shell-integration/supported-shells/powershell.js +100 -0
  107. package/src/shell-integration/supported-shells/windowsPowershell.js +100 -0
  108. package/src/shell-integration/supported-shells/zsh.js +92 -0
  109. package/src/shell-integration/teardown.js +112 -0
  110. package/src/ultimate/ultimateTroubleshooting.js +111 -0
  111. package/src/utils/safeSpawn.js +153 -0
  112. package/tsconfig.json +21 -0
@@ -0,0 +1,129 @@
1
+ import chalk from "chalk";
2
+ import { ui } from "../environment/userInteraction.js";
3
+ import { detectShells } from "./shellDetection.js";
4
+ import {
5
+ knownAikidoTools,
6
+ getPackageManagerList,
7
+ getScriptsDir,
8
+ } from "./helpers.js";
9
+ import fs from "fs";
10
+ import path from "path";
11
+ import { fileURLToPath } from "url";
12
+
13
+ /** @type {string} */
14
+ // This checks the current file's dirname in a way that's compatible with:
15
+ // - Modulejs (import.meta.url)
16
+ // - ES modules (__dirname)
17
+ // This is needed because safe-chain's npm package is built using ES modules,
18
+ // but building the binaries requires commonjs.
19
+ let dirname;
20
+ if (import.meta.url) {
21
+ const filename = fileURLToPath(import.meta.url);
22
+ dirname = path.dirname(filename);
23
+ } else {
24
+ dirname = __dirname;
25
+ }
26
+
27
+ /**
28
+ * Loops over the detected shells and calls the setup function for each.
29
+ */
30
+ export async function setup() {
31
+ ui.writeInformation(
32
+ chalk.bold("Setting up shell aliases.") +
33
+ ` This will wrap safe-chain around ${getPackageManagerList()}.`,
34
+ );
35
+ ui.emptyLine();
36
+
37
+ copyStartupFiles();
38
+
39
+ try {
40
+ const shells = detectShells();
41
+ if (shells.length === 0) {
42
+ ui.writeError("No supported shells detected. Cannot set up aliases.");
43
+ return;
44
+ }
45
+
46
+ ui.writeInformation(
47
+ `Detected ${shells.length} supported shell(s): ${shells
48
+ .map((shell) => chalk.bold(shell.name))
49
+ .join(", ")}.`,
50
+ );
51
+
52
+ let updatedCount = 0;
53
+ for (const shell of shells) {
54
+ if (await setupShell(shell)) {
55
+ updatedCount++;
56
+ }
57
+ }
58
+
59
+ if (updatedCount > 0) {
60
+ ui.emptyLine();
61
+ ui.writeInformation(`Please restart your terminal to apply the changes.`);
62
+ }
63
+ } catch (/** @type {any} */ error) {
64
+ ui.writeError(
65
+ `Failed to set up shell aliases: ${error.message}. Please check your shell configuration.`,
66
+ );
67
+ return;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Calls the setup function for the given shell and reports the result.
73
+ * @param {import("./shellDetection.js").Shell} shell
74
+ */
75
+ async function setupShell(shell) {
76
+ let success = false;
77
+ let error;
78
+ try {
79
+ shell.teardown(knownAikidoTools); // First, tear down to prevent duplicate aliases
80
+ success = await shell.setup(knownAikidoTools);
81
+ } catch (/** @type {any} */ err) {
82
+ success = false;
83
+ error = err;
84
+ }
85
+
86
+ if (success) {
87
+ ui.writeInformation(
88
+ `${chalk.bold("- " + shell.name + ":")} ${chalk.green(
89
+ "Setup successful",
90
+ )}`,
91
+ );
92
+ } else {
93
+ ui.writeError(
94
+ `${chalk.bold("- " + shell.name + ":")} ${chalk.red("Setup failed")}`,
95
+ );
96
+ if (error) {
97
+ let message = ` Error: ${error.message}`;
98
+ if (error.code) {
99
+ message += ` (code: ${error.code})`;
100
+ }
101
+ ui.writeError(message);
102
+ }
103
+ ui.emptyLine();
104
+ ui.writeInformation(` ${chalk.bold("To set up manually:")}`);
105
+ for (const instruction of shell.getManualSetupInstructions()) {
106
+ ui.writeInformation(` ${instruction}`);
107
+ }
108
+ ui.emptyLine();
109
+ }
110
+
111
+ return success;
112
+ }
113
+
114
+ function copyStartupFiles() {
115
+ const startupFiles = ["init-posix.sh", "init-pwsh.ps1", "init-fish.fish"];
116
+ const targetDir = getScriptsDir();
117
+
118
+ for (const file of startupFiles) {
119
+ const targetPath = path.join(targetDir, file);
120
+
121
+ if (!fs.existsSync(targetDir)) {
122
+ fs.mkdirSync(targetDir, { recursive: true });
123
+ }
124
+
125
+ // Use absolute path for source
126
+ const sourcePath = path.join(dirname, "startup-scripts", file);
127
+ fs.copyFileSync(sourcePath, targetPath);
128
+ }
129
+ }
@@ -0,0 +1,39 @@
1
+ import zsh from "./supported-shells/zsh.js";
2
+ import bash from "./supported-shells/bash.js";
3
+ import powershell from "./supported-shells/powershell.js";
4
+ import windowsPowershell from "./supported-shells/windowsPowershell.js";
5
+ import fish from "./supported-shells/fish.js";
6
+ import { ui } from "../environment/userInteraction.js";
7
+
8
+ /**
9
+ * @typedef {Object} Shell
10
+ * @property {string} name
11
+ * @property {() => boolean} isInstalled
12
+ * @property {(tools: import("./helpers.js").AikidoTool[]) => boolean|Promise<boolean>} setup
13
+ * @property {(tools: import("./helpers.js").AikidoTool[]) => boolean} teardown
14
+ * @property {() => string[]} getManualSetupInstructions
15
+ * @property {() => string[]} getManualTeardownInstructions
16
+ */
17
+
18
+ /**
19
+ * @returns {Shell[]}
20
+ */
21
+ export function detectShells() {
22
+ let possibleShells = [zsh, bash, powershell, windowsPowershell, fish];
23
+ let availableShells = [];
24
+
25
+ try {
26
+ for (const shell of possibleShells) {
27
+ if (shell.isInstalled()) {
28
+ availableShells.push(shell);
29
+ }
30
+ }
31
+ } catch (/** @type {any} */ error) {
32
+ ui.writeError(
33
+ `We were not able to detect which shells are installed on your system. Please check your shell configuration. Error: ${error.message}`,
34
+ );
35
+ return [];
36
+ }
37
+
38
+ return availableShells;
39
+ }
@@ -0,0 +1,115 @@
1
+ set -gx PATH $PATH $HOME/.safe-chain/bin
2
+
3
+ function npx
4
+ wrapSafeChainCommand "npx" $argv
5
+ end
6
+
7
+ function yarn
8
+ wrapSafeChainCommand "yarn" $argv
9
+ end
10
+
11
+ function pnpm
12
+ wrapSafeChainCommand "pnpm" $argv
13
+ end
14
+
15
+ function pnpx
16
+ wrapSafeChainCommand "pnpx" $argv
17
+ end
18
+
19
+ function bun
20
+ wrapSafeChainCommand "bun" $argv
21
+ end
22
+
23
+ function bunx
24
+ wrapSafeChainCommand "bunx" $argv
25
+ end
26
+
27
+ function npm
28
+ # If args is just -v or --version and nothing else, just run the `npm -v` command
29
+ # This is because nvm uses this to check the version of npm
30
+ set argc (count $argv)
31
+ if test $argc -eq 1
32
+ switch $argv[1]
33
+ case "-v" "--version"
34
+ command npm $argv
35
+ return
36
+ end
37
+ end
38
+
39
+ wrapSafeChainCommand "npm" $argv
40
+ end
41
+
42
+ function pip
43
+ wrapSafeChainCommand "pip" $argv
44
+ end
45
+
46
+ function pip3
47
+ wrapSafeChainCommand "pip3" $argv
48
+ end
49
+
50
+ function uv
51
+ wrapSafeChainCommand "uv" $argv
52
+ end
53
+
54
+ function poetry
55
+ wrapSafeChainCommand "poetry" $argv
56
+ end
57
+
58
+ # `python -m pip`, `python -m pip3`.
59
+ function python
60
+ wrapSafeChainCommand "python" $argv
61
+ end
62
+
63
+ # `python3 -m pip`, `python3 -m pip3'.
64
+ function python3
65
+ wrapSafeChainCommand "python3" $argv
66
+ end
67
+
68
+ function pipx
69
+ wrapSafeChainCommand "pipx" $argv
70
+ end
71
+
72
+ function printSafeChainWarning
73
+ set original_cmd $argv[1]
74
+
75
+ # Fish equivalent of ANSI color codes: yellow background, black text for "Warning:"
76
+ set_color -b yellow black
77
+ printf "Warning:"
78
+ set_color normal
79
+ printf " safe-chain is not available to protect you from installing malware. %s will run without it.\n" $original_cmd
80
+
81
+ # Cyan text for the install command
82
+ printf "Install safe-chain by using "
83
+ set_color cyan
84
+ printf "npm install -g @aikidosec/safe-chain"
85
+ set_color normal
86
+ printf ".\n"
87
+ end
88
+
89
+ function wrapSafeChainCommand
90
+ set original_cmd $argv[1]
91
+ set cmd_args $argv[2..-1]
92
+
93
+ if not type -fq $original_cmd
94
+ # If the original command is not available, don't try to wrap it: invoke
95
+ # it transparently, so the shell can report errors as if this wrapper
96
+ # didn't exist. fish always adds extra debug information when executing
97
+ # missing commands from within a function, so after the "command not
98
+ # found" handler, there will be information about how the
99
+ # wrapSafeChainCommand function errored out. To avoid users assuming this
100
+ # is a safe-chain bug, display an explicit error message afterwards.
101
+ command $original_cmd $cmd_args
102
+ set oldstatus $status
103
+ echo "safe-chain tried to run $original_cmd but it doesn't seem to be installed in your \$PATH." >&2
104
+ return $oldstatus
105
+ end
106
+
107
+ if type -q safe-chain
108
+ # If the safe-chain command is available, just run it with the provided arguments
109
+ safe-chain $original_cmd $cmd_args
110
+ else
111
+ # If the safe-chain command is not available, print a warning and run the original command
112
+ printSafeChainWarning $original_cmd
113
+ command $original_cmd $cmd_args
114
+ end
115
+ end
@@ -0,0 +1,96 @@
1
+ export PATH="$PATH:$HOME/.safe-chain/bin"
2
+
3
+ function npx() {
4
+ wrapSafeChainCommand "npx" "$@"
5
+ }
6
+
7
+ function yarn() {
8
+ wrapSafeChainCommand "yarn" "$@"
9
+ }
10
+
11
+ function pnpm() {
12
+ wrapSafeChainCommand "pnpm" "$@"
13
+ }
14
+
15
+ function pnpx() {
16
+ wrapSafeChainCommand "pnpx" "$@"
17
+ }
18
+
19
+ function bun() {
20
+ wrapSafeChainCommand "bun" "$@"
21
+ }
22
+
23
+ function bunx() {
24
+ wrapSafeChainCommand "bunx" "$@"
25
+ }
26
+
27
+ function npm() {
28
+ if [[ "$1" == "-v" || "$1" == "--version" ]] && [[ $# -eq 1 ]]; then
29
+ # If args is just -v or --version and nothing else, just run the npm version command
30
+ # This is because nvm uses this to check the version of npm
31
+ command npm "$@"
32
+ return
33
+ fi
34
+
35
+ wrapSafeChainCommand "npm" "$@"
36
+ }
37
+
38
+ function pip() {
39
+ wrapSafeChainCommand "pip" "$@"
40
+ }
41
+
42
+ function pip3() {
43
+ wrapSafeChainCommand "pip3" "$@"
44
+ }
45
+
46
+ function uv() {
47
+ wrapSafeChainCommand "uv" "$@"
48
+ }
49
+
50
+ function poetry() {
51
+ wrapSafeChainCommand "poetry" "$@"
52
+ }
53
+
54
+ # `python -m pip`, `python -m pip3`.
55
+ function python() {
56
+ wrapSafeChainCommand "python" "$@"
57
+ }
58
+
59
+ # `python3 -m pip`, `python3 -m pip3'.
60
+ function python3() {
61
+ wrapSafeChainCommand "python3" "$@"
62
+ }
63
+
64
+ function pipx() {
65
+ wrapSafeChainCommand "pipx" "$@"
66
+ }
67
+
68
+ function printSafeChainWarning() {
69
+ # \033[43;30m is used to set the background color to yellow and text color to black
70
+ # \033[0m is used to reset the text formatting
71
+ printf "\033[43;30mWarning:\033[0m safe-chain is not available to protect you from installing malware. %s will run without it.\n" "$1"
72
+ # \033[36m is used to set the text color to cyan
73
+ printf "Install safe-chain by using \033[36mnpm install -g @aikidosec/safe-chain\033[0m.\n"
74
+ }
75
+
76
+ function wrapSafeChainCommand() {
77
+ local original_cmd="$1"
78
+
79
+ if ! type -f "${original_cmd}" > /dev/null 2>&1; then
80
+ # If the original command is not available, don't try to wrap it: invoke it
81
+ # transparently, so the shell can report errors as if this wrapper didn't
82
+ # exist.
83
+ command "$@"
84
+ return $?
85
+ fi
86
+
87
+ if command -v safe-chain > /dev/null 2>&1; then
88
+ # If the aikido command is available, just run it with the provided arguments
89
+ safe-chain "$@"
90
+ else
91
+ # If the aikido command is not available, print a warning and run the original command
92
+ printSafeChainWarning "$original_cmd"
93
+
94
+ command "$@"
95
+ fi
96
+ }
@@ -0,0 +1,171 @@
1
+ # Use cross-platform path separator (: on Unix, ; on Windows)
2
+ # $IsWindows is only available in PowerShell Core 6.0+. If it doesn't exist, assume Windows PowerShell
3
+ $isWindowsPlatform = if (Test-Path variable:IsWindows) { $IsWindows } else { $true }
4
+ $pathSeparator = if ($isWindowsPlatform) { ';' } else { ':' }
5
+ $safeChainBin = Join-Path (Join-Path $HOME '.safe-chain') 'bin'
6
+ $env:PATH = "$env:PATH$pathSeparator$safeChainBin"
7
+
8
+ function npx {
9
+ Invoke-WrappedCommand "npx" $args $MyInvocation.Line $MyInvocation.OffsetInLine
10
+ }
11
+
12
+ function yarn {
13
+ Invoke-WrappedCommand "yarn" $args $MyInvocation.Line $MyInvocation.OffsetInLine
14
+ }
15
+
16
+ function pnpm {
17
+ Invoke-WrappedCommand "pnpm" $args $MyInvocation.Line $MyInvocation.OffsetInLine
18
+ }
19
+
20
+ function pnpx {
21
+ Invoke-WrappedCommand "pnpx" $args $MyInvocation.Line $MyInvocation.OffsetInLine
22
+ }
23
+
24
+ function bun {
25
+ Invoke-WrappedCommand "bun" $args $MyInvocation.Line $MyInvocation.OffsetInLine
26
+ }
27
+
28
+ function bunx {
29
+ Invoke-WrappedCommand "bunx" $args $MyInvocation.Line $MyInvocation.OffsetInLine
30
+ }
31
+
32
+ function npm {
33
+ # If args is just -v or --version and nothing else, just run the npm version command
34
+ # This is because nvm uses this to check the version of npm
35
+ if (($args.Length -eq 1) -and (($args[0] -eq "-v") -or ($args[0] -eq "--version"))) {
36
+ Invoke-RealCommand "npm" $args
37
+ return
38
+ }
39
+
40
+ Invoke-WrappedCommand "npm" $args $MyInvocation.Line $MyInvocation.OffsetInLine
41
+ }
42
+
43
+ function pip {
44
+ Invoke-WrappedCommand "pip" $args $MyInvocation.Line $MyInvocation.OffsetInLine
45
+ }
46
+
47
+ function pip3 {
48
+ Invoke-WrappedCommand "pip3" $args $MyInvocation.Line $MyInvocation.OffsetInLine
49
+ }
50
+
51
+ function uv {
52
+ Invoke-WrappedCommand "uv" $args $MyInvocation.Line $MyInvocation.OffsetInLine
53
+ }
54
+
55
+ function poetry {
56
+ Invoke-WrappedCommand "poetry" $args $MyInvocation.Line $MyInvocation.OffsetInLine
57
+ }
58
+
59
+ # `python -m pip`, `python -m pip3`.
60
+ function python {
61
+ Invoke-WrappedCommand 'python' $args $MyInvocation.Line $MyInvocation.OffsetInLine
62
+ }
63
+
64
+ # `python3 -m pip`, `python3 -m pip3'.
65
+ function python3 {
66
+ Invoke-WrappedCommand 'python3' $args $MyInvocation.Line $MyInvocation.OffsetInLine
67
+ }
68
+
69
+ function pipx {
70
+ Invoke-WrappedCommand "pipx" $args $MyInvocation.Line $MyInvocation.OffsetInLine
71
+ }
72
+
73
+ function Write-SafeChainWarning {
74
+ param([string]$Command)
75
+
76
+ # PowerShell equivalent of ANSI color codes: yellow background, black text for "Warning:"
77
+ Write-Host "Warning:" -BackgroundColor Yellow -ForegroundColor Black -NoNewline
78
+ Write-Host " safe-chain is not available to protect you from installing malware. $Command will run without it."
79
+
80
+ # Cyan text for the install command
81
+ Write-Host "Install safe-chain by using " -NoNewline
82
+ Write-Host "npm install -g @aikidosec/safe-chain" -ForegroundColor Cyan -NoNewline
83
+ Write-Host "."
84
+ }
85
+
86
+ function Test-CommandAvailable {
87
+ param([string]$Command)
88
+
89
+ try {
90
+ Get-Command $Command -ErrorAction Stop | Out-Null
91
+ return $true
92
+ }
93
+ catch {
94
+ return $false
95
+ }
96
+ }
97
+
98
+ function Invoke-RealCommand {
99
+ param(
100
+ [string]$Command,
101
+ [string[]]$Arguments
102
+ )
103
+
104
+ # Find the real executable to avoid calling our wrapped functions
105
+ $realCommand = Get-Command -Name $Command -CommandType Application | Select-Object -First 1
106
+ if ($realCommand) {
107
+ & $realCommand.Source @Arguments
108
+ }
109
+ }
110
+
111
+ function Get-ReconstructedArguments {
112
+ param(
113
+ [string]$RawLine,
114
+ [int]$RawOffset
115
+ )
116
+
117
+ if (-not $RawLine) { return $null }
118
+
119
+ $tokens = [System.Management.Automation.PSParser]::Tokenize($RawLine, [ref]$null)
120
+ $newArgs = @()
121
+ $foundCommand = $false
122
+
123
+ foreach ($t in $tokens) {
124
+ if (-not $foundCommand) {
125
+ if ($t.Start -eq ($RawOffset - 1)) { $foundCommand = $true }
126
+ continue
127
+ }
128
+
129
+ if ($t.Type -eq 'Operator' -and $t.Content -match '[|;&]') { break }
130
+
131
+ # Stop if complex variable expansion is used
132
+ if ($t.Type -eq 'Variable' -or $t.Type -eq 'Group' -or $t.Type -eq 'SubExpression') {
133
+ return $null
134
+ }
135
+
136
+ $newArgs += $t.Content
137
+ }
138
+
139
+ if ($foundCommand) {
140
+ return ,$newArgs
141
+ }
142
+ return $null
143
+ }
144
+
145
+ function Invoke-WrappedCommand {
146
+ param(
147
+ [string]$OriginalCmd,
148
+ [string[]]$Arguments,
149
+ [string]$RawLine = $null,
150
+ [int]$RawOffset = 0
151
+ )
152
+
153
+ # Use raw line parsing to recover arguments like '--' that PowerShell consumes
154
+ if ($RawLine) {
155
+ $reconstructedArgs = Get-ReconstructedArguments $RawLine $RawOffset
156
+ if ($null -ne $reconstructedArgs) {
157
+ $Arguments = $reconstructedArgs
158
+ }
159
+ }
160
+
161
+ if ($isWindowsPlatform -and (Test-CommandAvailable "safe-chain.cmd")) {
162
+ & safe-chain.cmd $OriginalCmd @Arguments
163
+ }
164
+ elseif (Test-CommandAvailable "safe-chain") {
165
+ & safe-chain $OriginalCmd @Arguments
166
+ }
167
+ else {
168
+ Write-SafeChainWarning $OriginalCmd
169
+ Invoke-RealCommand $OriginalCmd $Arguments
170
+ }
171
+ }
@@ -0,0 +1,152 @@
1
+ import {
2
+ addLineToFile,
3
+ doesExecutableExistOnSystem,
4
+ removeLinesMatchingPattern,
5
+ } from "../helpers.js";
6
+ import { execSync, spawnSync } from "child_process";
7
+ import * as os from "os";
8
+
9
+ const shellName = "Bash";
10
+ const executableName = "bash";
11
+ const startupFileCommand = "echo ~/.bashrc";
12
+ const eol = "\n"; // When bash runs on Windows (e.g., Git Bash or WSL), it expects LF line endings.
13
+
14
+ function isInstalled() {
15
+ return doesExecutableExistOnSystem(executableName);
16
+ }
17
+
18
+ /**
19
+ * @param {import("../helpers.js").AikidoTool[]} tools
20
+ *
21
+ * @returns {boolean}
22
+ */
23
+ function teardown(tools) {
24
+ const startupFile = getStartupFile();
25
+
26
+ for (const { tool } of tools) {
27
+ // Remove any existing alias for the tool
28
+ removeLinesMatchingPattern(
29
+ startupFile,
30
+ new RegExp(`^alias\\s+${tool}=`),
31
+ eol
32
+ );
33
+ }
34
+
35
+ // Removes the line that sources the safe-chain bash initialization script (~/.safe-chain/scripts/init-posix.sh)
36
+ removeLinesMatchingPattern(
37
+ startupFile,
38
+ /^source\s+~\/\.safe-chain\/scripts\/init-posix\.sh/,
39
+ eol
40
+ );
41
+
42
+ return true;
43
+ }
44
+
45
+ function setup() {
46
+ const startupFile = getStartupFile();
47
+
48
+ addLineToFile(
49
+ startupFile,
50
+ `source ~/.safe-chain/scripts/init-posix.sh # Safe-chain bash initialization script`,
51
+ eol
52
+ );
53
+
54
+ return true;
55
+ }
56
+
57
+ function getStartupFile() {
58
+ try {
59
+ var path = execSync(startupFileCommand, {
60
+ encoding: "utf8",
61
+ shell: executableName,
62
+ }).trim();
63
+
64
+ return windowsFixPath(path);
65
+ } catch (/** @type {any} */ error) {
66
+ throw new Error(
67
+ `Command failed: ${startupFileCommand}. Error: ${error.message}`
68
+ );
69
+ }
70
+ }
71
+
72
+ /**
73
+ * @param {string} path
74
+ *
75
+ * @returns {string}
76
+ */
77
+ function windowsFixPath(path) {
78
+ try {
79
+ if (os.platform() !== "win32") {
80
+ return path;
81
+ }
82
+
83
+ // On windows cygwin bash, paths are returned in format /c/user/..., but we need it in format C:\user\...
84
+ // To convert, the cygpath -w command can be used to convert to the desired format.
85
+ // Cygpath only exists on Cygwin, so we first check if the command is available.
86
+ // If it is, we use it to convert the path.
87
+ if (hasCygpath()) {
88
+ return cygpathw(path);
89
+ }
90
+
91
+ return path;
92
+ } catch {
93
+ return path;
94
+ }
95
+ }
96
+
97
+ function hasCygpath() {
98
+ try {
99
+ var result = spawnSync("where", ["cygpath"], { shell: executableName });
100
+ return result.status === 0;
101
+ } catch {
102
+ return false;
103
+ }
104
+ }
105
+
106
+ /**
107
+ * @param {string} path
108
+ *
109
+ * @returns {string}
110
+ */
111
+ function cygpathw(path) {
112
+ try {
113
+ var result = spawnSync("cygpath", ["-w", path], {
114
+ encoding: "utf8",
115
+ shell: executableName,
116
+ });
117
+ if (result.status === 0) {
118
+ return result.stdout.trim();
119
+ }
120
+ return path;
121
+ } catch {
122
+ return path;
123
+ }
124
+ }
125
+
126
+ function getManualTeardownInstructions() {
127
+ return [
128
+ `Remove the following line from your ~/.bashrc file:`,
129
+ ` source ~/.safe-chain/scripts/init-posix.sh`,
130
+ `Then restart your terminal or run: source ~/.bashrc`,
131
+ ];
132
+ }
133
+
134
+ function getManualSetupInstructions() {
135
+ return [
136
+ `Add the following line to your ~/.bashrc file:`,
137
+ ` source ~/.safe-chain/scripts/init-posix.sh`,
138
+ `Then restart your terminal or run: source ~/.bashrc`,
139
+ ];
140
+ }
141
+
142
+ /**
143
+ * @type {import("../shellDetection.js").Shell}
144
+ */
145
+ export default {
146
+ name: shellName,
147
+ isInstalled,
148
+ setup,
149
+ teardown,
150
+ getManualSetupInstructions,
151
+ getManualTeardownInstructions,
152
+ };