@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.
- package/LICENSE +674 -0
- package/README.md +257 -0
- package/bin/aikido-bun.js +14 -0
- package/bin/aikido-bunx.js +14 -0
- package/bin/aikido-npm.js +14 -0
- package/bin/aikido-npx.js +14 -0
- package/bin/aikido-pip.js +20 -0
- package/bin/aikido-pip3.js +21 -0
- package/bin/aikido-pnpm.js +14 -0
- package/bin/aikido-pnpx.js +14 -0
- package/bin/aikido-python.js +30 -0
- package/bin/aikido-python3.js +30 -0
- package/bin/aikido-uv.js +16 -0
- package/bin/aikido-yarn.js +14 -0
- package/bin/safe-chain.js +190 -0
- package/docs/banner.svg +151 -0
- package/docs/npm-to-binary-migration.md +89 -0
- package/docs/safe-package-manager-demo.gif +0 -0
- package/docs/safe-package-manager-demo.png +0 -0
- package/docs/shell-integration.md +149 -0
- package/package.json +68 -0
- package/src/api/aikido.js +54 -0
- package/src/api/npmApi.js +71 -0
- package/src/config/cliArguments.js +138 -0
- package/src/config/configFile.js +192 -0
- package/src/config/environmentVariables.js +7 -0
- package/src/config/settings.js +100 -0
- package/src/environment/environment.js +14 -0
- package/src/environment/userInteraction.js +122 -0
- package/src/main.js +104 -0
- package/src/packagemanager/_shared/matchesCommand.js +18 -0
- package/src/packagemanager/bun/createBunPackageManager.js +53 -0
- package/src/packagemanager/currentPackageManager.js +72 -0
- package/src/packagemanager/npm/createPackageManager.js +72 -0
- package/src/packagemanager/npm/dependencyScanner/commandArgumentScanner.js +74 -0
- package/src/packagemanager/npm/dependencyScanner/nullScanner.js +9 -0
- package/src/packagemanager/npm/parsing/parsePackagesFromInstallArgs.js +144 -0
- package/src/packagemanager/npm/runNpmCommand.js +25 -0
- package/src/packagemanager/npm/utils/abbrevs-generated.js +359 -0
- package/src/packagemanager/npm/utils/cmd-list.js +174 -0
- package/src/packagemanager/npm/utils/npmCommands.js +34 -0
- package/src/packagemanager/npx/createPackageManager.js +15 -0
- package/src/packagemanager/npx/dependencyScanner/commandArgumentScanner.js +43 -0
- package/src/packagemanager/npx/parsing/parsePackagesFromArguments.js +130 -0
- package/src/packagemanager/npx/runNpxCommand.js +25 -0
- package/src/packagemanager/pip/createPackageManager.js +21 -0
- package/src/packagemanager/pip/pipSettings.js +30 -0
- package/src/packagemanager/pip/runPipCommand.js +175 -0
- package/src/packagemanager/pnpm/createPackageManager.js +57 -0
- package/src/packagemanager/pnpm/dependencyScanner/commandArgumentScanner.js +35 -0
- package/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.js +109 -0
- package/src/packagemanager/pnpm/runPnpmCommand.js +36 -0
- package/src/packagemanager/uv/createUvPackageManager.js +18 -0
- package/src/packagemanager/uv/runUvCommand.js +71 -0
- package/src/packagemanager/yarn/createPackageManager.js +41 -0
- package/src/packagemanager/yarn/dependencyScanner/commandArgumentScanner.js +35 -0
- package/src/packagemanager/yarn/parsing/parsePackagesFromArguments.js +128 -0
- package/src/packagemanager/yarn/runYarnCommand.js +41 -0
- package/src/registryProxy/certBundle.js +95 -0
- package/src/registryProxy/certUtils.js +128 -0
- package/src/registryProxy/http-utils.js +17 -0
- package/src/registryProxy/interceptors/createInterceptorForEcoSystem.js +25 -0
- package/src/registryProxy/interceptors/interceptorBuilder.js +140 -0
- package/src/registryProxy/interceptors/npm/modifyNpmInfo.js +177 -0
- package/src/registryProxy/interceptors/npm/npmInterceptor.js +47 -0
- package/src/registryProxy/interceptors/npm/parseNpmPackageUrl.js +43 -0
- package/src/registryProxy/interceptors/pipInterceptor.js +115 -0
- package/src/registryProxy/mitmRequestHandler.js +231 -0
- package/src/registryProxy/plainHttpProxy.js +95 -0
- package/src/registryProxy/registryProxy.js +184 -0
- package/src/registryProxy/tunnelRequestHandler.js +180 -0
- package/src/scanning/audit/index.js +129 -0
- package/src/scanning/index.js +82 -0
- package/src/scanning/malwareDatabase.js +131 -0
- package/src/shell-integration/helpers.js +213 -0
- package/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh +22 -0
- package/src/shell-integration/path-wrappers/templates/windows-wrapper.template.cmd +24 -0
- package/src/shell-integration/setup-ci.js +170 -0
- package/src/shell-integration/setup.js +127 -0
- package/src/shell-integration/shellDetection.js +37 -0
- package/src/shell-integration/startup-scripts/include-python/init-fish.fish +94 -0
- package/src/shell-integration/startup-scripts/include-python/init-posix.sh +81 -0
- package/src/shell-integration/startup-scripts/include-python/init-pwsh.ps1 +115 -0
- package/src/shell-integration/startup-scripts/init-fish.fish +71 -0
- package/src/shell-integration/startup-scripts/init-posix.sh +58 -0
- package/src/shell-integration/startup-scripts/init-pwsh.ps1 +92 -0
- package/src/shell-integration/supported-shells/bash.js +134 -0
- package/src/shell-integration/supported-shells/fish.js +77 -0
- package/src/shell-integration/supported-shells/powershell.js +73 -0
- package/src/shell-integration/supported-shells/windowsPowershell.js +73 -0
- package/src/shell-integration/supported-shells/zsh.js +74 -0
- package/src/shell-integration/teardown.js +64 -0
- package/src/utils/safeSpawn.js +137 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {string[]} args
|
|
3
|
+
*
|
|
4
|
+
* @returns {{name: string, version: string}[]}
|
|
5
|
+
*/
|
|
6
|
+
export function parsePackagesFromArguments(args) {
|
|
7
|
+
let defaultTag = "latest";
|
|
8
|
+
|
|
9
|
+
for (let i = 0; i < args.length; i++) {
|
|
10
|
+
const arg = args[i];
|
|
11
|
+
const option = getOption(arg);
|
|
12
|
+
|
|
13
|
+
if (option) {
|
|
14
|
+
// If the option has a parameter, skip the next argument as well
|
|
15
|
+
i += option.numberOfParameters;
|
|
16
|
+
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const packageDetails = parsePackagename(arg, defaultTag);
|
|
21
|
+
if (packageDetails) {
|
|
22
|
+
return [packageDetails];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {string} arg
|
|
31
|
+
* @returns {{name: string, numberOfParameters: number} | undefined}
|
|
32
|
+
*/
|
|
33
|
+
function getOption(arg) {
|
|
34
|
+
if (isOptionWithParameter(arg)) {
|
|
35
|
+
return {
|
|
36
|
+
name: arg,
|
|
37
|
+
numberOfParameters: 1,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Arguments starting with "-" or "--" are considered options
|
|
42
|
+
// except for "--package=" which contains the package name
|
|
43
|
+
if (arg.startsWith("-") && !arg.startsWith("--package=")) {
|
|
44
|
+
return {
|
|
45
|
+
name: arg,
|
|
46
|
+
numberOfParameters: 0,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {string} arg
|
|
55
|
+
* @returns {boolean}
|
|
56
|
+
*/
|
|
57
|
+
function isOptionWithParameter(arg) {
|
|
58
|
+
const optionsWithParameters = [
|
|
59
|
+
"--access",
|
|
60
|
+
"--auth-type",
|
|
61
|
+
"--cache",
|
|
62
|
+
"--fetch-retries",
|
|
63
|
+
"--fetch-retry-mintimeout",
|
|
64
|
+
"--fetch-retry-maxtimeout",
|
|
65
|
+
"--fetch-retry-factor",
|
|
66
|
+
"--fetch-timeout",
|
|
67
|
+
"--https-proxy",
|
|
68
|
+
"--include",
|
|
69
|
+
"--location",
|
|
70
|
+
"--lockfile-version",
|
|
71
|
+
"--loglevel",
|
|
72
|
+
"--omit",
|
|
73
|
+
"--proxy",
|
|
74
|
+
"--registry",
|
|
75
|
+
"--replace-registry-host",
|
|
76
|
+
"--tag",
|
|
77
|
+
"--user-config",
|
|
78
|
+
"--workspace",
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
return optionsWithParameters.includes(arg);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param {string} arg
|
|
86
|
+
* @param {string} defaultTag
|
|
87
|
+
* @returns {{name: string, version: string}}
|
|
88
|
+
*/
|
|
89
|
+
function parsePackagename(arg, defaultTag) {
|
|
90
|
+
// format can be --package=name@version
|
|
91
|
+
// in that case, we need to remove the --package= part
|
|
92
|
+
if (arg.startsWith("--package=")) {
|
|
93
|
+
arg = arg.slice(10);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
arg = removeAlias(arg);
|
|
97
|
+
|
|
98
|
+
// Split at the last "@" to separate the package name and version
|
|
99
|
+
const lastAtIndex = arg.lastIndexOf("@");
|
|
100
|
+
|
|
101
|
+
let name, version;
|
|
102
|
+
// The index of the last "@" should be greater than 0
|
|
103
|
+
// If the index is 0, it means the package name starts with "@" (eg: "@vercel/otel")
|
|
104
|
+
if (lastAtIndex > 0) {
|
|
105
|
+
name = arg.slice(0, lastAtIndex);
|
|
106
|
+
version = arg.slice(lastAtIndex + 1);
|
|
107
|
+
} else {
|
|
108
|
+
name = arg;
|
|
109
|
+
version = defaultTag; // No tag specified (eg: "http-server"), use the default tag
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
name,
|
|
114
|
+
version,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* @param {string} arg
|
|
120
|
+
* @returns {string}
|
|
121
|
+
*/
|
|
122
|
+
function removeAlias(arg) {
|
|
123
|
+
// removes the alias.
|
|
124
|
+
// Eg.: server@npm:http-server@latest becomes http-server@latest
|
|
125
|
+
const aliasIndex = arg.indexOf("@npm:");
|
|
126
|
+
if (aliasIndex !== -1) {
|
|
127
|
+
return arg.slice(aliasIndex + 5);
|
|
128
|
+
}
|
|
129
|
+
return arg;
|
|
130
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ui } from "../../environment/userInteraction.js";
|
|
2
|
+
import { safeSpawn } from "../../utils/safeSpawn.js";
|
|
3
|
+
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {string[]} args
|
|
7
|
+
*
|
|
8
|
+
* @returns {Promise<{status: number}>}
|
|
9
|
+
*/
|
|
10
|
+
export async function runNpx(args) {
|
|
11
|
+
try {
|
|
12
|
+
const result = await safeSpawn("npx", args, {
|
|
13
|
+
stdio: "inherit",
|
|
14
|
+
env: mergeSafeChainProxyEnvironmentVariables(process.env),
|
|
15
|
+
});
|
|
16
|
+
return { status: result.status };
|
|
17
|
+
} catch (/** @type any */ error) {
|
|
18
|
+
if (error.status) {
|
|
19
|
+
return { status: error.status };
|
|
20
|
+
} else {
|
|
21
|
+
ui.writeError("Error executing command:", error.message);
|
|
22
|
+
return { status: 1 };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { runPip } from "./runPipCommand.js";
|
|
2
|
+
import { getCurrentPipInvocation } from "./pipSettings.js";
|
|
3
|
+
/**
|
|
4
|
+
* @returns {import("../currentPackageManager.js").PackageManager}
|
|
5
|
+
*/
|
|
6
|
+
export function createPipPackageManager() {
|
|
7
|
+
return {
|
|
8
|
+
/**
|
|
9
|
+
* @param {string[]} args
|
|
10
|
+
*/
|
|
11
|
+
runCommand: (args) => {
|
|
12
|
+
const invocation = getCurrentPipInvocation();
|
|
13
|
+
const fullArgs = [...invocation.args, ...args];
|
|
14
|
+
return runPip(invocation.command, fullArgs);
|
|
15
|
+
},
|
|
16
|
+
// For pip, rely solely on MITM proxy to detect/deny downloads from known registries.
|
|
17
|
+
isSupportedCommand: () => false,
|
|
18
|
+
getDependencyUpdatesForCommand: () => [],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const PIP_PACKAGE_MANAGER = "pip";
|
|
2
|
+
|
|
3
|
+
// All supported python/pip invocations for Safe Chain interception
|
|
4
|
+
export const PIP_INVOCATIONS = {
|
|
5
|
+
PIP: { command: "pip", args: [] },
|
|
6
|
+
PIP3: { command: "pip3", args: [] },
|
|
7
|
+
PY_PIP: { command: "python", args: ["-m", "pip"] },
|
|
8
|
+
PY3_PIP: { command: "python3", args: ["-m", "pip"] },
|
|
9
|
+
PY_PIP3: { command: "python", args: ["-m", "pip3"] },
|
|
10
|
+
PY3_PIP3: { command: "python3", args: ["-m", "pip3"] }
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @type {{ command: string, args: string[] }}
|
|
15
|
+
*/
|
|
16
|
+
let currentInvocation = PIP_INVOCATIONS.PY3_PIP; // Default to python3 -m pip
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {{ command: string, args: string[] }} invocation
|
|
20
|
+
*/
|
|
21
|
+
export function setCurrentPipInvocation(invocation) {
|
|
22
|
+
currentInvocation = invocation;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @returns {{ command: string, args: string[] }}
|
|
27
|
+
*/
|
|
28
|
+
export function getCurrentPipInvocation() {
|
|
29
|
+
return currentInvocation;
|
|
30
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { ui } from "../../environment/userInteraction.js";
|
|
2
|
+
import { safeSpawn } from "../../utils/safeSpawn.js";
|
|
3
|
+
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
|
|
4
|
+
import { getCombinedCaBundlePath } from "../../registryProxy/certBundle.js";
|
|
5
|
+
import fs from "node:fs/promises";
|
|
6
|
+
import fsSync from "node:fs";
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import ini from "ini";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Sets fallback CA bundle environment variables used by Python libraries.
|
|
13
|
+
* These are applied in addition to the PIP_CONFIG_FILE to ensure all Python
|
|
14
|
+
* network libraries respect the combined CA bundle, even if they don't read pip's config.
|
|
15
|
+
*
|
|
16
|
+
* @param {NodeJS.ProcessEnv} env - Environment object to modify
|
|
17
|
+
* @param {string} combinedCaPath - Path to the combined CA bundle
|
|
18
|
+
*/
|
|
19
|
+
function setFallbackCaBundleEnvironmentVariables(env, combinedCaPath) {
|
|
20
|
+
// REQUESTS_CA_BUNDLE: Used by the popular 'requests' library
|
|
21
|
+
if (env.REQUESTS_CA_BUNDLE) {
|
|
22
|
+
ui.writeWarning("Safe-chain: User defined REQUESTS_CA_BUNDLE found in environment. It will be overwritten.");
|
|
23
|
+
}
|
|
24
|
+
env.REQUESTS_CA_BUNDLE = combinedCaPath;
|
|
25
|
+
|
|
26
|
+
// SSL_CERT_FILE: Used by some Python SSL libraries and urllib
|
|
27
|
+
if (env.SSL_CERT_FILE) {
|
|
28
|
+
ui.writeWarning("Safe-chain: User defined SSL_CERT_FILE found in environment. It will be overwritten.");
|
|
29
|
+
}
|
|
30
|
+
env.SSL_CERT_FILE = combinedCaPath;
|
|
31
|
+
|
|
32
|
+
// PIP_CERT: Pip's own environment variable for certificate verification
|
|
33
|
+
if (env.PIP_CERT) {
|
|
34
|
+
ui.writeWarning("Safe-chain: User defined PIP_CERT found in environment. It will be overwritten.");
|
|
35
|
+
}
|
|
36
|
+
env.PIP_CERT = combinedCaPath;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Runs a pip command with safe-chain's certificate bundle and proxy configuration.
|
|
41
|
+
*
|
|
42
|
+
* Creates a temporary pip config file to configure:
|
|
43
|
+
* - Cert bundle for HTTPS verification
|
|
44
|
+
* - Proxy settings
|
|
45
|
+
*
|
|
46
|
+
* If the user has an existing PIP_CONFIG_FILE, a new temporary config is created that merges
|
|
47
|
+
* their settings with safe-chain's, leaving the original file unchanged.
|
|
48
|
+
*
|
|
49
|
+
* Special handling for commands that modify config/cache/state: PIP_CONFIG_FILE is NOT overridden to allow
|
|
50
|
+
* users to read/write persistent config. Only CA environment variables are set for these commands.
|
|
51
|
+
*
|
|
52
|
+
* @param {string} command - The pip command to execute (e.g., 'pip3')
|
|
53
|
+
* @param {string[]} args - Command line arguments to pass to pip
|
|
54
|
+
* @returns {Promise<{status: number}>} Exit status of the pip command
|
|
55
|
+
*/
|
|
56
|
+
export async function runPip(command, args) {
|
|
57
|
+
try {
|
|
58
|
+
const env = mergeSafeChainProxyEnvironmentVariables(process.env);
|
|
59
|
+
|
|
60
|
+
// Always provide Python with a complete CA bundle (Safe Chain CA + Mozilla + Node built-in roots)
|
|
61
|
+
// so that any network request made by pip, including those outside explicit CLI args,
|
|
62
|
+
// validates correctly under both MITM'd and tunneled HTTPS.
|
|
63
|
+
const combinedCaPath = getCombinedCaBundlePath();
|
|
64
|
+
|
|
65
|
+
// Commands that need access to persistent config/cache/state files
|
|
66
|
+
// These should not have PIP_CONFIG_FILE overridden as it would prevent them from
|
|
67
|
+
// reading/writing to the user's actual pip configuration and cache directories
|
|
68
|
+
const configRelatedCommands = ['config', 'cache', 'debug', 'completion'];
|
|
69
|
+
const isConfigRelatedCommand = args.length > 0 && configRelatedCommands.includes(args[0]);
|
|
70
|
+
|
|
71
|
+
// https://pip.pypa.io/en/stable/topics/https-certificates/ explains that the 'cert' param (which we're providing via INI file)
|
|
72
|
+
// will tell pip to use the provided CA bundle for HTTPS verification.
|
|
73
|
+
|
|
74
|
+
// Proxy settings: GLOBAL_AGENT_HTTP_PROXY is our safe-chain proxy (if active),
|
|
75
|
+
// otherwise fall back to user-defined HTTPS_PROXY or HTTP_PROXY environment variables
|
|
76
|
+
const proxy = env.GLOBAL_AGENT_HTTP_PROXY || env.HTTPS_PROXY || env.HTTP_PROXY || '';
|
|
77
|
+
|
|
78
|
+
const tmpDir = os.tmpdir();
|
|
79
|
+
const pipConfigPath = path.join(tmpDir, `safe-chain-pip-${Date.now()}.ini`);
|
|
80
|
+
let cleanupConfigPath = null; // Track temp file for cleanup
|
|
81
|
+
|
|
82
|
+
if (isConfigRelatedCommand) {
|
|
83
|
+
ui.writeVerbose(`Safe-chain: Skipping PIP_CONFIG_FILE override for 'pip ${args[0]}' command to allow persistent config/cache access.`);
|
|
84
|
+
|
|
85
|
+
// Still set the fallback CA bundle environment variables to avoid edge cases where a
|
|
86
|
+
// plugin or extension triggers a network call during config introspection
|
|
87
|
+
// This can do no harm
|
|
88
|
+
setFallbackCaBundleEnvironmentVariables(env, combinedCaPath);
|
|
89
|
+
|
|
90
|
+
const result = await safeSpawn(command, args, {
|
|
91
|
+
stdio: "inherit",
|
|
92
|
+
env,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return { status: result.status };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Note: Setting PIP_CONFIG_FILE overrides all pip config levels (Global/User/Site) per pip's loading order
|
|
99
|
+
if (!env.PIP_CONFIG_FILE) {
|
|
100
|
+
/** @type {{ global: { cert: string, proxy?: string } }} */
|
|
101
|
+
const configObj = { global: { cert: combinedCaPath } };
|
|
102
|
+
if (proxy) {
|
|
103
|
+
configObj.global.proxy = proxy;
|
|
104
|
+
}
|
|
105
|
+
const pipConfig = ini.stringify(configObj);
|
|
106
|
+
await fs.writeFile(pipConfigPath, pipConfig);
|
|
107
|
+
env.PIP_CONFIG_FILE = pipConfigPath;
|
|
108
|
+
cleanupConfigPath = pipConfigPath;
|
|
109
|
+
|
|
110
|
+
} else if (fsSync.existsSync(env.PIP_CONFIG_FILE)) {
|
|
111
|
+
ui.writeVerbose("Safe-chain: Merging user provided PIP_CONFIG_FILE with safe-chain certificate and proxy settings.");
|
|
112
|
+
const userConfig = env.PIP_CONFIG_FILE;
|
|
113
|
+
|
|
114
|
+
// Read the existing config without modifying it
|
|
115
|
+
let content = await fs.readFile(userConfig, "utf-8");
|
|
116
|
+
const parsed = ini.parse(content);
|
|
117
|
+
|
|
118
|
+
// Ensure [global] section exists
|
|
119
|
+
parsed.global = parsed.global || {};
|
|
120
|
+
|
|
121
|
+
// Cert
|
|
122
|
+
if (typeof parsed.global.cert !== "undefined") {
|
|
123
|
+
ui.writeWarning("Safe-chain: User defined cert found in PIP_CONFIG_FILE. It will be overwritten in the temporary config.");
|
|
124
|
+
}
|
|
125
|
+
parsed.global.cert = combinedCaPath;
|
|
126
|
+
|
|
127
|
+
// Proxy
|
|
128
|
+
if (typeof parsed.global.proxy !== "undefined") {
|
|
129
|
+
ui.writeWarning("Safe-chain: User defined proxy found in PIP_CONFIG_FILE. It will be overwritten in the temporary config.");
|
|
130
|
+
}
|
|
131
|
+
if (proxy) {
|
|
132
|
+
parsed.global.proxy = proxy;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const updated = ini.stringify(parsed);
|
|
136
|
+
|
|
137
|
+
// Save to a new temp file to avoid overwriting user's original config
|
|
138
|
+
await fs.writeFile(pipConfigPath, updated, "utf-8");
|
|
139
|
+
env.PIP_CONFIG_FILE = pipConfigPath;
|
|
140
|
+
cleanupConfigPath = pipConfigPath;
|
|
141
|
+
|
|
142
|
+
} else {
|
|
143
|
+
// The user provided PIP_CONFIG_FILE does not exist on disk
|
|
144
|
+
// PIP will handle this as an error and inform the user
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Set fallback CA bundle environment variables for Python libraries that don't read pip config
|
|
148
|
+
setFallbackCaBundleEnvironmentVariables(env, combinedCaPath);
|
|
149
|
+
|
|
150
|
+
const result = await safeSpawn(command, args, {
|
|
151
|
+
stdio: "inherit",
|
|
152
|
+
env,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Cleanup temporary config file if we created one
|
|
156
|
+
if (cleanupConfigPath) {
|
|
157
|
+
try {
|
|
158
|
+
await fs.unlink(cleanupConfigPath);
|
|
159
|
+
} catch {
|
|
160
|
+
// Ignore cleanup errors - the file may have already been deleted or is inaccessible
|
|
161
|
+
// Temp files in os.tmpdir() may eventually be cleaned by the OS, but timing varies by platform
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return { status: result.status };
|
|
166
|
+
} catch (/** @type any */ error) {
|
|
167
|
+
if (error.status) {
|
|
168
|
+
return { status: error.status };
|
|
169
|
+
} else {
|
|
170
|
+
ui.writeError(`Error executing command: ${error.message}`);
|
|
171
|
+
ui.writeError(`Is '${command}' installed and available on your system?`);
|
|
172
|
+
return { status: 1 };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { matchesCommand } from "../_shared/matchesCommand.js";
|
|
2
|
+
import { commandArgumentScanner } from "./dependencyScanner/commandArgumentScanner.js";
|
|
3
|
+
import { runPnpmCommand } from "./runPnpmCommand.js";
|
|
4
|
+
|
|
5
|
+
const scanner = commandArgumentScanner();
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @returns {import("../currentPackageManager.js").PackageManager}
|
|
9
|
+
*/
|
|
10
|
+
export function createPnpmPackageManager() {
|
|
11
|
+
return {
|
|
12
|
+
runCommand: (args) => runPnpmCommand(args, "pnpm"),
|
|
13
|
+
isSupportedCommand: (args) =>
|
|
14
|
+
matchesCommand(args, "add") ||
|
|
15
|
+
matchesCommand(args, "update") ||
|
|
16
|
+
matchesCommand(args, "upgrade") ||
|
|
17
|
+
matchesCommand(args, "up") ||
|
|
18
|
+
matchesCommand(args, "install") ||
|
|
19
|
+
matchesCommand(args, "i") ||
|
|
20
|
+
// dlx does not always come in the first position
|
|
21
|
+
// eg: pnpm --package=yo --package=generator-webapp dlx yo webapp
|
|
22
|
+
// documentation: https://pnpm.io/cli/dlx#--package-name
|
|
23
|
+
args.includes("dlx"),
|
|
24
|
+
getDependencyUpdatesForCommand: (args) =>
|
|
25
|
+
getDependencyUpdatesForCommand(args, false),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @returns {import("../currentPackageManager.js").PackageManager}
|
|
31
|
+
*/
|
|
32
|
+
export function createPnpxPackageManager() {
|
|
33
|
+
return {
|
|
34
|
+
runCommand: (args) => runPnpmCommand(args, "pnpx"),
|
|
35
|
+
isSupportedCommand: () => true,
|
|
36
|
+
getDependencyUpdatesForCommand: (args) =>
|
|
37
|
+
getDependencyUpdatesForCommand(args, true),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {string[]} args
|
|
43
|
+
* @param {boolean} isPnpx
|
|
44
|
+
* @returns {ReturnType<import("../currentPackageManager.js").PackageManager["getDependencyUpdatesForCommand"]>}
|
|
45
|
+
*/
|
|
46
|
+
function getDependencyUpdatesForCommand(args, isPnpx) {
|
|
47
|
+
if (isPnpx) {
|
|
48
|
+
return scanner.scan(args);
|
|
49
|
+
}
|
|
50
|
+
if (args.includes("dlx")) {
|
|
51
|
+
// dlx is not always the first argument (eg: `pnpm --package=yo --package=generator-webapp dlx yo webapp`)
|
|
52
|
+
// so we need to filter it out instead of slicing the array
|
|
53
|
+
// documentation: https://pnpm.io/cli/dlx#--package-name
|
|
54
|
+
return scanner.scan(args.filter((arg) => arg !== "dlx"));
|
|
55
|
+
}
|
|
56
|
+
return scanner.scan(args.slice(1));
|
|
57
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { resolvePackageVersion } from "../../../api/npmApi.js";
|
|
2
|
+
import { parsePackagesFromArguments } from "../parsing/parsePackagesFromArguments.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @returns {import("../../npm/dependencyScanner/commandArgumentScanner.js").CommandArgumentScanner}
|
|
6
|
+
*/
|
|
7
|
+
export function commandArgumentScanner() {
|
|
8
|
+
return {
|
|
9
|
+
scan: (args) => scanDependencies(args),
|
|
10
|
+
shouldScan: () => true, // There's no dry run for pnpm, so we always scan
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {string[]} args
|
|
16
|
+
* @returns {Promise<import("../../npm/dependencyScanner/commandArgumentScanner.js").ScanResult[]>}
|
|
17
|
+
*/
|
|
18
|
+
async function scanDependencies(args) {
|
|
19
|
+
const changes = [];
|
|
20
|
+
const packageUpdates = parsePackagesFromArguments(args);
|
|
21
|
+
|
|
22
|
+
for (const packageUpdate of packageUpdates) {
|
|
23
|
+
var exactVersion = await resolvePackageVersion(
|
|
24
|
+
packageUpdate.name,
|
|
25
|
+
packageUpdate.version
|
|
26
|
+
);
|
|
27
|
+
if (exactVersion) {
|
|
28
|
+
packageUpdate.version = exactVersion;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
changes.push({ ...packageUpdate, type: "add" });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return changes;
|
|
35
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {string[]} args
|
|
3
|
+
* @returns {{name: string, version: string}[]}
|
|
4
|
+
*/
|
|
5
|
+
export function parsePackagesFromArguments(args) {
|
|
6
|
+
const changes = [];
|
|
7
|
+
let defaultTag = "latest";
|
|
8
|
+
|
|
9
|
+
for (let i = 0; i < args.length; i++) {
|
|
10
|
+
const arg = args[i];
|
|
11
|
+
const option = getOption(arg);
|
|
12
|
+
|
|
13
|
+
if (option) {
|
|
14
|
+
// If the option has a parameter, skip the next argument as well
|
|
15
|
+
i += option.numberOfParameters;
|
|
16
|
+
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const packageDetails = parsePackagename(arg, defaultTag);
|
|
21
|
+
if (packageDetails) {
|
|
22
|
+
changes.push(packageDetails);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return changes;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {string} arg
|
|
31
|
+
* @returns {{name: string, numberOfParameters: number} | undefined}
|
|
32
|
+
*/
|
|
33
|
+
function getOption(arg) {
|
|
34
|
+
if (isOptionWithParameter(arg)) {
|
|
35
|
+
return {
|
|
36
|
+
name: arg,
|
|
37
|
+
numberOfParameters: 1,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Arguments starting with "-" or "--" are considered options
|
|
42
|
+
// except for "--package=" which contains the package name
|
|
43
|
+
if (arg.startsWith("-") && !arg.startsWith("--package=")) {
|
|
44
|
+
return {
|
|
45
|
+
name: arg,
|
|
46
|
+
numberOfParameters: 0,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {string} arg
|
|
55
|
+
* @returns {boolean}
|
|
56
|
+
*/
|
|
57
|
+
function isOptionWithParameter(arg) {
|
|
58
|
+
const optionsWithParameters = ["--C", "--dir"];
|
|
59
|
+
|
|
60
|
+
return optionsWithParameters.includes(arg);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @param {string} arg
|
|
65
|
+
* @param {string} defaultTag
|
|
66
|
+
* @returns {{name: string, version: string}}
|
|
67
|
+
*/
|
|
68
|
+
function parsePackagename(arg, defaultTag) {
|
|
69
|
+
// format can be --package=name@version
|
|
70
|
+
// in that case, we need to remove the --package= part
|
|
71
|
+
if (arg.startsWith("--package=")) {
|
|
72
|
+
arg = arg.slice(10);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
arg = removeAlias(arg);
|
|
76
|
+
|
|
77
|
+
// Split at the last "@" to separate the package name and version
|
|
78
|
+
const lastAtIndex = arg.lastIndexOf("@");
|
|
79
|
+
|
|
80
|
+
let name, version;
|
|
81
|
+
// The index of the last "@" should be greater than 0
|
|
82
|
+
// If the index is 0, it means the package name starts with "@" (eg: "@aikidosec/package-name")
|
|
83
|
+
if (lastAtIndex > 0) {
|
|
84
|
+
name = arg.slice(0, lastAtIndex);
|
|
85
|
+
version = arg.slice(lastAtIndex + 1);
|
|
86
|
+
} else {
|
|
87
|
+
name = arg;
|
|
88
|
+
version = defaultTag; // No tag specified (eg: "http-server"), use the default tag
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
name,
|
|
93
|
+
version,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @param {string} arg
|
|
99
|
+
* @returns {string}
|
|
100
|
+
*/
|
|
101
|
+
function removeAlias(arg) {
|
|
102
|
+
// removes the alias.
|
|
103
|
+
// Eg.: server@npm:http-server@latest becomes http-server@latest
|
|
104
|
+
const aliasIndex = arg.indexOf("@npm:");
|
|
105
|
+
if (aliasIndex !== -1) {
|
|
106
|
+
return arg.slice(aliasIndex + 5);
|
|
107
|
+
}
|
|
108
|
+
return arg;
|
|
109
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ui } from "../../environment/userInteraction.js";
|
|
2
|
+
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
|
|
3
|
+
import { safeSpawn } from "../../utils/safeSpawn.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {string[]} args
|
|
7
|
+
* @param {string} [toolName]
|
|
8
|
+
* @returns {Promise<{status: number}>}
|
|
9
|
+
*/
|
|
10
|
+
export async function runPnpmCommand(args, toolName = "pnpm") {
|
|
11
|
+
try {
|
|
12
|
+
let result;
|
|
13
|
+
if (toolName === "pnpm") {
|
|
14
|
+
result = await safeSpawn("pnpm", args, {
|
|
15
|
+
stdio: "inherit",
|
|
16
|
+
env: mergeSafeChainProxyEnvironmentVariables(process.env),
|
|
17
|
+
});
|
|
18
|
+
} else if (toolName === "pnpx") {
|
|
19
|
+
result = await safeSpawn("pnpx", args, {
|
|
20
|
+
stdio: "inherit",
|
|
21
|
+
env: mergeSafeChainProxyEnvironmentVariables(process.env),
|
|
22
|
+
});
|
|
23
|
+
} else {
|
|
24
|
+
throw new Error(`Unsupported tool name for aikido-pnpm: ${toolName}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return { status: result.status };
|
|
28
|
+
} catch (/** @type any */ error) {
|
|
29
|
+
if (error.status) {
|
|
30
|
+
return { status: error.status };
|
|
31
|
+
} else {
|
|
32
|
+
ui.writeError("Error executing command:", error.message);
|
|
33
|
+
return { status: 1 };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { runUv } from "./runUvCommand.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @returns {import("../currentPackageManager.js").PackageManager}
|
|
5
|
+
*/
|
|
6
|
+
export function createUvPackageManager() {
|
|
7
|
+
return {
|
|
8
|
+
/**
|
|
9
|
+
* @param {string[]} args
|
|
10
|
+
*/
|
|
11
|
+
runCommand: (args) => {
|
|
12
|
+
return runUv("uv", args);
|
|
13
|
+
},
|
|
14
|
+
// For uv, rely solely on MITM
|
|
15
|
+
isSupportedCommand: () => false,
|
|
16
|
+
getDependencyUpdatesForCommand: () => [],
|
|
17
|
+
};
|
|
18
|
+
}
|