@aikidosec/safe-chain 0.0.1-custom-install-dir

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 (116) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +537 -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-uvx.js +16 -0
  17. package/bin/aikido-yarn.js +14 -0
  18. package/bin/safe-chain.js +147 -0
  19. package/docs/Release.md +25 -0
  20. package/docs/banner.svg +151 -0
  21. package/docs/safe-package-manager-demo.gif +0 -0
  22. package/docs/safe-package-manager-demo.png +0 -0
  23. package/docs/shell-integration.md +149 -0
  24. package/docs/troubleshooting.md +321 -0
  25. package/npm-shrinkwrap.json +3180 -0
  26. package/package.json +71 -0
  27. package/src/api/aikido.js +187 -0
  28. package/src/api/npmApi.js +71 -0
  29. package/src/config/cliArguments.js +161 -0
  30. package/src/config/configFile.js +327 -0
  31. package/src/config/environmentVariables.js +57 -0
  32. package/src/config/safeChainDir.js +71 -0
  33. package/src/config/settings.js +247 -0
  34. package/src/environment/environment.js +14 -0
  35. package/src/environment/userInteraction.js +122 -0
  36. package/src/installLocation.js +42 -0
  37. package/src/main.js +123 -0
  38. package/src/packagemanager/_shared/commandErrors.js +17 -0
  39. package/src/packagemanager/_shared/matchesCommand.js +18 -0
  40. package/src/packagemanager/bun/createBunPackageManager.js +48 -0
  41. package/src/packagemanager/currentPackageManager.js +82 -0
  42. package/src/packagemanager/npm/createPackageManager.js +72 -0
  43. package/src/packagemanager/npm/dependencyScanner/commandArgumentScanner.js +74 -0
  44. package/src/packagemanager/npm/dependencyScanner/nullScanner.js +9 -0
  45. package/src/packagemanager/npm/parsing/parsePackagesFromInstallArgs.js +144 -0
  46. package/src/packagemanager/npm/runNpmCommand.js +20 -0
  47. package/src/packagemanager/npm/utils/abbrevs-generated.js +359 -0
  48. package/src/packagemanager/npm/utils/cmd-list.js +174 -0
  49. package/src/packagemanager/npm/utils/npmCommands.js +34 -0
  50. package/src/packagemanager/npx/createPackageManager.js +15 -0
  51. package/src/packagemanager/npx/dependencyScanner/commandArgumentScanner.js +43 -0
  52. package/src/packagemanager/npx/parsing/parsePackagesFromArguments.js +130 -0
  53. package/src/packagemanager/npx/runNpxCommand.js +20 -0
  54. package/src/packagemanager/pip/createPackageManager.js +25 -0
  55. package/src/packagemanager/pip/pipSettings.js +6 -0
  56. package/src/packagemanager/pip/runPipCommand.js +209 -0
  57. package/src/packagemanager/pipx/createPipXPackageManager.js +18 -0
  58. package/src/packagemanager/pipx/runPipXCommand.js +60 -0
  59. package/src/packagemanager/pnpm/createPackageManager.js +57 -0
  60. package/src/packagemanager/pnpm/dependencyScanner/commandArgumentScanner.js +35 -0
  61. package/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.js +109 -0
  62. package/src/packagemanager/pnpm/runPnpmCommand.js +32 -0
  63. package/src/packagemanager/poetry/createPoetryPackageManager.js +72 -0
  64. package/src/packagemanager/uv/createUvPackageManager.js +18 -0
  65. package/src/packagemanager/uv/runUvCommand.js +66 -0
  66. package/src/packagemanager/uvx/createUvxPackageManager.js +18 -0
  67. package/src/packagemanager/yarn/createPackageManager.js +41 -0
  68. package/src/packagemanager/yarn/dependencyScanner/commandArgumentScanner.js +35 -0
  69. package/src/packagemanager/yarn/parsing/parsePackagesFromArguments.js +128 -0
  70. package/src/packagemanager/yarn/runYarnCommand.js +36 -0
  71. package/src/registryProxy/certBundle.js +203 -0
  72. package/src/registryProxy/certUtils.js +178 -0
  73. package/src/registryProxy/getConnectTimeout.js +13 -0
  74. package/src/registryProxy/http-utils.js +80 -0
  75. package/src/registryProxy/interceptors/createInterceptorForEcoSystem.js +25 -0
  76. package/src/registryProxy/interceptors/interceptorBuilder.js +179 -0
  77. package/src/registryProxy/interceptors/minimumPackageAgeExclusions.js +33 -0
  78. package/src/registryProxy/interceptors/npm/modifyNpmInfo.js +180 -0
  79. package/src/registryProxy/interceptors/npm/npmInterceptor.js +101 -0
  80. package/src/registryProxy/interceptors/npm/parseNpmPackageUrl.js +60 -0
  81. package/src/registryProxy/interceptors/pip/modifyPipInfo.js +167 -0
  82. package/src/registryProxy/interceptors/pip/modifyPipJsonResponse.js +176 -0
  83. package/src/registryProxy/interceptors/pip/parsePipPackageUrl.js +162 -0
  84. package/src/registryProxy/interceptors/pip/pipInterceptor.js +122 -0
  85. package/src/registryProxy/interceptors/pip/pipMetadataResponseUtils.js +27 -0
  86. package/src/registryProxy/interceptors/pip/pipMetadataVersionUtils.js +131 -0
  87. package/src/registryProxy/interceptors/suppressedVersionsState.js +21 -0
  88. package/src/registryProxy/isImdsEndpoint.js +13 -0
  89. package/src/registryProxy/mitmRequestHandler.js +240 -0
  90. package/src/registryProxy/plainHttpProxy.js +95 -0
  91. package/src/registryProxy/registryProxy.js +255 -0
  92. package/src/registryProxy/tunnelRequestHandler.js +213 -0
  93. package/src/scanning/audit/index.js +129 -0
  94. package/src/scanning/index.js +82 -0
  95. package/src/scanning/malwareDatabase.js +131 -0
  96. package/src/scanning/newPackagesDatabaseBuilder.js +71 -0
  97. package/src/scanning/newPackagesDatabaseWarnings.js +17 -0
  98. package/src/scanning/newPackagesListCache.js +126 -0
  99. package/src/scanning/packageNameVariants.js +29 -0
  100. package/src/shell-integration/helpers.js +296 -0
  101. package/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh +37 -0
  102. package/src/shell-integration/path-wrappers/templates/windows-wrapper.template.cmd +25 -0
  103. package/src/shell-integration/setup-ci.js +152 -0
  104. package/src/shell-integration/setup.js +110 -0
  105. package/src/shell-integration/shellDetection.js +39 -0
  106. package/src/shell-integration/startup-scripts/init-fish.fish +122 -0
  107. package/src/shell-integration/startup-scripts/init-posix.sh +112 -0
  108. package/src/shell-integration/startup-scripts/init-pwsh.ps1 +176 -0
  109. package/src/shell-integration/supported-shells/bash.js +222 -0
  110. package/src/shell-integration/supported-shells/fish.js +97 -0
  111. package/src/shell-integration/supported-shells/powershell.js +102 -0
  112. package/src/shell-integration/supported-shells/windowsPowershell.js +102 -0
  113. package/src/shell-integration/supported-shells/zsh.js +94 -0
  114. package/src/shell-integration/teardown.js +114 -0
  115. package/src/utils/safeSpawn.js +153 -0
  116. package/tsconfig.json +21 -0
