@aikidosec/safe-chain 0.0.4-connect-timeout-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 (94) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +257 -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 +20 -0
  8. package/bin/aikido-pip3.js +21 -0
  9. package/bin/aikido-pnpm.js +14 -0
  10. package/bin/aikido-pnpx.js +14 -0
  11. package/bin/aikido-python.js +30 -0
  12. package/bin/aikido-python3.js +30 -0
  13. package/bin/aikido-uv.js +16 -0
  14. package/bin/aikido-yarn.js +14 -0
  15. package/bin/safe-chain.js +190 -0
  16. package/docs/banner.svg +151 -0
  17. package/docs/npm-to-binary-migration.md +89 -0
  18. package/docs/safe-package-manager-demo.gif +0 -0
  19. package/docs/safe-package-manager-demo.png +0 -0
  20. package/docs/shell-integration.md +149 -0
  21. package/package.json +68 -0
  22. package/src/api/aikido.js +54 -0
  23. package/src/api/npmApi.js +71 -0
  24. package/src/config/cliArguments.js +138 -0
  25. package/src/config/configFile.js +192 -0
  26. package/src/config/environmentVariables.js +7 -0
  27. package/src/config/settings.js +100 -0
  28. package/src/environment/environment.js +14 -0
  29. package/src/environment/userInteraction.js +122 -0
  30. package/src/main.js +104 -0
  31. package/src/packagemanager/_shared/matchesCommand.js +18 -0
  32. package/src/packagemanager/bun/createBunPackageManager.js +53 -0
  33. package/src/packagemanager/currentPackageManager.js +72 -0
  34. package/src/packagemanager/npm/createPackageManager.js +72 -0
  35. package/src/packagemanager/npm/dependencyScanner/commandArgumentScanner.js +74 -0
  36. package/src/packagemanager/npm/dependencyScanner/nullScanner.js +9 -0
  37. package/src/packagemanager/npm/parsing/parsePackagesFromInstallArgs.js +144 -0
  38. package/src/packagemanager/npm/runNpmCommand.js +25 -0
  39. package/src/packagemanager/npm/utils/abbrevs-generated.js +359 -0
  40. package/src/packagemanager/npm/utils/cmd-list.js +174 -0
  41. package/src/packagemanager/npm/utils/npmCommands.js +34 -0
  42. package/src/packagemanager/npx/createPackageManager.js +15 -0
  43. package/src/packagemanager/npx/dependencyScanner/commandArgumentScanner.js +43 -0
  44. package/src/packagemanager/npx/parsing/parsePackagesFromArguments.js +130 -0
  45. package/src/packagemanager/npx/runNpxCommand.js +25 -0
  46. package/src/packagemanager/pip/createPackageManager.js +21 -0
  47. package/src/packagemanager/pip/pipSettings.js +30 -0
  48. package/src/packagemanager/pip/runPipCommand.js +175 -0
  49. package/src/packagemanager/pnpm/createPackageManager.js +57 -0
  50. package/src/packagemanager/pnpm/dependencyScanner/commandArgumentScanner.js +35 -0
  51. package/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.js +109 -0
  52. package/src/packagemanager/pnpm/runPnpmCommand.js +36 -0
  53. package/src/packagemanager/uv/createUvPackageManager.js +18 -0
  54. package/src/packagemanager/uv/runUvCommand.js +71 -0
  55. package/src/packagemanager/yarn/createPackageManager.js +41 -0
  56. package/src/packagemanager/yarn/dependencyScanner/commandArgumentScanner.js +35 -0
  57. package/src/packagemanager/yarn/parsing/parsePackagesFromArguments.js +128 -0
  58. package/src/packagemanager/yarn/runYarnCommand.js +41 -0
  59. package/src/registryProxy/certBundle.js +95 -0
  60. package/src/registryProxy/certUtils.js +128 -0
  61. package/src/registryProxy/http-utils.js +17 -0
  62. package/src/registryProxy/interceptors/createInterceptorForEcoSystem.js +25 -0
  63. package/src/registryProxy/interceptors/interceptorBuilder.js +140 -0
  64. package/src/registryProxy/interceptors/npm/modifyNpmInfo.js +177 -0
  65. package/src/registryProxy/interceptors/npm/npmInterceptor.js +47 -0
  66. package/src/registryProxy/interceptors/npm/parseNpmPackageUrl.js +43 -0
  67. package/src/registryProxy/interceptors/pipInterceptor.js +115 -0
  68. package/src/registryProxy/mitmRequestHandler.js +231 -0
  69. package/src/registryProxy/plainHttpProxy.js +95 -0
  70. package/src/registryProxy/registryProxy.js +184 -0
  71. package/src/registryProxy/tunnelRequestHandler.js +180 -0
  72. package/src/scanning/audit/index.js +129 -0
  73. package/src/scanning/index.js +82 -0
  74. package/src/scanning/malwareDatabase.js +131 -0
  75. package/src/shell-integration/helpers.js +213 -0
  76. package/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh +22 -0
  77. package/src/shell-integration/path-wrappers/templates/windows-wrapper.template.cmd +24 -0
  78. package/src/shell-integration/setup-ci.js +170 -0
  79. package/src/shell-integration/setup.js +127 -0
  80. package/src/shell-integration/shellDetection.js +37 -0
  81. package/src/shell-integration/startup-scripts/include-python/init-fish.fish +94 -0
  82. package/src/shell-integration/startup-scripts/include-python/init-posix.sh +81 -0
  83. package/src/shell-integration/startup-scripts/include-python/init-pwsh.ps1 +115 -0
  84. package/src/shell-integration/startup-scripts/init-fish.fish +71 -0
  85. package/src/shell-integration/startup-scripts/init-posix.sh +58 -0
  86. package/src/shell-integration/startup-scripts/init-pwsh.ps1 +92 -0
  87. package/src/shell-integration/supported-shells/bash.js +134 -0
  88. package/src/shell-integration/supported-shells/fish.js +77 -0
  89. package/src/shell-integration/supported-shells/powershell.js +73 -0
  90. package/src/shell-integration/supported-shells/windowsPowershell.js +73 -0
  91. package/src/shell-integration/supported-shells/zsh.js +74 -0
  92. package/src/shell-integration/teardown.js +64 -0
  93. package/src/utils/safeSpawn.js +137 -0
  94. package/tsconfig.json +21 -0
