Adapt per review

This commit is contained in:
Reinier Criel 2026-03-28 16:51:33 -07:00
parent aa7bbbd4e9
commit d84270be8d
4 changed files with 76 additions and 36 deletions

View file

@ -2,7 +2,7 @@ import { describe, it, mock } from "node:test";
import assert from "node:assert"; import assert from "node:assert";
describe("pipInterceptor custom registries", async () => { describe("pipInterceptor custom registries", async () => {
let lastPackage; let scannedPackages;
let malwareResponse = false; let malwareResponse = false;
let customRegistries = []; let customRegistries = [];
@ -27,7 +27,7 @@ describe("pipInterceptor custom registries", async () => {
mock.module("../../../scanning/audit/index.js", { mock.module("../../../scanning/audit/index.js", {
namedExports: { namedExports: {
isMalwarePackage: async (packageName, version) => { isMalwarePackage: async (packageName, version) => {
lastPackage = { packageName, version }; scannedPackages.push({ packageName, version });
return malwareResponse; return malwareResponse;
}, },
}, },
@ -46,6 +46,7 @@ describe("pipInterceptor custom registries", async () => {
}); });
it("should parse package from custom registry URL", async () => { it("should parse package from custom registry URL", async () => {
scannedPackages = [];
customRegistries = ["my-custom-registry.example.com"]; customRegistries = ["my-custom-registry.example.com"];
const url = const url =
"https://my-custom-registry.example.com/packages/xx/yy/foobar-1.2.3.tar.gz"; "https://my-custom-registry.example.com/packages/xx/yy/foobar-1.2.3.tar.gz";
@ -55,13 +56,16 @@ describe("pipInterceptor custom registries", async () => {
await interceptor.handleRequest(url); await interceptor.handleRequest(url);
assert.deepEqual(lastPackage, { assert.ok(
packageName: "foobar", scannedPackages.some(
version: "1.2.3", ({ packageName, version }) =>
}); packageName === "foobar" && version === "1.2.3"
)
);
}); });
it("should parse wheel package from custom registry URL", async () => { it("should parse wheel package from custom registry URL", async () => {
scannedPackages = [];
customRegistries = ["private-pypi.internal.com"]; customRegistries = ["private-pypi.internal.com"];
const url = const url =
"https://private-pypi.internal.com/packages/foo_bar-2.0.0-py3-none-any.whl"; "https://private-pypi.internal.com/packages/foo_bar-2.0.0-py3-none-any.whl";
@ -71,10 +75,12 @@ describe("pipInterceptor custom registries", async () => {
await interceptor.handleRequest(url); await interceptor.handleRequest(url);
assert.deepEqual(lastPackage, { assert.ok(
packageName: "foo-bar", scannedPackages.some(
version: "2.0.0", ({ packageName, version }) =>
}); packageName === "foo-bar" && version === "2.0.0"
)
);
}); });
it("should handle multiple custom registries", async () => { it("should handle multiple custom registries", async () => {
@ -96,6 +102,7 @@ describe("pipInterceptor custom registries", async () => {
}); });
it("should block malicious package from custom registry", async () => { it("should block malicious package from custom registry", async () => {
scannedPackages = [];
customRegistries = ["my-custom-registry.example.com"]; customRegistries = ["my-custom-registry.example.com"];
malwareResponse = true; malwareResponse = true;
@ -115,6 +122,7 @@ describe("pipInterceptor custom registries", async () => {
}); });
it("should still work with known registries when custom registries are set", async () => { it("should still work with known registries when custom registries are set", async () => {
scannedPackages = [];
customRegistries = ["my-custom-registry.example.com"]; customRegistries = ["my-custom-registry.example.com"];
const url = const url =
@ -126,10 +134,12 @@ describe("pipInterceptor custom registries", async () => {
await interceptor.handleRequest(url); await interceptor.handleRequest(url);
assert.deepEqual(lastPackage, { assert.ok(
packageName: "foobar", scannedPackages.some(
version: "1.2.3", ({ packageName, version }) =>
}); packageName === "foobar" && version === "1.2.3"
)
);
}); });
it("should not create interceptor for unknown registry when custom registries are set", () => { it("should not create interceptor for unknown registry when custom registries are set", () => {
@ -152,6 +162,7 @@ describe("pipInterceptor custom registries", async () => {
}); });
it("should parse .whl.metadata from custom registry", async () => { it("should parse .whl.metadata from custom registry", async () => {
scannedPackages = [];
customRegistries = ["private-pypi.internal.com"]; customRegistries = ["private-pypi.internal.com"];
const url = const url =
"https://private-pypi.internal.com/packages/foo_bar-2.0.0-py3-none-any.whl.metadata"; "https://private-pypi.internal.com/packages/foo_bar-2.0.0-py3-none-any.whl.metadata";
@ -161,13 +172,16 @@ describe("pipInterceptor custom registries", async () => {
await interceptor.handleRequest(url); await interceptor.handleRequest(url);
assert.deepEqual(lastPackage, { assert.ok(
packageName: "foo-bar", scannedPackages.some(
version: "2.0.0", ({ packageName, version }) =>
}); packageName === "foo-bar" && version === "2.0.0"
)
);
}); });
it("should parse .tar.gz.metadata from custom registry", async () => { it("should parse .tar.gz.metadata from custom registry", async () => {
scannedPackages = [];
customRegistries = ["private-pypi.internal.com"]; customRegistries = ["private-pypi.internal.com"];
const url = const url =
"https://private-pypi.internal.com/packages/foo_bar-2.0.0.tar.gz.metadata"; "https://private-pypi.internal.com/packages/foo_bar-2.0.0.tar.gz.metadata";
@ -177,9 +191,11 @@ describe("pipInterceptor custom registries", async () => {
await interceptor.handleRequest(url); await interceptor.handleRequest(url);
assert.deepEqual(lastPackage, { assert.ok(
packageName: "foo-bar", scannedPackages.some(
version: "2.0.0", ({ packageName, version }) =>
}); packageName === "foo-bar" && version === "2.0.0"
)
);
}); });
}); });

View file

@ -1,8 +1,10 @@
import { import {
ECOSYSTEM_PY,
getPipCustomRegistries, getPipCustomRegistries,
skipMinimumPackageAge, skipMinimumPackageAge,
} from "../../../config/settings.js"; } from "../../../config/settings.js";
import { isMalwarePackage } from "../../../scanning/audit/index.js"; import { isMalwarePackage } from "../../../scanning/audit/index.js";
import { getEquivalentPackageNames } from "../../../scanning/packageNameVariants.js";
import { openNewPackagesDatabase } from "../../../scanning/newPackagesListCache.js"; import { openNewPackagesDatabase } from "../../../scanning/newPackagesListCache.js";
import { interceptRequests } from "../interceptorBuilder.js"; import { interceptRequests } from "../interceptorBuilder.js";
import { isExcludedFromMinimumPackageAge } from "../minimumPackageAgeExclusions.js"; import { isExcludedFromMinimumPackageAge } from "../minimumPackageAgeExclusions.js";
@ -50,14 +52,21 @@ function createPipRequestHandler(registry) {
registry registry
); );
// PyPI treats hyphens and underscores as equivalent distribution names. if (!packageName) {
const hyphenName = packageName?.includes("_") return;
? packageName.replace(/_/g, "-") }
: packageName;
const isMalicious = const equivalentPackageNames = getEquivalentPackageNames(
await isMalwarePackage(packageName, version) || packageName,
await isMalwarePackage(hyphenName, version); ECOSYSTEM_PY
);
let isMalicious = false;
for (const equivalentPackageName of equivalentPackageNames) {
if (await isMalwarePackage(equivalentPackageName, version)) {
isMalicious = true;
break;
}
}
if (isMalicious) { if (isMalicious) {
reqContext.blockMalware(packageName, version); reqContext.blockMalware(packageName, version);
@ -65,7 +74,6 @@ function createPipRequestHandler(registry) {
} }
if ( if (
packageName &&
version && version &&
!skipMinimumPackageAge() && !skipMinimumPackageAge() &&
!isExcludedFromMinimumPackageAge(packageName) !isExcludedFromMinimumPackageAge(packageName)

View file

@ -2,13 +2,13 @@ import { describe, it, mock } from "node:test";
import assert from "node:assert"; import assert from "node:assert";
describe("pipInterceptor", async () => { describe("pipInterceptor", async () => {
let lastPackage; let scannedPackages;
let malwareResponse = false; let malwareResponse = false;
mock.module("../../../scanning/audit/index.js", { mock.module("../../../scanning/audit/index.js", {
namedExports: { namedExports: {
isMalwarePackage: async (packageName, version) => { isMalwarePackage: async (packageName, version) => {
lastPackage = { packageName, version }; scannedPackages.push({ packageName, version });
return malwareResponse; return malwareResponse;
}, },
}, },
@ -111,12 +111,24 @@ describe("pipInterceptor", async () => {
parserCases.forEach(({ url, expected }, index) => { parserCases.forEach(({ url, expected }, index) => {
it(`should parse URL ${index + 1}: ${url}`, async () => { it(`should parse URL ${index + 1}: ${url}`, async () => {
scannedPackages = [];
const interceptor = pipInterceptorForUrl(url); const interceptor = pipInterceptorForUrl(url);
assert.ok(interceptor, "Interceptor should be created for known pip registry"); assert.ok(interceptor, "Interceptor should be created for known pip registry");
await interceptor.handleRequest(url); await interceptor.handleRequest(url);
assert.deepEqual(lastPackage, expected); if (expected.packageName === undefined) {
assert.deepEqual(scannedPackages, []);
return;
}
assert.ok(
scannedPackages.some(
({ packageName, version }) =>
packageName === expected.packageName &&
version === expected.version
)
);
}); });
}); });
@ -127,6 +139,7 @@ describe("pipInterceptor", async () => {
}); });
it("should block malicious package", async () => { it("should block malicious package", async () => {
scannedPackages = [];
const url = const url =
"https://files.pythonhosted.org/packages/xx/yy/malicious_package-1.0.0.tar.gz"; "https://files.pythonhosted.org/packages/xx/yy/malicious_package-1.0.0.tar.gz";
malwareResponse = true; malwareResponse = true;

View file

@ -10,7 +10,10 @@ export function getEquivalentPackageNames(packageName, ecosystem) {
return [packageName]; return [packageName];
} }
return [...new Set([packageName, ...["-", "_", "."].map((separator) => const pythonSeparatorPattern = /[._-]/g;
packageName.replaceAll(/[._-]/g, separator) const hyphenName = packageName.replaceAll(pythonSeparatorPattern, "-");
)])]; const underscoreName = packageName.replaceAll(pythonSeparatorPattern, "_");
const dotName = packageName.replaceAll(pythonSeparatorPattern, ".");
return [...new Set([packageName, hyphenName, underscoreName, dotName])];
} }