@aikidosec/safe-chain 1.2.2 → 1.3.0

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 (28) hide show
  1. package/README.md +24 -55
  2. package/bin/aikido-poetry.js +13 -0
  3. package/bin/safe-chain.js +2 -11
  4. package/package.json +2 -1
  5. package/src/config/cliArguments.js +1 -17
  6. package/src/config/configFile.js +1 -1
  7. package/src/config/settings.js +1 -1
  8. package/src/main.js +4 -2
  9. package/src/packagemanager/currentPackageManager.js +3 -0
  10. package/src/packagemanager/pip/runPipCommand.js +6 -3
  11. package/src/packagemanager/poetry/createPoetryPackageManager.js +77 -0
  12. package/src/registryProxy/certBundle.js +99 -13
  13. package/src/registryProxy/certUtils.js +55 -5
  14. package/src/registryProxy/getConnectTimeout.js +13 -0
  15. package/src/registryProxy/interceptors/interceptorBuilder.js +6 -0
  16. package/src/registryProxy/interceptors/pipInterceptor.js +23 -9
  17. package/src/registryProxy/registryProxy.js +15 -7
  18. package/src/registryProxy/tunnelRequestHandler.js +4 -14
  19. package/src/shell-integration/helpers.js +20 -0
  20. package/src/shell-integration/setup-ci.js +3 -9
  21. package/src/shell-integration/setup.js +4 -6
  22. package/src/shell-integration/startup-scripts/init-fish.fish +27 -0
  23. package/src/shell-integration/startup-scripts/init-posix.sh +27 -0
  24. package/src/shell-integration/startup-scripts/init-pwsh.ps1 +30 -1
  25. package/src/shell-integration/teardown.js +43 -1
  26. package/src/shell-integration/startup-scripts/include-python/init-fish.fish +0 -94
  27. package/src/shell-integration/startup-scripts/include-python/init-posix.sh +0 -81
  28. package/src/shell-integration/startup-scripts/include-python/init-pwsh.ps1 +0 -115
@@ -8,6 +8,17 @@ const ca = loadCa();
8
8
 
9
9
  const certCache = new Map();
10
10
 
11
+ /**
12
+ * @param {forge.pki.PublicKey} publicKey
13
+ * @returns {string}
14
+ */
15
+ function createKeyIdentifier(publicKey) {
16
+ return forge.pki.getPublicKeyFingerprint(publicKey, {
17
+ encoding: "binary",
18
+ md: forge.md.sha1.create(),
19
+ });
20
+ }
21
+
11
22
  export function getCaCertPath() {
12
23
  return path.join(certFolder, "ca-cert.pem");
13
24
  }
@@ -33,6 +44,7 @@ export function generateCertForHost(hostname) {
33
44
  const attrs = [{ name: "commonName", value: hostname }];
34
45
  cert.setSubject(attrs);
35
46
  cert.setIssuer(ca.certificate.subject.attributes);
47
+ const authorityKeyIdentifier = createKeyIdentifier(ca.certificate.publicKey);
36
48
  cert.setExtensions([
37
49
  {
38
50
  name: "subjectAltName",
@@ -50,14 +62,42 @@ export function generateCertForHost(hostname) {
50
62
  },
51
63
  {
52
64
  /*
53
- extKeyUsage serverAuth is required for TLS server authentication.
54
- This is especially important for Python venv environments, which use their own
55
- certificate validation logic and will reject certificates lacking the serverAuth EKU.
56
- Adding serverAuth does not impact other usages
65
+ Extended Key Usage (EKU) serverAuth extension
66
+
67
+ Needed for TLS server authentication. This extension indicates the certificate's
68
+ public key may be used for TLS WWW server authentication.
69
+ Python virtualenv environments (like pipx-installed Poetry) enforce this strictly
70
+ https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.12
57
71
  */
58
72
  name: "extKeyUsage",
59
73
  serverAuth: true,
60
74
  },
75
+ {
76
+ /*
77
+ Subject Key Identifier (SKI)
78
+
79
+ Needed for Python virtualenv SSL validation and certificate chain building.
80
+ This extension provides a means of identifying certificates containing a particular public key.
81
+ Python virtualenv environments require this for proper certificate chain validation.
82
+ System Python installations may be more lenient.
83
+ https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.2
84
+ */
85
+ name: "subjectKeyIdentifier",
86
+ subjectKeyIdentifier: createKeyIdentifier(cert.publicKey),
87
+ },
88
+ {
89
+ /*
90
+ Authority Key Identifier (AKI)
91
+
92
+ Needed for Python virtualenv SSL validation and certificate path validation.
93
+ This extension identifies the public key corresponding to the private key used to sign
94
+ this certificate. It links this certificate to its issuing CA certificate.
95
+ Without this, Python virtualenv certificate validation might fail (for instance for Poetry)
96
+ https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.1
97
+ */
98
+ name: "authorityKeyIdentifier",
99
+ keyIdentifier: authorityKeyIdentifier,
100
+ },
61
101
  ]);
62
102
  cert.sign(ca.privateKey, forge.md.sha256.create());
63
103
 
@@ -106,11 +146,13 @@ function generateCa() {
106
146
 
107
147
  const attrs = [{ name: "commonName", value: "safe-chain proxy" }];
108
148
  cert.setSubject(attrs);
109
- cert.setIssuer(attrs);
149
+ cert.setIssuer(attrs); // Self-signed: issuer === subject
150
+ const keyIdentifier = createKeyIdentifier(cert.publicKey);
110
151
  cert.setExtensions([
111
152
  {
112
153
  name: "basicConstraints",
113
154
  cA: true,
155
+ critical: true, // Marking basicConstraints as critical is required for CA certificates so clients must process it to trust the cert as a CA
114
156
  },
115
157
  {
116
158
  name: "keyUsage",
@@ -118,6 +160,14 @@ function generateCa() {
118
160
  digitalSignature: true,
119
161
  keyEncipherment: true,
120
162
  },
163
+ {
164
+ name: "subjectKeyIdentifier",
165
+ subjectKeyIdentifier: keyIdentifier,
166
+ },
167
+ {
168
+ name: "authorityKeyIdentifier",
169
+ keyIdentifier,
170
+ },
121
171
  ]);
122
172
  cert.sign(keys.privateKey, forge.md.sha256.create());
123
173
 
@@ -0,0 +1,13 @@
1
+ import { isImdsEndpoint } from "./isImdsEndpoint.js";
2
+
3
+ /**
4
+ * Returns appropriate connection timeout for a host.
5
+ * - IMDS endpoints: 3s (fail fast when outside cloud, reduce 5min delay to ~20s)
6
+ * - Other endpoints: 30s (allow for slow networks while preventing indefinite hangs)
7
+ */
8
+ export function getConnectTimeout(/** @type {string} */ host) {
9
+ if (isImdsEndpoint(host)) {
10
+ return 3000;
11
+ }
12
+ return 30000;
13
+ }
@@ -20,6 +20,12 @@ import { EventEmitter } from "events";
20
20
  * @property {(headers: NodeJS.Dict<string | string[]> | undefined) => NodeJS.Dict<string | string[]> | undefined} modifyRequestHeaders
21
21
  * @property {() => boolean} modifiesResponse
22
22
  * @property {(body: Buffer, headers: NodeJS.Dict<string | string[]> | undefined) => Buffer} modifyBody
23
+ *
24
+ * @typedef {Object} MalwareBlockedEvent
25
+ * @property {string} packageName
26
+ * @property {string} version
27
+ * @property {string} targetUrl
28
+ * @property {number} timestamp
23
29
  */
24
30
 
25
31
  /**
@@ -32,7 +32,16 @@ function buildPipInterceptor(registry) {
32
32
  reqContext.targetUrl,
33
33
  registry
34
34
  );
35
- if (await isMalwarePackage(packageName, version)) {
35
+
36
+ // Normalize underscores to hyphens for DB matching, as PyPI allows underscores in distribution names.
37
+ // Per python, packages that differ only by hyphen vs underscore are considered the same.
38
+ const hyphenName = packageName?.includes("_") ? packageName.replace(/_/g, "-") : packageName;
39
+
40
+ const isMalicious =
41
+ await isMalwarePackage(packageName, version)
42
+ || await isMalwarePackage(hyphenName, version);
43
+
44
+ if (isMalicious) {
36
45
  reqContext.blockMalware(packageName, version);
37
46
  }
38
47
  });
@@ -71,16 +80,21 @@ function parsePipPackageFromUrl(url, registry) {
71
80
  // Example wheel: https://files.pythonhosted.org/packages/xx/yy/requests-2.28.1-py3-none-any.whl
72
81
  // Example sdist: https://files.pythonhosted.org/packages/xx/yy/requests-2.28.1.tar.gz
73
82
 
74
- // Wheel (.whl)
75
- if (filename.endsWith(".whl")) {
76
- const base = filename.slice(0, -4); // remove ".whl"
83
+ // Wheel (.whl) and Poetry's preflight metadata (.whl.metadata)
84
+ // Examples:
85
+ // foo_bar-2.0.0-py3-none-any.whl
86
+ // foo_bar-2.0.0-py3-none-any.whl.metadata
87
+ const wheelExtRe = /\.whl(?:\.metadata)?$/;
88
+ const wheelExtMatch = filename.match(wheelExtRe);
89
+ if (wheelExtMatch) {
90
+ const base = filename.replace(wheelExtRe, "");
77
91
  const firstDash = base.indexOf("-");
78
92
  if (firstDash > 0) {
79
93
  const dist = base.slice(0, firstDash); // may contain underscores
80
94
  const rest = base.slice(firstDash + 1); // version + the rest of tags
81
95
  const secondDash = rest.indexOf("-");
82
96
  const rawVersion = secondDash >= 0 ? rest.slice(0, secondDash) : rest;
83
- packageName = dist; // preserve underscores
97
+ packageName = dist;
84
98
  version = rawVersion;
85
99
  // Reject "latest" as it's a placeholder, not a real version
86
100
  // When version is "latest", this signals the URL doesn't contain actual version info
@@ -92,10 +106,11 @@ function parsePipPackageFromUrl(url, registry) {
92
106
  }
93
107
  }
94
108
 
95
- // Source dist (sdist)
96
- const sdistExtMatch = filename.match(/\.(tar\.gz|zip|tar\.bz2|tar\.xz)$/i);
109
+ // Source dist (sdist) and potential metadata sidecars (e.g., .tar.gz.metadata)
110
+ const sdistExtWithMetadataRe = /\.(tar\.gz|zip|tar\.bz2|tar\.xz)(\.metadata)?$/i;
111
+ const sdistExtMatch = filename.match(sdistExtWithMetadataRe);
97
112
  if (sdistExtMatch) {
98
- const base = filename.slice(0, -sdistExtMatch[0].length);
113
+ const base = filename.replace(sdistExtWithMetadataRe, "");
99
114
  const lastDash = base.lastIndexOf("-");
100
115
  if (lastDash > 0 && lastDash < base.length - 1) {
101
116
  packageName = base.slice(0, lastDash);
@@ -109,7 +124,6 @@ function parsePipPackageFromUrl(url, registry) {
109
124
  return { packageName, version };
110
125
  }
111
126
  }
112
-
113
127
  // Unknown file type or invalid
114
128
  return { packageName: undefined, version: undefined };
115
129
  }
@@ -2,7 +2,7 @@ import * as http from "http";
2
2
  import { tunnelRequest } from "./tunnelRequestHandler.js";
3
3
  import { mitmConnect } from "./mitmRequestHandler.js";
4
4
  import { handleHttpProxyRequest } from "./plainHttpProxy.js";
5
- import { getCaCertPath } from "./certUtils.js";
5
+ import { getCombinedCaBundlePath } from "./certBundle.js";
6
6
  import { ui } from "../environment/userInteraction.js";
7
7
  import chalk from "chalk";
8
8
  import { createInterceptorForUrl } from "./interceptors/createInterceptorForEcoSystem.js";
@@ -36,10 +36,13 @@ function getSafeChainProxyEnvironmentVariables() {
36
36
  return {};
37
37
  }
38
38
 
39
+ const proxyUrl = `http://localhost:${state.port}`;
40
+ const caCertPath = getCombinedCaBundlePath();
41
+
39
42
  return {
40
- HTTPS_PROXY: `http://localhost:${state.port}`,
41
- GLOBAL_AGENT_HTTP_PROXY: `http://localhost:${state.port}`,
42
- NODE_EXTRA_CA_CERTS: getCaCertPath(),
43
+ HTTPS_PROXY: proxyUrl,
44
+ GLOBAL_AGENT_HTTP_PROXY: proxyUrl,
45
+ NODE_EXTRA_CA_CERTS: caCertPath,
43
46
  };
44
47
  }
45
48
 
@@ -136,9 +139,14 @@ function handleConnect(req, clientSocket, head) {
136
139
 
137
140
  if (interceptor) {
138
141
  // Subscribe to malware blocked events
139
- interceptor.on("malwareBlocked", (event) => {
140
- onMalwareBlocked(event.packageName, event.version, event.url);
141
- });
142
+ interceptor.on(
143
+ "malwareBlocked",
144
+ (
145
+ /** @type {import("./interceptors/interceptorBuilder.js").MalwareBlockedEvent} */ event
146
+ ) => {
147
+ onMalwareBlocked(event.packageName, event.version, event.targetUrl);
148
+ }
149
+ );
142
150
 
143
151
  mitmConnect(req, clientSocket, interceptor);
144
152
  } else {
@@ -1,9 +1,10 @@
1
1
  import * as net from "net";
2
2
  import { ui } from "../environment/userInteraction.js";
3
3
  import { isImdsEndpoint } from "./isImdsEndpoint.js";
4
+ import { getConnectTimeout } from "./getConnectTimeout.js";
4
5
 
5
6
  /** @type {string[]} */
6
- let timedoutEndpoints = [];
7
+ let timedoutImdsEndpoints = [];
7
8
 
8
9
  /**
9
10
  * @param {import("http").IncomingMessage} req
@@ -43,7 +44,7 @@ function tunnelRequestToDestination(req, clientSocket, head) {
43
44
  const { port, hostname } = new URL(`http://${req.url}`);
44
45
  const isImds = isImdsEndpoint(hostname);
45
46
 
46
- if (timedoutEndpoints.includes(hostname)) {
47
+ if (timedoutImdsEndpoints.includes(hostname)) {
47
48
  clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n");
48
49
  if (isImds) {
49
50
  ui.writeVerbose(
@@ -74,9 +75,9 @@ function tunnelRequestToDestination(req, clientSocket, head) {
74
75
  serverSocket.setTimeout(connectTimeout);
75
76
 
76
77
  serverSocket.on("timeout", () => {
77
- timedoutEndpoints.push(hostname);
78
78
  // Suppress error logging for IMDS endpoints - timeouts are expected when not in cloud
79
79
  if (isImds) {
80
+ timedoutImdsEndpoints.push(hostname);
80
81
  ui.writeVerbose(
81
82
  `Safe-chain: connect to ${hostname}:${
82
83
  port || 443
@@ -196,14 +197,3 @@ function tunnelRequestViaProxy(req, clientSocket, head, proxyUrl) {
196
197
  });
197
198
  }
198
199
 
199
- /**
200
- * Returns appropriate connection timeout for a host.
201
- * - IMDS endpoints: 3s (fail fast when outside cloud, reduce 5min delay to ~20s)
202
- * - Other endpoints: 30s (allow for slow networks while preventing indefinite hangs)
203
- */
204
- function getConnectTimeout(/** @type {string} */ host) {
205
- if (isImdsEndpoint(host)) {
206
- return 3000;
207
- }
208
- return 30000;
209
- }
@@ -76,6 +76,12 @@ export const knownAikidoTools = [
76
76
  ecoSystem: ECOSYSTEM_PY,
77
77
  internalPackageManagerName: "pip",
78
78
  },
79
+ {
80
+ tool: "poetry",
81
+ aikidoCommand: "aikido-poetry",
82
+ ecoSystem: ECOSYSTEM_PY,
83
+ internalPackageManagerName: "poetry",
84
+ },
79
85
  {
80
86
  tool: "python",
81
87
  aikidoCommand: "aikido-python",
@@ -107,6 +113,20 @@ export function getPackageManagerList() {
107
113
  return `${tools.join(", ")}, and ${lastTool} commands`;
108
114
  }
109
115
 
116
+ /**
117
+ * @returns {string}
118
+ */
119
+ export function getShimsDir() {
120
+ return path.join(os.homedir(), ".safe-chain", "shims");
121
+ }
122
+
123
+ /**
124
+ * @returns {string}
125
+ */
126
+ export function getScriptsDir() {
127
+ return path.join(os.homedir(), ".safe-chain", "scripts");
128
+ }
129
+
110
130
  /**
111
131
  * @param {string} executableName
112
132
  *
@@ -1,12 +1,10 @@
1
1
  import chalk from "chalk";
2
2
  import { ui } from "../environment/userInteraction.js";
3
- import { getPackageManagerList, knownAikidoTools } from "./helpers.js";
3
+ import { getPackageManagerList, knownAikidoTools, getShimsDir } from "./helpers.js";
4
4
  import fs from "fs";
5
5
  import os from "os";
6
6
  import path from "path";
7
7
  import { fileURLToPath } from "url";
8
- import { includePython } from "../config/cliArguments.js";
9
- import { ECOSYSTEM_PY } from "../config/settings.js";
10
8
 
11
9
  /** @type {string} */
12
10
  // This checks the current file's dirname in a way that's compatible with:
@@ -32,7 +30,7 @@ export async function setupCi() {
32
30
  );
33
31
  ui.emptyLine();
34
32
 
35
- const shimsDir = path.join(os.homedir(), ".safe-chain", "shims");
33
+ const shimsDir = getShimsDir();
36
34
  const binDir = path.join(os.homedir(), ".safe-chain", "bin");
37
35
  // Create the shims directory if it doesn't exist
38
36
  if (!fs.existsSync(shimsDir)) {
@@ -162,9 +160,5 @@ function modifyPathForCi(shimsDir, binDir) {
162
160
  }
163
161
 
164
162
  function getToolsToSetup() {
165
- if (includePython()) {
166
- return knownAikidoTools;
167
- } else {
168
- return knownAikidoTools.filter((tool) => tool.ecoSystem !== ECOSYSTEM_PY);
169
- }
163
+ return knownAikidoTools;
170
164
  }
@@ -1,11 +1,9 @@
1
1
  import chalk from "chalk";
2
2
  import { ui } from "../environment/userInteraction.js";
3
3
  import { detectShells } from "./shellDetection.js";
4
- import { knownAikidoTools, getPackageManagerList } from "./helpers.js";
4
+ import { knownAikidoTools, getPackageManagerList, getScriptsDir } from "./helpers.js";
5
5
  import fs from "fs";
6
- import os from "os";
7
6
  import path from "path";
8
- import { includePython } from "../config/cliArguments.js";
9
7
  import { fileURLToPath } from "url";
10
8
 
11
9
  /** @type {string} */
@@ -107,10 +105,10 @@ function setupShell(shell) {
107
105
 
108
106
  function copyStartupFiles() {
109
107
  const startupFiles = ["init-posix.sh", "init-pwsh.ps1", "init-fish.fish"];
108
+ const targetDir = getScriptsDir();
110
109
 
111
110
  for (const file of startupFiles) {
112
- const targetDir = path.join(os.homedir(), ".safe-chain", "scripts");
113
- const targetPath = path.join(os.homedir(), ".safe-chain", "scripts", file);
111
+ const targetPath = path.join(targetDir, file);
114
112
 
115
113
  if (!fs.existsSync(targetDir)) {
116
114
  fs.mkdirSync(targetDir, { recursive: true });
@@ -119,7 +117,7 @@ function copyStartupFiles() {
119
117
  // Use absolute path for source
120
118
  const sourcePath = path.join(
121
119
  dirname,
122
- includePython() ? "startup-scripts/include-python" : "startup-scripts",
120
+ "startup-scripts",
123
121
  file
124
122
  );
125
123
  fs.copyFileSync(sourcePath, targetPath);
@@ -39,6 +39,33 @@ function npm
39
39
  wrapSafeChainCommand "npm" $argv
40
40
  end
41
41
 
42
+
43
+ function pip
44
+ wrapSafeChainCommand "pip" $argv
45
+ end
46
+
47
+ function pip3
48
+ wrapSafeChainCommand "pip3" $argv
49
+ end
50
+
51
+ function uv
52
+ wrapSafeChainCommand "uv" $argv
53
+ end
54
+
55
+ function poetry
56
+ wrapSafeChainCommand "poetry" $argv
57
+ end
58
+
59
+ # `python -m pip`, `python -m pip3`.
60
+ function python
61
+ wrapSafeChainCommand "python" $argv
62
+ end
63
+
64
+ # `python3 -m pip`, `python3 -m pip3'.
65
+ function python3
66
+ wrapSafeChainCommand "python3" $argv
67
+ end
68
+
42
69
  function printSafeChainWarning
43
70
  set original_cmd $argv[1]
44
71
 
@@ -35,6 +35,33 @@ function npm() {
35
35
  wrapSafeChainCommand "npm" "$@"
36
36
  }
37
37
 
38
+
39
+ function pip() {
40
+ wrapSafeChainCommand "pip" "$@"
41
+ }
42
+
43
+ function pip3() {
44
+ wrapSafeChainCommand "pip3" "$@"
45
+ }
46
+
47
+ function uv() {
48
+ wrapSafeChainCommand "uv" "$@"
49
+ }
50
+
51
+ function poetry() {
52
+ wrapSafeChainCommand "poetry" "$@"
53
+ }
54
+
55
+ # `python -m pip`, `python -m pip3`.
56
+ function python() {
57
+ wrapSafeChainCommand "python" "$@"
58
+ }
59
+
60
+ # `python3 -m pip`, `python3 -m pip3'.
61
+ function python3() {
62
+ wrapSafeChainCommand "python3" "$@"
63
+ }
64
+
38
65
  function printSafeChainWarning() {
39
66
  # \033[43;30m is used to set the background color to yellow and text color to black
40
67
  # \033[0m is used to reset the text formatting
@@ -1,5 +1,7 @@
1
1
  # Use cross-platform path separator (: on Unix, ; on Windows)
2
- $pathSeparator = if ($IsWindows) { ';' } else { ':' }
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 { ':' }
3
5
  $safeChainBin = Join-Path (Join-Path $HOME '.safe-chain') 'bin'
4
6
  $env:PATH = "$env:PATH$pathSeparator$safeChainBin"
5
7
 
@@ -38,6 +40,33 @@ function npm {
38
40
  Invoke-WrappedCommand "npm" $args
39
41
  }
40
42
 
43
+ function pip {
44
+ Invoke-WrappedCommand "pip" $args
45
+ }
46
+
47
+ function pip3 {
48
+ Invoke-WrappedCommand "pip3" $args
49
+ }
50
+
51
+ function uv {
52
+ Invoke-WrappedCommand "uv" $args
53
+ }
54
+
55
+ function poetry {
56
+ Invoke-WrappedCommand "poetry" $args
57
+ }
58
+
59
+ # `python -m pip`, `python -m pip3`.
60
+ function python {
61
+ Invoke-WrappedCommand 'python' $args
62
+ }
63
+
64
+ # `python3 -m pip`, `python3 -m pip3'.
65
+ function python3 {
66
+ Invoke-WrappedCommand 'python3' $args
67
+ }
68
+
69
+
41
70
  function Write-SafeChainWarning {
42
71
  param([string]$Command)
43
72
 
@@ -1,7 +1,8 @@
1
1
  import chalk from "chalk";
2
2
  import { ui } from "../environment/userInteraction.js";
3
3
  import { detectShells } from "./shellDetection.js";
4
- import { knownAikidoTools, getPackageManagerList } from "./helpers.js";
4
+ import { knownAikidoTools, getPackageManagerList, getShimsDir, getScriptsDir } from "./helpers.js";
5
+ import fs from "fs";
5
6
 
6
7
  /**
7
8
  * @returns {Promise<void>}
@@ -62,3 +63,44 @@ export async function teardown() {
62
63
  return;
63
64
  }
64
65
  }
66
+
67
+ /**
68
+ * Removes directories created by setup-ci and setup commands
69
+ * @returns {Promise<void>}
70
+ */
71
+ export async function teardownDirectories() {
72
+ const shimsDir = getShimsDir();
73
+ const scriptsDir = getScriptsDir();
74
+
75
+ // Remove CI shims directory
76
+ if (fs.existsSync(shimsDir)) {
77
+ try {
78
+ fs.rmSync(shimsDir, { recursive: true, force: true });
79
+ ui.writeInformation(
80
+ `${chalk.bold("- CI Shims:")} ${chalk.green("Removed successfully")}`
81
+ );
82
+ } catch (/** @type {any} */ error) {
83
+ ui.writeError(
84
+ `${chalk.bold("- CI Shims:")} ${chalk.red(
85
+ "Failed to remove"
86
+ )}. Error: ${error.message}`
87
+ );
88
+ }
89
+ }
90
+
91
+ // Remove scripts directory
92
+ if (fs.existsSync(scriptsDir)) {
93
+ try {
94
+ fs.rmSync(scriptsDir, { recursive: true, force: true });
95
+ ui.writeInformation(
96
+ `${chalk.bold("- Scripts:")} ${chalk.green("Removed successfully")}`
97
+ );
98
+ } catch (/** @type {any} */ error) {
99
+ ui.writeError(
100
+ `${chalk.bold("- Scripts:")} ${chalk.red(
101
+ "Failed to remove"
102
+ )}. Error: ${error.message}`
103
+ );
104
+ }
105
+ }
106
+ }
@@ -1,94 +0,0 @@
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
-
43
- function pip
44
- wrapSafeChainCommand "pip" $argv
45
- end
46
-
47
- function pip3
48
- wrapSafeChainCommand "pip3" $argv
49
- end
50
-
51
- function uv
52
- wrapSafeChainCommand "uv" $argv
53
- end
54
-
55
- # `python -m pip`, `python -m pip3`.
56
- function python
57
- wrapSafeChainCommand "python" $argv
58
- end
59
-
60
- # `python3 -m pip`, `python3 -m pip3'.
61
- function python3
62
- wrapSafeChainCommand "python3" $argv
63
- end
64
-
65
- function printSafeChainWarning
66
- set original_cmd $argv[1]
67
-
68
- # Fish equivalent of ANSI color codes: yellow background, black text for "Warning:"
69
- set_color -b yellow black
70
- printf "Warning:"
71
- set_color normal
72
- printf " safe-chain is not available to protect you from installing malware. %s will run without it.\n" $original_cmd
73
-
74
- # Cyan text for the install command
75
- printf "Install safe-chain by using "
76
- set_color cyan
77
- printf "npm install -g @aikidosec/safe-chain"
78
- set_color normal
79
- printf ".\n"
80
- end
81
-
82
- function wrapSafeChainCommand
83
- set original_cmd $argv[1]
84
- set cmd_args $argv[2..-1]
85
-
86
- if type -q safe-chain
87
- # If the safe-chain command is available, just run it with the provided arguments
88
- safe-chain $original_cmd $cmd_args
89
- else
90
- # If the safe-chain command is not available, print a warning and run the original command
91
- printSafeChainWarning $original_cmd
92
- command $original_cmd $cmd_args
93
- end
94
- end