@@ -0,0 +1,129 @@
1
+ import { ui } from "../../environment/userInteraction.js";
2
+ import {
3
+ MALWARE_STATUS_MALWARE,
4
+ openMalwareDatabase,
5
+ } from "../malwareDatabase.js";
6
+
7
+ /**
8
+ * @typedef {Object} PackageChange
9
+ * @property {string} name
10
+ * @property {string} version
11
+ * @property {string} type
12
+ */
13
+
14
+ /**
15
+ * @typedef {Object} AuditResult
16
+ * @property {PackageChange[]} allowedChanges
17
+ * @property {(PackageChange & {reason: string})[]} disallowedChanges
18
+ * @property {boolean} isAllowed
19
+ */
20
+
21
+ /**
22
+ * @typedef {Object} AuditStats
23
+ * @property {number} totalPackages
24
+ * @property {number} safePackages
25
+ * @property {number} malwarePackages
26
+ */
27
+
28
+ /**
29
+ * @type AuditStats
30
+ */
31
+ const auditStats = {
32
+ totalPackages: 0,
33
+ safePackages: 0,
34
+ malwarePackages: 0,
35
+ };
36
+
37
+ /**
38
+ * @returns {AuditStats}
39
+ */
40
+ export function getAuditStats() {
41
+ return auditStats;
42
+ }
43
+
44
+ /**
45
+ *
46
+ * @param {string | undefined} name
47
+ * @param {string | undefined} version
48
+ * @returns {Promise<boolean>}
49
+ */
50
+ export async function isMalwarePackage(name, version) {
51
+ if (!name || !version) {
52
+ return false;
53
+ }
54
+
55
+ const auditResult = await auditChanges([{ name, version, type: "add" }]);
56
+
57
+ return !auditResult.isAllowed;
58
+ }
59
+
60
+ /**
61
+ * @param {PackageChange[]} changes
62
+ *
63
+ * @returns {Promise<AuditResult>}
64
+ */
65
+ export async function auditChanges(changes) {
66
+ const allowedChanges = [];
67
+ const disallowedChanges = [];
68
+
69
+ var malwarePackages = await getPackagesWithMalware(
70
+ changes.filter(
71
+ (change) => change.type === "add" || change.type === "change"
72
+ )
73
+ );
74
+
75
+ for (const change of changes) {
76
+ const malwarePackage = malwarePackages.find(
77
+ (pkg) => pkg.name === change.name && pkg.version === change.version
78
+ );
79
+
80
+ if (malwarePackage) {
81
+ auditStats.malwarePackages += 1;
82
+ ui.writeVerbose(
83
+ `Safe-chain: Package ${change.name}@${change.version} is marked as malware: ${malwarePackage.status}`
84
+ );
85
+ disallowedChanges.push({ ...change, reason: malwarePackage.status });
86
+ } else {
87
+ auditStats.safePackages += 1;
88
+ ui.writeVerbose(
89
+ `Safe-chain: Package ${change.name}@${change.version} is clean`
90
+ );
91
+ allowedChanges.push(change);
92
+ }
93
+
94
+ auditStats.totalPackages += 1;
95
+ }
96
+
97
+ const auditResults = {
98
+ allowedChanges,
99
+ disallowedChanges,
100
+ isAllowed: disallowedChanges.length === 0,
101
+ };
102
+
103
+ return auditResults;
104
+ }
105
+
106
+ /**
107
+ * @param {{name: string, version: string, type: string}[]} changes
108
+ * @returns {Promise<{name: string, version: string, status: string}[]>}
109
+ */
110
+ async function getPackagesWithMalware(changes) {
111
+ if (changes.length === 0) {
112
+ return [];
113
+ }
114
+
115
+ const malwareDb = await openMalwareDatabase();
116
+ let allVulnerablePackages = [];
117
+
118
+ for (const change of changes) {
119
+ if (malwareDb.isMalware(change.name, change.version)) {
120
+ allVulnerablePackages.push({
121
+ name: change.name,
122
+ version: change.version,
123
+ status: MALWARE_STATUS_MALWARE,
124
+ });
125
+ }
126
+ }
127
+
128
+ return allVulnerablePackages;
129
+ }
@@ -0,0 +1,82 @@
1
+ import { auditChanges } from "./audit/index.js";
2
+ import { getScanTimeout } from "../config/configFile.js";
3
+ import { setTimeout } from "timers/promises";
4
+ import chalk from "chalk";
5
+ import { getPackageManager } from "../packagemanager/currentPackageManager.js";
6
+ import { ui } from "../environment/userInteraction.js";
7
+
8
+ /**
9
+ * @param {string[]} args
10
+ *
11
+ * @returns {boolean}
12
+ */
13
+ export function shouldScanCommand(args) {
14
+ if (!args || args.length === 0) {
15
+ return false;
16
+ }
17
+
18
+ return getPackageManager().isSupportedCommand(args);
19
+ }
20
+
21
+ /**
22
+ * @param {string[]} args
23
+ *
24
+ * @returns {Promise<number>}
25
+ */
26
+ export async function scanCommand(args) {
27
+ if (!shouldScanCommand(args)) {
28
+ return 0;
29
+ }
30
+
31
+ let timedOut = false;
32
+ /** @type {import("./audit/index.js").AuditResult | undefined} */
33
+ let audit;
34
+
35
+ await Promise.race([
36
+ (async () => {
37
+ const packageManager = getPackageManager();
38
+ const changes = await packageManager.getDependencyUpdatesForCommand(args);
39
+
40
+ if (timedOut) {
41
+ return;
42
+ }
43
+
44
+ audit = await auditChanges(changes);
45
+ })(),
46
+ setTimeout(getScanTimeout()).then(() => {
47
+ timedOut = true;
48
+ }),
49
+ ]);
50
+
51
+ if (timedOut) {
52
+ throw new Error("Timeout exceeded while scanning npm install command.");
53
+ }
54
+
55
+ if (!audit || audit.isAllowed) {
56
+ return 0;
57
+ } else {
58
+ printMaliciousChanges(audit.disallowedChanges);
59
+ onMalwareFound();
60
+ return 1;
61
+ }
62
+ }
63
+
64
+ /**
65
+ * @param {import("./audit/index.js").PackageChange[]} changes
66
+ * @return {void}
67
+ */
68
+ function printMaliciousChanges(changes) {
69
+ ui.writeInformation(
70
+ chalk.red("✖") + " Safe-chain: " + chalk.bold("Malicious changes detected:")
71
+ );
72
+
73
+ for (const change of changes) {
74
+ ui.writeInformation(` - ${change.name}@${change.version}`);
75
+ }
76
+ }
77
+
78
+ function onMalwareFound() {
79
+ ui.emptyLine();
80
+ ui.writeExitWithoutInstallingMaliciousPackages();
81
+ ui.emptyLine();
82
+ }
@@ -0,0 +1,131 @@
1
+ import {
2
+ fetchMalwareDatabase,
3
+ fetchMalwareDatabaseVersion,
4
+ } from "../api/aikido.js";
5
+ import {
6
+ readDatabaseFromLocalCache,
7
+ writeDatabaseToLocalCache,
8
+ } from "../config/configFile.js";
9
+ import { ui } from "../environment/userInteraction.js";
10
+ import { getEcoSystem, ECOSYSTEM_PY } from "../config/settings.js";
11
+
12
+ /**
13
+ * @typedef {Object} MalwareDatabase
14
+ * @property {function(string, string): string} getPackageStatus
15
+ * @property {function(string, string): boolean} isMalware
16
+ */
17
+
18
+ /** @type {MalwareDatabase | null} */
19
+ let cachedMalwareDatabase = null;
20
+
21
+ /**
22
+ * Normalize package name for comparison.
23
+ * For Python packages (PEP-503): lowercase and replace _, -, . with -
24
+ * For js packages: keep as-is (case-sensitive)
25
+ * @param {string} name
26
+ * @returns {string}
27
+ */
28
+ function normalizePackageName(name) {
29
+ const ecosystem = getEcoSystem();
30
+ if (ecosystem === ECOSYSTEM_PY) {
31
+ return name.toLowerCase().replace(/[-_.]+/g, "-");
32
+ }
33
+
34
+ return name;
35
+ }
36
+
37
+ export async function openMalwareDatabase() {
38
+ if (cachedMalwareDatabase) {
39
+ return cachedMalwareDatabase;
40
+ }
41
+
42
+ const malwareDatabase = await getMalwareDatabase();
43
+
44
+ /**
45
+ * @param {string} name
46
+ * @param {string} version
47
+ * @returns {string}
48
+ */
49
+ function getPackageStatus(name, version) {
50
+ const normalizedName = normalizePackageName(name);
51
+ const packageData = malwareDatabase.find(
52
+ (pkg) => {
53
+ const normalizedPkgName = normalizePackageName(pkg.package_name);
54
+ return normalizedPkgName === normalizedName &&
55
+ (pkg.version === version || pkg.version === "*");
56
+ }
57
+ );
58
+
59
+ if (!packageData) {
60
+ return MALWARE_STATUS_OK;
61
+ }
62
+
63
+ return packageData.reason;
64
+ }
65
+
66
+ // This implicitly caches the malware database
67
+ // that's closed over by the getPackageStatus function
68
+ cachedMalwareDatabase = {
69
+ getPackageStatus,
70
+ isMalware: (name, version) => {
71
+ const status = getPackageStatus(name, version);
72
+ return isMalwareStatus(status);
73
+ },
74
+ };
75
+ return cachedMalwareDatabase;
76
+ }
77
+
78
+ /**
79
+ * @returns {Promise<import("../api/aikido.js").MalwarePackage[]>}
80
+ */
81
+ async function getMalwareDatabase() {
82
+ const { malwareDatabase: cachedDatabase, version: cachedVersion } =
83
+ readDatabaseFromLocalCache();
84
+
85
+ try {
86
+ if (cachedDatabase) {
87
+ const currentVersion = await fetchMalwareDatabaseVersion();
88
+ if (cachedVersion === currentVersion) {
89
+ return cachedDatabase;
90
+ }
91
+ }
92
+
93
+ const { malwareDatabase, version } = await fetchMalwareDatabase();
94
+
95
+ if (version) {
96
+ // Only cache the malware database when we have a version.
97
+ writeDatabaseToLocalCache(malwareDatabase, version);
98
+ return malwareDatabase;
99
+ } else {
100
+ // We received a valid malware database, but the response
101
+ // did not contain an etag header with the version
102
+ ui.writeWarning(
103
+ "The malware database was downloaded, but could not be cached due to a missing version."
104
+ );
105
+ return malwareDatabase;
106
+ }
107
+ } catch (/** @type any */ error) {
108
+ if (cachedDatabase) {
109
+ ui.writeWarning(
110
+ "Failed to fetch the latest malware database. Using cached version."
111
+ );
112
+ return cachedDatabase;
113
+ }
114
+ throw error;
115
+ }
116
+ }
117
+
118
+ /**
119
+ * @param {string} status
120
+ *
121
+ * @returns {boolean}
122
+ */
123
+ function isMalwareStatus(status) {
124
+ let malwareStatus = status.toUpperCase();
125
+ return malwareStatus === MALWARE_STATUS_MALWARE;
126
+ }
127
+
128
+ export const MALWARE_STATUS_OK = "OK";
129
+ export const MALWARE_STATUS_MALWARE = "MALWARE";
130
+ export const MALWARE_STATUS_TELEMETRY = "TELEMETRY";
131
+ export const MALWARE_STATUS_PROTESTWARE = "PROTESTWARE";
@@ -0,0 +1,213 @@
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
+
7
+ /**
8
+ * @typedef {Object} AikidoTool
9
+ * @property {string} tool
10
+ * @property {string} aikidoCommand
11
+ * @property {string} ecoSystem
12
+ * @property {string} internalPackageManagerName
13
+ */
14
+
15
+ /**
16
+ * @type {AikidoTool[]}
17
+ */
18
+ export const knownAikidoTools = [
19
+ {
20
+ tool: "npm",
21
+ aikidoCommand: "aikido-npm",
22
+ ecoSystem: ECOSYSTEM_JS,
23
+ internalPackageManagerName: "npm",
24
+ },
25
+ {
26
+ tool: "npx",
27
+ aikidoCommand: "aikido-npx",
28
+ ecoSystem: ECOSYSTEM_JS,
29
+ internalPackageManagerName: "npx",
30
+ },
31
+ {
32
+ tool: "yarn",
33
+ aikidoCommand: "aikido-yarn",
34
+ ecoSystem: ECOSYSTEM_JS,
35
+ internalPackageManagerName: "yarn",
36
+ },
37
+ {
38
+ tool: "pnpm",
39
+ aikidoCommand: "aikido-pnpm",
40
+ ecoSystem: ECOSYSTEM_JS,
41
+ internalPackageManagerName: "pnpm",
42
+ },
43
+ {
44
+ tool: "pnpx",
45
+ aikidoCommand: "aikido-pnpx",
46
+ ecoSystem: ECOSYSTEM_JS,
47
+ internalPackageManagerName: "pnpx",
48
+ },
49
+ {
50
+ tool: "bun",
51
+ aikidoCommand: "aikido-bun",
52
+ ecoSystem: ECOSYSTEM_JS,
53
+ internalPackageManagerName: "bun",
54
+ },
55
+ {
56
+ tool: "bunx",
57
+ aikidoCommand: "aikido-bunx",
58
+ ecoSystem: ECOSYSTEM_JS,
59
+ internalPackageManagerName: "bunx",
60
+ },
61
+ {
62
+ tool: "uv",
63
+ aikidoCommand: "aikido-uv",
64
+ ecoSystem: ECOSYSTEM_PY,
65
+ internalPackageManagerName: "uv",
66
+ },
67
+ {
68
+ tool: "pip",
69
+ aikidoCommand: "aikido-pip",
70
+ ecoSystem: ECOSYSTEM_PY,
71
+ internalPackageManagerName: "pip",
72
+ },
73
+ {
74
+ tool: "pip3",
75
+ aikidoCommand: "aikido-pip3",
76
+ ecoSystem: ECOSYSTEM_PY,
77
+ internalPackageManagerName: "pip",
78
+ },
79
+ {
80
+ tool: "python",
81
+ aikidoCommand: "aikido-python",
82
+ ecoSystem: ECOSYSTEM_PY,
83
+ internalPackageManagerName: "pip",
84
+ },
85
+ {
86
+ tool: "python3",
87
+ aikidoCommand: "aikido-python3",
88
+ ecoSystem: ECOSYSTEM_PY,
89
+ internalPackageManagerName: "pip",
90
+ },
91
+ // When adding a new tool here, also update the documentation for the new tool in the README.md
92
+ ];
93
+
94
+ /**
95
+ * Returns a formatted string listing all supported package managers.
96
+ * Example: "npm, npx, yarn, pnpm, and pnpx commands"
97
+ */
98
+ export function getPackageManagerList() {
99
+ const tools = knownAikidoTools.map((t) => t.tool);
100
+ if (tools.length <= 1) {
101
+ return `${tools[0] || ""} commands`;
102
+ }
103
+ if (tools.length === 2) {
104
+ return `${tools[0]} and ${tools[1]} commands`;
105
+ }
106
+ const lastTool = tools.pop();
107
+ return `${tools.join(", ")}, and ${lastTool} commands`;
108
+ }
109
+
110
+ /**
111
+ * @param {string} executableName
112
+ *
113
+ * @returns {boolean}
114
+ */
115
+ export function doesExecutableExistOnSystem(executableName) {
116
+ if (os.platform() === "win32") {
117
+ const result = spawnSync("where", [executableName], { stdio: "ignore" });
118
+ return result.status === 0;
119
+ } else {
120
+ const result = spawnSync("which", [executableName], { stdio: "ignore" });
121
+ return result.status === 0;
122
+ }
123
+ }
124
+
125
+ /**
126
+ * @param {string} filePath
127
+ * @param {RegExp} pattern
128
+ * @param {string} [eol]
129
+ *
130
+ * @returns {void}
131
+ */
132
+ export function removeLinesMatchingPattern(filePath, pattern, eol) {
133
+ if (!fs.existsSync(filePath)) {
134
+ return;
135
+ }
136
+
137
+ eol = eol || os.EOL;
138
+
139
+ const fileContent = fs.readFileSync(filePath, "utf-8");
140
+ const lines = fileContent.split(/\r?\n|\r|\u2028|\u2029/);
141
+ const updatedLines = lines.filter((line) => !shouldRemoveLine(line, pattern));
142
+ fs.writeFileSync(filePath, updatedLines.join(eol), "utf-8");
143
+ }
144
+
145
+ const maxLineLength = 100;
146
+
147
+ /**
148
+ * @param {string} line
149
+ * @param {RegExp} pattern
150
+ * @returns {boolean}
151
+ */
152
+ function shouldRemoveLine(line, pattern) {
153
+ const isPatternMatch = pattern.test(line);
154
+
155
+ if (!isPatternMatch) {
156
+ return false;
157
+ }
158
+
159
+ if (line.length > maxLineLength) {
160
+ // safe-chain only adds lines shorter than maxLineLength
161
+ // so if the line is longer, it must be from a different
162
+ // source and could be dangerous to remove
163
+ return false;
164
+ }
165
+
166
+ if (
167
+ line.includes("\n") ||
168
+ line.includes("\r") ||
169
+ line.includes("\u2028") ||
170
+ line.includes("\u2029")
171
+ ) {
172
+ // If the line contains newlines, something has gone wrong in splitting
173
+ // \u2028 and \u2029 are Unicode line separator characters (line and paragraph separators)
174
+ return false;
175
+ }
176
+
177
+ return true;
178
+ }
179
+
180
+ /**
181
+ * @param {string} filePath
182
+ * @param {string} line
183
+ * @param {string} [eol]
184
+ *
185
+ * @returns {void}
186
+ */
187
+ export function addLineToFile(filePath, line, eol) {
188
+ createFileIfNotExists(filePath);
189
+
190
+ eol = eol || os.EOL;
191
+
192
+ const fileContent = fs.readFileSync(filePath, "utf-8");
193
+ const updatedContent = fileContent + eol + line + eol;
194
+ fs.writeFileSync(filePath, updatedContent, "utf-8");
195
+ }
196
+
197
+ /**
198
+ * @param {string} filePath
199
+ *
200
+ * @returns {void}
201
+ */
202
+ function createFileIfNotExists(filePath) {
203
+ if (fs.existsSync(filePath)) {
204
+ return;
205
+ }
206
+
207
+ const dir = path.dirname(filePath);
208
+ if (!fs.existsSync(dir)) {
209
+ fs.mkdirSync(dir, { recursive: true });
210
+ }
211
+
212
+ fs.writeFileSync(filePath, "", "utf-8");
213
+ }
@@ -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
+ )