@@ -0,0 +1,126 @@
1
+ import fs from "fs";
2
+ import {
3
+ fetchNewPackagesList,
4
+ fetchNewPackagesListVersion,
5
+ } from "../api/aikido.js";
6
+ import {
7
+ getNewPackagesListPath,
8
+ getNewPackagesListVersionPath,
9
+ } from "../config/configFile.js";
10
+ import { ui } from "../environment/userInteraction.js";
11
+ import { buildNewPackagesDatabase } from "./newPackagesDatabaseBuilder.js";
12
+ import { warnOnceAboutUnavailableDatabase } from "./newPackagesDatabaseWarnings.js";
13
+
14
+ /**
15
+ * @typedef {import("./newPackagesDatabaseBuilder.js").NewPackagesDatabase} NewPackagesDatabase
16
+ */
17
+
18
+ // Shared per-process cache to avoid rebuilding the same feed-backed database on each request.
19
+ /** @type {NewPackagesDatabase | null} */
20
+ let cachedNewPackagesDatabase = null;
21
+
22
+ /**
23
+ * @returns {Promise<NewPackagesDatabase>}
24
+ */
25
+ export async function openNewPackagesDatabase() {
26
+ if (cachedNewPackagesDatabase) {
27
+ return cachedNewPackagesDatabase;
28
+ }
29
+
30
+ /** @type {import("../api/aikido.js").NewPackageEntry[]} */
31
+ let newPackagesList;
32
+
33
+ try {
34
+ newPackagesList = await getNewPackagesList();
35
+ } catch (/** @type {any} */ error) {
36
+ warnOnceAboutUnavailableDatabase(error);
37
+ cachedNewPackagesDatabase = { isNewlyReleasedPackage: () => false };
38
+ return cachedNewPackagesDatabase;
39
+ }
40
+
41
+ cachedNewPackagesDatabase = buildNewPackagesDatabase(newPackagesList);
42
+ return cachedNewPackagesDatabase;
43
+ }
44
+
45
+ /**
46
+ * @returns {Promise<import("../api/aikido.js").NewPackageEntry[]>}
47
+ */
48
+ async function getNewPackagesList() {
49
+ const { newPackagesList: cachedList, version: cachedVersion } =
50
+ readNewPackagesListFromLocalCache();
51
+
52
+ try {
53
+ if (cachedList) {
54
+ const currentVersion = await fetchNewPackagesListVersion();
55
+ if (cachedVersion === currentVersion) {
56
+ return cachedList;
57
+ }
58
+ }
59
+
60
+ const { newPackagesList, version } = await fetchNewPackagesList();
61
+
62
+ if (version) {
63
+ writeNewPackagesListToLocalCache(newPackagesList, version);
64
+ return newPackagesList;
65
+ } else {
66
+ ui.writeWarning(
67
+ "The new packages list for direct package download request blocking was downloaded, but could not be cached due to a missing version."
68
+ );
69
+ return newPackagesList;
70
+ }
71
+ } catch (/** @type {any} */ error) {
72
+ if (cachedList) {
73
+ ui.writeWarning(
74
+ "Failed to fetch the latest new packages list for direct package download request blocking. Using cached version."
75
+ );
76
+ return cachedList;
77
+ }
78
+ throw error;
79
+ }
80
+ }
81
+
82
+ /**
83
+ * @param {import("../api/aikido.js").NewPackageEntry[]} data
84
+ * @param {string | number} version
85
+ *
86
+ * @returns {void}
87
+ */
88
+ export function writeNewPackagesListToLocalCache(data, version) {
89
+ try {
90
+ const listPath = getNewPackagesListPath();
91
+ const versionPath = getNewPackagesListVersionPath();
92
+
93
+ fs.writeFileSync(listPath, JSON.stringify(data));
94
+ fs.writeFileSync(versionPath, version.toString());
95
+ } catch {
96
+ ui.writeWarning(
97
+ "Failed to write new packages list to local cache, next time the list will be fetched from the server again."
98
+ );
99
+ }
100
+ }
101
+
102
+ /**
103
+ * @returns {{newPackagesList: import("../api/aikido.js").NewPackageEntry[] | null, version: string | null}}
104
+ */
105
+ export function readNewPackagesListFromLocalCache() {
106
+ try {
107
+ const listPath = getNewPackagesListPath();
108
+ if (!fs.existsSync(listPath)) {
109
+ return { newPackagesList: null, version: null };
110
+ }
111
+
112
+ const data = fs.readFileSync(listPath, "utf8");
113
+ const newPackagesList = JSON.parse(data);
114
+ const versionPath = getNewPackagesListVersionPath();
115
+ let version = null;
116
+ if (fs.existsSync(versionPath)) {
117
+ version = fs.readFileSync(versionPath, "utf8").trim();
118
+ }
119
+ return { newPackagesList, version };
120
+ } catch {
121
+ ui.writeWarning(
122
+ "Failed to read new packages list from local cache. Continuing without local cache."
123
+ );
124
+ return { newPackagesList: null, version: null };
125
+ }
126
+ }
@@ -0,0 +1,29 @@
1
+ import { ECOSYSTEM_PY } from "../config/settings.js";
2
+
3
+ /**
4
+ * Normalises a Python package name per PEP 503: lowercase and collapse any
5
+ * run of `.`, `_`, or `-` into a single hyphen.
6
+ * @param {string} packageName
7
+ * @returns {string}
8
+ */
9
+ export function normalizePipPackageName(packageName) {
10
+ return packageName.toLowerCase().replace(/[._-]+/g, "-");
11
+ }
12
+
13
+ /**
14
+ * @param {string} packageName
15
+ * @param {string} ecosystem
16
+ * @returns {string[]}
17
+ */
18
+ export function getEquivalentPackageNames(packageName, ecosystem) {
19
+ if (ecosystem !== ECOSYSTEM_PY) {
20
+ return [packageName];
21
+ }
22
+
23
+ const pythonSeparatorPattern = /[._-]/g;
24
+ const hyphenName = packageName.replaceAll(pythonSeparatorPattern, "-");
25
+ const underscoreName = packageName.replaceAll(pythonSeparatorPattern, "_");
26
+ const dotName = packageName.replaceAll(pythonSeparatorPattern, ".");
27
+
28
+ return [...new Set([packageName, hyphenName, underscoreName, dotName])];
29
+ }
@@ -0,0 +1,296 @@
1
+ import { spawnSync } from "child_process";
2
+ import * as os from "os";
3
+ import fs from "fs";
4
+ import path from "path";
5
+ import { ECOSYSTEM_JS, ECOSYSTEM_PY } from "../config/settings.js";
6
+ import { safeSpawn } from "../utils/safeSpawn.js";
7
+ import { ui } from "../environment/userInteraction.js";
8
+
9
+ /**
10
+ * @typedef {Object} AikidoTool
11
+ * @property {string} tool
12
+ * @property {string} aikidoCommand
13
+ * @property {string} ecoSystem
14
+ * @property {string} internalPackageManagerName
15
+ */
16
+
17
+ /**
18
+ * @type {AikidoTool[]}
19
+ */
20
+ export const knownAikidoTools = [
21
+ {
22
+ tool: "npm",
23
+ aikidoCommand: "aikido-npm",
24
+ ecoSystem: ECOSYSTEM_JS,
25
+ internalPackageManagerName: "npm",
26
+ },
27
+ {
28
+ tool: "npx",
29
+ aikidoCommand: "aikido-npx",
30
+ ecoSystem: ECOSYSTEM_JS,
31
+ internalPackageManagerName: "npx",
32
+ },
33
+ {
34
+ tool: "yarn",
35
+ aikidoCommand: "aikido-yarn",
36
+ ecoSystem: ECOSYSTEM_JS,
37
+ internalPackageManagerName: "yarn",
38
+ },
39
+ {
40
+ tool: "pnpm",
41
+ aikidoCommand: "aikido-pnpm",
42
+ ecoSystem: ECOSYSTEM_JS,
43
+ internalPackageManagerName: "pnpm",
44
+ },
45
+ {
46
+ tool: "pnpx",
47
+ aikidoCommand: "aikido-pnpx",
48
+ ecoSystem: ECOSYSTEM_JS,
49
+ internalPackageManagerName: "pnpx",
50
+ },
51
+ {
52
+ tool: "bun",
53
+ aikidoCommand: "aikido-bun",
54
+ ecoSystem: ECOSYSTEM_JS,
55
+ internalPackageManagerName: "bun",
56
+ },
57
+ {
58
+ tool: "bunx",
59
+ aikidoCommand: "aikido-bunx",
60
+ ecoSystem: ECOSYSTEM_JS,
61
+ internalPackageManagerName: "bunx",
62
+ },
63
+ {
64
+ tool: "uv",
65
+ aikidoCommand: "aikido-uv",
66
+ ecoSystem: ECOSYSTEM_PY,
67
+ internalPackageManagerName: "uv",
68
+ },
69
+ {
70
+ tool: "uvx",
71
+ aikidoCommand: "aikido-uvx",
72
+ ecoSystem: ECOSYSTEM_PY,
73
+ internalPackageManagerName: "uvx",
74
+ },
75
+ {
76
+ tool: "pip",
77
+ aikidoCommand: "aikido-pip",
78
+ ecoSystem: ECOSYSTEM_PY,
79
+ internalPackageManagerName: "pip",
80
+ },
81
+ {
82
+ tool: "pip3",
83
+ aikidoCommand: "aikido-pip3",
84
+ ecoSystem: ECOSYSTEM_PY,
85
+ internalPackageManagerName: "pip",
86
+ },
87
+ {
88
+ tool: "poetry",
89
+ aikidoCommand: "aikido-poetry",
90
+ ecoSystem: ECOSYSTEM_PY,
91
+ internalPackageManagerName: "poetry",
92
+ },
93
+ {
94
+ tool: "python",
95
+ aikidoCommand: "aikido-python",
96
+ ecoSystem: ECOSYSTEM_PY,
97
+ internalPackageManagerName: "pip",
98
+ },
99
+ {
100
+ tool: "python3",
101
+ aikidoCommand: "aikido-python3",
102
+ ecoSystem: ECOSYSTEM_PY,
103
+ internalPackageManagerName: "pip",
104
+ },
105
+ {
106
+ tool: "pipx",
107
+ aikidoCommand: "aikido-pipx",
108
+ ecoSystem: ECOSYSTEM_PY,
109
+ internalPackageManagerName: "pipx",
110
+ },
111
+ // When adding a new tool here, also update the documentation for the new tool in the README.md
112
+ ];
113
+
114
+ /**
115
+ * Returns a formatted string listing all supported package managers.
116
+ * Example: "npm, npx, yarn, pnpm, and pnpx commands"
117
+ */
118
+ export function getPackageManagerList() {
119
+ const tools = knownAikidoTools.map((t) => t.tool);
120
+ if (tools.length <= 1) {
121
+ return `${tools[0] || ""} commands`;
122
+ }
123
+ if (tools.length === 2) {
124
+ return `${tools[0]} and ${tools[1]} commands`;
125
+ }
126
+ const lastTool = tools.pop();
127
+ return `${tools.join(", ")}, and ${lastTool} commands`;
128
+ }
129
+
130
+ /**
131
+ * @param {string} executableName
132
+ *
133
+ * @returns {boolean}
134
+ */
135
+ export function doesExecutableExistOnSystem(executableName) {
136
+ if (os.platform() === "win32") {
137
+ const result = spawnSync("where", [executableName], { stdio: "ignore" });
138
+ return result.status === 0;
139
+ } else {
140
+ const result = spawnSync("which", [executableName], { stdio: "ignore" });
141
+ return result.status === 0;
142
+ }
143
+ }
144
+
145
+ /**
146
+ * @param {string} filePath
147
+ * @param {RegExp} pattern
148
+ * @param {string} [eol]
149
+ *
150
+ * @returns {void}
151
+ */
152
+ export function removeLinesMatchingPattern(filePath, pattern, eol) {
153
+ if (!fs.existsSync(filePath)) {
154
+ return;
155
+ }
156
+
157
+ eol = eol || os.EOL;
158
+
159
+ const fileContent = fs.readFileSync(filePath, "utf-8");
160
+ const lines = fileContent.split(/\r?\n|\r|\u2028|\u2029/);
161
+ const updatedLines = lines.filter((line) => !shouldRemoveLine(line, pattern));
162
+ fs.writeFileSync(filePath, updatedLines.join(eol), "utf-8");
163
+ }
164
+
165
+ const maxLineLength = 100;
166
+
167
+ /**
168
+ * @param {string} line
169
+ * @param {RegExp} pattern
170
+ * @returns {boolean}
171
+ */
172
+ function shouldRemoveLine(line, pattern) {
173
+ const isPatternMatch = pattern.test(line);
174
+
175
+ if (!isPatternMatch) {
176
+ return false;
177
+ }
178
+
179
+ if (line.length > maxLineLength) {
180
+ // safe-chain only adds lines shorter than maxLineLength
181
+ // so if the line is longer, it must be from a different
182
+ // source and could be dangerous to remove
183
+ return false;
184
+ }
185
+
186
+ if (
187
+ line.includes("\n") ||
188
+ line.includes("\r") ||
189
+ line.includes("\u2028") ||
190
+ line.includes("\u2029")
191
+ ) {
192
+ // If the line contains newlines, something has gone wrong in splitting
193
+ // \u2028 and \u2029 are Unicode line separator characters (line and paragraph separators)
194
+ return false;
195
+ }
196
+
197
+ return true;
198
+ }
199
+
200
+ /**
201
+ * @param {string} filePath
202
+ * @param {string} line
203
+ * @param {string} [eol]
204
+ *
205
+ * @returns {void}
206
+ */
207
+ export function addLineToFile(filePath, line, eol) {
208
+ createFileIfNotExists(filePath);
209
+
210
+ eol = eol || os.EOL;
211
+
212
+ const fileContent = fs.readFileSync(filePath, "utf-8");
213
+ let updatedContent = fileContent;
214
+
215
+ if (!fileContent.endsWith(eol)) {
216
+ updatedContent += eol;
217
+ }
218
+
219
+ updatedContent += line + eol;
220
+ fs.writeFileSync(filePath, updatedContent, "utf-8");
221
+ }
222
+
223
+ /**
224
+ * @param {string} filePath
225
+ *
226
+ * @returns {void}
227
+ */
228
+ function createFileIfNotExists(filePath) {
229
+ if (fs.existsSync(filePath)) {
230
+ return;
231
+ }
232
+
233
+ const dir = path.dirname(filePath);
234
+ if (!fs.existsSync(dir)) {
235
+ fs.mkdirSync(dir, { recursive: true });
236
+ }
237
+
238
+ fs.writeFileSync(filePath, "", "utf-8");
239
+ }
240
+
241
+ /**
242
+ * Checks if PowerShell execution policy allows script execution
243
+ * @param {string} shellExecutableName - The name of the PowerShell executable ("pwsh" or "powershell")
244
+ * @returns {Promise<{isValid: boolean, policy: string}>} validation result
245
+ */
246
+ export async function validatePowerShellExecutionPolicy(shellExecutableName) {
247
+ // Security: Only allow known shell executables
248
+ const validShells = ["pwsh", "powershell"];
249
+ if (!validShells.includes(shellExecutableName)) {
250
+ return { isValid: false, policy: "Unknown" };
251
+ }
252
+
253
+ try {
254
+ // For Windows PowerShell (5.1), clean PSModulePath to avoid conflicts with PowerShell 7 modules
255
+ // When safe-chain is invoked from PowerShell 7, it sets its module paths to PSModulePath, causing
256
+ // Windows PowerShell to try loading incompatible PowerShell 7 modules.
257
+ // Setting the environment to Windows PowerShell's modules fixes this.
258
+ let spawnOptions;
259
+ if (shellExecutableName === "powershell") {
260
+ const userProfile = process.env.USERPROFILE || "";
261
+ const cleanPSModulePath = [
262
+ path.join(userProfile, "Documents", "WindowsPowerShell", "Modules"),
263
+ "C:\\Program Files\\WindowsPowerShell\\Modules",
264
+ "C:\\WINDOWS\\system32\\WindowsPowerShell\\v1.0\\Modules",
265
+ ].join(";");
266
+
267
+ spawnOptions = {
268
+ env: {
269
+ ...process.env,
270
+ PSModulePath: cleanPSModulePath,
271
+ },
272
+ };
273
+ } else {
274
+ spawnOptions = {};
275
+ }
276
+
277
+ const commandResult = await safeSpawn(
278
+ shellExecutableName,
279
+ ["-Command", "Get-ExecutionPolicy"],
280
+ spawnOptions,
281
+ );
282
+
283
+ const policy = commandResult.stdout.trim();
284
+
285
+ const acceptablePolicies = ["RemoteSigned", "Unrestricted", "Bypass"];
286
+ return {
287
+ isValid: acceptablePolicies.includes(policy),
288
+ policy: policy,
289
+ };
290
+ } catch (err) {
291
+ ui.writeWarning(
292
+ `An error happened while trying to find the current executionpolicy in powershell: ${err}`,
293
+ );
294
+ return { isValid: false, policy: "Unknown" };
295
+ }
296
+ }
@@ -0,0 +1,37 @@
1
+ #!/bin/sh
2
+ # Generated wrapper for {{PACKAGE_MANAGER}} by safe-chain
3
+ # This wrapper intercepts {{PACKAGE_MANAGER}} calls for non-interactive environments
4
+
5
+ # Function to remove shim from PATH (POSIX-compliant)
6
+ remove_shim_from_path() {
7
+ _safe_chain_phys=$(CDPATH= cd -- "$(dirname -- "$0")" 2>/dev/null && pwd -P)
8
+ if [ -z "$_safe_chain_phys" ]; then
9
+ echo "$PATH"
10
+ return
11
+ fi
12
+ _path=$(echo "$PATH" | sed "s|${_safe_chain_phys}:||g")
13
+ # Also remove via dirname of $0 directly — on macOS /tmp is a symlink to /private/tmp,
14
+ # so pwd -P resolves to /private/tmp/… but PATH may still contain /tmp/….
15
+ _dir=$(dirname -- "$0")
16
+ case "$_dir" in
17
+ /*) [ "$_dir" != "$_safe_chain_phys" ] && _path=$(echo "$_path" | sed "s|${_dir}:||g") ;;
18
+ esac
19
+ echo "$_path"
20
+ }
21
+
22
+ if command -v safe-chain >/dev/null 2>&1; then
23
+ # Remove shim directory from PATH when calling {{AIKIDO_COMMAND}} to prevent infinite loops
24
+ PATH=$(remove_shim_from_path) exec safe-chain {{PACKAGE_MANAGER}} "$@"
25
+ else
26
+ # safe-chain is not reachable — warn the user so they know protection is inactive
27
+ printf "\033[43;30mWarning:\033[0m safe-chain is not available to protect you from installing malware. {{PACKAGE_MANAGER}} will run without it.\n" >&2
28
+
29
+ # Dynamically find original {{PACKAGE_MANAGER}} (excluding this shim directory)
30
+ original_cmd=$(PATH=$(remove_shim_from_path) command -v {{PACKAGE_MANAGER}})
31
+ if [ -n "$original_cmd" ]; then
32
+ exec "$original_cmd" "$@"
33
+ else
34
+ echo "Error: Could not find original {{PACKAGE_MANAGER}}" >&2
35
+ exit 1
36
+ fi
37
+ fi
@@ -0,0 +1,25 @@
1
+ @echo off
2
+ REM Generated wrapper for {{PACKAGE_MANAGER}} by safe-chain
3
+ REM This wrapper intercepts {{PACKAGE_MANAGER}} calls for non-interactive environments
4
+
5
+ REM Remove shim directory from PATH to prevent infinite loops
6
+ set "SHIM_DIR=%~dp0"
7
+ if "%SHIM_DIR:~-1%"=="\" set "SHIM_DIR=%SHIM_DIR:~0,-1%"
8
+ call set "CLEAN_PATH=%%PATH:%SHIM_DIR%;=%%"
9
+
10
+ REM Check if aikido command is available with clean PATH
11
+ set "PATH=%CLEAN_PATH%" & where safe-chain >nul 2>&1
12
+ if %errorlevel%==0 (
13
+ REM Call aikido command with clean PATH
14
+ set "PATH=%CLEAN_PATH%" & safe-chain {{PACKAGE_MANAGER}} %*
15
+ ) else (
16
+ REM Find the original command with clean PATH
17
+ for /f "tokens=*" %%i in ('set "PATH=%CLEAN_PATH%" ^& where {{PACKAGE_MANAGER}} 2^>nul') do (
18
+ "%%i" %*
19
+ goto :eof
20
+ )
21
+
22
+ REM If we get here, original command was not found
23
+ echo Error: Could not find original {{PACKAGE_MANAGER}} >&2
24
+ exit /b 1
25
+ )
@@ -0,0 +1,152 @@
1
+ import chalk from "chalk";
2
+ import { ui } from "../environment/userInteraction.js";
3
+ import { getPackageManagerList, knownAikidoTools } from "./helpers.js";
4
+ import {
5
+ getShimsDir,
6
+ getBinDir,
7
+ getPathWrapperTemplatePath,
8
+ } from "../config/safeChainDir.js";
9
+ import fs from "fs";
10
+ import os from "os";
11
+ import path from "path";
12
+
13
+ /**
14
+ * Loops over the detected shells and calls the setup function for each.
15
+ */
16
+ export async function setupCi() {
17
+ ui.writeInformation(
18
+ chalk.bold("Setting up shell aliases.") +
19
+ ` This will wrap safe-chain around ${getPackageManagerList()}.`
20
+ );
21
+ ui.emptyLine();
22
+
23
+ const shimsDir = getShimsDir();
24
+ const binDir = getBinDir();
25
+ // Create the shims directory if it doesn't exist
26
+ if (!fs.existsSync(shimsDir)) {
27
+ fs.mkdirSync(shimsDir, { recursive: true });
28
+ }
29
+
30
+ createShims(shimsDir);
31
+ ui.writeInformation(`Created shims in ${shimsDir}`);
32
+ modifyPathForCi(shimsDir, binDir);
33
+ ui.writeInformation(`Added shims directory to PATH for CI environments.`);
34
+ }
35
+
36
+ /**
37
+ * @param {string} shimsDir
38
+ *
39
+ * @returns {void}
40
+ */
41
+ function createUnixShims(shimsDir) {
42
+ // Read the template file
43
+ const templatePath = getPathWrapperTemplatePath(import.meta.url, "unix-wrapper.template.sh");
44
+
45
+ if (!fs.existsSync(templatePath)) {
46
+ ui.writeError(`Template file not found: ${templatePath}`);
47
+ return;
48
+ }
49
+
50
+ const template = fs.readFileSync(templatePath, "utf-8");
51
+
52
+ // Create a shim for each tool
53
+ let created = 0;
54
+ for (const toolInfo of getToolsToSetup()) {
55
+ const shimContent = template
56
+ .replaceAll("{{PACKAGE_MANAGER}}", toolInfo.tool)
57
+ .replaceAll("{{AIKIDO_COMMAND}}", toolInfo.aikidoCommand);
58
+
59
+ const shimPath = path.join(shimsDir, toolInfo.tool);
60
+ fs.writeFileSync(shimPath, shimContent, "utf-8");
61
+
62
+ // Make the shim executable on Unix systems
63
+ fs.chmodSync(shimPath, 0o755);
64
+ created++;
65
+ }
66
+
67
+ ui.writeInformation(`Created ${created} Unix shim(s) in ${shimsDir}`);
68
+ }
69
+
70
+ /**
71
+ * @param {string} shimsDir
72
+ *
73
+ * @returns {void}
74
+ */
75
+ function createWindowsShims(shimsDir) {
76
+ // Read the template file
77
+ const templatePath = getPathWrapperTemplatePath(import.meta.url, "windows-wrapper.template.cmd");
78
+
79
+ if (!fs.existsSync(templatePath)) {
80
+ ui.writeError(`Windows template file not found: ${templatePath}`);
81
+ return;
82
+ }
83
+
84
+ const template = fs.readFileSync(templatePath, "utf-8");
85
+
86
+ // Create a shim for each tool
87
+ let created = 0;
88
+ for (const toolInfo of getToolsToSetup()) {
89
+ const shimContent = template
90
+ .replaceAll("{{PACKAGE_MANAGER}}", toolInfo.tool)
91
+ .replaceAll("{{AIKIDO_COMMAND}}", toolInfo.aikidoCommand);
92
+
93
+ const shimPath = `${shimsDir}/${toolInfo.tool}.cmd`;
94
+ fs.writeFileSync(shimPath, shimContent, "utf-8");
95
+ created++;
96
+ }
97
+
98
+ ui.writeInformation(`Created ${created} Windows shim(s) in ${shimsDir}`);
99
+ }
100
+
101
+ /**
102
+ * @param {string} shimsDir
103
+ *
104
+ * @returns {void}
105
+ */
106
+ function createShims(shimsDir) {
107
+ if (os.platform() === "win32") {
108
+ createWindowsShims(shimsDir);
109
+ } else {
110
+ createUnixShims(shimsDir);
111
+ }
112
+ }
113
+
114
+ /**
115
+ * @param {string} shimsDir
116
+ * @param {string} binDir
117
+ *
118
+ * @returns {void}
119
+ */
120
+ function modifyPathForCi(shimsDir, binDir) {
121
+ if (process.env.GITHUB_PATH) {
122
+ // In GitHub Actions, append the shims directory to GITHUB_PATH
123
+ fs.appendFileSync(
124
+ process.env.GITHUB_PATH,
125
+ shimsDir + os.EOL + binDir + os.EOL,
126
+ "utf-8"
127
+ );
128
+ ui.writeInformation(
129
+ `Added shims directory to GITHUB_PATH for GitHub Actions.`
130
+ );
131
+ }
132
+
133
+ if (process.env.TF_BUILD) {
134
+ // In Azure Pipelines, prepending the path is done via a logging command:
135
+ // ##vso[task.prependpath]/path/to/add
136
+ // Logging this to stdout will cause the Azure Pipelines agent to pick it up
137
+ ui.writeInformation("##vso[task.prependpath]" + shimsDir);
138
+ ui.writeInformation("##vso[task.prependpath]" + binDir);
139
+ }
140
+
141
+ if (process.env.BASH_ENV) {
142
+ // In CircleCI, persisting PATH across steps is done by appending shell exports
143
+ // to the file referenced by BASH_ENV. CircleCI sources this file for 'run' each step.
144
+ const exportLine = `export PATH="${shimsDir}:${binDir}:$PATH"` + os.EOL;
145
+ fs.appendFileSync(process.env.BASH_ENV, exportLine, "utf-8");
146
+ ui.writeInformation(`Added shims directory to BASH_ENV for CircleCI.`);
147
+ }
148
+ }
149
+
150
+ function getToolsToSetup() {
151
+ return knownAikidoTools;
152
+ }