mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 12:10:49 +00:00
Adapt per review
This commit is contained in:
parent
aa7bbbd4e9
commit
d84270be8d
4 changed files with 76 additions and 36 deletions
|
|
@ -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"
|
||||||
|
)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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])];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue