Merge pull request #232 from galargh/pip-custom-registries

feat: allow python custom registries configuration
This commit is contained in:
Sander Declerck 2026-01-05 14:01:51 +01:00 committed by GitHub
commit 8bfbe1c77d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 579 additions and 284 deletions

View file

@ -201,9 +201,13 @@ You can set the minimum package age through multiple sources (in order of priori
} }
``` ```
## Custom NPM Registries ## Custom Registries
Configure Safe Chain to scan packages from custom or private npm registries. Configure Safe Chain to scan packages from custom or private registries.
Supported ecosystems:
- Node.js
- Python
### Configuration Options ### Configuration Options
@ -213,6 +217,7 @@ You can set custom registries through environment variable or config file. Both
```shell ```shell
export SAFE_CHAIN_NPM_CUSTOM_REGISTRIES="npm.company.com,registry.internal.net" export SAFE_CHAIN_NPM_CUSTOM_REGISTRIES="npm.company.com,registry.internal.net"
export SAFE_CHAIN_PIP_CUSTOM_REGISTRIES="pip.company.com,registry.internal.net"
``` ```
2. **Config File** (`~/.aikido/config.json`): 2. **Config File** (`~/.aikido/config.json`):
@ -221,6 +226,9 @@ You can set custom registries through environment variable or config file. Both
{ {
"npm": { "npm": {
"customRegistries": ["npm.company.com", "registry.internal.net"] "customRegistries": ["npm.company.com", "registry.internal.net"]
},
"pip": {
"customRegistries": ["pip.company.com", "registry.internal.net"]
} }
} }
``` ```

View file

