@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,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,304 @@
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: "pip",
71
+ aikidoCommand: "aikido-pip",
72
+ ecoSystem: ECOSYSTEM_PY,
73
+ internalPackageManagerName: "pip",
74
+ },
75
+ {
76
+ tool: "pip3",
77
+ aikidoCommand: "aikido-pip3",
78
+ ecoSystem: ECOSYSTEM_PY,
79
+ internalPackageManagerName: "pip",
80
+ },
81
+ {
82
+ tool: "poetry",
83
+ aikidoCommand: "aikido-poetry",
84
+ ecoSystem: ECOSYSTEM_PY,
85
+ internalPackageManagerName: "poetry",
86
+ },
87
+ {
88
+ tool: "python",
89
+ aikidoCommand: "aikido-python",
90
+ ecoSystem: ECOSYSTEM_PY,
91
+ internalPackageManagerName: "pip",
92
+ },
93
+ {
94
+ tool: "python3",
95
+ aikidoCommand: "aikido-python3",
96
+ ecoSystem: ECOSYSTEM_PY,
97
+ internalPackageManagerName: "pip",
98
+ },
99
+ {
100
+ tool: "pipx",
101
+ aikidoCommand: "aikido-pipx",
102
+ ecoSystem: ECOSYSTEM_PY,
103
+ internalPackageManagerName: "pipx",
104
+ },
105
+ // When adding a new tool here, also update the documentation for the new tool in the README.md
106
+ ];
107
+
108
+ /**
109
+ * Returns a formatted string listing all supported package managers.
110
+ * Example: "npm, npx, yarn, pnpm, and pnpx commands"
111
+ */
112
+ export function getPackageManagerList() {
113
+ const tools = knownAikidoTools.map((t) => t.tool);
114
+ if (tools.length <= 1) {
115
+ return `${tools[0] || ""} commands`;
116
+ }
117
+ if (tools.length === 2) {
118
+ return `${tools[0]} and ${tools[1]} commands`;
119
+ }
120
+ const lastTool = tools.pop();
121
+ return `${tools.join(", ")}, and ${lastTool} commands`;
122
+ }
123
+
124
+ /**
125
+ * @returns {string}
126
+ */
127
+ export function getShimsDir() {
128
+ return path.join(os.homedir(), ".safe-chain", "shims");
129
+ }
130
+
131
+ /**
132
+ * @returns {string}
133
+ */
134
+ export function getScriptsDir() {
135
+ return path.join(os.homedir(), ".safe-chain", "scripts");
136
+ }
137
+
138
+ /**
139
+ * @param {string} executableName
140
+ *
141
+ * @returns {boolean}
142
+ */
143
+ export function doesExecutableExistOnSystem(executableName) {
144
+ if (os.platform() === "win32") {
145
+ const result = spawnSync("where", [executableName], { stdio: "ignore" });
146
+ return result.status === 0;
147
+ } else {
148
+ const result = spawnSync("which", [executableName], { stdio: "ignore" });
149
+ return result.status === 0;
150
+ }
151
+ }
152
+
153
+ /**
154
+ * @param {string} filePath
155
+ * @param {RegExp} pattern
156
+ * @param {string} [eol]
157
+ *
158
+ * @returns {void}
159
+ */
160
+ export function removeLinesMatchingPattern(filePath, pattern, eol) {
161
+ if (!fs.existsSync(filePath)) {
162
+ return;
163
+ }
164
+
165
+ eol = eol || os.EOL;
166
+
167
+ const fileContent = fs.readFileSync(filePath, "utf-8");
168
+ const lines = fileContent.split(/\r?\n|\r|\u2028|\u2029/);
169
+ const updatedLines = lines.filter((line) => !shouldRemoveLine(line, pattern));
170
+ fs.writeFileSync(filePath, updatedLines.join(eol), "utf-8");
171
+ }
172
+
173
+ const maxLineLength = 100;
174
+
175
+ /**
176
+ * @param {string} line
177
+ * @param {RegExp} pattern
178
+ * @returns {boolean}
179
+ */
180
+ function shouldRemoveLine(line, pattern) {
181
+ const isPatternMatch = pattern.test(line);
182
+
183
+ if (!isPatternMatch) {
184
+ return false;
185
+ }
186
+
187
+ if (line.length > maxLineLength) {
188
+ // safe-chain only adds lines shorter than maxLineLength
189
+ // so if the line is longer, it must be from a different
190
+ // source and could be dangerous to remove
191
+ return false;
192
+ }
193
+
194
+ if (
195
+ line.includes("\n") ||
196
+ line.includes("\r") ||
197
+ line.includes("\u2028") ||
198
+ line.includes("\u2029")
199
+ ) {
200
+ // If the line contains newlines, something has gone wrong in splitting
201
+ // \u2028 and \u2029 are Unicode line separator characters (line and paragraph separators)
202
+ return false;
203
+ }
204
+
205
+ return true;
206
+ }
207
+
208
+ /**
209
+ * @param {string} filePath
210
+ * @param {string} line
211
+ * @param {string} [eol]
212
+ *
213
+ * @returns {void}
214
+ */
215
+ export function addLineToFile(filePath, line, eol) {
216
+ createFileIfNotExists(filePath);
217
+
218
+ eol = eol || os.EOL;
219
+
220
+ const fileContent = fs.readFileSync(filePath, "utf-8");
221
+ let updatedContent = fileContent;
222
+
223
+ if (!fileContent.endsWith(eol)) {
224
+ updatedContent += eol;
225
+ }
226
+
227
+ updatedContent += line + eol;
228
+ fs.writeFileSync(filePath, updatedContent, "utf-8");
229
+ }
230
+
231
+ /**
232
+ * @param {string} filePath
233
+ *
234
+ * @returns {void}
235
+ */
236
+ function createFileIfNotExists(filePath) {
237
+ if (fs.existsSync(filePath)) {
238
+ return;
239
+ }
240
+
241
+ const dir = path.dirname(filePath);
242
+ if (!fs.existsSync(dir)) {
243
+ fs.mkdirSync(dir, { recursive: true });
244
+ }
245
+
246
+ fs.writeFileSync(filePath, "", "utf-8");
247
+ }
248
+
249
+ /**
250
+ * Checks if PowerShell execution policy allows script execution
251
+ * @param {string} shellExecutableName - The name of the PowerShell executable ("pwsh" or "powershell")
252
+ * @returns {Promise<{isValid: boolean, policy: string}>} validation result
253
+ */
254
+ export async function validatePowerShellExecutionPolicy(shellExecutableName) {
255
+ // Security: Only allow known shell executables
256
+ const validShells = ["pwsh", "powershell"];
257
+ if (!validShells.includes(shellExecutableName)) {
258
+ return { isValid: false, policy: "Unknown" };
259
+ }
260
+
261
+ try {
262
+ // For Windows PowerShell (5.1), clean PSModulePath to avoid conflicts with PowerShell 7 modules
263
+ // When safe-chain is invoked from PowerShell 7, it sets its module paths to PSModulePath, causing
264
+ // Windows PowerShell to try loading incompatible PowerShell 7 modules.
265
+ // Setting the environment to Windows PowerShell's modules fixes this.
266
+ let spawnOptions;
267
+ if (shellExecutableName === "powershell") {
268
+ const userProfile = process.env.USERPROFILE || "";
269
+ const cleanPSModulePath = [
270
+ path.join(userProfile, "Documents", "WindowsPowerShell", "Modules"),
271
+ "C:\\Program Files\\WindowsPowerShell\\Modules",
272
+ "C:\\WINDOWS\\system32\\WindowsPowerShell\\v1.0\\Modules",
273
+ ].join(";");
274
+
275
+ spawnOptions = {
276
+ env: {
277
+ ...process.env,
278
+ PSModulePath: cleanPSModulePath,
279
+ },
280
+ };
281
+ } else {
282
+ spawnOptions = {};
283
+ }
284
+
285
+ const commandResult = await safeSpawn(
286
+ shellExecutableName,
287
+ ["-Command", "Get-ExecutionPolicy"],
288
+ spawnOptions,
289
+ );
290
+
291
+ const policy = commandResult.stdout.trim();
292
+
293
+ const acceptablePolicies = ["RemoteSigned", "Unrestricted", "Bypass"];
294
+ return {
295
+ isValid: acceptablePolicies.includes(policy),
296
+ policy: policy,
297
+ };
298
+ } catch (err) {
299
+ ui.writeWarning(
300
+ `An error happened while trying to find the current executionpolicy in powershell: ${err}`,
301
+ );
302
+ return { isValid: false, policy: "Unknown" };
303
+ }
304
+ }
@@ -0,0 +1,22 @@
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
+ echo "$PATH" | sed "s|$HOME/.safe-chain/shims:||g"
8
+ }
9
+
10
+ if command -v safe-chain >/dev/null 2>&1; then
11
+ # Remove shim directory from PATH when calling {{AIKIDO_COMMAND}} to prevent infinite loops
12
+ PATH=$(remove_shim_from_path) exec safe-chain {{PACKAGE_MANAGER}} "$@"
13
+ else
14
+ # Dynamically find original {{PACKAGE_MANAGER}} (excluding this shim directory)
15
+ original_cmd=$(PATH=$(remove_shim_from_path) command -v {{PACKAGE_MANAGER}})
16
+ if [ -n "$original_cmd" ]; then
17
+ exec "$original_cmd" "$@"
18
+ else
19
+ echo "Error: Could not find original {{PACKAGE_MANAGER}}" >&2
20
+ exit 1
21
+ fi
22
+ fi
@@ -0,0 +1,24 @@
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=%USERPROFILE%\.safe-chain\shims"
7
+ call set "CLEAN_PATH=%%PATH:%SHIM_DIR%;=%%"
8
+
9
+ REM Check if aikido command is available with clean PATH
10
+ set "PATH=%CLEAN_PATH%" & where safe-chain >nul 2>&1
11
+ if %errorlevel%==0 (
12
+ REM Call aikido command with clean PATH
13
+ set "PATH=%CLEAN_PATH%" & safe-chain {{PACKAGE_MANAGER}} %*
14
+ ) else (
15
+ REM Find the original command with clean PATH
16
+ for /f "tokens=*" %%i in ('set "PATH=%CLEAN_PATH%" ^& where {{PACKAGE_MANAGER}} 2^>nul') do (
17
+ "%%i" %*
18
+ goto :eof
19
+ )
20
+
21
+ REM If we get here, original command was not found
22
+ echo Error: Could not find original {{PACKAGE_MANAGER}} >&2
23
+ exit /b 1
24
+ )
@@ -0,0 +1,172 @@
1
+ import chalk from "chalk";
2
+ import { ui } from "../environment/userInteraction.js";
3
+ import { getPackageManagerList, knownAikidoTools, getShimsDir } from "./helpers.js";
4
+ import fs from "fs";
5
+ import os from "os";
6
+ import path from "path";
7
+ import { fileURLToPath } from "url";
8
+
9
+ /** @type {string} */
10
+ // This checks the current file's dirname in a way that's compatible with:
11
+ // - Modulejs (import.meta.url)
12
+ // - ES modules (__dirname)
13
+ // This is needed because safe-chain's npm package is built using ES modules,
14
+ // but building the binaries requires commonjs.
15
+ let dirname;
16
+ if (import.meta.url) {
17
+ const filename = fileURLToPath(import.meta.url);
18
+ dirname = path.dirname(filename);
19
+ } else {
20
+ dirname = __dirname;
21
+ }
22
+
23
+ /**
24
+ * Loops over the detected shells and calls the setup function for each.
25
+ */
26
+ export async function setupCi() {
27
+ ui.writeInformation(
28
+ chalk.bold("Setting up shell aliases.") +
29
+ ` This will wrap safe-chain around ${getPackageManagerList()}.`
30
+ );
31
+ ui.emptyLine();
32
+
33
+ const shimsDir = getShimsDir();
34
+ const binDir = path.join(os.homedir(), ".safe-chain", "bin");
35
+ // Create the shims directory if it doesn't exist
36
+ if (!fs.existsSync(shimsDir)) {
37
+ fs.mkdirSync(shimsDir, { recursive: true });
38
+ }
39
+
40
+ createShims(shimsDir);
41
+ ui.writeInformation(`Created shims in ${shimsDir}`);
42
+ modifyPathForCi(shimsDir, binDir);
43
+ ui.writeInformation(`Added shims directory to PATH for CI environments.`);
44
+ }
45
+
46
+ /**
47
+ * @param {string} shimsDir
48
+ *
49
+ * @returns {void}
50
+ */
51
+ function createUnixShims(shimsDir) {
52
+ // Read the template file
53
+ const templatePath = path.resolve(
54
+ dirname,
55
+ "path-wrappers",
56
+ "templates",
57
+ "unix-wrapper.template.sh"
58
+ );
59
+
60
+ if (!fs.existsSync(templatePath)) {
61
+ ui.writeError(`Template file not found: ${templatePath}`);
62
+ return;
63
+ }
64
+
65
+ const template = fs.readFileSync(templatePath, "utf-8");
66
+
67
+ // Create a shim for each tool
68
+ let created = 0;
69
+ for (const toolInfo of getToolsToSetup()) {
70
+ const shimContent = template
71
+ .replaceAll("{{PACKAGE_MANAGER}}", toolInfo.tool)
72
+ .replaceAll("{{AIKIDO_COMMAND}}", toolInfo.aikidoCommand);
73
+
74
+ const shimPath = path.join(shimsDir, toolInfo.tool);
75
+ fs.writeFileSync(shimPath, shimContent, "utf-8");
76
+
77
+ // Make the shim executable on Unix systems
78
+ fs.chmodSync(shimPath, 0o755);
79
+ created++;
80
+ }
81
+
82
+ ui.writeInformation(`Created ${created} Unix shim(s) in ${shimsDir}`);
83
+ }
84
+
85
+ /**
86
+ * @param {string} shimsDir
87
+ *
88
+ * @returns {void}
89
+ */
90
+ function createWindowsShims(shimsDir) {
91
+ // Read the template file
92
+ const templatePath = path.resolve(
93
+ dirname,
94
+ "path-wrappers",
95
+ "templates",
96
+ "windows-wrapper.template.cmd"
97
+ );
98
+
99
+ if (!fs.existsSync(templatePath)) {
100
+ ui.writeError(`Windows template file not found: ${templatePath}`);
101
+ return;
102
+ }
103
+
104
+ const template = fs.readFileSync(templatePath, "utf-8");
105
+
106
+ // Create a shim for each tool
107
+ let created = 0;
108
+ for (const toolInfo of getToolsToSetup()) {
109
+ const shimContent = template
110
+ .replaceAll("{{PACKAGE_MANAGER}}", toolInfo.tool)
111
+ .replaceAll("{{AIKIDO_COMMAND}}", toolInfo.aikidoCommand);
112
+
113
+ const shimPath = `${shimsDir}/${toolInfo.tool}.cmd`;
114
+ fs.writeFileSync(shimPath, shimContent, "utf-8");
115
+ created++;
116
+ }
117
+
118
+ ui.writeInformation(`Created ${created} Windows shim(s) in ${shimsDir}`);
119
+ }
120
+
121
+ /**
122
+ * @param {string} shimsDir
123
+ *
124
+ * @returns {void}
125
+ */
126
+ function createShims(shimsDir) {
127
+ if (os.platform() === "win32") {
128
+ createWindowsShims(shimsDir);
129
+ } else {
130
+ createUnixShims(shimsDir);
131
+ }
132
+ }
133
+
134
+ /**
135
+ * @param {string} shimsDir
136
+ * @param {string} binDir
137
+ *
138
+ * @returns {void}
139
+ */
140
+ function modifyPathForCi(shimsDir, binDir) {
141
+ if (process.env.GITHUB_PATH) {
142
+ // In GitHub Actions, append the shims directory to GITHUB_PATH
143
+ fs.appendFileSync(
144
+ process.env.GITHUB_PATH,
145
+ shimsDir + os.EOL + binDir + os.EOL,
146
+ "utf-8"
147
+ );
148
+ ui.writeInformation(
149
+ `Added shims directory to GITHUB_PATH for GitHub Actions.`
150
+ );
151
+ }
152
+
153
+ if (process.env.TF_BUILD) {
154
+ // In Azure Pipelines, prepending the path is done via a logging command:
155
+ // ##vso[task.prependpath]/path/to/add
156
+ // Logging this to stdout will cause the Azure Pipelines agent to pick it up
157
+ ui.writeInformation("##vso[task.prependpath]" + shimsDir);
158
+ ui.writeInformation("##vso[task.prependpath]" + binDir);
159
+ }
160
+
161
+ if (process.env.BASH_ENV) {
162
+ // In CircleCI, persisting PATH across steps is done by appending shell exports
163
+ // to the file referenced by BASH_ENV. CircleCI sources this file for 'run' each step.
164
+ const exportLine = `export PATH="${shimsDir}:${binDir}:$PATH"` + os.EOL;
165
+ fs.appendFileSync(process.env.BASH_ENV, exportLine, "utf-8");
166
+ ui.writeInformation(`Added shims directory to BASH_ENV for CircleCI.`);
167
+ }
168
+ }
169
+
170
+ function getToolsToSetup() {
171
+ return knownAikidoTools;
172
+ }