@aikidosec/safe-chain 0.0.1-custom-install-dir
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 +537 -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 +17 -0
- package/bin/aikido-pip3.js +17 -0
- package/bin/aikido-pipx.js +16 -0
- package/bin/aikido-pnpm.js +14 -0
- package/bin/aikido-pnpx.js +14 -0
- package/bin/aikido-poetry.js +13 -0
- package/bin/aikido-python.js +19 -0
- package/bin/aikido-python3.js +19 -0
- package/bin/aikido-uv.js +16 -0
- package/bin/aikido-uvx.js +16 -0
- package/bin/aikido-yarn.js +14 -0
- package/bin/safe-chain.js +147 -0
- package/docs/Release.md +25 -0
- package/docs/banner.svg +151 -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/docs/troubleshooting.md +321 -0
- package/npm-shrinkwrap.json +3180 -0
- package/package.json +71 -0
- package/src/api/aikido.js +187 -0
- package/src/api/npmApi.js +71 -0
- package/src/config/cliArguments.js +161 -0
- package/src/config/configFile.js +327 -0
- package/src/config/environmentVariables.js +57 -0
- package/src/config/safeChainDir.js +71 -0
- package/src/config/settings.js +247 -0
- package/src/environment/environment.js +14 -0
- package/src/environment/userInteraction.js +122 -0
- package/src/installLocation.js +42 -0
- package/src/main.js +123 -0
- package/src/packagemanager/_shared/commandErrors.js +17 -0
- package/src/packagemanager/_shared/matchesCommand.js +18 -0
- package/src/packagemanager/bun/createBunPackageManager.js +48 -0
- package/src/packagemanager/currentPackageManager.js +82 -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 +20 -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 +20 -0
- package/src/packagemanager/pip/createPackageManager.js +25 -0
- package/src/packagemanager/pip/pipSettings.js +6 -0
- package/src/packagemanager/pip/runPipCommand.js +209 -0
- package/src/packagemanager/pipx/createPipXPackageManager.js +18 -0
- package/src/packagemanager/pipx/runPipXCommand.js +60 -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 +32 -0
- package/src/packagemanager/poetry/createPoetryPackageManager.js +72 -0
- package/src/packagemanager/uv/createUvPackageManager.js +18 -0
- package/src/packagemanager/uv/runUvCommand.js +66 -0
- package/src/packagemanager/uvx/createUvxPackageManager.js +18 -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 +36 -0
- package/src/registryProxy/certBundle.js +203 -0
- package/src/registryProxy/certUtils.js +178 -0
- package/src/registryProxy/getConnectTimeout.js +13 -0
- package/src/registryProxy/http-utils.js +80 -0
- package/src/registryProxy/interceptors/createInterceptorForEcoSystem.js +25 -0
- package/src/registryProxy/interceptors/interceptorBuilder.js +179 -0
- package/src/registryProxy/interceptors/minimumPackageAgeExclusions.js +33 -0
- package/src/registryProxy/interceptors/npm/modifyNpmInfo.js +180 -0
- package/src/registryProxy/interceptors/npm/npmInterceptor.js +101 -0
- package/src/registryProxy/interceptors/npm/parseNpmPackageUrl.js +60 -0
- package/src/registryProxy/interceptors/pip/modifyPipInfo.js +167 -0
- package/src/registryProxy/interceptors/pip/modifyPipJsonResponse.js +176 -0
- package/src/registryProxy/interceptors/pip/parsePipPackageUrl.js +162 -0
- package/src/registryProxy/interceptors/pip/pipInterceptor.js +122 -0
- package/src/registryProxy/interceptors/pip/pipMetadataResponseUtils.js +27 -0
- package/src/registryProxy/interceptors/pip/pipMetadataVersionUtils.js +131 -0
- package/src/registryProxy/interceptors/suppressedVersionsState.js +21 -0
- package/src/registryProxy/isImdsEndpoint.js +13 -0
- package/src/registryProxy/mitmRequestHandler.js +240 -0
- package/src/registryProxy/plainHttpProxy.js +95 -0
- package/src/registryProxy/registryProxy.js +255 -0
- package/src/registryProxy/tunnelRequestHandler.js +213 -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/scanning/newPackagesDatabaseBuilder.js +71 -0
- package/src/scanning/newPackagesDatabaseWarnings.js +17 -0
- package/src/scanning/newPackagesListCache.js +126 -0
- package/src/scanning/packageNameVariants.js +29 -0
- package/src/shell-integration/helpers.js +296 -0
- package/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh +37 -0
- package/src/shell-integration/path-wrappers/templates/windows-wrapper.template.cmd +25 -0
- package/src/shell-integration/setup-ci.js +152 -0
- package/src/shell-integration/setup.js +110 -0
- package/src/shell-integration/shellDetection.js +39 -0
- package/src/shell-integration/startup-scripts/init-fish.fish +122 -0
- package/src/shell-integration/startup-scripts/init-posix.sh +112 -0
- package/src/shell-integration/startup-scripts/init-pwsh.ps1 +176 -0
- package/src/shell-integration/supported-shells/bash.js +222 -0
- package/src/shell-integration/supported-shells/fish.js +97 -0
- package/src/shell-integration/supported-shells/powershell.js +102 -0
- package/src/shell-integration/supported-shells/windowsPowershell.js +102 -0
- package/src/shell-integration/supported-shells/zsh.js +94 -0
- package/src/shell-integration/teardown.js +114 -0
- package/src/utils/safeSpawn.js +153 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import {
|
|
2
|
+
calculateLatestVersion,
|
|
3
|
+
getAvailableVersionsFromJson,
|
|
4
|
+
getPackageVersionFromMetadataFile,
|
|
5
|
+
} from "./pipMetadataVersionUtils.js";
|
|
6
|
+
import { logSuppressedVersion } from "./pipMetadataResponseUtils.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {any} json
|
|
10
|
+
* @param {string} metadataUrl
|
|
11
|
+
* @param {(packageName: string | undefined, version: string | undefined) => boolean} isNewlyReleasedPackage
|
|
12
|
+
* @param {string} packageName
|
|
13
|
+
* @returns {boolean}
|
|
14
|
+
*/
|
|
15
|
+
export function modifyPipJsonResponse(
|
|
16
|
+
json,
|
|
17
|
+
metadataUrl,
|
|
18
|
+
isNewlyReleasedPackage,
|
|
19
|
+
packageName
|
|
20
|
+
) {
|
|
21
|
+
const filesModified = filterJsonMetadataFiles(
|
|
22
|
+
json,
|
|
23
|
+
metadataUrl,
|
|
24
|
+
isNewlyReleasedPackage,
|
|
25
|
+
packageName
|
|
26
|
+
);
|
|
27
|
+
const releasesModified = removeJsonMetadataReleases(
|
|
28
|
+
json,
|
|
29
|
+
isNewlyReleasedPackage,
|
|
30
|
+
packageName
|
|
31
|
+
);
|
|
32
|
+
const urlsModified = filterJsonMetadataUrls(
|
|
33
|
+
json,
|
|
34
|
+
metadataUrl,
|
|
35
|
+
isNewlyReleasedPackage,
|
|
36
|
+
packageName
|
|
37
|
+
);
|
|
38
|
+
const versionModified = updateJsonInfoVersion(json, metadataUrl);
|
|
39
|
+
|
|
40
|
+
return filesModified || releasesModified || urlsModified || versionModified;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {any} json
|
|
45
|
+
* @param {string} metadataUrl
|
|
46
|
+
* @param {(packageName: string | undefined, version: string | undefined) => boolean} isNewlyReleasedPackage
|
|
47
|
+
* @param {string} packageName
|
|
48
|
+
* @returns {boolean}
|
|
49
|
+
*/
|
|
50
|
+
function filterJsonMetadataFiles(
|
|
51
|
+
json,
|
|
52
|
+
metadataUrl,
|
|
53
|
+
isNewlyReleasedPackage,
|
|
54
|
+
packageName
|
|
55
|
+
) {
|
|
56
|
+
if (!Array.isArray(json.files)) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let modified = false;
|
|
61
|
+
const loggedVersions = new Set();
|
|
62
|
+
json.files = json.files.filter((/** @type {any} */ file) => {
|
|
63
|
+
const version = getPackageVersionFromMetadataFile(file, metadataUrl);
|
|
64
|
+
|
|
65
|
+
if (version && isNewlyReleasedPackage(packageName, version)) {
|
|
66
|
+
modified = true;
|
|
67
|
+
if (!loggedVersions.has(version)) {
|
|
68
|
+
logSuppressedVersion(packageName, version);
|
|
69
|
+
loggedVersions.add(version);
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return true;
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return modified;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {any} json
|
|
82
|
+
* @param {(packageName: string | undefined, version: string | undefined) => boolean} isNewlyReleasedPackage
|
|
83
|
+
* @param {string} packageName
|
|
84
|
+
* @returns {boolean}
|
|
85
|
+
*/
|
|
86
|
+
function removeJsonMetadataReleases(json, isNewlyReleasedPackage, packageName) {
|
|
87
|
+
if (!json.releases || typeof json.releases !== "object") {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let modified = false;
|
|
92
|
+
|
|
93
|
+
for (const [version, files] of Object.entries(json.releases)) {
|
|
94
|
+
if (
|
|
95
|
+
Array.isArray(/** @type {unknown[]} */ (files)) &&
|
|
96
|
+
isNewlyReleasedPackage(packageName, version)
|
|
97
|
+
) {
|
|
98
|
+
delete json.releases[version];
|
|
99
|
+
modified = true;
|
|
100
|
+
logSuppressedVersion(packageName, version);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return modified;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @param {any} json
|
|
109
|
+
* @param {string} metadataUrl
|
|
110
|
+
* @param {(packageName: string | undefined, version: string | undefined) => boolean} isNewlyReleasedPackage
|
|
111
|
+
* @param {string} packageName
|
|
112
|
+
* @returns {boolean}
|
|
113
|
+
*/
|
|
114
|
+
function filterJsonMetadataUrls(
|
|
115
|
+
json,
|
|
116
|
+
metadataUrl,
|
|
117
|
+
isNewlyReleasedPackage,
|
|
118
|
+
packageName
|
|
119
|
+
) {
|
|
120
|
+
if (!Array.isArray(json.urls)) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let modified = false;
|
|
125
|
+
const loggedVersions = new Set();
|
|
126
|
+
json.urls = json.urls.filter((/** @type {any} */ file) => {
|
|
127
|
+
const version = getPackageVersionFromMetadataFile(file, metadataUrl);
|
|
128
|
+
|
|
129
|
+
if (version && isNewlyReleasedPackage(packageName, version)) {
|
|
130
|
+
modified = true;
|
|
131
|
+
if (!loggedVersions.has(version)) {
|
|
132
|
+
logSuppressedVersion(packageName, version);
|
|
133
|
+
loggedVersions.add(version);
|
|
134
|
+
}
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return true;
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
return modified;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* @param {any} json
|
|
146
|
+
* @param {string} metadataUrl
|
|
147
|
+
* @returns {boolean}
|
|
148
|
+
*/
|
|
149
|
+
function updateJsonInfoVersion(json, metadataUrl) {
|
|
150
|
+
if (!json.info || typeof json.info !== "object") {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const replacementVersion = computeReplacementVersion(json, metadataUrl);
|
|
155
|
+
|
|
156
|
+
if (
|
|
157
|
+
typeof json.info.version !== "string" ||
|
|
158
|
+
!replacementVersion ||
|
|
159
|
+
json.info.version === replacementVersion
|
|
160
|
+
) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
json.info.version = replacementVersion;
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* @param {any} json
|
|
170
|
+
* @param {string} metadataUrl
|
|
171
|
+
* @returns {string | undefined}
|
|
172
|
+
*/
|
|
173
|
+
function computeReplacementVersion(json, metadataUrl) {
|
|
174
|
+
const candidateVersions = getAvailableVersionsFromJson(json, metadataUrl);
|
|
175
|
+
return calculateLatestVersion(candidateVersions);
|
|
176
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parses a PyPI metadata URL and returns the package name and API type.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* parsePipMetadataUrl("https://pypi.org/simple/requests/")
|
|
6
|
+
* // => { packageName: "requests", type: "simple" }
|
|
7
|
+
*
|
|
8
|
+
* parsePipMetadataUrl("https://pypi.org/pypi/requests/json")
|
|
9
|
+
* // => { packageName: "requests", type: "json" }
|
|
10
|
+
*
|
|
11
|
+
* parsePipMetadataUrl("https://pypi.org/pypi/requests/2.28.1/json")
|
|
12
|
+
* // => { packageName: "requests", type: "json" }
|
|
13
|
+
*
|
|
14
|
+
* parsePipMetadataUrl("https://files.pythonhosted.org/packages/requests-2.28.1.tar.gz")
|
|
15
|
+
* // => { packageName: undefined, type: undefined }
|
|
16
|
+
*
|
|
17
|
+
* @param {string} url
|
|
18
|
+
* @returns {{ packageName: string | undefined, type: "simple" | "json" | undefined }}
|
|
19
|
+
*/
|
|
20
|
+
export function parsePipMetadataUrl(url) {
|
|
21
|
+
if (typeof url !== "string") {
|
|
22
|
+
return { packageName: undefined, type: undefined };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let urlObj;
|
|
26
|
+
try {
|
|
27
|
+
urlObj = new URL(url);
|
|
28
|
+
} catch {
|
|
29
|
+
return { packageName: undefined, type: undefined };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const pathSegments = urlObj.pathname.split("/").filter(Boolean);
|
|
33
|
+
if (pathSegments[0] === "simple" && pathSegments[1]) {
|
|
34
|
+
return {
|
|
35
|
+
packageName: decodeURIComponent(pathSegments[1]),
|
|
36
|
+
type: "simple",
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (
|
|
41
|
+
pathSegments[0] === "pypi" &&
|
|
42
|
+
pathSegments[pathSegments.length - 1] === "json" &&
|
|
43
|
+
pathSegments[1]
|
|
44
|
+
) {
|
|
45
|
+
return {
|
|
46
|
+
packageName: decodeURIComponent(pathSegments[1]),
|
|
47
|
+
type: "json",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return { packageName: undefined, type: undefined };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {string} url
|
|
56
|
+
* @returns {boolean}
|
|
57
|
+
*/
|
|
58
|
+
export function isPipPackageInfoUrl(url) {
|
|
59
|
+
return !!parsePipMetadataUrl(url).packageName;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Parse Python package artifact URLs from PyPI-style registries.
|
|
64
|
+
* Examples:
|
|
65
|
+
* - Wheel: https://files.pythonhosted.org/packages/.../requests-2.28.1-py3-none-any.whl
|
|
66
|
+
* - Wheel metadata: https://files.pythonhosted.org/packages/.../requests-2.28.1-py3-none-any.whl.metadata
|
|
67
|
+
* - Sdist: https://files.pythonhosted.org/packages/.../requests-2.28.1.tar.gz
|
|
68
|
+
*
|
|
69
|
+
* @param {string} url
|
|
70
|
+
* @param {string} registry
|
|
71
|
+
* @returns {{packageName: string | undefined, version: string | undefined}}
|
|
72
|
+
*/
|
|
73
|
+
export function parsePipPackageFromUrl(url, registry) {
|
|
74
|
+
if (!registry || typeof url !== "string") {
|
|
75
|
+
return { packageName: undefined, version: undefined };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let urlObj;
|
|
79
|
+
try {
|
|
80
|
+
urlObj = new URL(url);
|
|
81
|
+
} catch {
|
|
82
|
+
return { packageName: undefined, version: undefined };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const lastSegment = urlObj.pathname.split("/").filter(Boolean).pop();
|
|
86
|
+
if (!lastSegment) {
|
|
87
|
+
return { packageName: undefined, version: undefined };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const filename = decodeURIComponent(lastSegment);
|
|
91
|
+
|
|
92
|
+
const wheelExtRe = /\.whl(?:\.metadata)?$/;
|
|
93
|
+
if (wheelExtRe.test(filename)) {
|
|
94
|
+
return parseWheelFilename(filename, wheelExtRe);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const sdistExtWithMetadataRe = /\.(tar\.gz|zip|tar\.bz2|tar\.xz)(\.metadata)?$/i;
|
|
98
|
+
if (!sdistExtWithMetadataRe.test(filename)) {
|
|
99
|
+
return { packageName: undefined, version: undefined };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return parseSdistFilename(filename, sdistExtWithMetadataRe);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Parse wheel filenames and Poetry preflight metadata.
|
|
107
|
+
* Examples:
|
|
108
|
+
* - foo_bar-2.0.0-py3-none-any.whl
|
|
109
|
+
* - foo_bar-2.0.0-py3-none-any.whl.metadata
|
|
110
|
+
*
|
|
111
|
+
* @param {string} filename
|
|
112
|
+
* @param {RegExp} wheelExtRe
|
|
113
|
+
* @returns {{packageName: string | undefined, version: string | undefined}}
|
|
114
|
+
*/
|
|
115
|
+
function parseWheelFilename(filename, wheelExtRe) {
|
|
116
|
+
const base = filename.replace(wheelExtRe, "");
|
|
117
|
+
const firstDash = base.indexOf("-");
|
|
118
|
+
if (firstDash <= 0) {
|
|
119
|
+
return { packageName: undefined, version: undefined };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const packageName = base.slice(0, firstDash);
|
|
123
|
+
const rest = base.slice(firstDash + 1);
|
|
124
|
+
const secondDash = rest.indexOf("-");
|
|
125
|
+
const version = secondDash >= 0 ? rest.slice(0, secondDash) : rest;
|
|
126
|
+
|
|
127
|
+
// "latest" is a resolver-style token, not an actual published artifact version.
|
|
128
|
+
if (version === "latest" || !packageName || !version) {
|
|
129
|
+
return { packageName: undefined, version: undefined };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { packageName, version };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Parse source distribution filenames, with optional metadata suffix.
|
|
137
|
+
* Examples:
|
|
138
|
+
* - requests-2.28.1.tar.gz
|
|
139
|
+
* - requests-2.28.1.zip
|
|
140
|
+
* - requests-2.28.1.tar.gz.metadata
|
|
141
|
+
*
|
|
142
|
+
* @param {string} filename
|
|
143
|
+
* @param {RegExp} sdistExtWithMetadataRe
|
|
144
|
+
* @returns {{packageName: string | undefined, version: string | undefined}}
|
|
145
|
+
*/
|
|
146
|
+
function parseSdistFilename(filename, sdistExtWithMetadataRe) {
|
|
147
|
+
const base = filename.replace(sdistExtWithMetadataRe, "");
|
|
148
|
+
const lastDash = base.lastIndexOf("-");
|
|
149
|
+
if (lastDash <= 0 || lastDash >= base.length - 1) {
|
|
150
|
+
return { packageName: undefined, version: undefined };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const packageName = base.slice(0, lastDash);
|
|
154
|
+
const version = base.slice(lastDash + 1);
|
|
155
|
+
|
|
156
|
+
// "latest" is a resolver-style token, not an actual published artifact version.
|
|
157
|
+
if (version === "latest" || !packageName || !version) {
|
|
158
|
+
return { packageName: undefined, version: undefined };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return { packageName, version };
|
|
162
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ECOSYSTEM_PY,
|
|
3
|
+
getPipCustomRegistries,
|
|
4
|
+
skipMinimumPackageAge,
|
|
5
|
+
} from "../../../config/settings.js";
|
|
6
|
+
import { isMalwarePackage } from "../../../scanning/audit/index.js";
|
|
7
|
+
import { getEquivalentPackageNames } from "../../../scanning/packageNameVariants.js";
|
|
8
|
+
import { openNewPackagesDatabase } from "../../../scanning/newPackagesListCache.js";
|
|
9
|
+
import { interceptRequests } from "../interceptorBuilder.js";
|
|
10
|
+
import { isExcludedFromMinimumPackageAge } from "../minimumPackageAgeExclusions.js";
|
|
11
|
+
import {
|
|
12
|
+
modifyPipInfoResponse,
|
|
13
|
+
parsePipMetadataUrl,
|
|
14
|
+
} from "./modifyPipInfo.js";
|
|
15
|
+
import { parsePipPackageFromUrl } from "./parsePipPackageUrl.js";
|
|
16
|
+
|
|
17
|
+
const knownPipRegistries = [
|
|
18
|
+
"files.pythonhosted.org",
|
|
19
|
+
"pypi.org",
|
|
20
|
+
"pypi.python.org",
|
|
21
|
+
"pythonhosted.org",
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {string} url
|
|
26
|
+
* @returns {import("../interceptorBuilder.js").Interceptor | undefined}
|
|
27
|
+
*/
|
|
28
|
+
export function pipInterceptorForUrl(url) {
|
|
29
|
+
const customRegistries = getPipCustomRegistries();
|
|
30
|
+
const registries = [...knownPipRegistries, ...customRegistries];
|
|
31
|
+
const registry = registries.find((reg) => url.includes(reg));
|
|
32
|
+
|
|
33
|
+
if (registry) {
|
|
34
|
+
return buildPipInterceptor(registry);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {string} registry
|
|
42
|
+
* @returns {import("../interceptorBuilder.js").Interceptor | undefined}
|
|
43
|
+
*/
|
|
44
|
+
function buildPipInterceptor(registry) {
|
|
45
|
+
return interceptRequests(createPipRequestHandler(registry));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {string} registry
|
|
50
|
+
* @returns {(reqContext: import("../interceptorBuilder.js").RequestInterceptionContext) => Promise<void>}
|
|
51
|
+
*/
|
|
52
|
+
function createPipRequestHandler(registry) {
|
|
53
|
+
return async (reqContext) => {
|
|
54
|
+
const minimumAgeChecksEnabled = !skipMinimumPackageAge();
|
|
55
|
+
const metadataInfo = parsePipMetadataUrl(reqContext.targetUrl);
|
|
56
|
+
const metadataPackageName = metadataInfo.packageName;
|
|
57
|
+
|
|
58
|
+
if (
|
|
59
|
+
minimumAgeChecksEnabled &&
|
|
60
|
+
metadataPackageName &&
|
|
61
|
+
!isExcludedFromMinimumPackageAge(metadataPackageName)
|
|
62
|
+
) {
|
|
63
|
+
const newPackagesDatabase = await openNewPackagesDatabase();
|
|
64
|
+
reqContext.modifyBody((body, headers) =>
|
|
65
|
+
modifyPipInfoResponse(
|
|
66
|
+
body,
|
|
67
|
+
headers,
|
|
68
|
+
reqContext.targetUrl,
|
|
69
|
+
newPackagesDatabase.isNewlyReleasedPackage,
|
|
70
|
+
metadataPackageName
|
|
71
|
+
)
|
|
72
|
+
);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const { packageName, version } = parsePipPackageFromUrl(
|
|
77
|
+
reqContext.targetUrl,
|
|
78
|
+
registry
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
if (!packageName) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const equivalentPackageNames = getEquivalentPackageNames(
|
|
86
|
+
packageName,
|
|
87
|
+
ECOSYSTEM_PY
|
|
88
|
+
);
|
|
89
|
+
let isMalicious = false;
|
|
90
|
+
for (const equivalentPackageName of equivalentPackageNames) {
|
|
91
|
+
if (await isMalwarePackage(equivalentPackageName, version)) {
|
|
92
|
+
isMalicious = true;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (isMalicious) {
|
|
98
|
+
reqContext.blockMalware(packageName, version);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (
|
|
103
|
+
version &&
|
|
104
|
+
minimumAgeChecksEnabled &&
|
|
105
|
+
!isExcludedFromMinimumPackageAge(packageName)
|
|
106
|
+
) {
|
|
107
|
+
const newPackagesDatabase = await openNewPackagesDatabase();
|
|
108
|
+
const isNewlyReleased = newPackagesDatabase.isNewlyReleasedPackage(
|
|
109
|
+
packageName,
|
|
110
|
+
version
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
if (isNewlyReleased) {
|
|
114
|
+
reqContext.blockMinimumAgeRequest(
|
|
115
|
+
packageName,
|
|
116
|
+
version,
|
|
117
|
+
`Forbidden - blocked by safe-chain direct download minimum package age (${packageName}@${version})`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { getMinimumPackageAgeHours } from "../../../config/settings.js";
|
|
2
|
+
import { ui } from "../../../environment/userInteraction.js";
|
|
3
|
+
import { getHeaderValueAsString } from "../../http-utils.js";
|
|
4
|
+
import { recordSuppressedVersion } from "../suppressedVersionsState.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {NodeJS.Dict<string | string[]> | undefined} headers
|
|
8
|
+
* @returns {string | undefined}
|
|
9
|
+
*/
|
|
10
|
+
export function getPipMetadataContentType(headers) {
|
|
11
|
+
return getHeaderValueAsString(headers, "content-type")
|
|
12
|
+
?.toLowerCase()
|
|
13
|
+
.split(";")[0]
|
|
14
|
+
.trim();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {string} packageName
|
|
19
|
+
* @param {string} version
|
|
20
|
+
* @returns {void}
|
|
21
|
+
*/
|
|
22
|
+
export function logSuppressedVersion(packageName, version) {
|
|
23
|
+
recordSuppressedVersion();
|
|
24
|
+
ui.writeVerbose(
|
|
25
|
+
`Safe-chain: ${packageName}@${version} is newer than ${getMinimumPackageAgeHours()} hours and was removed (minimumPackageAgeInHours setting).`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { parsePipPackageFromUrl } from "./parsePipPackageUrl.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {any} file
|
|
5
|
+
* @param {string} metadataUrl
|
|
6
|
+
* @returns {string | undefined}
|
|
7
|
+
*/
|
|
8
|
+
export function getPackageVersionFromMetadataFile(file, metadataUrl) {
|
|
9
|
+
const href = typeof file?.url === "string" ? file.url : undefined;
|
|
10
|
+
const filename = typeof file?.filename === "string" ? file.filename : undefined;
|
|
11
|
+
|
|
12
|
+
if (href) {
|
|
13
|
+
const resolvedHref = new URL(href, metadataUrl).toString();
|
|
14
|
+
return parsePipPackageFromUrl(
|
|
15
|
+
resolvedHref,
|
|
16
|
+
new URL(resolvedHref).host
|
|
17
|
+
).version;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (filename) {
|
|
21
|
+
return parsePipPackageFromUrl(
|
|
22
|
+
new URL(filename, metadataUrl).toString(),
|
|
23
|
+
new URL(metadataUrl).host
|
|
24
|
+
).version;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {any} json
|
|
32
|
+
* @param {string} metadataUrl
|
|
33
|
+
* @returns {string[]}
|
|
34
|
+
*/
|
|
35
|
+
export function getAvailableVersionsFromJson(json, metadataUrl) {
|
|
36
|
+
if (json.releases && typeof json.releases === "object") {
|
|
37
|
+
return Object.keys(json.releases);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!Array.isArray(json.files)) {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return [
|
|
45
|
+
...new Set(
|
|
46
|
+
json.files
|
|
47
|
+
.map((/** @type {any} */ file) =>
|
|
48
|
+
getPackageVersionFromMetadataFile(file, metadataUrl)
|
|
49
|
+
)
|
|
50
|
+
.filter(isDefinedString)
|
|
51
|
+
),
|
|
52
|
+
];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @param {string | undefined} value
|
|
57
|
+
* @returns {value is string}
|
|
58
|
+
*/
|
|
59
|
+
function isDefinedString(value) {
|
|
60
|
+
return typeof value === "string";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @param {string[]} versions
|
|
65
|
+
* @returns {string | undefined}
|
|
66
|
+
*/
|
|
67
|
+
export function calculateLatestVersion(versions) {
|
|
68
|
+
const stableVersions = versions.filter((version) => !isPrerelease(version));
|
|
69
|
+
if (stableVersions.length > 0) {
|
|
70
|
+
return stableVersions.sort(comparePep440ishVersions).at(-1);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return versions.sort(comparePep440ishVersions).at(-1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @param {string} left
|
|
78
|
+
* @param {string} right
|
|
79
|
+
* @returns {number}
|
|
80
|
+
*/
|
|
81
|
+
function comparePep440ishVersions(left, right) {
|
|
82
|
+
const leftParts = tokenizeVersion(left);
|
|
83
|
+
const rightParts = tokenizeVersion(right);
|
|
84
|
+
const maxLength = Math.max(leftParts.length, rightParts.length);
|
|
85
|
+
|
|
86
|
+
for (let index = 0; index < maxLength; index += 1) {
|
|
87
|
+
const leftPart = leftParts[index];
|
|
88
|
+
const rightPart = rightParts[index];
|
|
89
|
+
|
|
90
|
+
if (leftPart === undefined) return -1;
|
|
91
|
+
if (rightPart === undefined) return 1;
|
|
92
|
+
|
|
93
|
+
if (leftPart === rightPart) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const leftNumeric = typeof leftPart === "number";
|
|
98
|
+
const rightNumeric = typeof rightPart === "number";
|
|
99
|
+
|
|
100
|
+
if (leftNumeric && rightNumeric) {
|
|
101
|
+
return leftPart - rightPart;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (leftNumeric) return 1;
|
|
105
|
+
if (rightNumeric) return -1;
|
|
106
|
+
|
|
107
|
+
return String(leftPart).localeCompare(String(rightPart));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @param {string} version
|
|
115
|
+
* @returns {(string | number)[]}
|
|
116
|
+
*/
|
|
117
|
+
function tokenizeVersion(version) {
|
|
118
|
+
return version
|
|
119
|
+
.toLowerCase()
|
|
120
|
+
.split(/[^a-z0-9]+/)
|
|
121
|
+
.flatMap((part) => part.match(/[a-z]+|\d+/g) || [])
|
|
122
|
+
.map((part) => (/^\d+$/.test(part) ? Number(part) : part));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* @param {string} version
|
|
127
|
+
* @returns {boolean}
|
|
128
|
+
*/
|
|
129
|
+
function isPrerelease(version) {
|
|
130
|
+
return /(a|b|rc|dev)\d+/i.test(version);
|
|
131
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const state = {
|
|
2
|
+
hasSuppressedVersions: false,
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Tracks whether any rewritten metadata response suppressed versions during the
|
|
7
|
+
* current process lifetime. This is intentional shared state used only for the
|
|
8
|
+
* end-of-run summary message exposed through the proxy API.
|
|
9
|
+
*
|
|
10
|
+
* @returns {void}
|
|
11
|
+
*/
|
|
12
|
+
export function recordSuppressedVersion() {
|
|
13
|
+
state.hasSuppressedVersions = true;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @returns {boolean}
|
|
18
|
+
*/
|
|
19
|
+
export function getHasSuppressedVersions() {
|
|
20
|
+
return state.hasSuppressedVersions;
|
|
21
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Instance Metadata Service (IMDS) endpoints used by cloud providers.
|
|
2
|
+
// Cloud SDK tools probe these to detect environment and retrieve credentials.
|
|
3
|
+
// When outside cloud environments, connections timeout - we reduce timeout (3s vs 30s)
|
|
4
|
+
// and suppress error logging since this is expected behavior.
|
|
5
|
+
const imdsEndpoints = [
|
|
6
|
+
"metadata.google.internal",
|
|
7
|
+
"metadata.goog",
|
|
8
|
+
"169.254.169.254", // AWS, Azure, Oracle Cloud, GCP
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
export function isImdsEndpoint(/** @type {string} */ host) {
|
|
12
|
+
return imdsEndpoints.includes(host);
|
|
13
|
+
}
|