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
8353f353ae
commit
2df8ce463c
6 changed files with 127 additions and 107 deletions
|
|
@ -203,70 +203,6 @@ export function readDatabaseFromLocalCache() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {import("../api/aikido.js").NewPackageEntry[]} data
|
|
||||||
* @param {string | number} version
|
|
||||||
*
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
export function writeNewPackagesListToLocalCache(data, version) {
|
|
||||||
try {
|
|
||||||
const listPath = getNewPackagesListPath();
|
|
||||||
const versionPath = getNewPackagesListVersionPath();
|
|
||||||
|
|
||||||
fs.writeFileSync(listPath, JSON.stringify(data));
|
|
||||||
fs.writeFileSync(versionPath, version.toString());
|
|
||||||
} catch {
|
|
||||||
ui.writeWarning(
|
|
||||||
"Failed to write new packages list to local cache, next time the list will be fetched from the server again."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {{newPackagesList: import("../api/aikido.js").NewPackageEntry[] | null, version: string | null}}
|
|
||||||
*/
|
|
||||||
export function readNewPackagesListFromLocalCache() {
|
|
||||||
try {
|
|
||||||
const listPath = getNewPackagesListPath();
|
|
||||||
if (!fs.existsSync(listPath)) {
|
|
||||||
return { newPackagesList: null, version: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = fs.readFileSync(listPath, "utf8");
|
|
||||||
const newPackagesList = JSON.parse(data);
|
|
||||||
const versionPath = getNewPackagesListVersionPath();
|
|
||||||
let version = null;
|
|
||||||
if (fs.existsSync(versionPath)) {
|
|
||||||
version = fs.readFileSync(versionPath, "utf8").trim();
|
|
||||||
}
|
|
||||||
return { newPackagesList, version };
|
|
||||||
} catch {
|
|
||||||
ui.writeWarning(
|
|
||||||
"Failed to read new packages list from local cache. Continuing without local cache."
|
|
||||||
);
|
|
||||||
return { newPackagesList: null, version: null };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
function getNewPackagesListPath() {
|
|
||||||
const safeChainDir = getSafeChainDirectory();
|
|
||||||
const ecosystem = getEcoSystem();
|
|
||||||
return path.join(safeChainDir, `newPackagesList_${ecosystem}.json`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
function getNewPackagesListVersionPath() {
|
|
||||||
const safeChainDir = getSafeChainDirectory();
|
|
||||||
const ecosystem = getEcoSystem();
|
|
||||||
return path.join(safeChainDir, `newPackagesList_version_${ecosystem}.txt`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {SafeChainConfig}
|
* @returns {SafeChainConfig}
|
||||||
*/
|
*/
|
||||||
|
|
@ -312,6 +248,24 @@ function getDatabaseVersionPath() {
|
||||||
return path.join(aikidoDir, `version_${ecosystem}.txt`);
|
return path.join(aikidoDir, `version_${ecosystem}.txt`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function getNewPackagesListPath() {
|
||||||
|
const safeChainDir = getSafeChainDirectory();
|
||||||
|
const ecosystem = getEcoSystem();
|
||||||
|
return path.join(safeChainDir, `newPackagesList_${ecosystem}.json`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function getNewPackagesListVersionPath() {
|
||||||
|
const safeChainDir = getSafeChainDirectory();
|
||||||
|
const ecosystem = getEcoSystem();
|
||||||
|
return path.join(safeChainDir, `newPackagesList_version_${ecosystem}.txt`);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
|
|
@ -332,7 +286,7 @@ function getConfigFilePath() {
|
||||||
/**
|
/**
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
function getSafeChainDirectory() {
|
export function getSafeChainDirectory() {
|
||||||
const homeDir = os.homedir();
|
const homeDir = os.homedir();
|
||||||
const safeChainDir = path.join(homeDir, ".safe-chain");
|
const safeChainDir = path.join(homeDir, ".safe-chain");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -64,11 +64,11 @@ export async function main(args) {
|
||||||
// Write all buffered logs
|
// Write all buffered logs
|
||||||
ui.writeBufferedLogsAndStopBuffering();
|
ui.writeBufferedLogsAndStopBuffering();
|
||||||
|
|
||||||
if (!proxy.verifyNoMaliciousPackages()) {
|
if (proxy.hasBlockedMaliciousPackages()) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!proxy.verifyNoMinimumAgeBlockedRequests()) {
|
if (proxy.hasBlockedMinimumAgeRequests()) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,16 +5,29 @@
|
||||||
*/
|
*/
|
||||||
export function parseNpmPackageUrl(url, registry) {
|
export function parseNpmPackageUrl(url, registry) {
|
||||||
let packageName, version;
|
let packageName, version;
|
||||||
const urlWithoutParams = url.split("?")[0].split("#")[0];
|
let parsedUrl;
|
||||||
|
|
||||||
if (!registry || !urlWithoutParams.endsWith(".tgz")) {
|
try {
|
||||||
|
parsedUrl = new URL(url);
|
||||||
|
} catch {
|
||||||
return { packageName, version };
|
return { packageName, version };
|
||||||
}
|
}
|
||||||
|
|
||||||
const registryIndex = urlWithoutParams.indexOf(registry);
|
const pathname = parsedUrl.pathname;
|
||||||
const afterRegistry = decodeURIComponent(urlWithoutParams.substring(
|
|
||||||
registryIndex + registry.length + 1
|
if (!registry || !pathname.endsWith(".tgz")) {
|
||||||
)); // +1 to skip the slash
|
return { packageName, version };
|
||||||
|
}
|
||||||
|
|
||||||
|
const registryPrefix = `${registry}/`;
|
||||||
|
const urlAfterProtocol = `${parsedUrl.host}${pathname}`;
|
||||||
|
if (!urlAfterProtocol.startsWith(registryPrefix)) {
|
||||||
|
return { packageName, version };
|
||||||
|
}
|
||||||
|
|
||||||
|
const afterRegistry = decodeURIComponent(
|
||||||
|
urlAfterProtocol.substring(registryPrefix.length)
|
||||||
|
);
|
||||||
|
|
||||||
const separatorIndex = afterRegistry.indexOf("/-/");
|
const separatorIndex = afterRegistry.indexOf("/-/");
|
||||||
if (separatorIndex === -1) {
|
if (separatorIndex === -1) {
|
||||||
|
|
|
||||||
|
|
@ -28,8 +28,8 @@ export function createSafeChainProxy() {
|
||||||
return {
|
return {
|
||||||
startServer: () => startServer(server),
|
startServer: () => startServer(server),
|
||||||
stopServer: () => stopServer(server),
|
stopServer: () => stopServer(server),
|
||||||
verifyNoMaliciousPackages,
|
hasBlockedMaliciousPackages,
|
||||||
verifyNoMinimumAgeBlockedRequests,
|
hasBlockedMinimumAgeRequests,
|
||||||
hasSuppressedVersions: getHasSuppressedVersions,
|
hasSuppressedVersions: getHasSuppressedVersions,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -198,10 +198,9 @@ function onMinimumAgeRequestBlocked(packageName, version, url) {
|
||||||
state.blockedMinimumAgeRequests.push({ packageName, version, url });
|
state.blockedMinimumAgeRequests.push({ packageName, version, url });
|
||||||
}
|
}
|
||||||
|
|
||||||
function verifyNoMaliciousPackages() {
|
function hasBlockedMaliciousPackages() {
|
||||||
if (state.blockedRequests.length === 0) {
|
if (state.blockedRequests.length === 0) {
|
||||||
// No malicious packages were blocked, so nothing to block
|
return false;
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.emptyLine();
|
ui.emptyLine();
|
||||||
|
|
@ -220,12 +219,12 @@ function verifyNoMaliciousPackages() {
|
||||||
ui.writeExitWithoutInstallingMaliciousPackages();
|
ui.writeExitWithoutInstallingMaliciousPackages();
|
||||||
ui.emptyLine();
|
ui.emptyLine();
|
||||||
|
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function verifyNoMinimumAgeBlockedRequests() {
|
function hasBlockedMinimumAgeRequests() {
|
||||||
if (state.blockedMinimumAgeRequests.length === 0) {
|
if (state.blockedMinimumAgeRequests.length === 0) {
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.emptyLine();
|
ui.emptyLine();
|
||||||
|
|
@ -252,5 +251,5 @@ function verifyNoMinimumAgeBlockedRequests() {
|
||||||
);
|
);
|
||||||
ui.emptyLine();
|
ui.emptyLine();
|
||||||
|
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
|
import fs from "fs";
|
||||||
import {
|
import {
|
||||||
fetchNewPackagesList,
|
fetchNewPackagesList,
|
||||||
fetchNewPackagesListVersion,
|
fetchNewPackagesListVersion,
|
||||||
} from "../api/aikido.js";
|
} from "../api/aikido.js";
|
||||||
import {
|
import {
|
||||||
readNewPackagesListFromLocalCache,
|
getNewPackagesListPath,
|
||||||
writeNewPackagesListToLocalCache,
|
getNewPackagesListVersionPath,
|
||||||
} from "../config/configFile.js";
|
} from "../config/configFile.js";
|
||||||
import { ui } from "../environment/userInteraction.js";
|
import { ui } from "../environment/userInteraction.js";
|
||||||
import {
|
import {
|
||||||
|
|
@ -138,3 +139,49 @@ async function getNewPackagesList() {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import("../api/aikido.js").NewPackageEntry[]} data
|
||||||
|
* @param {string | number} version
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export function writeNewPackagesListToLocalCache(data, version) {
|
||||||
|
try {
|
||||||
|
const listPath = getNewPackagesListPath();
|
||||||
|
const versionPath = getNewPackagesListVersionPath();
|
||||||
|
|
||||||
|
fs.writeFileSync(listPath, JSON.stringify(data));
|
||||||
|
fs.writeFileSync(versionPath, version.toString());
|
||||||
|
} catch {
|
||||||
|
ui.writeWarning(
|
||||||
|
"Failed to write new packages list to local cache, next time the list will be fetched from the server again."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {{newPackagesList: import("../api/aikido.js").NewPackageEntry[] | null, version: string | null}}
|
||||||
|
*/
|
||||||
|
export function readNewPackagesListFromLocalCache() {
|
||||||
|
try {
|
||||||
|
const listPath = getNewPackagesListPath();
|
||||||
|
if (!fs.existsSync(listPath)) {
|
||||||
|
return { newPackagesList: null, version: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = fs.readFileSync(listPath, "utf8");
|
||||||
|
const newPackagesList = JSON.parse(data);
|
||||||
|
const versionPath = getNewPackagesListVersionPath();
|
||||||
|
let version = null;
|
||||||
|
if (fs.existsSync(versionPath)) {
|
||||||
|
version = fs.readFileSync(versionPath, "utf8").trim();
|
||||||
|
}
|
||||||
|
return { newPackagesList, version };
|
||||||
|
} catch {
|
||||||
|
ui.writeWarning(
|
||||||
|
"Failed to read new packages list from local cache. Continuing without local cache."
|
||||||
|
);
|
||||||
|
return { newPackagesList: null, version: null };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import { describe, it, mock, beforeEach } from "node:test";
|
import { describe, it, mock, beforeEach } from "node:test";
|
||||||
import assert from "node:assert";
|
import assert from "node:assert";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import os from "os";
|
||||||
|
|
||||||
// --- shared mutable state for mocks ---
|
// --- shared mutable state for mocks ---
|
||||||
let cachedList = null;
|
|
||||||
let cachedVersion = null;
|
|
||||||
let fetchedList = [];
|
let fetchedList = [];
|
||||||
let fetchedVersion = "etag-1";
|
let fetchedVersion = "etag-1";
|
||||||
let fetchVersionResult = "etag-1";
|
let fetchVersionResult = "etag-1";
|
||||||
|
|
@ -13,6 +14,7 @@ let writeWarningCalls = [];
|
||||||
let fetchListError = null;
|
let fetchListError = null;
|
||||||
let fetchVersionError = null;
|
let fetchVersionError = null;
|
||||||
let importCounter = 0;
|
let importCounter = 0;
|
||||||
|
let testHomeDir = "";
|
||||||
|
|
||||||
mock.module("../api/aikido.js", {
|
mock.module("../api/aikido.js", {
|
||||||
namedExports: {
|
namedExports: {
|
||||||
|
|
@ -36,16 +38,6 @@ mock.module("../api/aikido.js", {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
mock.module("../config/configFile.js", {
|
|
||||||
namedExports: {
|
|
||||||
readNewPackagesListFromLocalCache: () => ({
|
|
||||||
newPackagesList: cachedList,
|
|
||||||
version: cachedVersion,
|
|
||||||
}),
|
|
||||||
writeNewPackagesListToLocalCache: () => {},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
mock.module("../environment/userInteraction.js", {
|
mock.module("../environment/userInteraction.js", {
|
||||||
namedExports: {
|
namedExports: {
|
||||||
ui: {
|
ui: {
|
||||||
|
|
@ -66,8 +58,6 @@ mock.module("../config/settings.js", {
|
||||||
|
|
||||||
describe("newPackagesDatabase", async () => {
|
describe("newPackagesDatabase", async () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cachedList = null;
|
|
||||||
cachedVersion = null;
|
|
||||||
fetchedList = [];
|
fetchedList = [];
|
||||||
fetchedVersion = "etag-1";
|
fetchedVersion = "etag-1";
|
||||||
fetchVersionResult = "etag-1";
|
fetchVersionResult = "etag-1";
|
||||||
|
|
@ -76,6 +66,13 @@ describe("newPackagesDatabase", async () => {
|
||||||
writeWarningCalls = [];
|
writeWarningCalls = [];
|
||||||
fetchListError = null;
|
fetchListError = null;
|
||||||
fetchVersionError = null;
|
fetchVersionError = null;
|
||||||
|
testHomeDir = path.join(
|
||||||
|
os.tmpdir(),
|
||||||
|
`safe-chain-new-packages-db-${process.pid}-${importCounter}`
|
||||||
|
);
|
||||||
|
fs.rmSync(testHomeDir, { recursive: true, force: true });
|
||||||
|
fs.mkdirSync(testHomeDir, { recursive: true });
|
||||||
|
process.env.HOME = testHomeDir;
|
||||||
});
|
});
|
||||||
|
|
||||||
async function openNewPackagesDatabase() {
|
async function openNewPackagesDatabase() {
|
||||||
|
|
@ -93,6 +90,19 @@ describe("newPackagesDatabase", async () => {
|
||||||
return Math.floor((Date.now() - hours * 3600 * 1000) / 1000);
|
return Math.floor((Date.now() - hours * 3600 * 1000) / 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function writeCachedList(list, version) {
|
||||||
|
const safeChainDir = path.join(testHomeDir, ".safe-chain");
|
||||||
|
fs.mkdirSync(safeChainDir, { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(safeChainDir, `newPackagesList_${ecosystem}.json`),
|
||||||
|
JSON.stringify(list)
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(safeChainDir, `newPackagesList_version_${ecosystem}.txt`),
|
||||||
|
version
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
describe("isNewlyReleasedPackage", () => {
|
describe("isNewlyReleasedPackage", () => {
|
||||||
it("returns true for a package released within the age threshold", async () => {
|
it("returns true for a package released within the age threshold", async () => {
|
||||||
fetchedList = [
|
fetchedList = [
|
||||||
|
|
@ -171,10 +181,9 @@ describe("newPackagesDatabase", async () => {
|
||||||
|
|
||||||
describe("caching behaviour", () => {
|
describe("caching behaviour", () => {
|
||||||
it("uses local cache when etag matches", async () => {
|
it("uses local cache when etag matches", async () => {
|
||||||
cachedList = [
|
writeCachedList([
|
||||||
{ package_name: "cached-pkg", version: "1.0.0", released_on: hoursAgo(1), scraped_on: hoursAgo(1) },
|
{ package_name: "cached-pkg", version: "1.0.0", released_on: hoursAgo(1), scraped_on: hoursAgo(1) },
|
||||||
];
|
], "etag-1");
|
||||||
cachedVersion = "etag-1";
|
|
||||||
fetchVersionResult = "etag-1";
|
fetchVersionResult = "etag-1";
|
||||||
// fetchedList is empty — if we used the remote list, the lookup would return false
|
// fetchedList is empty — if we used the remote list, the lookup would return false
|
||||||
fetchedList = [];
|
fetchedList = [];
|
||||||
|
|
@ -184,10 +193,9 @@ describe("newPackagesDatabase", async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("fetches fresh list when etag does not match", async () => {
|
it("fetches fresh list when etag does not match", async () => {
|
||||||
cachedList = [
|
writeCachedList([
|
||||||
{ package_name: "stale-pkg", version: "1.0.0", released_on: hoursAgo(1), scraped_on: hoursAgo(1) },
|
{ package_name: "stale-pkg", version: "1.0.0", released_on: hoursAgo(1), scraped_on: hoursAgo(1) },
|
||||||
];
|
], "etag-old");
|
||||||
cachedVersion = "etag-old";
|
|
||||||
fetchVersionResult = "etag-new";
|
fetchVersionResult = "etag-new";
|
||||||
fetchedList = [
|
fetchedList = [
|
||||||
{ package_name: "fresh-pkg", version: "2.0.0", released_on: hoursAgo(1), scraped_on: hoursAgo(1) },
|
{ package_name: "fresh-pkg", version: "2.0.0", released_on: hoursAgo(1), scraped_on: hoursAgo(1) },
|
||||||
|
|
@ -199,15 +207,14 @@ describe("newPackagesDatabase", async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("falls back to local cache when fetch fails", async () => {
|
it("falls back to local cache when fetch fails", async () => {
|
||||||
cachedList = [
|
writeCachedList([
|
||||||
{
|
{
|
||||||
package_name: "cached-pkg",
|
package_name: "cached-pkg",
|
||||||
version: "1.0.0",
|
version: "1.0.0",
|
||||||
released_on: hoursAgo(1),
|
released_on: hoursAgo(1),
|
||||||
scraped_on: hoursAgo(1),
|
scraped_on: hoursAgo(1),
|
||||||
},
|
},
|
||||||
];
|
], "etag-old");
|
||||||
cachedVersion = "etag-old";
|
|
||||||
fetchVersionResult = "etag-new";
|
fetchVersionResult = "etag-new";
|
||||||
fetchListError = new Error("Network error");
|
fetchListError = new Error("Network error");
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue