@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,213 @@
1
+ import * as net from "net";
2
+ import { ui } from "../environment/userInteraction.js";
3
+ import { isImdsEndpoint } from "./isImdsEndpoint.js";
4
+ import { getConnectTimeout } from "./getConnectTimeout.js";
5
+
6
+ /** @type {string[]} */
7
+ let timedoutImdsEndpoints = [];
8
+
9
+ /**
10
+ * @param {import("http").IncomingMessage} req
11
+ * @param {import("http").ServerResponse} clientSocket
12
+ * @param {Buffer} head
13
+ *
14
+ * @returns {void}
15
+ */
16
+ export function tunnelRequest(req, clientSocket, head) {
17
+ const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy;
18
+
19
+ if (httpsProxy) {
20
+ // If an HTTPS proxy is set, tunnel the request via the proxy
21
+ // This is the system proxy, not the safe-chain proxy
22
+ // The package manager will run via the safe-chain proxy
23
+ // The safe-chain proxy will then send the request to the system proxy
24
+ // Typical flow: package manager -> safe-chain proxy -> system proxy -> destination
25
+
26
+ // There are 2 processes involved in this:
27
+ // 1. Safe-chain process: has HTTPS_PROXY set to system proxy
28
+ // 2. Package manager process: has HTTPS_PROXY set to safe-chain proxy
29
+
30
+ tunnelRequestViaProxy(req, clientSocket, head, httpsProxy);
31
+ } else {
32
+ tunnelRequestToDestination(req, clientSocket, head);
33
+ }
34
+ }
35
+
36
+ /**
37
+ * @param {import("http").IncomingMessage} req
38
+ * @param {import("http").ServerResponse} clientSocket
39
+ * @param {Buffer} head
40
+ *
41
+ * @returns {void}
42
+ */
43
+ function tunnelRequestToDestination(req, clientSocket, head) {
44
+ const { port, hostname } = new URL(`http://${req.url}`);
45
+ const isImds = isImdsEndpoint(hostname);
46
+ const targetPort = Number.parseInt(port) || 443;
47
+
48
+ if (timedoutImdsEndpoints.includes(hostname)) {
49
+ clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n");
50
+ if (isImds) {
51
+ ui.writeVerbose(
52
+ `Safe-chain: Closing connection because previously timedout connect to ${hostname}`
53
+ );
54
+ } else {
55
+ ui.writeError(
56
+ `Safe-chain: Closing connection because previously timedout connect to ${hostname}`
57
+ );
58
+ }
59
+ return;
60
+ }
61
+
62
+ const connectTimeout = getConnectTimeout(hostname);
63
+
64
+ // Use JS setTimeout for true connection timeout (not idle timeout).
65
+ // socket.setTimeout() measures inactivity, not time since connection attempt.
66
+ const connectTimer = setTimeout(() => {
67
+ if (isImds) {
68
+ timedoutImdsEndpoints.push(hostname);
69
+ ui.writeVerbose(
70
+ `Safe-chain: connect to ${hostname}:${targetPort} timed out after ${connectTimeout}ms`
71
+ );
72
+ } else {
73
+ ui.writeError(
74
+ `Safe-chain: connect to ${hostname}:${targetPort} timed out after ${connectTimeout}ms`
75
+ );
76
+ }
77
+ serverSocket.destroy();
78
+ if (clientSocket.writable) {
79
+ clientSocket.end("HTTP/1.1 504 Gateway Timeout\r\n\r\n");
80
+ }
81
+ }, connectTimeout);
82
+
83
+ const serverSocket = net.connect(targetPort, hostname, () => {
84
+ // Clear timer to prevent false timeout errors after successful connection
85
+ clearTimeout(connectTimer);
86
+
87
+ clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
88
+ serverSocket.write(head);
89
+ serverSocket.pipe(clientSocket);
90
+ clientSocket.pipe(serverSocket);
91
+ });
92
+
93
+ clientSocket.on("error", () => {
94
+ // This can happen if the client TCP socket sends RST instead of FIN.
95
+ // Not subscribing to 'error' event will cause node to throw and crash.
96
+ clearTimeout(connectTimer);
97
+ if (serverSocket.writable) {
98
+ serverSocket.end();
99
+ }
100
+ });
101
+
102
+ clientSocket.on("close", () => {
103
+ // Client closed connection - clean up server socket
104
+ clearTimeout(connectTimer);
105
+ if (serverSocket.writable) {
106
+ serverSocket.end();
107
+ }
108
+ });
109
+
110
+ serverSocket.on("error", (err) => {
111
+ clearTimeout(connectTimer);
112
+ if (isImds) {
113
+ ui.writeVerbose(
114
+ `Safe-chain: error connecting to ${hostname}:${targetPort} - ${err.message}`
115
+ );
116
+ } else {
117
+ ui.writeError(
118
+ `Safe-chain: error connecting to ${hostname}:${targetPort} - ${err.message}`
119
+ );
120
+ }
121
+ if (clientSocket.writable) {
122
+ clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n");
123
+ }
124
+ });
125
+
126
+ serverSocket.on("close", () => {
127
+ // Server closed connection - clean up client socket
128
+ clearTimeout(connectTimer);
129
+ if (clientSocket.writable) {
130
+ clientSocket.end();
131
+ }
132
+ });
133
+ }
134
+
135
+ /**
136
+ * @param {import("http").IncomingMessage} req
137
+ * @param {import("http").ServerResponse} clientSocket
138
+ * @param {Buffer} head
139
+ * @param {string} proxyUrl
140
+ */
141
+ function tunnelRequestViaProxy(req, clientSocket, head, proxyUrl) {
142
+ const { port, hostname } = new URL(`http://${req.url}`);
143
+ const proxy = new URL(proxyUrl);
144
+
145
+ // Connect to proxy server
146
+ const proxySocket = net.connect({
147
+ host: proxy.hostname,
148
+ port: Number.parseInt(proxy.port) || 80,
149
+ });
150
+
151
+ proxySocket.on("connect", () => {
152
+ // Send CONNECT request to proxy
153
+ const connectRequest = [
154
+ `CONNECT ${hostname}:${port || 443} HTTP/1.1`,
155
+ `Host: ${hostname}:${port || 443}`,
156
+ "",
157
+ "",
158
+ ].join("\r\n");
159
+
160
+ proxySocket.write(connectRequest);
161
+ });
162
+
163
+ let isConnected = false;
164
+ proxySocket.once("data", (data) => {
165
+ const response = data.toString();
166
+
167
+ // Check if CONNECT succeeded (HTTP/1.1 200)
168
+ if (response.startsWith("HTTP/1.1 200")) {
169
+ isConnected = true;
170
+ clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
171
+ proxySocket.write(head);
172
+ proxySocket.pipe(clientSocket);
173
+ clientSocket.pipe(proxySocket);
174
+ } else {
175
+ ui.writeError(
176
+ `Safe-chain: proxy CONNECT failed: ${response.split("\r\n")[0]}`
177
+ );
178
+ if (clientSocket.writable) {
179
+ clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n");
180
+ }
181
+ if (proxySocket.writable) {
182
+ proxySocket.end();
183
+ }
184
+ }
185
+ });
186
+
187
+ proxySocket.on("error", (err) => {
188
+ if (!isConnected) {
189
+ ui.writeError(
190
+ `Safe-chain: error connecting to proxy ${proxy.hostname}:${
191
+ proxy.port || 8080
192
+ } - ${err.message}`
193
+ );
194
+ if (clientSocket.writable) {
195
+ clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n");
196
+ }
197
+ } else {
198
+ ui.writeError(
199
+ `Safe-chain: proxy socket error after connection - ${err.message}`
200
+ );
201
+ if (clientSocket.writable) {
202
+ clientSocket.end();
203
+ }
204
+ }
205
+ });
206
+
207
+ clientSocket.on("error", () => {
208
+ if (proxySocket.writable) {
209
+ proxySocket.end();
210
+ }
211
+ });
212
+ }
213
+
@@ -0,0 +1,129 @@
1
+ import { ui } from "../../environment/userInteraction.js";
2
+ import {
3
+ MALWARE_STATUS_MALWARE,
4
+ openMalwareDatabase,
5
+ } from "../malwareDatabase.js";
6
+
7
+ /**
8
+ * @typedef {Object} PackageChange
9
+ * @property {string} name
10
+ * @property {string} version
11
+ * @property {string} type
12
+ */
13
+
14
+ /**
15
+ * @typedef {Object} AuditResult
16
+ * @property {PackageChange[]} allowedChanges
17
+ * @property {(PackageChange & {reason: string})[]} disallowedChanges
18
+ * @property {boolean} isAllowed
19
+ */
20
+
21
+ /**
22
+ * @typedef {Object} AuditStats
23
+ * @property {number} totalPackages
24
+ * @property {number} safePackages
25
+ * @property {number} malwarePackages
26
+ */
27
+
28
+ /**
29
+ * @type AuditStats
30
+ */
31
+ const auditStats = {
32
+ totalPackages: 0,
33
+ safePackages: 0,
34
+ malwarePackages: 0,
35
+ };
36
+
37
+ /**
38
+ * @returns {AuditStats}
39
+ */
40
+ export function getAuditStats() {
41
+ return auditStats;
42
+ }
43
+
44
+ /**
45
+ *
46
+ * @param {string | undefined} name
47
+ * @param {string | undefined} version
48
+ * @returns {Promise<boolean>}
49
+ */
50
+ export async function isMalwarePackage(name, version) {
51
+ if (!name || !version) {
52
+ return false;
53
+ }
54
+
55
+ const auditResult = await auditChanges([{ name, version, type: "add" }]);
56
+
57
+ return !auditResult.isAllowed;
58
+ }
59
+
60
+ /**
61
+ * @param {PackageChange[]} changes
62
+ *
63
+ * @returns {Promise<AuditResult>}
64
+ */
65
+ export async function auditChanges(changes) {
66
+ const allowedChanges = [];
67
+ const disallowedChanges = [];
68
+
69
+ var malwarePackages = await getPackagesWithMalware(
70
+ changes.filter(
71
+ (change) => change.type === "add" || change.type === "change"
72
+ )
73
+ );
74
+
75
+ for (const change of changes) {
76
+ const malwarePackage = malwarePackages.find(
77
+ (pkg) => pkg.name === change.name && pkg.version === change.version
78
+ );
79
+
80
+ if (malwarePackage) {
81
+ auditStats.malwarePackages += 1;
82
+ ui.writeVerbose(
83
+ `Safe-chain: Package ${change.name}@${change.version} is marked as malware: ${malwarePackage.status}`
84
+ );
85
+ disallowedChanges.push({ ...change, reason: malwarePackage.status });
86
+ } else {
87
+ auditStats.safePackages += 1;
88
+ ui.writeVerbose(
89
+ `Safe-chain: Package ${change.name}@${change.version} is clean`
90
+ );
91
+ allowedChanges.push(change);
92
+ }
93
+
94
+ auditStats.totalPackages += 1;
95
+ }
96
+
97
+ const auditResults = {
98
+ allowedChanges,
99
+ disallowedChanges,
100
+ isAllowed: disallowedChanges.length === 0,
101
+ };
102
+
103
+ return auditResults;
104
+ }
105
+
106
+ /**
107
+ * @param {{name: string, version: string, type: string}[]} changes
108
+ * @returns {Promise<{name: string, version: string, status: string}[]>}
109
+ */
110
+ async function getPackagesWithMalware(changes) {
111
+ if (changes.length === 0) {
112
+ return [];
113
+ }
114
+
115
+ const malwareDb = await openMalwareDatabase();
116
+ let allVulnerablePackages = [];
117
+
118
+ for (const change of changes) {
119
+ if (malwareDb.isMalware(change.name, change.version)) {
120
+ allVulnerablePackages.push({
121
+ name: change.name,
122
+ version: change.version,
123
+ status: MALWARE_STATUS_MALWARE,
124
+ });
125
+ }
126
+ }
127
+
128
+ return allVulnerablePackages;
129
+ }
@@ -0,0 +1,82 @@
1
+ import { auditChanges } from "./audit/index.js";
2
+ import { getScanTimeout } from "../config/configFile.js";
3
+ import { setTimeout } from "timers/promises";
4
+ import chalk from "chalk";
5
+ import { getPackageManager } from "../packagemanager/currentPackageManager.js";
6
+ import { ui } from "../environment/userInteraction.js";
7
+
8
+ /**
9
+ * @param {string[]} args
10
+ *
11
+ * @returns {boolean}
12
+ */
13
+ export function shouldScanCommand(args) {
14
+ if (!args || args.length === 0) {
15
+ return false;
16
+ }
17
+
18
+ return getPackageManager().isSupportedCommand(args);
19
+ }
20
+
21
+ /**
22
+ * @param {string[]} args
23
+ *
24
+ * @returns {Promise<number>}
25
+ */
26
+ export async function scanCommand(args) {
27
+ if (!shouldScanCommand(args)) {
28
+ return 0;
29
+ }
30
+
31
+ let timedOut = false;
32
+ /** @type {import("./audit/index.js").AuditResult | undefined} */
33
+ let audit;
34
+
35
+ await Promise.race([
36
+ (async () => {
37
+ const packageManager = getPackageManager();
38
+ const changes = await packageManager.getDependencyUpdatesForCommand(args);
39
+
40
+ if (timedOut) {
41
+ return;
42
+ }
43
+
44
+ audit = await auditChanges(changes);
45
+ })(),
46
+ setTimeout(getScanTimeout()).then(() => {
47
+ timedOut = true;
48
+ }),
49
+ ]);
50
+
51
+ if (timedOut) {
52
+ throw new Error("Timeout exceeded while scanning npm install command.");
53
+ }
54
+
55
+ if (!audit || audit.isAllowed) {
56
+ return 0;
57
+ } else {
58
+ printMaliciousChanges(audit.disallowedChanges);
59
+ onMalwareFound();
60
+ return 1;
61
+ }
62
+ }
63
+
64
+ /**
65
+ * @param {import("./audit/index.js").PackageChange[]} changes
66
+ * @return {void}
67
+ */
68
+ function printMaliciousChanges(changes) {
69
+ ui.writeInformation(
70
+ chalk.red("✖") + " Safe-chain: " + chalk.bold("Malicious changes detected:")
71
+ );
72
+
73
+ for (const change of changes) {
74
+ ui.writeInformation(` - ${change.name}@${change.version}`);
75
+ }
76
+ }
77
+
78
+ function onMalwareFound() {
79
+ ui.emptyLine();
80
+ ui.writeExitWithoutInstallingMaliciousPackages();
81
+ ui.emptyLine();
82
+ }
@@ -0,0 +1,131 @@
1
+ import {
2
+ fetchMalwareDatabase,
3
+ fetchMalwareDatabaseVersion,
4
+ } from "../api/aikido.js";
5
+ import {
6
+ readDatabaseFromLocalCache,
7
+ writeDatabaseToLocalCache,
8
+ } from "../config/configFile.js";
9
+ import { ui } from "../environment/userInteraction.js";
10
+ import { getEcoSystem, ECOSYSTEM_PY } from "../config/settings.js";
11
+
12
+ /**
13
+ * @typedef {Object} MalwareDatabase
14
+ * @property {function(string, string): string} getPackageStatus
15
+ * @property {function(string, string): boolean} isMalware
16
+ */
17
+
18
+ /** @type {MalwareDatabase | null} */
19
+ let cachedMalwareDatabase = null;
20
+
21
+ /**
22
+ * Normalize package name for comparison.
23
+ * For Python packages (PEP-503): lowercase and replace _, -, . with -
24
+ * For js packages: keep as-is (case-sensitive)
25
+ * @param {string} name
26
+ * @returns {string}
27
+ */
28
+ function normalizePackageName(name) {
29
+ const ecosystem = getEcoSystem();
30
+ if (ecosystem === ECOSYSTEM_PY) {
31
+ return name.toLowerCase().replace(/[-_.]+/g, "-");
32
+ }
33
+
34
+ return name;
35
+ }
36
+
37
+ export async function openMalwareDatabase() {
38
+ if (cachedMalwareDatabase) {
39
+ return cachedMalwareDatabase;
40
+ }
41
+
42
+ const malwareDatabase = await getMalwareDatabase();
43
+
44
+ /**
45
+ * @param {string} name
46
+ * @param {string} version
47
+ * @returns {string}
48
+ */
49
+ function getPackageStatus(name, version) {
50
+ const normalizedName = normalizePackageName(name);
51
+ const packageData = malwareDatabase.find(
52
+ (pkg) => {
53
+ const normalizedPkgName = normalizePackageName(pkg.package_name);
54
+ return normalizedPkgName === normalizedName &&
55
+ (pkg.version === version || pkg.version === "*");
56
+ }
57
+ );
58
+
59
+ if (!packageData) {
60
+ return MALWARE_STATUS_OK;
61
+ }
62
+
63
+ return packageData.reason;
64
+ }
65
+
66
+ // This implicitly caches the malware database
67
+ // that's closed over by the getPackageStatus function
68
+ cachedMalwareDatabase = {
69
+ getPackageStatus,
70
+ isMalware: (name, version) => {
71
+ const status = getPackageStatus(name, version);
72
+ return isMalwareStatus(status);
73
+ },
74
+ };
75
+ return cachedMalwareDatabase;
76
+ }
77
+
78
+ /**
79
+ * @returns {Promise<import("../api/aikido.js").MalwarePackage[]>}
80
+ */
81
+ async function getMalwareDatabase() {
82
+ const { malwareDatabase: cachedDatabase, version: cachedVersion } =
83
+ readDatabaseFromLocalCache();
84
+
85
+ try {
86
+ if (cachedDatabase) {
87
+ const currentVersion = await fetchMalwareDatabaseVersion();
88
+ if (cachedVersion === currentVersion) {
89
+ return cachedDatabase;
90
+ }
91
+ }
92
+
93
+ const { malwareDatabase, version } = await fetchMalwareDatabase();
94
+
95
+ if (version) {
96
+ // Only cache the malware database when we have a version.
97
+ writeDatabaseToLocalCache(malwareDatabase, version);
98
+ return malwareDatabase;
99
+ } else {
100
+ // We received a valid malware database, but the response
101
+ // did not contain an etag header with the version
102
+ ui.writeWarning(
103
+ "The malware database was downloaded, but could not be cached due to a missing version."
104
+ );
105
+ return malwareDatabase;
106
+ }
107
+ } catch (/** @type any */ error) {
108
+ if (cachedDatabase) {
109
+ ui.writeWarning(
110
+ "Failed to fetch the latest malware database. Using cached version."
111
+ );
112
+ return cachedDatabase;
113
+ }
114
+ throw error;
115
+ }
116
+ }
117
+
118
+ /**
119
+ * @param {string} status
120
+ *
121
+ * @returns {boolean}
122
+ */
123
+ function isMalwareStatus(status) {
124
+ let malwareStatus = status.toUpperCase();
125
+ return malwareStatus === MALWARE_STATUS_MALWARE;
126
+ }
127
+
128
+ export const MALWARE_STATUS_OK = "OK";
129
+ export const MALWARE_STATUS_MALWARE = "MALWARE";
130
+ export const MALWARE_STATUS_TELEMETRY = "TELEMETRY";
131
+ export const MALWARE_STATUS_PROTESTWARE = "PROTESTWARE";
@@ -0,0 +1,71 @@
1
+ import {
2
+ getMinimumPackageAgeHours,
3
+ getEcoSystem,
4
+ ECOSYSTEM_JS,
5
+ ECOSYSTEM_PY,
6
+ } from "../config/settings.js";
7
+ import { getEquivalentPackageNames } from "./packageNameVariants.js";
8
+
9
+ /**
10
+ * @typedef {Object} NewPackagesDatabase
11
+ * @property {function(string | undefined, string | undefined): boolean} isNewlyReleasedPackage
12
+ */
13
+
14
+ /**
15
+ * Returns the ecosystem identifier expected in upstream/core release feeds.
16
+ * @returns {string}
17
+ */
18
+ function getCurrentFeedSource() {
19
+ const ecosystem = getEcoSystem();
20
+
21
+ if (ecosystem === ECOSYSTEM_JS) {
22
+ return "npm";
23
+ }
24
+
25
+ if (ecosystem === ECOSYSTEM_PY) {
26
+ return "pypi";
27
+ }
28
+
29
+ return ecosystem;
30
+ }
31
+
32
+ /**
33
+ * @param {import("../api/aikido.js").NewPackageEntry[]} newPackagesList
34
+ * @returns {NewPackagesDatabase}
35
+ */
36
+ export function buildNewPackagesDatabase(newPackagesList) {
37
+ const ecosystem = getEcoSystem();
38
+
39
+ /**
40
+ * @param {string | undefined} name
41
+ * @param {string | undefined} version
42
+ * @returns {boolean}
43
+ */
44
+ function isNewlyReleasedPackage(name, version) {
45
+ if (!name || !version) {
46
+ return false;
47
+ }
48
+
49
+ const cutOff = new Date(
50
+ new Date().getTime() - getMinimumPackageAgeHours() * 3600 * 1000
51
+ );
52
+ const expectedSource = getCurrentFeedSource();
53
+ const candidateNames = getEquivalentPackageNames(name, ecosystem);
54
+
55
+ const entry = newPackagesList.find(
56
+ (pkg) =>
57
+ (!pkg.source || pkg.source.toLowerCase() === expectedSource) &&
58
+ candidateNames.includes(pkg.package_name) &&
59
+ pkg.version === version
60
+ );
61
+
62
+ if (!entry) {
63
+ return false;
64
+ }
65
+
66
+ const releasedOn = new Date(entry.released_on * 1000);
67
+ return releasedOn > cutOff;
68
+ }
69
+
70
+ return { isNewlyReleasedPackage };
71
+ }
@@ -0,0 +1,17 @@
1
+ import { ui } from "../environment/userInteraction.js";
2
+
3
+ let hasWarnedAboutUnavailableNewPackagesDatabase = false;
4
+
5
+ /** @param {Error} error */
6
+ export function warnOnceAboutUnavailableDatabase(error) {
7
+ if (!hasWarnedAboutUnavailableNewPackagesDatabase) {
8
+ ui.writeWarning(
9
+ `Failed to load the new packages list used for direct package download request blocking. Continuing with metadata-based minimum age checks only. ${error.message}`
10
+ );
11
+ hasWarnedAboutUnavailableNewPackagesDatabase = true;
12
+ }
13
+ }
14
+
15
+ export function resetWarningState() {
16
+ hasWarnedAboutUnavailableNewPackagesDatabase = false;
17
+ }