@ -11,6 +11,7 @@ import { getEcoSystem } from "./settings.js";
* @property {unknown | Number} scanTimeout * @property {unknown | Number} scanTimeout
* @property {unknown | Number} minimumPackageAgeHours * @property {unknown | Number} minimumPackageAgeHours
* @property {unknown | SafeChainRegistryConfiguration} npm * @property {unknown | SafeChainRegistryConfiguration} npm
* @property {unknown | SafeChainRegistryConfiguration} pip
* *
* @typedef {Object} SafeChainRegistryConfiguration * @typedef {Object} SafeChainRegistryConfiguration
* We cannot trust the input and should add the necessary validations. * We cannot trust the input and should add the necessary validations.
@ -104,6 +105,28 @@ export function getNpmCustomRegistries() {
return customRegistries.filter((item) => typeof item === "string"); return customRegistries.filter((item) => typeof item === "string");
} }
/**
* Gets the custom npm registries from the config file (format parsing only, no validation)
* @returns {string[]}
*/
export function getPipCustomRegistries() {
const config = readConfigFile();
if (!config || !config.pip) {
return [];
}
// TypeScript needs help understanding that config.pip exists and has customRegistries
const pipConfig = /** @type {SafeChainRegistryConfiguration} */ (config.pip);
const customRegistries = pipConfig.customRegistries;
if (!Array.isArray(customRegistries)) {
return [];
}
return customRegistries.filter((item) => typeof item === "string");
}
/** /**
* @param {import("../api/aikido.js").MalwarePackage[]} data * @param {import("../api/aikido.js").MalwarePackage[]} data
* @param {string | number} version * @param {string | number} version
@ -169,6 +192,9 @@ function readConfigFile() {
npm: { npm: {
customRegistries: undefined, customRegistries: undefined,
}, },
pip: {
customRegistries: undefined,
},
}; };
const configFilePath = getConfigFilePath(); const configFilePath = getConfigFilePath();

View file

@ -232,9 +232,22 @@ describe("getMinimumPackageAgeHours", async () => {
}); });
}); });
describe("getNpmCustomRegistries", async () => { const { getNpmCustomRegistries, getPipCustomRegistries } = await import(
const { getNpmCustomRegistries } = await import("./configFile.js"); "./configFile.js"
);
for (const { packageManager, getCustomRegistries } of [
{
packageManager: "npm",
getCustomRegistries: getNpmCustomRegistries,
},
{
packageManager: "pip",
getCustomRegistries: getPipCustomRegistries,
},
])
{
describe(getCustomRegistries.name, async () => {
afterEach(() => { afterEach(() => {
configFileContent = undefined; configFileContent = undefined;
}); });
@ -242,49 +255,49 @@ describe("getNpmCustomRegistries", async () => {
it("should return empty array when config file doesn't exist", () => { it("should return empty array when config file doesn't exist", () => {
configFileContent = undefined; configFileContent = undefined;
const registries = getNpmCustomRegistries(); const registries = getCustomRegistries();
assert.deepStrictEqual(registries, []); assert.deepStrictEqual(registries, []);
}); });
it("should return empty array when npm config is not set", () => { it(`should return empty array when ${packageManager} config is not set`, () => {
configFileContent = JSON.stringify({ scanTimeout: 5000 }); configFileContent = JSON.stringify({ scanTimeout: 5000 });
const registries = getNpmCustomRegistries(); const registries = getCustomRegistries();
assert.deepStrictEqual(registries, []); assert.deepStrictEqual(registries, []);
}); });
it("should return empty array when customRegistries is not an array", () => { it("should return empty array when customRegistries is not an array", () => {
configFileContent = JSON.stringify({ configFileContent = JSON.stringify({
npm: { customRegistries: "not-an-array" }, [packageManager]: { customRegistries: "not-an-array" },
}); });
const registries = getNpmCustomRegistries(); const registries = getCustomRegistries();
assert.deepStrictEqual(registries, []); assert.deepStrictEqual(registries, []);
}); });
it("should return array of custom registries when set", () => { it("should return array of custom registries when set", () => {
configFileContent = JSON.stringify({ configFileContent = JSON.stringify({
npm: { [packageManager]: {
customRegistries: ["npm.company.com", "registry.internal.net"], customRegistries: [`${packageManager}.company.com`, "registry.internal.net"],
}, },
}); });
const registries = getNpmCustomRegistries(); const registries = getCustomRegistries();
assert.deepStrictEqual(registries, [ assert.deepStrictEqual(registries, [
"npm.company.com", `${packageManager}.company.com`,
"registry.internal.net", "registry.internal.net",
]); ]);
}); });
it("should filter out non-string values", () => { it("should filter out non-string values", () => {
configFileContent = JSON.stringify({ configFileContent = JSON.stringify({
npm: { [packageManager]: {
customRegistries: [ customRegistries: [
"npm.company.com", `${packageManager}.company.com`,
123, 123,
null, null,
"registry.internal.net", "registry.internal.net",
@ -294,20 +307,20 @@ describe("getNpmCustomRegistries", async () => {
}, },
}); });
const registries = getNpmCustomRegistries(); const registries = getCustomRegistries();
assert.deepStrictEqual(registries, [ assert.deepStrictEqual(registries, [
"npm.company.com", `${packageManager}.company.com`,
"registry.internal.net", "registry.internal.net",
]); ]);
}); });
it("should return empty array for empty customRegistries array", () => { it("should return empty array for empty customRegistries array", () => {
configFileContent = JSON.stringify({ configFileContent = JSON.stringify({
npm: { customRegistries: [] }, [packageManager]: { customRegistries: [] },
}); });
const registries = getNpmCustomRegistries(); const registries = getCustomRegistries();
assert.deepStrictEqual(registries, []); assert.deepStrictEqual(registries, []);
}); });
@ -315,8 +328,9 @@ describe("getNpmCustomRegistries", async () => {
it("should handle malformed JSON and return empty array", () => { it("should handle malformed JSON and return empty array", () => {
configFileContent = "{ invalid json"; configFileContent = "{ invalid json";
const registries = getNpmCustomRegistries(); const registries = getCustomRegistries();
assert.deepStrictEqual(registries, []); assert.deepStrictEqual(registries, []);
}); });
}); });
}

View file

@ -15,3 +15,13 @@ export function getMinimumPackageAgeHours() {
export function getNpmCustomRegistries() { export function getNpmCustomRegistries() {
return process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES; return process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES;
} }
/**
* Gets the custom pip registries from environment variable
* Expected format: comma-separated list of registry domains
* Example: "pip.company.com,registry.internal.net"
* @returns {string | undefined}
*/
export function getPipCustomRegistries() {
return process.env.SAFE_CHAIN_PIP_CUSTOM_REGISTRIES;
}

View file

