Merge branch 'main' into feature/cert-beta

This commit is contained in:
Reinier Criel 2025-11-13 11:15:33 -08:00
commit 84b8c2f2cf
10 changed files with 463 additions and 206 deletions

View file

@ -0,0 +1,25 @@
import {
ECOSYSTEM_JS,
ECOSYSTEM_PY,
getEcoSystem,
} from "../../config/settings.js";
import { npmInterceptorForUrl } from "./npmInterceptor.js";
import { pipInterceptorForUrl } from "./pipInterceptor.js";
/**
* @param {string} url
* @returns {import("./interceptorBuilder.js").Interceptor | undefined}
*/
export function createInterceptorForUrl(url) {
const ecosystem = getEcoSystem();
if (ecosystem === ECOSYSTEM_JS) {
return npmInterceptorForUrl(url);
}
if (ecosystem === ECOSYSTEM_PY) {
return pipInterceptorForUrl(url);
}
return undefined;
}

View file

@ -0,0 +1,92 @@
import { EventEmitter } from "events";
/**
* @typedef {Object} Interceptor
* @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
*/
/**
* @param {(requestHandlerBuilder: RequestInterceptionContext) => Promise<void>} requestInterceptionFunc
* @returns {Interceptor}
*/
export function interceptRequests(requestInterceptionFunc) {
return buildInterceptor([requestInterceptionFunc]);
}
/**
* @param {Array<(requestHandlerBuilder: RequestInterceptionContext) => Promise<void>>} requestHandlers
* @returns {Interceptor}
*/
function buildInterceptor(requestHandlers) {
const eventEmitter = new EventEmitter();
return {
async handleRequest(targetUrl) {
const requestContext = createRequestContext(targetUrl, eventEmitter);
for (const handler of requestHandlers) {
await handler(requestContext);
}
return requestContext.build();
},
on(event, listener) {
eventEmitter.on(event, listener);
return this;
},
emit(event, ...args) {
return eventEmitter.emit(event, ...args);
},
};
}
/**
* @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,
};
},
};
}

View file

@ -0,0 +1,78 @@
import { isMalwarePackage } from "../../scanning/audit/index.js";
import { interceptRequests } from "./interceptorBuilder.js";
const knownJsRegistries = ["registry.npmjs.org", "registry.yarnpkg.com"];
/**
* @param {string} url
* @returns {import("./interceptorBuilder.js").Interceptor | undefined}
*/
export function npmInterceptorForUrl(url) {
const registry = knownJsRegistries.find((reg) => url.includes(reg));
if (registry) {
return buildNpmInterceptor(registry);
}
return undefined;
}
/**
* @param {string} registry
* @returns {import("./interceptorBuilder.js").Interceptor | undefined}
*/
function buildNpmInterceptor(registry) {
return interceptRequests(async (reqContext) => {
const { packageName, version } = parseNpmPackageUrl(
reqContext.targetUrl,
registry
);
if (await isMalwarePackage(packageName, version)) {
reqContext.blockMalware(packageName, version);
}
});
}
/**
* @param {string} url
* @param {string} registry
* @returns {{packageName: string | undefined, version: string | undefined}}
*/
function parseNpmPackageUrl(url, registry) {
let packageName, version;
if (!registry || !url.endsWith(".tgz")) {
return { packageName, version };
}
const registryIndex = url.indexOf(registry);
const afterRegistry = url.substring(registryIndex + registry.length + 1); // +1 to skip the slash
const separatorIndex = afterRegistry.indexOf("/-/");
if (separatorIndex === -1) {
return { packageName, version };
}
packageName = afterRegistry.substring(0, separatorIndex);
const filename = afterRegistry.substring(
separatorIndex + 3,
afterRegistry.length - 4
); // Remove /-/ and .tgz
// Extract version from filename
// For scoped packages like @babel/core, the filename is core-7.21.4.tgz
// For regular packages like lodash, the filename is lodash-4.17.21.tgz
if (packageName.startsWith("@")) {
const scopedPackageName = packageName.substring(
packageName.lastIndexOf("/") + 1
);
if (filename.startsWith(scopedPackageName + "-")) {
version = filename.substring(scopedPackageName.length + 1);
}
} else {
if (filename.startsWith(packageName + "-")) {
version = filename.substring(packageName.length + 1);
}
}
return { packageName, version };
}

