@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.
- package/LICENSE +674 -0
- package/README.md +517 -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-yarn.js +14 -0
- package/bin/safe-chain.js +130 -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 +4069 -0
- package/package.json +72 -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/settings.js +247 -0
- package/src/environment/environment.js +14 -0
- package/src/environment/userInteraction.js +122 -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 +79 -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/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 +304 -0
- package/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh +22 -0
- package/src/shell-integration/path-wrappers/templates/windows-wrapper.template.cmd +24 -0
- package/src/shell-integration/setup-ci.js +172 -0
- package/src/shell-integration/setup.js +129 -0
- package/src/shell-integration/shellDetection.js +39 -0
- package/src/shell-integration/startup-scripts/init-fish.fish +115 -0
- package/src/shell-integration/startup-scripts/init-posix.sh +96 -0
- package/src/shell-integration/startup-scripts/init-pwsh.ps1 +171 -0
- package/src/shell-integration/supported-shells/bash.js +152 -0
- package/src/shell-integration/supported-shells/fish.js +95 -0
- package/src/shell-integration/supported-shells/powershell.js +100 -0
- package/src/shell-integration/supported-shells/windowsPowershell.js +100 -0
- package/src/shell-integration/supported-shells/zsh.js +92 -0
- package/src/shell-integration/teardown.js +112 -0
- package/src/ultimate/ultimateTroubleshooting.js +111 -0
- package/src/utils/safeSpawn.js +153 -0
- 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
|
+
}
|