@ -143,3 +143,21 @@ export function getNpmCustomRegistries() {
// Normalize each registry (remove protocol if any) // Normalize each registry (remove protocol if any)
return uniqueRegistries.map(normalizeRegistry); return uniqueRegistries.map(normalizeRegistry);
} }
/**
* Gets the custom npm registries from both environment variable and config file (merged)
* @returns {string[]}
*/
export function getPipCustomRegistries() {
const envRegistries = parseRegistriesFromEnv(
environmentVariables.getPipCustomRegistries()
);
const configRegistries = configFile.getPipCustomRegistries();
// Merge both sources and remove duplicates
const allRegistries = [...envRegistries, ...configRegistries];
const uniqueRegistries = [...new Set(allRegistries)];
// Normalize each registry (remove protocol if any)
return uniqueRegistries.map(normalizeRegistry);
}

View file

@ -11,19 +11,35 @@ mock.module("fs", {
}, },
}); });
describe("getNpmCustomRegistries", async () => { const { getNpmCustomRegistries, getPipCustomRegistries } = await import(
"./settings.js"
);
for (const { packageManager, getCustomRegistries, envVarName } of [
{
packageManager: "npm",
getCustomRegistries: getNpmCustomRegistries,
envVarName: "SAFE_CHAIN_NPM_CUSTOM_REGISTRIES",
},
{
packageManager: "pip",
getCustomRegistries: getPipCustomRegistries,
envVarName: "SAFE_CHAIN_PIP_CUSTOM_REGISTRIES",
},
])
{
describe(getCustomRegistries.name, async () => {
let originalEnv; let originalEnv;
const { getNpmCustomRegistries } = await import("./settings.js");
beforeEach(() => { beforeEach(() => {
originalEnv = process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES; originalEnv = process.env[envVarName];
}); });
afterEach(() => { afterEach(() => {
if (originalEnv !== undefined) { if (originalEnv !== undefined) {
process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES = originalEnv; process.env[envVarName] = originalEnv;
} else { } else {
delete process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES; delete process.env[envVarName];
} }
configFileContent = undefined; configFileContent = undefined;
}); });
@ -31,77 +47,77 @@ describe("getNpmCustomRegistries", async () => {
it("should return empty array when no registries configured", () => { it("should return empty array when no registries configured", () => {
configFileContent = undefined; configFileContent = undefined;
const registries = getNpmCustomRegistries(); const registries = getCustomRegistries();
assert.deepStrictEqual(registries, []); assert.deepStrictEqual(registries, []);
}); });
it("should return registries without protocol", () => { it("should return registries without protocol", () => {
configFileContent = JSON.stringify({ configFileContent = JSON.stringify({
npm: { [packageManager]: {
customRegistries: ["npm.company.com", "registry.internal.net"], customRegistries: [`${packageManager}.company.com`, "registry.internal.net"],
}, },
}); });
const registries = getNpmCustomRegistries(); const registries = getCustomRegistries();
assert.deepStrictEqual(registries, [ assert.deepStrictEqual(registries, [
"npm.company.com", `${packageManager}.company.com`,
"registry.internal.net", "registry.internal.net",
]); ]);
}); });
it("should strip https:// protocol from registries", () => { it("should strip https:// protocol from registries", () => {
configFileContent = JSON.stringify({ configFileContent = JSON.stringify({
npm: { [packageManager]: {
customRegistries: [ customRegistries: [
"https://npm.company.com", `https://${packageManager}.company.com`,
"https://registry.internal.net", "https://registry.internal.net",
], ],
}, },
}); });
const registries = getNpmCustomRegistries(); const registries = getCustomRegistries();
assert.deepStrictEqual(registries, [ assert.deepStrictEqual(registries, [
"npm.company.com", `${packageManager}.company.com`,
"registry.internal.net", "registry.internal.net",
]); ]);
}); });
it("should strip http:// protocol from registries", () => { it("should strip http:// protocol from registries", () => {
configFileContent = JSON.stringify({ configFileContent = JSON.stringify({
npm: { [packageManager]: {
customRegistries: [ customRegistries: [
"http://npm.company.com", `http://${packageManager}.company.com`,
"http://registry.internal.net", "http://registry.internal.net",
], ],
}, },
}); });
const registries = getNpmCustomRegistries(); const registries = getCustomRegistries();
assert.deepStrictEqual(registries, [ assert.deepStrictEqual(registries, [
"npm.company.com", `${packageManager}.company.com`,
"registry.internal.net", "registry.internal.net",
]); ]);
}); });
it("should handle mixed protocols and no protocol", () => { it("should handle mixed protocols and no protocol", () => {
configFileContent = JSON.stringify({ configFileContent = JSON.stringify({
npm: { [packageManager]: {
customRegistries: [ customRegistries: [
"https://npm.company.com", `https://${packageManager}.company.com`,
"registry.internal.net", "registry.internal.net",
"http://private.registry.io", "http://private.registry.io",
], ],
}, },
}); });
const registries = getNpmCustomRegistries(); const registries = getCustomRegistries();
assert.deepStrictEqual(registries, [ assert.deepStrictEqual(registries, [
"npm.company.com", `${packageManager}.company.com`,
"registry.internal.net", "registry.internal.net",
"private.registry.io", "private.registry.io",
]); ]);
@ -109,29 +125,29 @@ describe("getNpmCustomRegistries", async () => {
it("should preserve registry path after stripping protocol", () => { it("should preserve registry path after stripping protocol", () => {
configFileContent = JSON.stringify({ configFileContent = JSON.stringify({
npm: { [packageManager]: {
customRegistries: [ customRegistries: [
"https://npm.company.com/custom/path", `https://${packageManager}.company.com/custom/path`,
"registry.internal.net/npm", `registry.internal.net/${packageManager}`,
], ],
}, },
}); });
const registries = getNpmCustomRegistries(); const registries = getCustomRegistries();
assert.deepStrictEqual(registries, [ assert.deepStrictEqual(registries, [
"npm.company.com/custom/path", `${packageManager}.company.com/custom/path`,
"registry.internal.net/npm", `registry.internal.net/${packageManager}`,
]); ]);
}); });
it("should parse comma-separated registries from environment variable", () => { it("should parse comma-separated registries from environment variable", () => {
delete process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES; delete process.env[envVarName];
process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES = process.env[envVarName] =
"env1.registry.com,env2.registry.net"; "env1.registry.com,env2.registry.net";
configFileContent = undefined; configFileContent = undefined;
const registries = getNpmCustomRegistries(); const registries = getCustomRegistries();
assert.deepStrictEqual(registries, [ assert.deepStrictEqual(registries, [
"env1.registry.com", "env1.registry.com",
@ -140,12 +156,12 @@ describe("getNpmCustomRegistries", async () => {
}); });
it("should trim whitespace from environment variable registries", () => { it("should trim whitespace from environment variable registries", () => {
delete process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES; delete process.env[envVarName];
process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES = process.env[envVarName] =
" env1.registry.com , env2.registry.net "; " env1.registry.com , env2.registry.net ";
configFileContent = undefined; configFileContent = undefined;
const registries = getNpmCustomRegistries(); const registries = getCustomRegistries();
assert.deepStrictEqual(registries, [ assert.deepStrictEqual(registries, [
"env1.registry.com", "env1.registry.com",
@ -154,15 +170,15 @@ describe("getNpmCustomRegistries", async () => {
}); });
it("should merge environment variable and config file registries", () => { it("should merge environment variable and config file registries", () => {
delete process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES; delete process.env[envVarName];
process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES = "env1.registry.com"; process.env[envVarName] = "env1.registry.com";
configFileContent = JSON.stringify({ configFileContent = JSON.stringify({
npm: { [packageManager]: {
customRegistries: ["config1.registry.net"], customRegistries: ["config1.registry.net"],
}, },
}); });
const registries = getNpmCustomRegistries(); const registries = getCustomRegistries();
assert.deepStrictEqual(registries, [ assert.deepStrictEqual(registries, [
"env1.registry.com", "env1.registry.com",
@ -171,31 +187,31 @@ describe("getNpmCustomRegistries", async () => {
}); });
it("should remove duplicate registries when merging env and config", () => { it("should remove duplicate registries when merging env and config", () => {
delete process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES; delete process.env[envVarName];
process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES = process.env[envVarName] =
"npm.company.com,env.registry.com"; `${packageManager}.company.com,env.registry.com`;
configFileContent = JSON.stringify({ configFileContent = JSON.stringify({
npm: { [packageManager]: {
customRegistries: ["npm.company.com", "config.registry.net"], customRegistries: [`${packageManager}.company.com`, "config.registry.net"],
}, },
}); });
const registries = getNpmCustomRegistries(); const registries = getCustomRegistries();
assert.deepStrictEqual(registries, [ assert.deepStrictEqual(registries, [
"npm.company.com", `${packageManager}.company.com`,
"env.registry.com", "env.registry.com",
"config.registry.net", "config.registry.net",
]); ]);
}); });
it("should normalize protocols from environment variable registries", () => { it("should normalize protocols from environment variable registries", () => {
delete process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES; delete process.env[envVarName];
process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES = process.env[envVarName] =
"https://env1.registry.com,http://env2.registry.net"; "https://env1.registry.com,http://env2.registry.net";
configFileContent = undefined; configFileContent = undefined;
const registries = getNpmCustomRegistries(); const registries = getCustomRegistries();
assert.deepStrictEqual(registries, [ assert.deepStrictEqual(registries, [
"env1.registry.com", "env1.registry.com",
@ -204,12 +220,12 @@ describe("getNpmCustomRegistries", async () => {
}); });
it("should handle empty strings in comma-separated list", () => { it("should handle empty strings in comma-separated list", () => {
delete process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES; delete process.env[envVarName];
process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES = process.env[envVarName] =
"env1.registry.com,,env2.registry.net,"; "env1.registry.com,,env2.registry.net,";
configFileContent = undefined; configFileContent = undefined;
const registries = getNpmCustomRegistries(); const registries = getCustomRegistries();
assert.deepStrictEqual(registries, [ assert.deepStrictEqual(registries, [
"env1.registry.com", "env1.registry.com",
@ -218,32 +234,33 @@ describe("getNpmCustomRegistries", async () => {
}); });
it("should handle single registry in environment variable", () => { it("should handle single registry in environment variable", () => {
delete process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES; delete process.env[envVarName];
process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES = "single.registry.com"; process.env[envVarName] = "single.registry.com";
configFileContent = undefined; configFileContent = undefined;
const registries = getNpmCustomRegistries(); const registries = getCustomRegistries();
assert.deepStrictEqual(registries, ["single.registry.com"]); assert.deepStrictEqual(registries, ["single.registry.com"]);
}); });
it("should return empty array for empty environment variable", () => { it("should return empty array for empty environment variable", () => {
delete process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES; delete process.env[envVarName];
process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES = ""; process.env[envVarName] = "";
configFileContent = undefined; configFileContent = undefined;
const registries = getNpmCustomRegistries(); const registries = getCustomRegistries();
assert.deepStrictEqual(registries, []); assert.deepStrictEqual(registries, []);
}); });
it("should return empty array for whitespace-only environment variable", () => { it("should return empty array for whitespace-only environment variable", () => {
delete process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES; delete process.env[envVarName];
process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES = " , , "; process.env[envVarName] = " , , ";
configFileContent = undefined; configFileContent = undefined;
const registries = getNpmCustomRegistries(); const registries = getCustomRegistries();
assert.deepStrictEqual(registries, []); assert.deepStrictEqual(registries, []);
}); });
}); });
}

View file

@ -1,3 +1,4 @@
import { getPipCustomRegistries } from "../../config/settings.js";
import { isMalwarePackage } from "../../scanning/audit/index.js"; import { isMalwarePackage } from "../../scanning/audit/index.js";
import { interceptRequests } from "./interceptorBuilder.js"; import { interceptRequests } from "./interceptorBuilder.js";
@ -13,7 +14,9 @@ const knownPipRegistries = [
* @returns {import("./interceptorBuilder.js").Interceptor | undefined} * @returns {import("./interceptorBuilder.js").Interceptor | undefined}
*/ */
export function pipInterceptorForUrl(url) { export function pipInterceptorForUrl(url) {
const registry = knownPipRegistries.find((reg) => url.includes(reg)); const customRegistries = getPipCustomRegistries();
const registries = [...knownPipRegistries, ...customRegistries];
const registry = registries.find((reg) => url.includes(reg));
if (registry) { if (registry) {
return buildPipInterceptor(registry); return buildPipInterceptor(registry);

View file

@ -0,0 +1,199 @@
import { describe, it, mock } from "node:test";
import assert from "node:assert";
describe("pipInterceptor custom registries", async () => {
let lastPackage;
let malwareResponse = false;
let customRegistries = [];
mock.module("../../config/settings.js", {
namedExports: {
getPipCustomRegistries: () => customRegistries,
},
});
mock.module("../../scanning/audit/index.js", {
namedExports: {
isMalwarePackage: async (packageName, version) => {
lastPackage = { packageName, version };
return malwareResponse;
},
},
});
const { pipInterceptorForUrl } = await import("./pipInterceptor.js");
it("should create interceptor for custom registry", () => {
customRegistries = ["my-custom-registry.example.com"];
const url =
"https://my-custom-registry.example.com/packages/xx/yy/foobar-1.2.3.tar.gz";
const interceptor = pipInterceptorForUrl(url);
assert.ok(
interceptor,
"Interceptor should be created for custom registry"
);
});
it("should parse package from custom registry URL", async () => {
customRegistries = ["my-custom-registry.example.com"];
const url =
"https://my-custom-registry.example.com/packages/xx/yy/foobar-1.2.3.tar.gz";
const interceptor = pipInterceptorForUrl(url);
assert.ok(interceptor, "Interceptor should be created");
await interceptor.handleRequest(url);
assert.deepEqual(lastPackage, {
packageName: "foobar",
version: "1.2.3",
});
});
it("should parse wheel package from custom registry URL", async () => {
customRegistries = ["private-pypi.internal.com"];
const url =
"https://private-pypi.internal.com/packages/foo_bar-2.0.0-py3-none-any.whl";
const interceptor = pipInterceptorForUrl(url);
assert.ok(interceptor, "Interceptor should be created");
await interceptor.handleRequest(url);
assert.deepEqual(lastPackage, {
packageName: "foo-bar",
version: "2.0.0",
});
});
it("should handle multiple custom registries", async () => {
customRegistries = [
"registry-one.example.com",
"registry-two.example.com",
];
const url1 =
"https://registry-one.example.com/packages/package1-1.0.0.tar.gz";
const url2 =
"https://registry-two.example.com/packages/package2-2.0.0.tar.gz";
const interceptor1 = pipInterceptorForUrl(url1);
const interceptor2 = pipInterceptorForUrl(url2);
assert.ok(interceptor1, "Interceptor should be created for first registry");
assert.ok(
interceptor2,
"Interceptor should be created for second registry"
);
});
it("should block malicious package from custom registry", async () => {
customRegistries = ["my-custom-registry.example.com"];
malwareResponse = true;
const url =
"https://my-custom-registry.example.com/packages/malicious_package-1.0.0.tar.gz";
const interceptor = pipInterceptorForUrl(url);
assert.ok(interceptor, "Interceptor should be created");
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"
);
malwareResponse = false;
});
it("should still work with known registries when custom registries are set", async () => {
customRegistries = ["my-custom-registry.example.com"];
const url =
"https://files.pythonhosted.org/packages/xx/yy/foobar-1.2.3.tar.gz";
const interceptor = pipInterceptorForUrl(url);
assert.ok(
interceptor,
"Interceptor should be created for known registry even with custom registries set"
);
await interceptor.handleRequest(url);
assert.deepEqual(lastPackage, {
packageName: "foobar",
version: "1.2.3",
});
});
it("should not create interceptor for unknown registry when custom registries are set", () => {
customRegistries = ["my-custom-registry.example.com"];
const url = "https://unknown-registry.example.com/packages/foobar-1.0.0.tar.gz";
const interceptor = pipInterceptorForUrl(url);
assert.equal(
interceptor,
undefined,
"Interceptor should be undefined for unknown registry"
);
});
it("should handle empty custom registries array", () => {
customRegistries = [];
const url =
"https://my-custom-registry.example.com/packages/foobar-1.0.0.tar.gz";
const interceptor = pipInterceptorForUrl(url);
assert.equal(
interceptor,
undefined,
"Interceptor should be undefined when no custom registries are configured"
);
});
it("should parse .whl.metadata from custom registry", async () => {
customRegistries = ["private-pypi.internal.com"];
const url =
"https://private-pypi.internal.com/packages/foo_bar-2.0.0-py3-none-any.whl.metadata";
const interceptor = pipInterceptorForUrl(url);
assert.ok(interceptor, "Interceptor should be created");
await interceptor.handleRequest(url);
assert.deepEqual(lastPackage, {
packageName: "foo-bar",
version: "2.0.0",
});
});
it("should parse .tar.gz.metadata from custom registry", async () => {
customRegistries = ["private-pypi.internal.com"];
const url =
"https://private-pypi.internal.com/packages/foo_bar-2.0.0.tar.gz.metadata";
const interceptor = pipInterceptorForUrl(url);
assert.ok(interceptor, "Interceptor should be created");
await interceptor.handleRequest(url);
assert.deepEqual(lastPackage, {
packageName: "foo-bar",
version: "2.0.0",
});
});
});