View file

@ -1,14 +1,22 @@
import { describe, it, beforeEach } from "node:test";
import { describe, it, mock } from "node:test";
import assert from "node:assert";
import { parsePackageFromUrl } from "./parsePackageFromUrl.js";
import { setEcoSystem, ECOSYSTEM_JS, ECOSYSTEM_PY } from "../config/settings.js";
describe("parsePackageFromUrl", () => {
beforeEach(() => {
setEcoSystem(ECOSYSTEM_JS);
describe("npmInterceptor", async () => {
let lastPackage;
let malwareResponse = false;
mock.module("../../scanning/audit/index.js", {
namedExports: {
isMalwarePackage: async (packageName, version) => {
lastPackage = { packageName, version };
return malwareResponse;
},
},
});
const testCases = [
const { npmInterceptorForUrl } = await import("./npmInterceptor.js");
const parserCases = [
// Regular packages
{
url: "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
@ -83,11 +91,6 @@ describe("parsePackageFromUrl", () => {
url: "https://registry.yarnpkg.com/@babel/core/-/core-7.21.4.tgz",
expected: { packageName: "@babel/core", version: "7.21.4" },
},
// Invalid URLs should return undefined values
{
url: "https://example.com/package.tgz",
expected: { packageName: undefined, version: undefined },
},
// URL to get package info, not tarball
{
url: "https://registry.npmjs.org/lodash",
@ -110,92 +113,51 @@ describe("parsePackageFromUrl", () => {
},
];
testCases.forEach(({ url, expected }, index) => {
it(`should parse URL ${index + 1}: ${url}`, () => {
const result = parsePackageFromUrl(url);
assert.deepEqual(result, expected);
parserCases.forEach(({ url, expected }, index) => {
it(`should parse URL ${index + 1}: ${url}`, async () => {
const interceptor = npmInterceptorForUrl(url);
assert.ok(
interceptor,
"Interceptor should be created for known npm registry"
);
await interceptor.handleRequest(url);
assert.deepEqual(lastPackage, expected);
});
});
});
describe("parsePackageFromUrl - pip URLs", () => {
beforeEach(() => {
setEcoSystem(ECOSYSTEM_PY);
it("should not create interceptor for unknown registry", () => {
const url = "https://example.com/some-package/-/some-package-1.0.0.tgz";
const interceptor = npmInterceptorForUrl(url);
assert.equal(
interceptor,
undefined,
"Interceptor should be undefined for unknown registry"
);
});
const pipTestCases = [
// Valid pip URLs
{
url: "https://files.pythonhosted.org/packages/xx/yy/foobar-1.2.3.tar.gz",
expected: { packageName: "foobar", version: "1.2.3" },
},
{
url: "https://pypi.org/packages/source/f/foobar/foobar-1.2.3.tar.gz",
expected: { packageName: "foobar", version: "1.2.3" },
},
{
url: "https://pypi.org/packages/source/f/foo-bar/foo-bar-0.9.0.tar.gz",
expected: { packageName: "foo-bar", version: "0.9.0" },
},
{
url: "https://pypi.org/packages/source/f/foo_bar/foo_bar-2.0.0-py3-none-any.whl",
expected: { packageName: "foo_bar", version: "2.0.0" },
},
{
url: "https://files.pythonhosted.org/packages/xx/yy/foo_bar-2.0.0-py3-none-any.whl",
expected: { packageName: "foo_bar", version: "2.0.0" },
},
{
url: "https://pypi.org/packages/source/f/foo.bar/foo.bar-1.0.0.tar.gz",
expected: { packageName: "foo.bar", version: "1.0.0" },
},
{
url: "https://pypi.org/packages/source/f/foo_bar/foo_bar-2.0.0b1.tar.gz",
expected: { packageName: "foo_bar", version: "2.0.0b1" },
},
{
url: "https://pypi.org/packages/source/f/foo_bar/foo_bar-2.0.0rc1.tar.gz",
expected: { packageName: "foo_bar", version: "2.0.0rc1" },
},
{
url: "https://pypi.org/packages/source/f/foo_bar/foo_bar-2.0.0.post1.tar.gz",
expected: { packageName: "foo_bar", version: "2.0.0.post1" },
},
{
url: "https://pypi.org/packages/source/f/foo_bar/foo_bar-2.0.0.dev1.tar.gz",
expected: { packageName: "foo_bar", version: "2.0.0.dev1" },
},
{
url: "https://pypi.org/packages/source/f/foo_bar/foo_bar-2.0.0a1.tar.gz",
expected: { packageName: "foo_bar", version: "2.0.0a1" },
},
{
url: "https://pypi.org/packages/source/f/foo_bar/foo_bar-2.0.0-cp38-cp38-manylinux1_x86_64.whl",
expected: { packageName: "foo_bar", version: "2.0.0" },
},
// Invalid pip URLs
{
url: "https://pypi.org/simple/",
expected: { packageName: undefined, version: undefined },
},
{
url: "https://pypi.org/project/foobar/",
expected: { packageName: undefined, version: undefined },
},
{
url: "https://files.pythonhosted.org/packages/xx/yy/foobar-latest.tar.gz",
expected: { packageName: undefined, version: undefined },
},
{
url: "https://pypi.org/packages/source/f/foo_bar/foo_bar-latest.tar.gz",
expected: { packageName: undefined, version: undefined },
},
];
it("should block malicious package", async () => {
const url =
"https://registry.npmjs.org/malicious-package/-/malicious-package-1.0.0.tgz";
malwareResponse = true;
pipTestCases.forEach(({ url, expected }, index) => {
it(`should parse pip URL ${index + 1}: ${url}`, () => {
const result = parsePackageFromUrl(url);
assert.deepEqual(result, expected);
});
const interceptor = npmInterceptorForUrl(url);
const result = await interceptor.handleRequest(url);
assert.ok(result.blockResponse, "Should contain a blockResponse");
assert.equal(
result.blockResponse.statusCode,
403,
"Block response should have status code 403"
);
assert.equal(
result.blockResponse.message,
"Forbidden - blocked by safe-chain",
"Block response should have correct status message"
);
});
});

View file

@ -1,79 +1,41 @@
import { getEcoSystem, ECOSYSTEM_JS, ECOSYSTEM_PY } from "../config/settings.js";
import { isMalwarePackage } from "../../scanning/audit/index.js";
import { interceptRequests } from "./interceptorBuilder.js";
export const knownJsRegistries = ["registry.npmjs.org","registry.yarnpkg.com"];
export const knownPipRegistries = ["files.pythonhosted.org", "pypi.org", "pypi.python.org", "pythonhosted.org"];
const knownPipRegistries = [
"files.pythonhosted.org",
"pypi.org",
"pypi.python.org",
"pythonhosted.org",
];
/**
* @param {string} url
* @returns {{packageName: string | undefined, version: string | undefined}}
* @returns {import("./interceptorBuilder.js").Interceptor | undefined}
*/
export function parsePackageFromUrl(url) {
const ecosystem = getEcoSystem();
let registry;
export function pipInterceptorForUrl(url) {
const registry = knownPipRegistries.find((reg) => url.includes(reg));
// Only check registries that match the current ecosystem
if (ecosystem === ECOSYSTEM_JS) {
for (const knownRegistry of knownJsRegistries) {
if (url.includes(knownRegistry)) {
registry = knownRegistry;
return parseJsPackageFromUrl(url, registry);
}
}
} else if (ecosystem === ECOSYSTEM_PY) {
for (const knownRegistry of knownPipRegistries) {
if (url.includes(knownRegistry)) {
registry = knownRegistry;
return parsePipPackageFromUrl(url, registry);
}
}
if (registry) {
return buildPipInterceptor(registry);
}
// If no known registry matched, return { packageName: undefined, version: undefined }
return { packageName: undefined, version: undefined };
return undefined;
}
/**
* @param {string} url
* @param {string} registry
* @returns {{packageName: string | undefined, version: string | undefined}}
* @returns {import("./interceptorBuilder.js").Interceptor | undefined}
*/
function parseJsPackageFromUrl(url, registry) {
let packageName, version;
if (!registry || !url.endsWith(".tgz")) {
return { packageName, version };
}
const registryIndex = url.indexOf(registry);
const afterRegistry = url.substring(registryIndex + registry.length + 1); // +1 to skip the slash
const separatorIndex = afterRegistry.indexOf("/-/");
if (separatorIndex === -1) {
return { packageName, version };
}
packageName = afterRegistry.substring(0, separatorIndex);
const filename = afterRegistry.substring(
separatorIndex + 3,
afterRegistry.length - 4
); // Remove /-/ and .tgz
// Extract version from filename
// For scoped packages like @babel/core, the filename is core-7.21.4.tgz
// For regular packages like lodash, the filename is lodash-4.17.21.tgz
if (packageName.startsWith("@")) {
const scopedPackageName = packageName.substring(
packageName.lastIndexOf("/") + 1
function buildPipInterceptor(registry) {
return interceptRequests(async (reqContext) => {
const { packageName, version } = parsePipPackageFromUrl(
reqContext.targetUrl,
registry
);
if (filename.startsWith(scopedPackageName + "-")) {
version = filename.substring(scopedPackageName.length + 1);
if (await isMalwarePackage(packageName, version)) {
reqContext.blockMalware(packageName, version);
}
} else {
if (filename.startsWith(packageName + "-")) {
version = filename.substring(packageName.length + 1);
}
}
return { packageName, version };
});
}
/**
@ -82,11 +44,11 @@ function parseJsPackageFromUrl(url, registry) {
* @returns {{packageName: string | undefined, version: string | undefined}}
*/
function parsePipPackageFromUrl(url, registry) {
let packageName, version
let packageName, version;
// Basic validation
if (!registry || typeof url !== "string") {
return { packageName, version};
return { packageName, version };
}
// Quick sanity check on the URL + parse
@ -94,13 +56,13 @@ function parsePipPackageFromUrl(url, registry) {
try {
urlObj = new URL(url);
} catch {
return { packageName, version};
return { packageName, version };
}
// Get the last path segment (filename) and decode it (strip query & fragment automatically)
const lastSegment = urlObj.pathname.split("/").filter(Boolean).pop();
if (!lastSegment){
return { packageName, version};
if (!lastSegment) {
return { packageName, version };
}
const filename = decodeURIComponent(lastSegment);
@ -114,8 +76,8 @@ function parsePipPackageFromUrl(url, registry) {
const base = filename.slice(0, -4); // remove ".whl"
const firstDash = base.indexOf("-");
if (firstDash > 0) {
const dist = base.slice(0, firstDash); // may contain underscores
const rest = base.slice(firstDash + 1); // version + the rest of tags
const dist = base.slice(0, firstDash); // may contain underscores
const rest = base.slice(firstDash + 1); // version + the rest of tags
const secondDash = rest.indexOf("-");
const rawVersion = secondDash >= 0 ? rest.slice(0, secondDash) : rest;
packageName = dist; // preserve underscores

View file

@ -0,0 +1,135 @@
import { describe, it, mock } from "node:test";
import assert from "node:assert";
describe("pipInterceptor", async () => {
let lastPackage;
let malwareResponse = false;
mock.module("../../scanning/audit/index.js", {
namedExports: {
isMalwarePackage: async (packageName, version) => {
lastPackage = { packageName, version };
return malwareResponse;
},
},
});
const { pipInterceptorForUrl } = await import("./pipInterceptor.js");
const parserCases = [
// Valid pip URLs
{
url: "https://files.pythonhosted.org/packages/xx/yy/foobar-1.2.3.tar.gz",
expected: { packageName: "foobar", version: "1.2.3" },
},
{
url: "https://pypi.org/packages/source/f/foobar/foobar-1.2.3.tar.gz",
expected: { packageName: "foobar", version: "1.2.3" },
},
{
url: "https://pypi.org/packages/source/f/foo-bar/foo-bar-0.9.0.tar.gz",
expected: { packageName: "foo-bar", version: "0.9.0" },
},
{
url: "https://pypi.org/packages/source/f/foo_bar/foo_bar-2.0.0-py3-none-any.whl",
expected: { packageName: "foo_bar", version: "2.0.0" },
},
{
url: "https://files.pythonhosted.org/packages/xx/yy/foo_bar-2.0.0-py3-none-any.whl",
expected: { packageName: "foo_bar", version: "2.0.0" },
},
{
url: "https://pypi.org/packages/source/f/foo.bar/foo.bar-1.0.0.tar.gz",
expected: { packageName: "foo.bar", version: "1.0.0" },
},
{
url: "https://pypi.org/packages/source/f/foo_bar/foo_bar-2.0.0b1.tar.gz",
expected: { packageName: "foo_bar", version: "2.0.0b1" },
},
{
url: "https://pypi.org/packages/source/f/foo_bar/foo_bar-2.0.0rc1.tar.gz",
expected: { packageName: "foo_bar", version: "2.0.0rc1" },
},
{
url: "https://pypi.org/packages/source/f/foo_bar/foo_bar-2.0.0.post1.tar.gz",
expected: { packageName: "foo_bar", version: "2.0.0.post1" },
},
{
url: "https://pypi.org/packages/source/f/foo_bar/foo_bar-2.0.0.dev1.tar.gz",
expected: { packageName: "foo_bar", version: "2.0.0.dev1" },
},
{
url: "https://pypi.org/packages/source/f/foo_bar/foo_bar-2.0.0a1.tar.gz",
expected: { packageName: "foo_bar", version: "2.0.0a1" },
},
{
url: "https://pypi.org/packages/source/f/foo_bar/foo_bar-2.0.0-cp38-cp38-manylinux1_x86_64.whl",
expected: { packageName: "foo_bar", version: "2.0.0" },
},
// Invalid pip URLs
{
url: "https://pypi.org/simple/",
expected: { packageName: undefined, version: undefined },
},
{
url: "https://pypi.org/project/foobar/",
expected: { packageName: undefined, version: undefined },
},
{
url: "https://files.pythonhosted.org/packages/xx/yy/foobar-latest.tar.gz",
expected: { packageName: undefined, version: undefined },
},
{
url: "https://pypi.org/packages/source/f/foo_bar/foo_bar-latest.tar.gz",
expected: { packageName: undefined, version: undefined },
},
];
parserCases.forEach(({ url, expected }, index) => {
it(`should parse URL ${index + 1}: ${url}`, async () => {
const interceptor = pipInterceptorForUrl(url);
assert.ok(
interceptor,
"Interceptor should be created for known npm registry"
);
await interceptor.handleRequest(url);
assert.deepEqual(lastPackage, expected);
});
});
it("should not create interceptor for unknown registry", () => {
const url = "https://example.com/packages/xx/yy/foobar-1.2.3.tar.gz";
const interceptor = pipInterceptorForUrl(url);
assert.equal(
interceptor,
undefined,
"Interceptor should be undefined for unknown registry"
);
});
it("should block malicious package", async () => {
const url =
"https://files.pythonhosted.org/packages/xx/yy/malicious_package-1.0.0.tar.gz";
malwareResponse = true;
const interceptor = pipInterceptorForUrl(url);
const result = await interceptor.handleRequest(url);
assert.ok(result.blockResponse, "Should contain a blockResponse");
assert.equal(
result.blockResponse.statusCode,
403,
"Block response should have status code 403"
);
assert.equal(
result.blockResponse.message,
"Forbidden - blocked by safe-chain",
"Block response should have correct status message"
);
});
});

View file

@ -3,12 +3,16 @@ import { generateCertForHost } from "./certUtils.js";
import { HttpsProxyAgent } from "https-proxy-agent";
import { ui } from "../environment/userInteraction.js";
/**
* @typedef {import("./interceptors/interceptorBuilder.js").Interceptor} Interceptor
*/
/**
* @param {import("http").IncomingMessage} req
* @param {import("http").ServerResponse} clientSocket
* @param {(target: string) => Promise<boolean>} isAllowed
* @param {Interceptor} interceptor
*/
export function mitmConnect(req, clientSocket, isAllowed) {
export function mitmConnect(req, clientSocket, interceptor) {
ui.writeVerbose(`Safe-chain: Set up MITM tunnel for ${req.url}`);
const { hostname } = new URL(`http://${req.url}`);
@ -21,7 +25,7 @@ export function mitmConnect(req, clientSocket, isAllowed) {
// Not subscribing to 'close' event will cause node to throw and crash.
});
const server = createHttpsServer(hostname, isAllowed);
const server = createHttpsServer(hostname, interceptor);
server.on("error", (err) => {
ui.writeError(`Safe-chain: HTTPS server error: ${err.message}`);
@ -41,10 +45,10 @@ export function mitmConnect(req, clientSocket, isAllowed) {
/**
* @param {string} hostname
* @param {(target: string) => Promise<boolean>} isAllowed
* @param {Interceptor} interceptor
* @returns {import("https").Server}
*/
function createHttpsServer(hostname, isAllowed) {
function createHttpsServer(hostname, interceptor) {
const cert = generateCertForHost(hostname);
/**
@ -64,10 +68,13 @@ function createHttpsServer(hostname, isAllowed) {
const pathAndQuery = getRequestPathAndQuery(req.url);
const targetUrl = `https://${hostname}${pathAndQuery}`;
if (!(await isAllowed(targetUrl))) {
const interceptorResult = await interceptor.handleRequest(targetUrl);
const blockResponse = interceptorResult.blockResponse;
if (blockResponse) {
ui.writeVerbose(`Safe-chain: Blocking request to ${targetUrl}`);
res.writeHead(403, "Forbidden - blocked by safe-chain");
res.end("Blocked by safe-chain");
res.writeHead(blockResponse.statusCode, blockResponse.message);
res.end(blockResponse.message);
return;
}

View file

@ -3,11 +3,9 @@ import { tunnelRequest } from "./tunnelRequestHandler.js";
import { mitmConnect } from "./mitmRequestHandler.js";
import { handleHttpProxyRequest } from "./plainHttpProxy.js";
import { getCaCertPath } from "./certUtils.js";
import { auditChanges } from "../scanning/audit/index.js";
import { knownJsRegistries, knownPipRegistries, parsePackageFromUrl } from "./parsePackageFromUrl.js";
import { getEcoSystem, ECOSYSTEM_JS, ECOSYSTEM_PY } from "../config/settings.js";
import { ui } from "../environment/userInteraction.js";
import chalk from "chalk";
import { createInterceptorForUrl } from "./interceptors/createInterceptorForEcoSystem.js";
const SERVER_STOP_TIMEOUT_MS = 1000;
/**
@ -132,18 +130,15 @@ function handleConnect(req, clientSocket, head) {
// CONNECT method is used for HTTPS requests
// It establishes a tunnel to the server identified by the request URL
const ecosystem = getEcoSystem();
const url = req.url || "";
const interceptor = createInterceptorForUrl(req.url || "");
let isKnownRegistry = false;
if (ecosystem === ECOSYSTEM_JS) {
isKnownRegistry = knownJsRegistries.some((reg) => url.includes(reg));
} else if (ecosystem === ECOSYSTEM_PY) {
isKnownRegistry = knownPipRegistries.some((reg) => url.includes(reg));
}
if (interceptor) {
// Subscribe to malware blocked events
interceptor.on("malwareBlocked", (event) => {
onMalwareBlocked(event.packageName, event.version, event.url);
});
if (isKnownRegistry) {
mitmConnect(req, clientSocket, isAllowedUrl);
mitmConnect(req, clientSocket, interceptor);
} else {
// For other hosts, just tunnel the request to the destination tcp socket
ui.writeVerbose(`Safe-chain: Tunneling request to ${req.url}`);
@ -152,28 +147,13 @@ function handleConnect(req, clientSocket, head) {
}
/**
*
* @param {string} packageName
* @param {string} version
* @param {string} url
* @returns {Promise<boolean>}
*/
async function isAllowedUrl(url) {
const { packageName, version } = parsePackageFromUrl(url);
// packageName and version are undefined when the URL is not a package download
// In that case, we can allow the request to proceed
if (!packageName || !version) {
return true;
}
const auditResult = await auditChanges([
{ name: packageName, version, type: "add" },
]);
if (!auditResult.isAllowed) {
state.blockedRequests.push({ packageName, version, url });
return false;
}
return true;
function onMalwareBlocked(packageName, version, url) {
state.blockedRequests.push({ packageName, version, url });
}
function verifyNoMaliciousPackages() {