@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.
- package/README.md +24 -55
- package/bin/aikido-poetry.js +13 -0
- package/bin/safe-chain.js +2 -11
- package/package.json +2 -1
- package/src/config/cliArguments.js +1 -17
- package/src/config/configFile.js +1 -1
- package/src/config/settings.js +1 -1
- package/src/main.js +4 -2
- package/src/packagemanager/currentPackageManager.js +3 -0
- package/src/packagemanager/pip/runPipCommand.js +6 -3
- package/src/packagemanager/poetry/createPoetryPackageManager.js +77 -0
- package/src/registryProxy/certBundle.js +99 -13
- package/src/registryProxy/certUtils.js +55 -5
- package/src/registryProxy/getConnectTimeout.js +13 -0
- package/src/registryProxy/interceptors/interceptorBuilder.js +6 -0
- package/src/registryProxy/interceptors/pipInterceptor.js +23 -9
- package/src/registryProxy/registryProxy.js +15 -7
- package/src/registryProxy/tunnelRequestHandler.js +4 -14
- package/src/shell-integration/helpers.js +20 -0
- package/src/shell-integration/setup-ci.js +3 -9
- package/src/shell-integration/setup.js +4 -6
- package/src/shell-integration/startup-scripts/init-fish.fish +27 -0
- package/src/shell-integration/startup-scripts/init-posix.sh +27 -0
- package/src/shell-integration/startup-scripts/init-pwsh.ps1 +30 -1
- package/src/shell-integration/teardown.js +43 -1
- package/src/shell-integration/startup-scripts/include-python/init-fish.fish +0 -94
- package/src/shell-integration/startup-scripts/include-python/init-posix.sh +0 -81
- 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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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;
|
|
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
|
|
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.
|
|
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 {
|
|
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:
|
|
41
|
-
GLOBAL_AGENT_HTTP_PROXY:
|
|
42
|
-
NODE_EXTRA_CA_CERTS:
|
|
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(
|
|
140
|
-
|
|
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
|
|
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 (
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
$
|
|
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
|