mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 12:10:49 +00:00
Allow to configure custom/prinvate npm registries
This commit is contained in:
parent
0925279521
commit
41cc24d1f5
9 changed files with 576 additions and 15 deletions
24
README.md
24
README.md
|
|
@ -183,6 +183,30 @@ You can set the minimum package age through multiple sources (in order of priori
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Custom NPM Registries
|
||||||
|
|
||||||
|
Configure Safe Chain to scan packages from custom or private npm registries.
|
||||||
|
|
||||||
|
### Configuration Options
|
||||||
|
|
||||||
|
You can set custom registries through environment variable or config file. Both sources are merged together.
|
||||||
|
|
||||||
|
1. **Environment Variable** (comma-separated):
|
||||||
|
|
||||||
|
```shell
|
||||||
|
export SAFE_CHAIN_NPM_CUSTOM_REGISTRIES="npm.company.com,registry.internal.net"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Config File** (`~/.aikido/config.json`):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"npm": {
|
||||||
|
"customRegistries": ["npm.company.com", "registry.internal.net"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
# Usage in CI/CD
|
# Usage in CI/CD
|
||||||
|
|
||||||
You can protect your CI/CD pipelines from malicious packages by integrating Aikido Safe Chain into your build process. This ensures that any packages installed during your automated builds are checked for malware before installation.
|
You can protect your CI/CD pipelines from malicious packages by integrating Aikido Safe Chain into your build process. This ensures that any packages installed during your automated builds are checked for malware before installation.
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,14 @@ import { getEcoSystem } from "./settings.js";
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} SafeChainConfig
|
* @typedef {Object} SafeChainConfig
|
||||||
*
|
*
|
||||||
* This should be a number, but can be anything because it is user-input.
|
* We cannot trust the input and should add the necessary validations
|
||||||
|
* @property {unknown | Number} scanTimeout
|
||||||
|
* @property {unknown | Number} minimumPackageAgeHours
|
||||||
|
* @property {unknown | SafeChainRegistryConfiguration} npm
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
* @property {unknown} scanTimeout
|
* @property {unknown | string[]} customRegistries
|
||||||
* @property {unknown} minimumPackageAgeHours
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -78,6 +82,30 @@ export function getMinimumPackageAgeHours() {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the custom npm registries from the config file (format parsing only, no validation)
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
export function getNpmCustomRegistries() {
|
||||||
|
const config = readConfigFile();
|
||||||
|
|
||||||
|
if (!config || !config.npm) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// TypeScript needs help understanding that config.npm exists and has customRegistries
|
||||||
|
const npmConfig = /** @type {SafeChainRegistryConfiguration} */ (config.npm);
|
||||||
|
const customRegistries = npmConfig.customRegistries;
|
||||||
|
|
||||||
|
// Handle format: ensure it's an array of strings
|
||||||
|
if (!Array.isArray(customRegistries)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter to only string values (format checking, not validation)
|
||||||
|
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
|
||||||
|
|
@ -142,6 +170,9 @@ function readConfigFile() {
|
||||||
return {
|
return {
|
||||||
scanTimeout: undefined,
|
scanTimeout: undefined,
|
||||||
minimumPackageAgeHours: undefined,
|
minimumPackageAgeHours: undefined,
|
||||||
|
npm: {
|
||||||
|
customRegistries: undefined,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -152,6 +183,9 @@ function readConfigFile() {
|
||||||
return {
|
return {
|
||||||
scanTimeout: undefined,
|
scanTimeout: undefined,
|
||||||
minimumPackageAgeHours: undefined,
|
minimumPackageAgeHours: undefined,
|
||||||
|
npm: {
|
||||||
|
customRegistries: undefined,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -231,3 +231,92 @@ describe("getMinimumPackageAgeHours", async () => {
|
||||||
assert.strictEqual(hours, -48);
|
assert.strictEqual(hours, -48);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("getNpmCustomRegistries", async () => {
|
||||||
|
const { getNpmCustomRegistries } = await import("./configFile.js");
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
configFileContent = undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return empty array when config file doesn't exist", () => {
|
||||||
|
configFileContent = undefined;
|
||||||
|
|
||||||
|
const registries = getNpmCustomRegistries();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(registries, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return empty array when npm config is not set", () => {
|
||||||
|
configFileContent = JSON.stringify({ scanTimeout: 5000 });
|
||||||
|
|
||||||
|
const registries = getNpmCustomRegistries();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(registries, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return empty array when customRegistries is not an array", () => {
|
||||||
|
configFileContent = JSON.stringify({
|
||||||
|
npm: { customRegistries: "not-an-array" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const registries = getNpmCustomRegistries();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(registries, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return array of custom registries when set", () => {
|
||||||
|
configFileContent = JSON.stringify({
|
||||||
|
npm: {
|
||||||
|
customRegistries: ["npm.company.com", "registry.internal.net"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const registries = getNpmCustomRegistries();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(registries, [
|
||||||
|
"npm.company.com",
|
||||||
|
"registry.internal.net",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should filter out non-string values", () => {
|
||||||
|
configFileContent = JSON.stringify({
|
||||||
|
npm: {
|
||||||
|
customRegistries: [
|
||||||
|
"npm.company.com",
|
||||||
|
123,
|
||||||
|
null,
|
||||||
|
"registry.internal.net",
|
||||||
|
undefined,
|
||||||
|
{},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const registries = getNpmCustomRegistries();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(registries, [
|
||||||
|
"npm.company.com",
|
||||||
|
"registry.internal.net",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return empty array for empty customRegistries array", () => {
|
||||||
|
configFileContent = JSON.stringify({
|
||||||
|
npm: { customRegistries: [] },
|
||||||
|
});
|
||||||
|
|
||||||
|
const registries = getNpmCustomRegistries();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(registries, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle malformed JSON and return empty array", () => {
|
||||||
|
configFileContent = "{ invalid json";
|
||||||
|
|
||||||
|
const registries = getNpmCustomRegistries();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(registries, []);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -5,3 +5,13 @@
|
||||||
export function getMinimumPackageAgeHours() {
|
export function getMinimumPackageAgeHours() {
|
||||||
return process.env.SAFE_CHAIN_MINIMUM_PACKAGE_AGE_HOURS;
|
return process.env.SAFE_CHAIN_MINIMUM_PACKAGE_AGE_HOURS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the custom npm registries from environment variable
|
||||||
|
* Expected format: comma-separated list of registry domains
|
||||||
|
* Example: "npm.company.com,registry.internal.net"
|
||||||
|
* @returns {string | undefined}
|
||||||
|
*/
|
||||||
|
export function getNpmCustomRegistries() {
|
||||||
|
return process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -98,3 +98,48 @@ export function skipMinimumPackageAge() {
|
||||||
|
|
||||||
return defaultSkipMinimumPackageAge;
|
return defaultSkipMinimumPackageAge;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes a registry URL by removing protocol if present
|
||||||
|
* @param {string} registry
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function normalizeRegistry(registry) {
|
||||||
|
// Remove protocol (http://, https://) if present
|
||||||
|
return registry.replace(/^https?:\/\//, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses comma-separated registries from environment variable
|
||||||
|
* @param {string | undefined} envValue
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
function parseRegistriesFromEnv(envValue) {
|
||||||
|
if (!envValue || typeof envValue !== "string") {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split by comma and trim whitespace
|
||||||
|
return envValue
|
||||||
|
.split(",")
|
||||||
|
.map((registry) => registry.trim())
|
||||||
|
.filter((registry) => registry.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the custom npm registries from both environment variable and config file (merged)
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
export function getNpmCustomRegistries() {
|
||||||
|
const envRegistries = parseRegistriesFromEnv(
|
||||||
|
environmentVariables.getNpmCustomRegistries()
|
||||||
|
);
|
||||||
|
const configRegistries = configFile.getNpmCustomRegistries();
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
|
||||||
249
packages/safe-chain/src/config/settings.spec.js
Normal file
249
packages/safe-chain/src/config/settings.spec.js
Normal file
|
|
@ -0,0 +1,249 @@
|
||||||
|
import { describe, it, beforeEach, afterEach, mock } from "node:test";
|
||||||
|
import assert from "node:assert";
|
||||||
|
|
||||||
|
let configFileContent = undefined;
|
||||||
|
mock.module("fs", {
|
||||||
|
namedExports: {
|
||||||
|
existsSync: () => configFileContent !== undefined,
|
||||||
|
readFileSync: () => configFileContent,
|
||||||
|
writeFileSync: (content) => (configFileContent = content),
|
||||||
|
mkdirSync: () => {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getNpmCustomRegistries", async () => {
|
||||||
|
let originalEnv;
|
||||||
|
const { getNpmCustomRegistries } = await import("./settings.js");
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
originalEnv = process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (originalEnv !== undefined) {
|
||||||
|
process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES = originalEnv;
|
||||||
|
} else {
|
||||||
|
delete process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES;
|
||||||
|
}
|
||||||
|
configFileContent = undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return empty array when no registries configured", () => {
|
||||||
|
configFileContent = undefined;
|
||||||
|
|
||||||
|
const registries = getNpmCustomRegistries();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(registries, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return registries without protocol", () => {
|
||||||
|
configFileContent = JSON.stringify({
|
||||||
|
npm: {
|
||||||
|
customRegistries: ["npm.company.com", "registry.internal.net"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const registries = getNpmCustomRegistries();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(registries, [
|
||||||
|
"npm.company.com",
|
||||||
|
"registry.internal.net",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should strip https:// protocol from registries", () => {
|
||||||
|
configFileContent = JSON.stringify({
|
||||||
|
npm: {
|
||||||
|
customRegistries: [
|
||||||
|
"https://npm.company.com",
|
||||||
|
"https://registry.internal.net",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const registries = getNpmCustomRegistries();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(registries, [
|
||||||
|
"npm.company.com",
|
||||||
|
"registry.internal.net",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should strip http:// protocol from registries", () => {
|
||||||
|
configFileContent = JSON.stringify({
|
||||||
|
npm: {
|
||||||
|
customRegistries: [
|
||||||
|
"http://npm.company.com",
|
||||||
|
"http://registry.internal.net",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const registries = getNpmCustomRegistries();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(registries, [
|
||||||
|
"npm.company.com",
|
||||||
|
"registry.internal.net",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle mixed protocols and no protocol", () => {
|
||||||
|
configFileContent = JSON.stringify({
|
||||||
|
npm: {
|
||||||
|
customRegistries: [
|
||||||
|
"https://npm.company.com",
|
||||||
|
"registry.internal.net",
|
||||||
|
"http://private.registry.io",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const registries = getNpmCustomRegistries();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(registries, [
|
||||||
|
"npm.company.com",
|
||||||
|
"registry.internal.net",
|
||||||
|
"private.registry.io",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should preserve registry path after stripping protocol", () => {
|
||||||
|
configFileContent = JSON.stringify({
|
||||||
|
npm: {
|
||||||
|
customRegistries: [
|
||||||
|
"https://npm.company.com/custom/path",
|
||||||
|
"registry.internal.net/npm",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const registries = getNpmCustomRegistries();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(registries, [
|
||||||
|
"npm.company.com/custom/path",
|
||||||
|
"registry.internal.net/npm",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should parse comma-separated registries from environment variable", () => {
|
||||||
|
delete process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES;
|
||||||
|
process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES =
|
||||||
|
"env1.registry.com,env2.registry.net";
|
||||||
|
configFileContent = undefined;
|
||||||
|
|
||||||
|
const registries = getNpmCustomRegistries();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(registries, [
|
||||||
|
"env1.registry.com",
|
||||||
|
"env2.registry.net",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should trim whitespace from environment variable registries", () => {
|
||||||
|
delete process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES;
|
||||||
|
process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES =
|
||||||
|
" env1.registry.com , env2.registry.net ";
|
||||||
|
configFileContent = undefined;
|
||||||
|
|
||||||
|
const registries = getNpmCustomRegistries();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(registries, [
|
||||||
|
"env1.registry.com",
|
||||||
|
"env2.registry.net",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should merge environment variable and config file registries", () => {
|
||||||
|
delete process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES;
|
||||||
|
process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES = "env1.registry.com";
|
||||||
|
configFileContent = JSON.stringify({
|
||||||
|
npm: {
|
||||||
|
customRegistries: ["config1.registry.net"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const registries = getNpmCustomRegistries();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(registries, [
|
||||||
|
"env1.registry.com",
|
||||||
|
"config1.registry.net",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove duplicate registries when merging env and config", () => {
|
||||||
|
delete process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES;
|
||||||
|
process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES =
|
||||||
|
"npm.company.com,env.registry.com";
|
||||||
|
configFileContent = JSON.stringify({
|
||||||
|
npm: {
|
||||||
|
customRegistries: ["npm.company.com", "config.registry.net"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const registries = getNpmCustomRegistries();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(registries, [
|
||||||
|
"npm.company.com",
|
||||||
|
"env.registry.com",
|
||||||
|
"config.registry.net",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should normalize protocols from environment variable registries", () => {
|
||||||
|
delete process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES;
|
||||||
|
process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES =
|
||||||
|
"https://env1.registry.com,http://env2.registry.net";
|
||||||
|
configFileContent = undefined;
|
||||||
|
|
||||||
|
const registries = getNpmCustomRegistries();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(registries, [
|
||||||
|
"env1.registry.com",
|
||||||
|
"env2.registry.net",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle empty strings in comma-separated list", () => {
|
||||||
|
delete process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES;
|
||||||
|
process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES =
|
||||||
|
"env1.registry.com,,env2.registry.net,";
|
||||||
|
configFileContent = undefined;
|
||||||
|
|
||||||
|
const registries = getNpmCustomRegistries();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(registries, [
|
||||||
|
"env1.registry.com",
|
||||||
|
"env2.registry.net",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle single registry in environment variable", () => {
|
||||||
|
delete process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES;
|
||||||
|
process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES = "single.registry.com";
|
||||||
|
configFileContent = undefined;
|
||||||
|
|
||||||
|
const registries = getNpmCustomRegistries();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(registries, ["single.registry.com"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return empty array for empty environment variable", () => {
|
||||||
|
delete process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES;
|
||||||
|
process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES = "";
|
||||||
|
configFileContent = undefined;
|
||||||
|
|
||||||
|
const registries = getNpmCustomRegistries();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(registries, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return empty array for whitespace-only environment variable", () => {
|
||||||
|
delete process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES;
|
||||||
|
process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES = " , , ";
|
||||||
|
configFileContent = undefined;
|
||||||
|
|
||||||
|
const registries = getNpmCustomRegistries();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(registries, []);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
import { skipMinimumPackageAge } from "../../../config/settings.js";
|
import {
|
||||||
|
getNpmCustomRegistries,
|
||||||
|
skipMinimumPackageAge,
|
||||||
|
} 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";
|
||||||
import {
|
import {
|
||||||
|
|
@ -15,7 +18,9 @@ const knownJsRegistries = ["registry.npmjs.org", "registry.yarnpkg.com"];
|
||||||
* @returns {import("../interceptorBuilder.js").Interceptor | undefined}
|
* @returns {import("../interceptorBuilder.js").Interceptor | undefined}
|
||||||
*/
|
*/
|
||||||
export function npmInterceptorForUrl(url) {
|
export function npmInterceptorForUrl(url) {
|
||||||
const registry = knownJsRegistries.find((reg) => url.includes(reg));
|
const registry = [...knownJsRegistries, ...getNpmCustomRegistries()].find(
|
||||||
|
(reg) => url.includes(reg)
|
||||||
|
);
|
||||||
|
|
||||||
if (registry) {
|
if (registry) {
|
||||||
return buildNpmInterceptor(registry);
|
return buildNpmInterceptor(registry);
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ describe("npmInterceptor minimum package age", async () => {
|
||||||
namedExports: {
|
namedExports: {
|
||||||
getMinimumPackageAgeHours: () => minimumPackageAgeSettings,
|
getMinimumPackageAgeHours: () => minimumPackageAgeSettings,
|
||||||
skipMinimumPackageAge: () => skipMinimumPackageAgeSetting,
|
skipMinimumPackageAge: () => skipMinimumPackageAgeSetting,
|
||||||
|
getNpmCustomRegistries: () => [],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { describe, it, mock } from "node:test";
|
import { describe, it, mock } from "node:test";
|
||||||
import assert from "node:assert";
|
import assert from "node:assert";
|
||||||
|
|
||||||
describe("npmInterceptor", async () => {
|
|
||||||
let lastPackage;
|
let lastPackage;
|
||||||
let malwareResponse = false;
|
let malwareResponse = false;
|
||||||
|
let customRegistries = [];
|
||||||
|
|
||||||
mock.module("../../../scanning/audit/index.js", {
|
mock.module("../../../scanning/audit/index.js", {
|
||||||
namedExports: {
|
namedExports: {
|
||||||
|
|
@ -14,6 +14,23 @@ describe("npmInterceptor", async () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
mock.module("../../../config/settings.js", {
|
||||||
|
namedExports: {
|
||||||
|
LOGGING_SILENT: "silent",
|
||||||
|
LOGGING_NORMAL: "normal",
|
||||||
|
LOGGING_VERBOSE: "verbose",
|
||||||
|
ECOSYSTEM_JS: "js",
|
||||||
|
ECOSYSTEM_PY: "py",
|
||||||
|
getLoggingLevel: () => "normal",
|
||||||
|
getEcoSystem: () => "js",
|
||||||
|
setEcoSystem: () => {},
|
||||||
|
getMinimumPackageAgeHours: () => 24,
|
||||||
|
getNpmCustomRegistries: () => customRegistries,
|
||||||
|
skipMinimumPackageAge: () => false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("npmInterceptor", async () => {
|
||||||
const { npmInterceptorForUrl } = await import("./npmInterceptor.js");
|
const { npmInterceptorForUrl } = await import("./npmInterceptor.js");
|
||||||
|
|
||||||
const parserCases = [
|
const parserCases = [
|
||||||
|
|
@ -161,3 +178,90 @@ describe("npmInterceptor", async () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("npmInterceptor with custom registries", async () => {
|
||||||
|
const { npmInterceptorForUrl } = await import("./npmInterceptor.js");
|
||||||
|
|
||||||
|
it("should create interceptor for custom registry", async () => {
|
||||||
|
// Set custom registries for this test
|
||||||
|
customRegistries = ["npm.company.com", "registry.internal.net"];
|
||||||
|
const url = "https://npm.company.com/lodash/-/lodash-4.17.21.tgz";
|
||||||
|
|
||||||
|
const interceptor = npmInterceptorForUrl(url);
|
||||||
|
|
||||||
|
assert.ok(interceptor, "Interceptor should be created for custom registry");
|
||||||
|
|
||||||
|
await interceptor.handleRequest(url);
|
||||||
|
|
||||||
|
assert.deepEqual(lastPackage, {
|
||||||
|
packageName: "lodash",
|
||||||
|
version: "4.17.21",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create interceptor for custom registry with scoped packages", async () => {
|
||||||
|
// Set custom registries for this test
|
||||||
|
customRegistries = ["npm.company.com", "registry.internal.net"];
|
||||||
|
malwareResponse = false;
|
||||||
|
|
||||||
|
const url =
|
||||||
|
"https://registry.internal.net/@company/package/-/package-1.0.0.tgz";
|
||||||
|
|
||||||
|
const interceptor = npmInterceptorForUrl(url);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
interceptor,
|
||||||
|
"Interceptor should be created for custom registry with scoped package"
|
||||||
|
);
|
||||||
|
|
||||||
|
await interceptor.handleRequest(url);
|
||||||
|
|
||||||
|
assert.deepEqual(lastPackage, {
|
||||||
|
packageName: "@company/package",
|
||||||
|
version: "1.0.0",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle multiple custom registries", async () => {
|
||||||
|
// Set custom registries for this test
|
||||||
|
customRegistries = ["npm.company.com", "registry.internal.net"];
|
||||||
|
malwareResponse = false;
|
||||||
|
|
||||||
|
const url1 = "https://npm.company.com/lodash/-/lodash-4.17.21.tgz";
|
||||||
|
const url2 = "https://registry.internal.net/express/-/express-4.18.2.tgz";
|
||||||
|
|
||||||
|
const interceptor1 = npmInterceptorForUrl(url1);
|
||||||
|
const interceptor2 = npmInterceptorForUrl(url2);
|
||||||
|
|
||||||
|
assert.ok(interceptor1, "Should create interceptor for first registry");
|
||||||
|
assert.ok(interceptor2, "Should create interceptor for second registry");
|
||||||
|
|
||||||
|
await interceptor1.handleRequest(url1);
|
||||||
|
assert.deepEqual(lastPackage, {
|
||||||
|
packageName: "lodash",
|
||||||
|
version: "4.17.21",
|
||||||
|
});
|
||||||
|
|
||||||
|
await interceptor2.handleRequest(url2);
|
||||||
|
assert.deepEqual(lastPackage, {
|
||||||
|
packageName: "express",
|
||||||
|
version: "4.18.2",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not create interceptor for non-custom registry", () => {
|
||||||
|
// Set custom registries for this test
|
||||||
|
customRegistries = ["npm.company.com", "registry.internal.net"];
|
||||||
|
malwareResponse = false;
|
||||||
|
|
||||||
|
const url = "https://unknown.registry.com/package/-/package-1.0.0.tgz";
|
||||||
|
|
||||||
|
const interceptor = npmInterceptorForUrl(url);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
interceptor,
|
||||||
|
undefined,
|
||||||
|
"Should not create interceptor for unknown registry"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue