@aikidosec/safe-chain 0.0.1-immutable-releases-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.
Files changed (112) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +517 -0
  3. package/bin/aikido-bun.js +14 -0
  4. package/bin/aikido-bunx.js +14 -0
  5. package/bin/aikido-npm.js +14 -0
  6. package/bin/aikido-npx.js +14 -0
  7. package/bin/aikido-pip.js +17 -0
  8. package/bin/aikido-pip3.js +17 -0
  9. package/bin/aikido-pipx.js +16 -0
  10. package/bin/aikido-pnpm.js +14 -0
  11. package/bin/aikido-pnpx.js +14 -0
  12. package/bin/aikido-poetry.js +13 -0
  13. package/bin/aikido-python.js +19 -0
  14. package/bin/aikido-python3.js +19 -0
  15. package/bin/aikido-uv.js +16 -0
  16. package/bin/aikido-yarn.js +14 -0
  17. package/bin/safe-chain.js +130 -0
  18. package/docs/banner.svg +151 -0
  19. package/docs/safe-package-manager-demo.gif +0 -0
  20. package/docs/safe-package-manager-demo.png +0 -0
  21. package/docs/shell-integration.md +149 -0
  22. package/docs/troubleshooting.md +321 -0
  23. package/npm-shrinkwrap.json +4069 -0
  24. package/package.json +72 -0
  25. package/src/api/aikido.js +187 -0
  26. package/src/api/npmApi.js +71 -0
  27. package/src/config/cliArguments.js +161 -0
  28. package/src/config/configFile.js +327 -0
  29. package/src/config/environmentVariables.js +57 -0
  30. package/src/config/settings.js +247 -0
  31. package/src/environment/environment.js +14 -0
  32. package/src/environment/userInteraction.js +122 -0
  33. package/src/main.js +123 -0
  34. package/src/packagemanager/_shared/commandErrors.js +17 -0
  35. package/src/packagemanager/_shared/matchesCommand.js +18 -0
  36. package/src/packagemanager/bun/createBunPackageManager.js +48 -0
  37. package/src/packagemanager/currentPackageManager.js +79 -0
  38. package/src/packagemanager/npm/createPackageManager.js +72 -0
  39. package/src/packagemanager/npm/dependencyScanner/commandArgumentScanner.js +74 -0
  40. package/src/packagemanager/npm/dependencyScanner/nullScanner.js +9 -0
  41. package/src/packagemanager/npm/parsing/parsePackagesFromInstallArgs.js +144 -0
  42. package/src/packagemanager/npm/runNpmCommand.js +20 -0
  43. package/src/packagemanager/npm/utils/abbrevs-generated.js +359 -0
  44. package/src/packagemanager/npm/utils/cmd-list.js +174 -0
  45. package/src/packagemanager/npm/utils/npmCommands.js +34 -0
  46. package/src/packagemanager/npx/createPackageManager.js +15 -0
  47. package/src/packagemanager/npx/dependencyScanner/commandArgumentScanner.js +43 -0
  48. package/src/packagemanager/npx/parsing/parsePackagesFromArguments.js +130 -0
  49. package/src/packagemanager/npx/runNpxCommand.js +20 -0
  50. package/src/packagemanager/pip/createPackageManager.js +25 -0
  51. package/src/packagemanager/pip/pipSettings.js +6 -0
  52. package/src/packagemanager/pip/runPipCommand.js +209 -0
  53. package/src/packagemanager/pipx/createPipXPackageManager.js +18 -0
  54. package/src/packagemanager/pipx/runPipXCommand.js +60 -0
  55. package/src/packagemanager/pnpm/createPackageManager.js +57 -0
  56. package/src/packagemanager/pnpm/dependencyScanner/commandArgumentScanner.js +35 -0
  57. package/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.js +109 -0
  58. package/src/packagemanager/pnpm/runPnpmCommand.js +32 -0
  59. package/src/packagemanager/poetry/createPoetryPackageManager.js +72 -0
  60. package/src/packagemanager/uv/createUvPackageManager.js +18 -0
  61. package/src/packagemanager/uv/runUvCommand.js +66 -0
  62. package/src/packagemanager/yarn/createPackageManager.js +41 -0
  63. package/src/packagemanager/yarn/dependencyScanner/commandArgumentScanner.js +35 -0
  64. package/src/packagemanager/yarn/parsing/parsePackagesFromArguments.js +128 -0
  65. package/src/packagemanager/yarn/runYarnCommand.js +36 -0
  66. package/src/registryProxy/certBundle.js +203 -0
  67. package/src/registryProxy/certUtils.js +178 -0
  68. package/src/registryProxy/getConnectTimeout.js +13 -0
  69. package/src/registryProxy/http-utils.js +80 -0
  70. package/src/registryProxy/interceptors/createInterceptorForEcoSystem.js +25 -0
  71. package/src/registryProxy/interceptors/interceptorBuilder.js +179 -0
  72. package/src/registryProxy/interceptors/minimumPackageAgeExclusions.js +33 -0
  73. package/src/registryProxy/interceptors/npm/modifyNpmInfo.js +180 -0
  74. package/src/registryProxy/interceptors/npm/npmInterceptor.js +101 -0
  75. package/src/registryProxy/interceptors/npm/parseNpmPackageUrl.js +60 -0
  76. package/src/registryProxy/interceptors/pip/modifyPipInfo.js +167 -0
  77. package/src/registryProxy/interceptors/pip/modifyPipJsonResponse.js +176 -0
  78. package/src/registryProxy/interceptors/pip/parsePipPackageUrl.js +162 -0
  79. package/src/registryProxy/interceptors/pip/pipInterceptor.js +122 -0
  80. package/src/registryProxy/interceptors/pip/pipMetadataResponseUtils.js +27 -0
  81. package/src/registryProxy/interceptors/pip/pipMetadataVersionUtils.js +131 -0
  82. package/src/registryProxy/interceptors/suppressedVersionsState.js +21 -0
  83. package/src/registryProxy/isImdsEndpoint.js +13 -0
  84. package/src/registryProxy/mitmRequestHandler.js +240 -0
  85. package/src/registryProxy/plainHttpProxy.js +95 -0
  86. package/src/registryProxy/registryProxy.js +255 -0
  87. package/src/registryProxy/tunnelRequestHandler.js +213 -0
  88. package/src/scanning/audit/index.js +129 -0
  89. package/src/scanning/index.js +82 -0
  90. package/src/scanning/malwareDatabase.js +131 -0
  91. package/src/scanning/newPackagesDatabaseBuilder.js +71 -0
  92. package/src/scanning/newPackagesDatabaseWarnings.js +17 -0
  93. package/src/scanning/newPackagesListCache.js +126 -0
  94. package/src/scanning/packageNameVariants.js +29 -0
  95. package/src/shell-integration/helpers.js +304 -0
  96. package/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh +22 -0
  97. package/src/shell-integration/path-wrappers/templates/windows-wrapper.template.cmd +24 -0
  98. package/src/shell-integration/setup-ci.js +172 -0
  99. package/src/shell-integration/setup.js +129 -0
  100. package/src/shell-integration/shellDetection.js +39 -0
  101. package/src/shell-integration/startup-scripts/init-fish.fish +115 -0
  102. package/src/shell-integration/startup-scripts/init-posix.sh +96 -0
  103. package/src/shell-integration/startup-scripts/init-pwsh.ps1 +171 -0
  104. package/src/shell-integration/supported-shells/bash.js +152 -0
  105. package/src/shell-integration/supported-shells/fish.js +95 -0
  106. package/src/shell-integration/supported-shells/powershell.js +100 -0
  107. package/src/shell-integration/supported-shells/windowsPowershell.js +100 -0
  108. package/src/shell-integration/supported-shells/zsh.js +92 -0
  109. package/src/shell-integration/teardown.js +112 -0
  110. package/src/ultimate/ultimateTroubleshooting.js +111 -0
  111. package/src/utils/safeSpawn.js +153 -0
  112. 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
+ }