@aikidosec/safe-chain 1.4.3 → 1.4.7

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 (54) hide show
  1. package/README.md +65 -8
  2. package/bin/safe-chain.js +1 -81
  3. package/package.json +1 -1
  4. package/src/api/aikido.js +93 -18
  5. package/src/config/cliArguments.js +24 -1
  6. package/src/config/configFile.js +64 -6
  7. package/src/config/environmentVariables.js +13 -2
  8. package/src/config/settings.js +53 -4
  9. package/src/main.js +6 -2
  10. package/src/packagemanager/_shared/commandErrors.js +17 -0
  11. package/src/packagemanager/bun/createBunPackageManager.js +2 -7
  12. package/src/packagemanager/npm/runNpmCommand.js +2 -7
  13. package/src/packagemanager/npx/runNpxCommand.js +2 -7
  14. package/src/packagemanager/pip/runPipCommand.js +2 -7
  15. package/src/packagemanager/pipx/runPipXCommand.js +2 -7
  16. package/src/packagemanager/pnpm/runPnpmCommand.js +3 -7
  17. package/src/packagemanager/poetry/createPoetryPackageManager.js +2 -7
  18. package/src/packagemanager/uv/runUvCommand.js +2 -7
  19. package/src/packagemanager/yarn/runYarnCommand.js +2 -7
  20. package/src/registryProxy/certBundle.js +25 -3
  21. package/src/registryProxy/http-utils.js +63 -0
  22. package/src/registryProxy/interceptors/createInterceptorForEcoSystem.js +1 -1
  23. package/src/registryProxy/interceptors/interceptorBuilder.js +37 -4
  24. package/src/registryProxy/interceptors/minimumPackageAgeExclusions.js +33 -0
  25. package/src/registryProxy/interceptors/npm/modifyNpmInfo.js +18 -41
  26. package/src/registryProxy/interceptors/npm/npmInterceptor.js +47 -2
  27. package/src/registryProxy/interceptors/npm/parseNpmPackageUrl.js +20 -3
  28. package/src/registryProxy/interceptors/pip/modifyPipInfo.js +167 -0
  29. package/src/registryProxy/interceptors/pip/modifyPipJsonResponse.js +176 -0
  30. package/src/registryProxy/interceptors/pip/parsePipPackageUrl.js +162 -0
  31. package/src/registryProxy/interceptors/pip/pipInterceptor.js +122 -0
  32. package/src/registryProxy/interceptors/pip/pipMetadataResponseUtils.js +27 -0
  33. package/src/registryProxy/interceptors/pip/pipMetadataVersionUtils.js +131 -0
  34. package/src/registryProxy/interceptors/suppressedVersionsState.js +21 -0
  35. package/src/registryProxy/mitmRequestHandler.js +12 -6
  36. package/src/registryProxy/registryProxy.js +72 -9
  37. package/src/scanning/newPackagesDatabaseBuilder.js +71 -0
  38. package/src/scanning/newPackagesDatabaseWarnings.js +17 -0
  39. package/src/scanning/newPackagesListCache.js +126 -0
  40. package/src/scanning/packageNameVariants.js +29 -0
  41. package/src/shell-integration/setup.js +7 -3
  42. package/src/shell-integration/shellDetection.js +2 -0
  43. package/src/shell-integration/supported-shells/bash.js +19 -1
  44. package/src/shell-integration/supported-shells/fish.js +18 -0
  45. package/src/shell-integration/supported-shells/powershell.js +18 -0
  46. package/src/shell-integration/supported-shells/windowsPowershell.js +18 -0
  47. package/src/shell-integration/supported-shells/zsh.js +19 -1
  48. package/src/shell-integration/teardown.js +7 -1
  49. package/src/ultimate/ultimateTroubleshooting.js +1 -1
  50. package/src/installation/downloadAgent.js +0 -125
  51. package/src/installation/installOnMacOS.js +0 -155
  52. package/src/installation/installOnWindows.js +0 -203
  53. package/src/installation/installUltimate.js +0 -35
  54. package/src/registryProxy/interceptors/pipInterceptor.js +0 -132
@@ -0,0 +1,167 @@
1
+ import { ui } from "../../../environment/userInteraction.js";
2
+ import { clearCachingHeaders } from "../../http-utils.js";
3
+ import { normalizePipPackageName } from "../../../scanning/packageNameVariants.js";
4
+ import { parsePipPackageFromUrl } from "./parsePipPackageUrl.js";
5
+ export { parsePipMetadataUrl, isPipPackageInfoUrl } from "./parsePipPackageUrl.js";
6
+ import { getPipMetadataContentType, logSuppressedVersion } from "./pipMetadataResponseUtils.js";
7
+ import { modifyPipJsonResponse } from "./modifyPipJsonResponse.js";
8
+
9
+ // Match simple-index anchor tags and capture their href so we can suppress
10
+ // individual distribution links from PyPI HTML metadata responses.
11
+ const HTML_ANCHOR_HREF_RE =
12
+ /<a\b[^>]*href\s*=\s*(["'])([^"']+)\1[^>]*>[\s\S]*?<\/a>/gi;
13
+
14
+ /**
15
+ * @param {Buffer} body
16
+ * @param {NodeJS.Dict<string | string[]> | undefined} headers
17
+ * @param {string} metadataUrl
18
+ * @param {(packageName: string | undefined, version: string | undefined) => boolean} isNewlyReleasedPackage
19
+ * @param {string} packageName
20
+ * @returns {Buffer}
21
+ */
22
+ export function modifyPipInfoResponse(
23
+ body,
24
+ headers,
25
+ metadataUrl,
26
+ isNewlyReleasedPackage,
27
+ packageName
28
+ ) {
29
+ try {
30
+ const contentType = getPipMetadataContentType(headers);
31
+
32
+ if (!contentType || body.byteLength === 0) {
33
+ return body;
34
+ }
35
+
36
+ if (
37
+ contentType.includes("html") ||
38
+ contentType.includes("application/vnd.pypi.simple.v1+html")
39
+ ) {
40
+ return modifyHtmlSimpleResponse(
41
+ body,
42
+ headers,
43
+ metadataUrl,
44
+ isNewlyReleasedPackage,
45
+ packageName
46
+ );
47
+ }
48
+
49
+ if (
50
+ contentType.includes("json") ||
51
+ contentType.includes("application/vnd.pypi.simple.v1+json")
52
+ ) {
53
+ return modifyJsonResponse(
54
+ body,
55
+ headers,
56
+ metadataUrl,
57
+ isNewlyReleasedPackage,
58
+ packageName
59
+ );
60
+ }
61
+
62
+ return body;
63
+ } catch (/** @type {any} */ err) {
64
+ ui.writeVerbose(
65
+ `Safe-chain: PyPI package metadata not in expected format - bypassing modification. Error: ${err.message}`
66
+ );
67
+ return body;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * @param {Buffer} body
73
+ * @param {NodeJS.Dict<string | string[]> | undefined} headers
74
+ * @param {string} metadataUrl
75
+ * @param {(packageName: string | undefined, version: string | undefined) => boolean} isNewlyReleasedPackage
76
+ * @param {string} packageName
77
+ * @returns {Buffer}
78
+ */
79
+ function modifyHtmlSimpleResponse(
80
+ body,
81
+ headers,
82
+ metadataUrl,
83
+ isNewlyReleasedPackage,
84
+ packageName
85
+ ) {
86
+ const html = body.toString("utf8");
87
+ let modified = false;
88
+ const rewriteHtmlAnchor = createHtmlAnchorRewriter(
89
+ metadataUrl,
90
+ isNewlyReleasedPackage,
91
+ packageName,
92
+ () => {
93
+ modified = true;
94
+ }
95
+ );
96
+ const updatedHtml = html.replace(HTML_ANCHOR_HREF_RE, rewriteHtmlAnchor);
97
+
98
+ if (!modified) return body;
99
+ const modifiedBuffer = Buffer.from(updatedHtml);
100
+ clearCachingHeaders(headers);
101
+ return modifiedBuffer;
102
+ }
103
+
104
+ /**
105
+ * @param {string} metadataUrl
106
+ * @param {(packageName: string | undefined, version: string | undefined) => boolean} isNewlyReleasedPackage
107
+ * @param {string} packageName
108
+ * @param {() => void} onModified
109
+ * @returns {(anchor: string, quote: string, href: string) => string}
110
+ */
111
+ function createHtmlAnchorRewriter(
112
+ metadataUrl,
113
+ isNewlyReleasedPackage,
114
+ packageName,
115
+ onModified
116
+ ) {
117
+ return (anchor, _quote, href) => {
118
+ const resolvedHref = new URL(href, metadataUrl).toString();
119
+ const { packageName: hrefPackageName, version } = parsePipPackageFromUrl(
120
+ resolvedHref,
121
+ new URL(resolvedHref).host
122
+ );
123
+
124
+ if (
125
+ hrefPackageName &&
126
+ normalizePipPackageName(hrefPackageName) ===
127
+ normalizePipPackageName(packageName) &&
128
+ version &&
129
+ isNewlyReleasedPackage(packageName, version)
130
+ ) {
131
+ onModified();
132
+ logSuppressedVersion(packageName, version);
133
+ return "";
134
+ }
135
+
136
+ return anchor;
137
+ };
138
+ }
139
+
140
+ /**
141
+ * @param {Buffer} body
142
+ * @param {NodeJS.Dict<string | string[]> | undefined} headers
143
+ * @param {string} metadataUrl
144
+ * @param {(packageName: string | undefined, version: string | undefined) => boolean} isNewlyReleasedPackage
145
+ * @param {string} packageName
146
+ * @returns {Buffer}
147
+ */
148
+ function modifyJsonResponse(
149
+ body,
150
+ headers,
151
+ metadataUrl,
152
+ isNewlyReleasedPackage,
153
+ packageName
154
+ ) {
155
+ const json = JSON.parse(body.toString("utf8"));
156
+ const modified = modifyPipJsonResponse(
157
+ json,
158
+ metadataUrl,
159
+ isNewlyReleasedPackage,
160
+ packageName
161
+ );
162
+
163
+ if (!modified) return body;
164
+ const modifiedBuffer = Buffer.from(JSON.stringify(json));
165
+ clearCachingHeaders(headers);
166
+ return modifiedBuffer;
167
+ }
@@ -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
+ }