mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 12:10:49 +00:00
Merge branch 'main' into package-min-age
This commit is contained in:
commit
3b905d490b
33 changed files with 407 additions and 970 deletions
|
|
@ -1,41 +1,32 @@
|
|||
import { EventEmitter } from "events";
|
||||
|
||||
/**
|
||||
* @typedef {import('./requestInterceptorBuilder.js').RequestInterceptorBuilder} RequestInterceptorBuilder
|
||||
* @typedef {import('./requestInterceptorBuilder.js').RequestInterceptor} RequestInterceptor
|
||||
*
|
||||
* @typedef {Object} InterceptorBuilder
|
||||
* @property {(requestFunc: (requestHandlerBuilder: RequestInterceptorBuilder) => Promise<void>) => void} onRequest
|
||||
* @property {() => Interceptor} build
|
||||
*
|
||||
* @typedef {Object} Interceptor
|
||||
* @property {(targetUrl: string) => Promise<RequestInterceptor>} handleRequest
|
||||
* @property {(targetUrl: string) => Promise<RequestInterceptionHandler>} handleRequest
|
||||
* @property {(event: string, listener: (...args: any[]) => void) => Interceptor} on
|
||||
* @property {(event: string, ...args: any[]) => boolean} emit
|
||||
*
|
||||
*
|
||||
* @typedef {Object} RequestInterceptionContext
|
||||
* @property {string} targetUrl
|
||||
* @property {(packageName: string | undefined, version: string | undefined) => void} blockMalware
|
||||
* @property {() => RequestInterceptionHandler} build
|
||||
*
|
||||
*
|
||||
* @typedef {Object} RequestInterceptionHandler
|
||||
* @property {{statusCode: number, message: string} | undefined} blockResponse
|
||||
*/
|
||||
|
||||
import { EventEmitter } from "events";
|
||||
import { createRequestInterceptorBuilder } from "./requestInterceptorBuilder.js";
|
||||
|
||||
/**
|
||||
* @returns {InterceptorBuilder}
|
||||
* @param {(requestHandlerBuilder: RequestInterceptionContext) => Promise<void>} requestInterceptionFunc
|
||||
* @returns {Interceptor}
|
||||
*/
|
||||
export function createInterceptorBuilder() {
|
||||
/**
|
||||
* @type {Array<(requestHandlerBuilder: RequestInterceptorBuilder) => Promise<void>>}
|
||||
*/
|
||||
const requestHandlers = [];
|
||||
|
||||
return {
|
||||
onRequest(requestFunc) {
|
||||
requestHandlers.push(requestFunc);
|
||||
},
|
||||
build() {
|
||||
return buildInterceptor(requestHandlers);
|
||||
},
|
||||
};
|
||||
export function interceptRequests(requestInterceptionFunc) {
|
||||
return buildInterceptor([requestInterceptionFunc]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<(requestHandlerBuilder: RequestInterceptorBuilder) => Promise<void>>} requestHandlers
|
||||
* @param {Array<(requestHandlerBuilder: RequestInterceptionContext) => Promise<void>>} requestHandlers
|
||||
* @returns {Interceptor}
|
||||
*/
|
||||
function buildInterceptor(requestHandlers) {
|
||||
|
|
@ -43,16 +34,13 @@ function buildInterceptor(requestHandlers) {
|
|||
|
||||
return {
|
||||
async handleRequest(targetUrl) {
|
||||
const reqInterceptorBuilder = createRequestInterceptorBuilder(
|
||||
targetUrl,
|
||||
eventEmitter
|
||||
);
|
||||
const requestContext = createRequestContext(targetUrl, eventEmitter);
|
||||
|
||||
for (const handler of requestHandlers) {
|
||||
await handler(reqInterceptorBuilder);
|
||||
await handler(requestContext);
|
||||
}
|
||||
|
||||
return reqInterceptorBuilder.build();
|
||||
return requestContext.build();
|
||||
},
|
||||
on(event, listener) {
|
||||
eventEmitter.on(event, listener);
|
||||
|
|
@ -63,3 +51,42 @@ function buildInterceptor(requestHandlers) {
|
|||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} targetUrl
|
||||
* @param {import('events').EventEmitter} eventEmitter
|
||||
* @returns {RequestInterceptionContext}
|
||||
*/
|
||||
function createRequestContext(targetUrl, eventEmitter) {
|
||||
/** @type {{statusCode: number, message: string} | undefined} */
|
||||
let blockResponse = undefined;
|
||||
|
||||
/**
|
||||
* @param {string | undefined} packageName
|
||||
* @param {string | undefined} version
|
||||
*/
|
||||
function blockMalware(packageName, version) {
|
||||
blockResponse = {
|
||||
statusCode: 403,
|
||||
message: "Forbidden - blocked by safe-chain",
|
||||
};
|
||||
|
||||
// Emit the malwareBlocked event
|
||||
eventEmitter.emit("malwareBlocked", {
|
||||
packageName,
|
||||
version,
|
||||
targetUrl,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
targetUrl,
|
||||
blockMalware,
|
||||
build() {
|
||||
return {
|
||||
blockResponse,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
import chalk from "chalk";
|
||||
import { getMinimumPackageAgeHours } from "../../../config/settings.js";
|
||||
import { isMalwarePackage } from "../../../scanning/audit/index.js";
|
||||
import { createInterceptorBuilder } from "../interceptorBuilder.js";
|
||||
import { interceptRequests } from "../interceptorBuilder.js";
|
||||
import { ui } from "../../../environment/userInteraction.js";
|
||||
import { writeFileSync } from "node:fs";
|
||||
|
||||
const knownJsRegistries = ["registry.npmjs.org", "registry.yarnpkg.com"];
|
||||
|
||||
|
|
@ -23,22 +21,20 @@ export function npmInterceptorForUrl(url) {
|
|||
|
||||
/**
|
||||
* @param {string} registry
|
||||
* @returns {import("../interceptorBuilder.js").Interceptor | undefined}
|
||||
* @returns {import("../interceptorBuilder.js").Interceptor}
|
||||
*/
|
||||
function buildNpmInterceptor(registry) {
|
||||
const builder = createInterceptorBuilder();
|
||||
|
||||
builder.onRequest(async (req) => {
|
||||
return interceptRequests(async (reqContext) => {
|
||||
const { packageName, version } = parseNpmPackageUrl(
|
||||
req.targetUrl,
|
||||
reqContext.targetUrl,
|
||||
registry
|
||||
);
|
||||
if (await isMalwarePackage(packageName, version)) {
|
||||
req.blockMalware(packageName, version, req.targetUrl);
|
||||
reqContext.blockMalware(packageName, version);
|
||||
}
|
||||
|
||||
if (isPackageInfoUrl(req.targetUrl)) {
|
||||
req.modifyRequestHeaders((headers) => {
|
||||
if (isPackageInfoUrl(reqContext.targetUrl)) {
|
||||
reqContext.modifyRequestHeaders((headers) => {
|
||||
if (
|
||||
headers["accept"]?.includes("application/vnd.npm.install-v1+json")
|
||||
) {
|
||||
|
|
@ -49,13 +45,11 @@ function buildNpmInterceptor(registry) {
|
|||
}
|
||||
});
|
||||
|
||||
req.modifyResponse((res) => {
|
||||
reqContext.modifyResponse((res) => {
|
||||
res.modifyBody(modifyNpmInfoRequestBody);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { isMalwarePackage } from "../../scanning/audit/index.js";
|
||||
import { createInterceptorBuilder } from "./interceptorBuilder.js";
|
||||
import { interceptRequests } from "./interceptorBuilder.js";
|
||||
|
||||
const knownPipRegistries = [
|
||||
"files.pythonhosted.org",
|
||||
|
|
@ -27,19 +27,15 @@ export function pipInterceptorForUrl(url) {
|
|||
* @returns {import("./interceptorBuilder.js").Interceptor | undefined}
|
||||
*/
|
||||
function buildPipInterceptor(registry) {
|
||||
const builder = createInterceptorBuilder();
|
||||
|
||||
builder.onRequest(async (req) => {
|
||||
return interceptRequests(async (reqContext) => {
|
||||
const { packageName, version } = parsePipPackageFromUrl(
|
||||
req.targetUrl,
|
||||
reqContext.targetUrl,
|
||||
registry
|
||||
);
|
||||
if (await isMalwarePackage(packageName, version)) {
|
||||
req.blockMalware(packageName, version, req.targetUrl);
|
||||
reqContext.blockMalware(packageName, version);
|
||||
}
|
||||
});
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,121 +0,0 @@
|
|||
/**
|
||||
* @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 {(modificationFunc: (headers: NodeJS.Dict<string | string[]>) => void) => void} modifyRequestHeaders
|
||||
* @property {(requestFunc: (responseInterceptorBuilder: import('./responseInterceptorBuilder.js').ResponseInterceptorBuilder) => void) => void} modifyResponse
|
||||
* @property {() => RequestInterceptor} build
|
||||
*
|
||||
* @typedef {Object} RequestInterceptor
|
||||
* @property {{statusCode: number, message: string} | undefined} blockResponse
|
||||
* @property {(headers: NodeJS.Dict<string | string[]> | undefined) => void} modifyRequestHeaders
|
||||
* @property {() => import("./responseInterceptorBuilder.js").ResponseInterceptor} handleResponse
|
||||
* @property {() => boolean} modifiesResponse
|
||||
*/
|
||||
|
||||
import { createResponseInterceptorBuilder } from "./responseInterceptorBuilder.js";
|
||||
|
||||
/**
|
||||
* @param {string} targetUrl
|
||||
* @param {import('events').EventEmitter} eventEmitter
|
||||
* @returns {RequestInterceptorBuilder}
|
||||
*/
|
||||
export function createRequestInterceptorBuilder(targetUrl, eventEmitter) {
|
||||
/** @type {{statusCode: number, message: string} | undefined} */
|
||||
let blockResponse = undefined;
|
||||
/** @type {Array<(headers: NodeJS.Dict<string | string[]>) => void>} */
|
||||
let requestHeaderFuncs = [];
|
||||
/** @type {Array<(requestFunc: import('./responseInterceptorBuilder.js').ResponseInterceptorBuilder) => void>} */
|
||||
let responseModifierFuncs = [];
|
||||
|
||||
/**
|
||||
* @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,
|
||||
blockMalware,
|
||||
modifyRequestHeaders(modificationFunc) {
|
||||
requestHeaderFuncs.push(modificationFunc);
|
||||
},
|
||||
modifyResponse(modificationFunc) {
|
||||
responseModifierFuncs.push(modificationFunc);
|
||||
},
|
||||
build() {
|
||||
return createRequestInterceptor(
|
||||
blockResponse,
|
||||
requestHeaderFuncs,
|
||||
responseModifierFuncs
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{statusCode: number, message: string} | undefined} blockResponse
|
||||
* @param {Array<(headers: NodeJS.Dict<string | string[]>) => void>} requestHeadersModficationFuncs
|
||||
* @param {Array<(requestFunc: import('./responseInterceptorBuilder.js').ResponseInterceptorBuilder) => void>} responseModifierFuncs
|
||||
* @returns {RequestInterceptor}
|
||||
*/
|
||||
function createRequestInterceptor(
|
||||
blockResponse,
|
||||
requestHeadersModficationFuncs,
|
||||
responseModifierFuncs
|
||||
) {
|
||||
/**
|
||||
* @param {NodeJS.Dict<string | string[]> | undefined} headers
|
||||
*/
|
||||
function modifyRequestHeaders(headers) {
|
||||
if (!headers) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const modificationFunc of requestHeadersModficationFuncs) {
|
||||
modificationFunc(headers);
|
||||
}
|
||||
}
|
||||
|
||||
function modifiesResponse() {
|
||||
return responseModifierFuncs.length > 0;
|
||||
}
|
||||
|
||||
function handleResponse() {
|
||||
const responseInterceptorBuilder = createResponseInterceptorBuilder();
|
||||
|
||||
for (const func of responseModifierFuncs) {
|
||||
func(responseInterceptorBuilder);
|
||||
}
|
||||
|
||||
return responseInterceptorBuilder.build();
|
||||
}
|
||||
|
||||
return {
|
||||
blockResponse,
|
||||
modifyRequestHeaders,
|
||||
modifiesResponse,
|
||||
handleResponse,
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue