From f4694ba11954a7b7a257924c24a4d8a688fc6c04 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Fri, 7 Nov 2025 10:10:27 +0100 Subject: [PATCH] Move npm and pip mitm interception to separate files --- .../createInterceptorForEcoSystem.js | 25 +++ .../interceptors/npmInterceptor.js | 82 ++++++++++ .../npmInterceptor.spec.js} | 148 +++++++----------- .../pipInterceptor.js} | 98 ++++-------- .../interceptors/pipInterceptor.spec.js | 135 ++++++++++++++++ .../src/registryProxy/mitmRequestHandler.js | 2 +- .../src/registryProxy/registryProxy.js | 68 +------- .../safe-chain/src/scanning/audit/index.js | 16 ++ 8 files changed, 350 insertions(+), 224 deletions(-) create mode 100644 packages/safe-chain/src/registryProxy/interceptors/createInterceptorForEcoSystem.js create mode 100644 packages/safe-chain/src/registryProxy/interceptors/npmInterceptor.js rename packages/safe-chain/src/registryProxy/{parsePackageFromUrl.spec.js => interceptors/npmInterceptor.spec.js} (50%) rename packages/safe-chain/src/registryProxy/{parsePackageFromUrl.js => interceptors/pipInterceptor.js} (50%) create mode 100644 packages/safe-chain/src/registryProxy/interceptors/pipInterceptor.spec.js diff --git a/packages/safe-chain/src/registryProxy/interceptors/createInterceptorForEcoSystem.js b/packages/safe-chain/src/registryProxy/interceptors/createInterceptorForEcoSystem.js new file mode 100644 index 0000000..c97d867 --- /dev/null +++ b/packages/safe-chain/src/registryProxy/interceptors/createInterceptorForEcoSystem.js @@ -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; +} diff --git a/packages/safe-chain/src/registryProxy/interceptors/npmInterceptor.js b/packages/safe-chain/src/registryProxy/interceptors/npmInterceptor.js new file mode 100644 index 0000000..557e9cb --- /dev/null +++ b/packages/safe-chain/src/registryProxy/interceptors/npmInterceptor.js @@ -0,0 +1,82 @@ +import { isMalwarePackage } from "../../scanning/audit/index.js"; +import { createInterceptorBuilder } 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) { + const builder = createInterceptorBuilder(); + + builder.onRequest(async (req) => { + const { packageName, version } = parseNpmPackageUrl( + req.targetUrl, + registry + ); + if (await isMalwarePackage(packageName, version)) { + req.blockRequest(403, "Forbidden - blocked by safe-chain"); + } + }); + + return builder.build(); +} + +/** + * @param {string} url + * @param {string} registry + * @returns {{packageName: string | undefined, version: string | undefined}} + */ +export 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 }; +} diff --git a/packages/safe-chain/src/registryProxy/parsePackageFromUrl.spec.js b/packages/safe-chain/src/registryProxy/interceptors/npmInterceptor.spec.js similarity index 50% rename from packages/safe-chain/src/registryProxy/parsePackageFromUrl.spec.js rename to packages/safe-chain/src/registryProxy/interceptors/npmInterceptor.spec.js index d052e9d..dd09527 100644 --- a/packages/safe-chain/src/registryProxy/parsePackageFromUrl.spec.js +++ b/packages/safe-chain/src/registryProxy/interceptors/npmInterceptor.spec.js @@ -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" + ); }); }); diff --git a/packages/safe-chain/src/registryProxy/parsePackageFromUrl.js b/packages/safe-chain/src/registryProxy/interceptors/pipInterceptor.js similarity index 50% rename from packages/safe-chain/src/registryProxy/parsePackageFromUrl.js rename to packages/safe-chain/src/registryProxy/interceptors/pipInterceptor.js index 1fda121..90099b1 100644 --- a/packages/safe-chain/src/registryProxy/parsePackageFromUrl.js +++ b/packages/safe-chain/src/registryProxy/interceptors/pipInterceptor.js @@ -1,79 +1,45 @@ -import { getEcoSystem, ECOSYSTEM_JS, ECOSYSTEM_PY } from "../config/settings.js"; +import { isMalwarePackage } from "../../scanning/audit/index.js"; +import { createInterceptorBuilder } 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 }; - } +function buildPipInterceptor(registry) { + const builder = createInterceptorBuilder(); - 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 + builder.onRequest(async (req) => { + const { packageName, version } = parsePipPackageFromUrl( + req.targetUrl, + registry ); - if (filename.startsWith(scopedPackageName + "-")) { - version = filename.substring(scopedPackageName.length + 1); + if (await isMalwarePackage(packageName, version)) { + req.blockRequest(403, "Forbidden - blocked by safe-chain"); } - } else { - if (filename.startsWith(packageName + "-")) { - version = filename.substring(packageName.length + 1); - } - } + }); - return { packageName, version }; + return builder.build(); } /** @@ -82,11 +48,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 +60,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 +80,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 diff --git a/packages/safe-chain/src/registryProxy/interceptors/pipInterceptor.spec.js b/packages/safe-chain/src/registryProxy/interceptors/pipInterceptor.spec.js new file mode 100644 index 0000000..8b60b9b --- /dev/null +++ b/packages/safe-chain/src/registryProxy/interceptors/pipInterceptor.spec.js @@ -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" + ); + }); +}); diff --git a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js index 58f220c..c3ad934 100644 --- a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js @@ -69,7 +69,7 @@ function createHttpsServer(hostname, interceptor) { const targetUrl = `https://${hostname}${pathAndQuery}`; const interceptorResult = await interceptor.handleRequest(targetUrl); - const blockResponse = interceptorResult?.blockResponse; + const blockResponse = interceptorResult.blockResponse; if (blockResponse) { ui.writeVerbose(`Safe-chain: Blocking request to ${targetUrl}`); diff --git a/packages/safe-chain/src/registryProxy/registryProxy.js b/packages/safe-chain/src/registryProxy/registryProxy.js index d66f397..d41a8bb 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.js @@ -3,20 +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 { createInterceptorBuilder } from "./interceptors/interceptorBuilder.js"; +import { createInterceptorForUrl } from "./interceptors/createInterceptorForEcoSystem.js"; const SERVER_STOP_TIMEOUT_MS = 1000; /** @@ -141,18 +130,10 @@ 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 (isKnownRegistry) { - mitmConnect(req, clientSocket, createMitmInterceptor()); + if (interceptor) { + 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}`); @@ -160,47 +141,6 @@ function handleConnect(req, clientSocket, head) { } } -/** - * - * @returns {import("./interceptors/interceptorBuilder.js").Interceptor} - */ -function createMitmInterceptor() { - const builder = createInterceptorBuilder(); - - builder.onRequest(async (req) => { - if (!(await isAllowedUrl(req.targetUrl))) { - req.blockRequest(403, "Forbidden - blocked by safe-chain"); - } - }); - - return builder.build(); -} - -/** - * @param {string} url - * @returns {Promise} - */ -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 verifyNoMaliciousPackages() { if (state.blockedRequests.length === 0) { // No malicious packages were blocked, so nothing to block diff --git a/packages/safe-chain/src/scanning/audit/index.js b/packages/safe-chain/src/scanning/audit/index.js index 803051a..09fcfd8 100644 --- a/packages/safe-chain/src/scanning/audit/index.js +++ b/packages/safe-chain/src/scanning/audit/index.js @@ -41,6 +41,22 @@ export function getAuditStats() { return auditStats; } +/** + * + * @param {string | undefined} name + * @param {string | undefined} version + * @returns {Promise} + */ +export async function isMalwarePackage(name, version) { + if (!name || !version) { + return false; + } + + const auditResult = await auditChanges([{ name, version, type: "add" }]); + + return !auditResult.isAllowed; +} + /** * @param {PackageChange[]} changes *