Keep track of amount of malware packages blocked

This commit is contained in:
Sander Declerck 2025-11-07 11:39:41 +01:00
parent f4694ba119
commit 1f570a9f39
No known key found for this signature in database
5 changed files with 65 additions and 8 deletions

View file

@ -8,8 +8,11 @@
* *
* @typedef {Object} Interceptor * @typedef {Object} Interceptor
* @property {(targetUrl: string) => Promise<RequestInterceptor>} handleRequest * @property {(targetUrl: string) => Promise<RequestInterceptor>} 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"; import { createRequestInterceptorBuilder } from "./requestInterceptorBuilder.js";
/** /**
@ -36,9 +39,14 @@ export function createInterceptorBuilder() {
* @returns {Interceptor} * @returns {Interceptor}
*/ */
function buildInterceptor(requestHandlers) { function buildInterceptor(requestHandlers) {
const eventEmitter = new EventEmitter();
return { return {
async handleRequest(targetUrl) { async handleRequest(targetUrl) {
const reqInterceptorBuilder = createRequestInterceptorBuilder(targetUrl); const reqInterceptorBuilder = createRequestInterceptorBuilder(
targetUrl,
eventEmitter
);
for (const handler of requestHandlers) { for (const handler of requestHandlers) {
await handler(reqInterceptorBuilder); await handler(reqInterceptorBuilder);
@ -46,5 +54,12 @@ function buildInterceptor(requestHandlers) {
return reqInterceptorBuilder.build(); return reqInterceptorBuilder.build();
}, },
on(event, listener) {
eventEmitter.on(event, listener);
return this;
},
emit(event, ...args) {
return eventEmitter.emit(event, ...args);
},
}; };
} }

View file

@ -30,7 +30,7 @@ function buildNpmInterceptor(registry) {
registry registry
); );
if (await isMalwarePackage(packageName, version)) { 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 * @param {string} registry
* @returns {{packageName: string | undefined, version: string | undefined}} * @returns {{packageName: string | undefined, version: string | undefined}}
*/ */
export function parseNpmPackageUrl(url, registry) { function parseNpmPackageUrl(url, registry) {
let packageName, version; let packageName, version;
if (!registry || !url.endsWith(".tgz")) { if (!registry || !url.endsWith(".tgz")) {
return { packageName, version }; return { packageName, version };

View file

@ -35,7 +35,7 @@ function buildPipInterceptor(registry) {
registry registry
); );
if (await isMalwarePackage(packageName, version)) { if (await isMalwarePackage(packageName, version)) {
req.blockRequest(403, "Forbidden - blocked by safe-chain"); req.blockMalware(packageName, version, req.targetUrl);
} }
}); });

View file

@ -2,6 +2,7 @@
* @typedef {Object} RequestInterceptorBuilder * @typedef {Object} RequestInterceptorBuilder
* @property {string} targetUrl * @property {string} targetUrl
* @property {(statusCode: number, message: string) => void} blockRequest * @property {(statusCode: number, message: string) => void} blockRequest
* @property {(packageName: string | undefined, version: string | undefined, url: string) => void} blockMalware
* @property {() => RequestInterceptor} build * @property {() => RequestInterceptor} build
* *
* @typedef {Object} RequestInterceptor * @typedef {Object} RequestInterceptor
@ -10,17 +11,42 @@
/** /**
* @param {string} targetUrl * @param {string} targetUrl
* @param {import('events').EventEmitter} eventEmitter
* @returns {RequestInterceptorBuilder} * @returns {RequestInterceptorBuilder}
*/ */
export function createRequestInterceptorBuilder(targetUrl) { export function createRequestInterceptorBuilder(targetUrl, eventEmitter) {
/** @type {{statusCode: number, message: string} | undefined} */ /** @type {{statusCode: number, message: string} | undefined} */
let blockResponse = 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 { return {
targetUrl, targetUrl,
blockRequest(statusCode, message) { blockRequest,
blockResponse = { statusCode, message }; blockMalware,
},
build() { build() {
return { return {
blockResponse, blockResponse,

View file

@ -6,6 +6,7 @@ import { getCaCertPath } from "./certUtils.js";
import { ui } from "../environment/userInteraction.js"; import { ui } from "../environment/userInteraction.js";
import chalk from "chalk"; import chalk from "chalk";
import { createInterceptorForUrl } from "./interceptors/createInterceptorForEcoSystem.js"; import { createInterceptorForUrl } from "./interceptors/createInterceptorForEcoSystem.js";
import { on } from "events";
const SERVER_STOP_TIMEOUT_MS = 1000; const SERVER_STOP_TIMEOUT_MS = 1000;
/** /**
@ -133,6 +134,11 @@ function handleConnect(req, clientSocket, head) {
const interceptor = createInterceptorForUrl(req.url || ""); const interceptor = createInterceptorForUrl(req.url || "");
if (interceptor) { if (interceptor) {
// Subscribe to malware blocked events
interceptor.on("malwareBlocked", (event) => {
onMalwareBlocked(event.packageName, event.version, event.url);
});
mitmConnect(req, clientSocket, interceptor); mitmConnect(req, clientSocket, interceptor);
} else { } else {
// For other hosts, just tunnel the request to the destination tcp socket // 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() { function verifyNoMaliciousPackages() {
if (state.blockedRequests.length === 0) { if (state.blockedRequests.length === 0) {
// No malicious packages were blocked, so nothing to block // No malicious packages were blocked, so nothing to block