mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 12:10:49 +00:00
Add extra check
This commit is contained in:
parent
cddcec9ba5
commit
2f4268f1af
10 changed files with 298 additions and 12 deletions
|
|
@ -68,6 +68,10 @@ export async function main(args) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!proxy.verifyNoMinimumAgeBlockedRequests()) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
const auditStats = getAuditStats();
|
const auditStats = getAuditStats();
|
||||||
if (auditStats.totalPackages > 0) {
|
if (auditStats.totalPackages > 0) {
|
||||||
ui.writeVerbose(
|
ui.writeVerbose(
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import { EventEmitter } from "events";
|
||||||
* @typedef {Object} RequestInterceptionContext
|
* @typedef {Object} RequestInterceptionContext
|
||||||
* @property {string} targetUrl
|
* @property {string} targetUrl
|
||||||
* @property {(packageName: string | undefined, version: string | undefined) => void} blockMalware
|
* @property {(packageName: string | undefined, version: string | undefined) => void} blockMalware
|
||||||
|
* @property {(packageName: string, version: string, message: string) => void} blockMinimumAgeRequest
|
||||||
* @property {(modificationFunc: (headers: NodeJS.Dict<string | string[]>) => NodeJS.Dict<string | string[]>) => void} modifyRequestHeaders
|
* @property {(modificationFunc: (headers: NodeJS.Dict<string | string[]>) => NodeJS.Dict<string | string[]>) => void} modifyRequestHeaders
|
||||||
* @property {(modificationFunc: (body: Buffer, headers: NodeJS.Dict<string | string[]> | undefined) => Buffer) => void} modifyBody
|
* @property {(modificationFunc: (body: Buffer, headers: NodeJS.Dict<string | string[]> | undefined) => Buffer) => void} modifyBody
|
||||||
* @property {() => RequestInterceptionHandler} build
|
* @property {() => RequestInterceptionHandler} build
|
||||||
|
|
@ -26,6 +27,12 @@ import { EventEmitter } from "events";
|
||||||
* @property {string} version
|
* @property {string} version
|
||||||
* @property {string} targetUrl
|
* @property {string} targetUrl
|
||||||
* @property {number} timestamp
|
* @property {number} timestamp
|
||||||
|
*
|
||||||
|
* @typedef {Object} MinimumAgeRequestBlockedEvent
|
||||||
|
* @property {string} packageName
|
||||||
|
* @property {string} version
|
||||||
|
* @property {string} targetUrl
|
||||||
|
* @property {number} timestamp
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -81,10 +88,7 @@ function createRequestContext(targetUrl, eventEmitter) {
|
||||||
* @param {string | undefined} version
|
* @param {string | undefined} version
|
||||||
*/
|
*/
|
||||||
function blockMalwareSetup(packageName, version) {
|
function blockMalwareSetup(packageName, version) {
|
||||||
blockResponse = {
|
blockResponse = createBlockResponse("Forbidden - blocked by safe-chain");
|
||||||
statusCode: 403,
|
|
||||||
message: "Forbidden - blocked by safe-chain",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Emit the malwareBlocked event
|
// Emit the malwareBlocked event
|
||||||
eventEmitter.emit("malwareBlocked", {
|
eventEmitter.emit("malwareBlocked", {
|
||||||
|
|
@ -95,6 +99,34 @@ function createRequestContext(targetUrl, eventEmitter) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} message
|
||||||
|
*/
|
||||||
|
function blockMinimumAgeRequestSetup(
|
||||||
|
/** @type {string} */ packageName,
|
||||||
|
/** @type {string} */ version,
|
||||||
|
/** @type {string} */ message
|
||||||
|
) {
|
||||||
|
blockResponse = createBlockResponse(message);
|
||||||
|
eventEmitter.emit("minimumAgeRequestBlocked", {
|
||||||
|
packageName,
|
||||||
|
version,
|
||||||
|
targetUrl,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} message
|
||||||
|
* @returns {{statusCode: number, message: string}}
|
||||||
|
*/
|
||||||
|
function createBlockResponse(message) {
|
||||||
|
return {
|
||||||
|
statusCode: 403,
|
||||||
|
message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** @returns {RequestInterceptionHandler} */
|
/** @returns {RequestInterceptionHandler} */
|
||||||
function build() {
|
function build() {
|
||||||
/**
|
/**
|
||||||
|
|
@ -139,6 +171,7 @@ function createRequestContext(targetUrl, eventEmitter) {
|
||||||
return {
|
return {
|
||||||
targetUrl,
|
targetUrl,
|
||||||
blockMalware: blockMalwareSetup,
|
blockMalware: blockMalwareSetup,
|
||||||
|
blockMinimumAgeRequest: blockMinimumAgeRequestSetup,
|
||||||
modifyRequestHeaders: (func) => reqheaderModificationFuncs.push(func),
|
modifyRequestHeaders: (func) => reqheaderModificationFuncs.push(func),
|
||||||
modifyBody: (func) => modifyBodyFuncs.push(func),
|
modifyBody: (func) => modifyBodyFuncs.push(func),
|
||||||
build,
|
build,
|
||||||
|
|
|
||||||
|
|
@ -195,7 +195,7 @@ export function getHasSuppressedVersions() {
|
||||||
* @param {string} pattern
|
* @param {string} pattern
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
function matchesExclusionPattern(packageName, pattern) {
|
export function matchesExclusionPattern(packageName, pattern) {
|
||||||
if (pattern.endsWith("/*")) {
|
if (pattern.endsWith("/*")) {
|
||||||
return packageName.startsWith(pattern.slice(0, -1));
|
return packageName.startsWith(pattern.slice(0, -1));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,18 @@
|
||||||
import {
|
import {
|
||||||
getNpmCustomRegistries,
|
getNpmCustomRegistries,
|
||||||
|
getNpmMinimumPackageAgeExclusions,
|
||||||
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 { interceptRequests } from "../interceptorBuilder.js";
|
import { interceptRequests } from "../interceptorBuilder.js";
|
||||||
import {
|
import {
|
||||||
isPackageInfoUrl,
|
isPackageInfoUrl,
|
||||||
|
matchesExclusionPattern,
|
||||||
modifyNpmInfoRequestHeaders,
|
modifyNpmInfoRequestHeaders,
|
||||||
modifyNpmInfoResponse,
|
modifyNpmInfoResponse,
|
||||||
} from "./modifyNpmInfo.js";
|
} from "./modifyNpmInfo.js";
|
||||||
import { parseNpmPackageUrl } from "./parseNpmPackageUrl.js";
|
import { parseNpmPackageUrl } from "./parseNpmPackageUrl.js";
|
||||||
|
import { openNewPackagesDatabase } from "../../../scanning/newPackagesDatabase.js";
|
||||||
|
|
||||||
const knownJsRegistries = [
|
const knownJsRegistries = [
|
||||||
"registry.npmjs.org",
|
"registry.npmjs.org",
|
||||||
|
|
@ -46,11 +49,34 @@ function buildNpmInterceptor(registry) {
|
||||||
|
|
||||||
if (await isMalwarePackage(packageName, version)) {
|
if (await isMalwarePackage(packageName, version)) {
|
||||||
reqContext.blockMalware(packageName, version);
|
reqContext.blockMalware(packageName, version);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!skipMinimumPackageAge() && isPackageInfoUrl(reqContext.targetUrl)) {
|
if (!skipMinimumPackageAge() && isPackageInfoUrl(reqContext.targetUrl)) {
|
||||||
reqContext.modifyRequestHeaders(modifyNpmInfoRequestHeaders);
|
reqContext.modifyRequestHeaders(modifyNpmInfoRequestHeaders);
|
||||||
reqContext.modifyBody(modifyNpmInfoResponse);
|
reqContext.modifyBody(modifyNpmInfoResponse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For tarball requests the metadata check above is skipped, so we check the
|
||||||
|
// new packages list as a fallback (covers e.g. frozen-lockfile installs).
|
||||||
|
if (!skipMinimumPackageAge() && packageName && version) {
|
||||||
|
const exclusions = getNpmMinimumPackageAgeExclusions();
|
||||||
|
const isExcluded = exclusions.some((pattern) =>
|
||||||
|
matchesExclusionPattern(packageName, pattern)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isExcluded) {
|
||||||
|
const newPackagesDatabase = await openNewPackagesDatabase();
|
||||||
|
|
||||||
|
if (newPackagesDatabase.isNewlyReleasedPackage(packageName, version)) {
|
||||||
|
reqContext.blockMinimumAgeRequest(
|
||||||
|
packageName,
|
||||||
|
version,
|
||||||
|
`Forbidden - blocked by safe-chain minimum package age (${packageName}@${version})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,25 @@ describe("npmInterceptor minimum package age", async () => {
|
||||||
let minimumPackageAgeSettings = 48;
|
let minimumPackageAgeSettings = 48;
|
||||||
let skipMinimumPackageAgeSetting = false;
|
let skipMinimumPackageAgeSetting = false;
|
||||||
let minimumPackageAgeExclusionsSetting = [];
|
let minimumPackageAgeExclusionsSetting = [];
|
||||||
|
let newlyReleasedPackages = new Set();
|
||||||
|
|
||||||
mock.module("../../../config/settings.js", {
|
mock.module("../../../config/settings.js", {
|
||||||
namedExports: {
|
namedExports: {
|
||||||
|
ECOSYSTEM_JS: "js",
|
||||||
|
ECOSYSTEM_PY: "py",
|
||||||
getMinimumPackageAgeHours: () => minimumPackageAgeSettings,
|
getMinimumPackageAgeHours: () => minimumPackageAgeSettings,
|
||||||
skipMinimumPackageAge: () => skipMinimumPackageAgeSetting,
|
skipMinimumPackageAge: () => skipMinimumPackageAgeSetting,
|
||||||
getNpmCustomRegistries: () => [],
|
getNpmCustomRegistries: () => [],
|
||||||
getNpmMinimumPackageAgeExclusions: () => minimumPackageAgeExclusionsSetting,
|
getNpmMinimumPackageAgeExclusions: () => minimumPackageAgeExclusionsSetting,
|
||||||
|
getEcoSystem: () => "js",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
mock.module("../../../scanning/newPackagesDatabase.js", {
|
||||||
|
namedExports: {
|
||||||
|
openNewPackagesDatabase: async () => ({
|
||||||
|
isNewlyReleasedPackage: (name, version) =>
|
||||||
|
newlyReleasedPackages.has(`${name}@${version}`),
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -359,6 +371,67 @@ describe("npmInterceptor minimum package age", async () => {
|
||||||
assert.equal(modifiedJson["dist-tags"]["latest"], "2.0.0");
|
assert.equal(modifiedJson["dist-tags"]["latest"], "2.0.0");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("Should suppress too-young versions on metadata requests without directly blocking the request", async () => {
|
||||||
|
minimumPackageAgeSettings = 5;
|
||||||
|
skipMinimumPackageAgeSetting = false;
|
||||||
|
const packageUrl = "https://registry.npmjs.org/lodash";
|
||||||
|
|
||||||
|
const interceptor = npmInterceptorForUrl(packageUrl);
|
||||||
|
const requestHandler = await interceptor.handleRequest(packageUrl);
|
||||||
|
|
||||||
|
assert.equal(requestHandler.blockResponse, undefined);
|
||||||
|
assert.equal(requestHandler.modifiesResponse(), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should directly block tarball requests when the new packages list marks them as too young", async () => {
|
||||||
|
minimumPackageAgeSettings = 5;
|
||||||
|
skipMinimumPackageAgeSetting = false;
|
||||||
|
newlyReleasedPackages = new Set(["lodash@4.17.21"]);
|
||||||
|
const packageUrl =
|
||||||
|
"https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz?integrity=sha512-abc123";
|
||||||
|
|
||||||
|
const interceptor = npmInterceptorForUrl(packageUrl);
|
||||||
|
const requestHandler = await interceptor.handleRequest(packageUrl);
|
||||||
|
|
||||||
|
assert.ok(requestHandler.blockResponse);
|
||||||
|
assert.equal(requestHandler.modifiesResponse(), false);
|
||||||
|
assert.equal(requestHandler.blockResponse.statusCode, 403);
|
||||||
|
assert.equal(
|
||||||
|
requestHandler.blockResponse.message,
|
||||||
|
"Forbidden - blocked by safe-chain minimum package age (lodash@4.17.21)"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should not block tarball requests when skipMinimumPackageAge is enabled", async () => {
|
||||||
|
minimumPackageAgeSettings = 5;
|
||||||
|
skipMinimumPackageAgeSetting = true;
|
||||||
|
minimumPackageAgeExclusionsSetting = [];
|
||||||
|
newlyReleasedPackages = new Set(["lodash@4.17.21"]);
|
||||||
|
const packageUrl =
|
||||||
|
"https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz";
|
||||||
|
|
||||||
|
const interceptor = npmInterceptorForUrl(packageUrl);
|
||||||
|
const requestHandler = await interceptor.handleRequest(packageUrl);
|
||||||
|
|
||||||
|
assert.equal(requestHandler.blockResponse, undefined);
|
||||||
|
assert.equal(requestHandler.modifiesResponse(), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should not block tarball requests when the package is excluded from minimum age", async () => {
|
||||||
|
minimumPackageAgeSettings = 5;
|
||||||
|
skipMinimumPackageAgeSetting = false;
|
||||||
|
minimumPackageAgeExclusionsSetting = ["lodash"];
|
||||||
|
newlyReleasedPackages = new Set(["lodash@4.17.21"]);
|
||||||
|
const packageUrl =
|
||||||
|
"https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz";
|
||||||
|
|
||||||
|
const interceptor = npmInterceptorForUrl(packageUrl);
|
||||||
|
const requestHandler = await interceptor.handleRequest(packageUrl);
|
||||||
|
|
||||||
|
assert.equal(requestHandler.blockResponse, undefined);
|
||||||
|
assert.equal(requestHandler.modifiesResponse(), false);
|
||||||
|
});
|
||||||
|
|
||||||
it("Should not filter packages when package is in exclusion list", async () => {
|
it("Should not filter packages when package is in exclusion list", async () => {
|
||||||
minimumPackageAgeSettings = 5;
|
minimumPackageAgeSettings = 5;
|
||||||
skipMinimumPackageAgeSetting = false;
|
skipMinimumPackageAgeSetting = false;
|
||||||
|
|
@ -540,6 +613,7 @@ describe("npmInterceptor minimum package age", async () => {
|
||||||
minimumPackageAgeSettings = 5;
|
minimumPackageAgeSettings = 5;
|
||||||
skipMinimumPackageAgeSetting = false;
|
skipMinimumPackageAgeSetting = false;
|
||||||
minimumPackageAgeExclusionsSetting = []; // Reset to empty
|
minimumPackageAgeExclusionsSetting = []; // Reset to empty
|
||||||
|
newlyReleasedPackages = new Set();
|
||||||
|
|
||||||
const packageUrl = "https://registry.npmjs.org/lodash";
|
const packageUrl = "https://registry.npmjs.org/lodash";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
import { describe, it, mock } from "node:test";
|
import { describe, it, mock, beforeEach } from "node:test";
|
||||||
import assert from "node:assert";
|
import assert from "node:assert";
|
||||||
|
|
||||||
let lastPackage;
|
let lastPackage;
|
||||||
let malwareResponse = false;
|
let malwareResponse = false;
|
||||||
let customRegistries = [];
|
let customRegistries = [];
|
||||||
|
let newlyReleasedPackages = new Set();
|
||||||
|
let skipMinimumPackageAgeSetting = false;
|
||||||
|
|
||||||
mock.module("../../../scanning/audit/index.js", {
|
mock.module("../../../scanning/audit/index.js", {
|
||||||
namedExports: {
|
namedExports: {
|
||||||
|
|
@ -27,13 +29,29 @@ mock.module("../../../config/settings.js", {
|
||||||
getMinimumPackageAgeHours: () => 24,
|
getMinimumPackageAgeHours: () => 24,
|
||||||
getNpmCustomRegistries: () => customRegistries,
|
getNpmCustomRegistries: () => customRegistries,
|
||||||
getNpmMinimumPackageAgeExclusions: () => [],
|
getNpmMinimumPackageAgeExclusions: () => [],
|
||||||
skipMinimumPackageAge: () => false,
|
skipMinimumPackageAge: () => skipMinimumPackageAgeSetting,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
mock.module("../../../scanning/newPackagesDatabase.js", {
|
||||||
|
namedExports: {
|
||||||
|
openNewPackagesDatabase: async () => ({
|
||||||
|
isNewlyReleasedPackage: (name, version) =>
|
||||||
|
newlyReleasedPackages.has(`${name}@${version}`),
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("npmInterceptor", async () => {
|
describe("npmInterceptor", async () => {
|
||||||
const { npmInterceptorForUrl } = await import("./npmInterceptor.js");
|
const { npmInterceptorForUrl } = await import("./npmInterceptor.js");
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
lastPackage = undefined;
|
||||||
|
malwareResponse = false;
|
||||||
|
customRegistries = [];
|
||||||
|
newlyReleasedPackages = new Set();
|
||||||
|
skipMinimumPackageAgeSetting = false;
|
||||||
|
});
|
||||||
|
|
||||||
const parserCases = [
|
const parserCases = [
|
||||||
// Regular packages
|
// Regular packages
|
||||||
{
|
{
|
||||||
|
|
@ -178,6 +196,36 @@ describe("npmInterceptor", async () => {
|
||||||
"Block response should have correct status message"
|
"Block response should have correct status message"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should block direct tarball downloads for newly released packages", async () => {
|
||||||
|
const url =
|
||||||
|
"https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz?integrity=sha512-abc123";
|
||||||
|
malwareResponse = false;
|
||||||
|
skipMinimumPackageAgeSetting = false;
|
||||||
|
newlyReleasedPackages = new Set(["lodash@4.17.21"]);
|
||||||
|
|
||||||
|
const interceptor = npmInterceptorForUrl(url);
|
||||||
|
const result = await interceptor.handleRequest(url);
|
||||||
|
|
||||||
|
assert.ok(result.blockResponse);
|
||||||
|
assert.equal(result.blockResponse.statusCode, 403);
|
||||||
|
assert.equal(
|
||||||
|
result.blockResponse.message,
|
||||||
|
"Forbidden - blocked by safe-chain minimum package age (lodash@4.17.21)"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not block direct tarball downloads when minimum age checks are skipped", async () => {
|
||||||
|
const url = "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz";
|
||||||
|
malwareResponse = false;
|
||||||
|
skipMinimumPackageAgeSetting = true;
|
||||||
|
newlyReleasedPackages = new Set(["lodash@4.17.21"]);
|
||||||
|
|
||||||
|
const interceptor = npmInterceptorForUrl(url);
|
||||||
|
const result = await interceptor.handleRequest(url);
|
||||||
|
|
||||||
|
assert.equal(result.blockResponse, undefined);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("npmInterceptor with custom registries", async () => {
|
describe("npmInterceptor with custom registries", async () => {
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,16 @@
|
||||||
*/
|
*/
|
||||||
export function parseNpmPackageUrl(url, registry) {
|
export function parseNpmPackageUrl(url, registry) {
|
||||||
let packageName, version;
|
let packageName, version;
|
||||||
if (!registry || !url.endsWith(".tgz")) {
|
const urlWithoutParams = url.split("?")[0].split("#")[0];
|
||||||
|
|
||||||
|
if (!registry || !urlWithoutParams.endsWith(".tgz")) {
|
||||||
return { packageName, version };
|
return { packageName, version };
|
||||||
}
|
}
|
||||||
|
|
||||||
const registryIndex = url.indexOf(registry);
|
const registryIndex = urlWithoutParams.indexOf(registry);
|
||||||
const afterRegistry = url.substring(registryIndex + registry.length + 1); // +1 to skip the slash
|
const afterRegistry = urlWithoutParams.substring(
|
||||||
|
registryIndex + registry.length + 1
|
||||||
|
); // +1 to skip the slash
|
||||||
|
|
||||||
const separatorIndex = afterRegistry.indexOf("/-/");
|
const separatorIndex = afterRegistry.indexOf("/-/");
|
||||||
if (separatorIndex === -1) {
|
if (separatorIndex === -1) {
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,16 @@ import { getHasSuppressedVersions } from "./interceptors/npm/modifyNpmInfo.js";
|
||||||
|
|
||||||
const SERVER_STOP_TIMEOUT_MS = 1000;
|
const SERVER_STOP_TIMEOUT_MS = 1000;
|
||||||
/**
|
/**
|
||||||
* @type {{port: number | null, blockedRequests: {packageName: string, version: string, url: string}[]}}
|
* @type {{
|
||||||
|
* port: number | null,
|
||||||
|
* blockedRequests: {packageName: string, version: string, url: string}[],
|
||||||
|
* blockedMinimumAgeRequests: {packageName: string, version: string, url: string}[]
|
||||||
|
* }}
|
||||||
*/
|
*/
|
||||||
const state = {
|
const state = {
|
||||||
port: null,
|
port: null,
|
||||||
blockedRequests: [],
|
blockedRequests: [],
|
||||||
|
blockedMinimumAgeRequests: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createSafeChainProxy() {
|
export function createSafeChainProxy() {
|
||||||
|
|
@ -24,6 +29,7 @@ export function createSafeChainProxy() {
|
||||||
startServer: () => startServer(server),
|
startServer: () => startServer(server),
|
||||||
stopServer: () => stopServer(server),
|
stopServer: () => stopServer(server),
|
||||||
verifyNoMaliciousPackages,
|
verifyNoMaliciousPackages,
|
||||||
|
verifyNoMinimumAgeBlockedRequests,
|
||||||
hasSuppressedVersions: getHasSuppressedVersions,
|
hasSuppressedVersions: getHasSuppressedVersions,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -151,6 +157,18 @@ function handleConnect(req, clientSocket, head) {
|
||||||
onMalwareBlocked(event.packageName, event.version, event.targetUrl);
|
onMalwareBlocked(event.packageName, event.version, event.targetUrl);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
interceptor.on(
|
||||||
|
"minimumAgeRequestBlocked",
|
||||||
|
(
|
||||||
|
/** @type {import("./interceptors/interceptorBuilder.js").MinimumAgeRequestBlockedEvent} */ event
|
||||||
|
) => {
|
||||||
|
onMinimumAgeRequestBlocked(
|
||||||
|
event.packageName,
|
||||||
|
event.version,
|
||||||
|
event.targetUrl
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
mitmConnect(req, clientSocket, interceptor);
|
mitmConnect(req, clientSocket, interceptor);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -170,6 +188,16 @@ function onMalwareBlocked(packageName, version, url) {
|
||||||
state.blockedRequests.push({ packageName, version, url });
|
state.blockedRequests.push({ packageName, version, url });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} packageName
|
||||||
|
* @param {string} version
|
||||||
|
* @param {string} url
|
||||||
|
*/
|
||||||
|
function onMinimumAgeRequestBlocked(packageName, version, url) {
|
||||||
|
state.blockedMinimumAgeRequests.push({ packageName, version, url });
|
||||||
|
}
|
||||||
|
|
||||||
function verifyNoMaliciousPackages() {
|
function verifyNoMaliciousPackages() {
|
||||||
if (state.blockedRequests.length === 0) {
|
if (state.blockedRequests.length === 0) {
|
||||||
// No malicious packages were blocked, so nothing to block
|
// No malicious packages were blocked, so nothing to block
|
||||||
|
|
@ -194,3 +222,35 @@ function verifyNoMaliciousPackages() {
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function verifyNoMinimumAgeBlockedRequests() {
|
||||||
|
if (state.blockedMinimumAgeRequests.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.emptyLine();
|
||||||
|
|
||||||
|
ui.writeInformation(
|
||||||
|
`Safe-chain: ${chalk.bold(
|
||||||
|
`blocked ${state.blockedMinimumAgeRequests.length} package downloads due to minimum age`
|
||||||
|
)}:`
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const req of state.blockedMinimumAgeRequests) {
|
||||||
|
ui.writeInformation(` - ${req.packageName}@${req.version} (${req.url})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.writeInformation(
|
||||||
|
` To disable this check, use: ${chalk.cyan(
|
||||||
|
"--safe-chain-skip-minimum-package-age"
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
|
||||||
|
ui.emptyLine();
|
||||||
|
ui.writeError(
|
||||||
|
"Safe-chain: Exiting without installing packages blocked by minimum age."
|
||||||
|
);
|
||||||
|
ui.emptyLine();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import {
|
||||||
|
|
||||||
/** @type {NewPackagesDatabase | null} */
|
/** @type {NewPackagesDatabase | null} */
|
||||||
let cachedNewPackagesDatabase = null;
|
let cachedNewPackagesDatabase = null;
|
||||||
|
let hasWarnedAboutUnavailableNewPackagesDatabase = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the source identifier used in the feed for the current ecosystem.
|
* Returns the source identifier used in the feed for the current ecosystem.
|
||||||
|
|
@ -42,7 +43,22 @@ export async function openNewPackagesDatabase() {
|
||||||
return cachedNewPackagesDatabase;
|
return cachedNewPackagesDatabase;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newPackagesList = await getNewPackagesList();
|
/** @type {import("../api/aikido.js").NewPackageEntry[]} */
|
||||||
|
let newPackagesList;
|
||||||
|
|
||||||
|
try {
|
||||||
|
newPackagesList = await getNewPackagesList();
|
||||||
|
} catch (/** @type {any} */ error) {
|
||||||
|
if (!hasWarnedAboutUnavailableNewPackagesDatabase) {
|
||||||
|
ui.writeWarning(
|
||||||
|
`Failed to load the new packages list. Continuing without tarball minimum age fallback. ${error.message}`
|
||||||
|
);
|
||||||
|
hasWarnedAboutUnavailableNewPackagesDatabase = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
cachedNewPackagesDatabase = { isNewlyReleasedPackage: () => false };
|
||||||
|
return cachedNewPackagesDatabase;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} name
|
* @param {string} name
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,10 @@ describe("newPackagesDatabase", async () => {
|
||||||
return module.openNewPackagesDatabase();
|
return module.openNewPackagesDatabase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadNewPackagesDatabaseModule() {
|
||||||
|
return import(`./newPackagesDatabase.js?test_case=${importCounter++}`);
|
||||||
|
}
|
||||||
|
|
||||||
function hoursAgo(hours) {
|
function hoursAgo(hours) {
|
||||||
return Math.floor((Date.now() - hours * 3600 * 1000) / 1000);
|
return Math.floor((Date.now() - hours * 3600 * 1000) / 1000);
|
||||||
}
|
}
|
||||||
|
|
@ -226,5 +230,22 @@ describe("newPackagesDatabase", async () => {
|
||||||
assert.strictEqual(writeWarningCalls.length, 1);
|
assert.strictEqual(writeWarningCalls.length, 1);
|
||||||
assert.ok(writeWarningCalls[0].includes("could not be cached"));
|
assert.ok(writeWarningCalls[0].includes("could not be cached"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("fails open and only warns once when the new packages list cannot be loaded", async () => {
|
||||||
|
fetchListError = new Error("feed unavailable");
|
||||||
|
|
||||||
|
const module = await loadNewPackagesDatabaseModule();
|
||||||
|
const db1 = await module.openNewPackagesDatabase();
|
||||||
|
const db2 = await module.openNewPackagesDatabase();
|
||||||
|
|
||||||
|
assert.strictEqual(db1.isNewlyReleasedPackage("foo", "1.0.0"), false);
|
||||||
|
assert.strictEqual(db2.isNewlyReleasedPackage("foo", "1.0.0"), false);
|
||||||
|
assert.strictEqual(writeWarningCalls.length, 1);
|
||||||
|
assert.ok(
|
||||||
|
writeWarningCalls[0].includes(
|
||||||
|
"Continuing without tarball minimum age fallback"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue