diff --git a/packages/safe-chain/src/registryProxy/interceptors/interceptorBuilder.js b/packages/safe-chain/src/registryProxy/interceptors/interceptorBuilder.js index 574abb9..73bde02 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/interceptorBuilder.js +++ b/packages/safe-chain/src/registryProxy/interceptors/interceptorBuilder.js @@ -8,8 +8,11 @@ * * @typedef {Object} Interceptor * @property {(targetUrl: string) => Promise} handleRequest + * @property {(event: string, listener: (...args: any[]) => void) => Interceptor} on + * @property {(event: string, ...args: any[]) => boolean} emit */ +import { EventEmitter } from "events"; import { createRequestInterceptorBuilder } from "./requestInterceptorBuilder.js"; /** @@ -36,9 +39,14 @@ export function createInterceptorBuilder() { * @returns {Interceptor} */ function buildInterceptor(requestHandlers) { + const eventEmitter = new EventEmitter(); + return { async handleRequest(targetUrl) { - const reqInterceptorBuilder = createRequestInterceptorBuilder(targetUrl); + const reqInterceptorBuilder = createRequestInterceptorBuilder( + targetUrl, + eventEmitter + ); for (const handler of requestHandlers) { await handler(reqInterceptorBuilder); @@ -46,5 +54,12 @@ function buildInterceptor(requestHandlers) { return reqInterceptorBuilder.build(); }, + on(event, listener) { + eventEmitter.on(event, listener); + return this; + }, + emit(event, ...args) { + return eventEmitter.emit(event, ...args); + }, }; } diff --git a/packages/safe-chain/src/registryProxy/interceptors/npmInterceptor.js b/packages/safe-chain/src/registryProxy/interceptors/npmInterceptor.js index 557e9cb..6e33dd0 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/npmInterceptor.js +++ b/packages/safe-chain/src/registryProxy/interceptors/npmInterceptor.js @@ -30,7 +30,7 @@ function buildNpmInterceptor(registry) { registry ); if (await isMalwarePackage(packageName, version)) { - req.blockRequest(403, "Forbidden - blocked by safe-chain"); + req.blockMalware(packageName, version, req.targetUrl); } }); @@ -42,7 +42,7 @@ function buildNpmInterceptor(registry) { * @param {string} registry * @returns {{packageName: string | undefined, version: string | undefined}} */ -export function parseNpmPackageUrl(url, registry) { +function parseNpmPackageUrl(url, registry) { let packageName, version; if (!registry || !url.endsWith(".tgz")) { return { packageName, version }; diff --git a/packages/safe-chain/src/registryProxy/interceptors/pipInterceptor.js b/packages/safe-chain/src/registryProxy/interceptors/pipInterceptor.js index 90099b1..7d793d3 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/pipInterceptor.js +++ b/packages/safe-chain/src/registryProxy/interceptors/pipInterceptor.js @@ -35,7 +35,7 @@ function buildPipInterceptor(registry) { registry ); if (await isMalwarePackage(packageName, version)) { - req.blockRequest(403, "Forbidden - blocked by safe-chain"); + req.blockMalware(packageName, version, req.targetUrl); } }); diff --git a/packages/safe-chain/src/registryProxy/interceptors/requestInterceptorBuilder.js b/packages/safe-chain/src/registryProxy/interceptors/requestInterceptorBuilder.js index e0d560a..a8b98c6 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/requestInterceptorBuilder.js +++ b/packages/safe-chain/src/registryProxy/interceptors/requestInterceptorBuilder.js @@ -2,6 +2,7 @@ * @typedef {Object} RequestInterceptorBuilder * @property {string} targetUrl * @property {(statusCode: number, message: string) => void} blockRequest + * @property {(packageName: string | undefined, version: string | undefined, url: string) => void} blockMalware * @property {() => RequestInterceptor} build * * @typedef {Object} RequestInterceptor @@ -10,17 +11,42 @@ /** * @param {string} targetUrl + * @param {import('events').EventEmitter} eventEmitter * @returns {RequestInterceptorBuilder} */ -export function createRequestInterceptorBuilder(targetUrl) { +export function createRequestInterceptorBuilder(targetUrl, eventEmitter) { /** @type {{statusCode: number, message: string} | undefined} */ let blockResponse = undefined; + /** + * @param {number} statusCode + * @param {string} message + */ + function blockRequest(statusCode, message) { + blockResponse = { statusCode, message }; + } + + /** + * @param {string | undefined} packageName + * @param {string | undefined} version + * @param {string} url + */ + function blockMalware(packageName, version, url) { + blockRequest(403, "Forbidden - blocked by safe-chain"); + + // Emit the malwareBlocked event + eventEmitter.emit("malwareBlocked", { + packageName, + version, + url, + timestamp: Date.now(), + }); + } + return { targetUrl, - blockRequest(statusCode, message) { - blockResponse = { statusCode, message }; - }, + blockRequest, + blockMalware, build() { return { blockResponse, diff --git a/packages/safe-chain/src/registryProxy/registryProxy.js b/packages/safe-chain/src/registryProxy/registryProxy.js index d41a8bb..f366a93 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.js @@ -6,6 +6,7 @@ import { getCaCertPath } from "./certUtils.js"; import { ui } from "../environment/userInteraction.js"; import chalk from "chalk"; import { createInterceptorForUrl } from "./interceptors/createInterceptorForEcoSystem.js"; +import { on } from "events"; const SERVER_STOP_TIMEOUT_MS = 1000; /** @@ -133,6 +134,11 @@ function handleConnect(req, clientSocket, head) { const interceptor = createInterceptorForUrl(req.url || ""); if (interceptor) { + // Subscribe to malware blocked events + interceptor.on("malwareBlocked", (event) => { + onMalwareBlocked(event.packageName, event.version, event.url); + }); + mitmConnect(req, clientSocket, interceptor); } else { // For other hosts, just tunnel the request to the destination tcp socket @@ -141,6 +147,16 @@ function handleConnect(req, clientSocket, head) { } } +/** + * + * @param {string} packageName + * @param {string} version + * @param {string} url + */ +function onMalwareBlocked(packageName, version, url) { + state.blockedRequests.push({ packageName, version, url }); +} + function verifyNoMaliciousPackages() { if (state.blockedRequests.length === 0) { // No malicious packages were blocked, so nothing to block