mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 20:20:49 +00:00
Expose minPackageAgeVersionsSuppressed for pypi as well.
This commit is contained in:
parent
f2479ad866
commit
62a3c1330c
4 changed files with 63 additions and 46 deletions
|
|
@ -24,7 +24,6 @@ export async function main(args) {
|
|||
let malwareBlockedEvents = [];
|
||||
/** @type {import("./registryProxy/registryProxy.js").PackageBlockedEvent[]} */
|
||||
let minPackageAgeBlocks = [];
|
||||
|
||||
/** @type {import("./registryProxy/registryProxy.js").MinPackageAgeSuppressionEvent[]} */
|
||||
let suppressedVersionEvents = [];
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { getCaCertPath } from "./certUtils.js";
|
|||
import { readFileSync } from "fs";
|
||||
import EventEmitter from "events";
|
||||
import { modifyResponseEventEmitter } from "./interceptors/npm/modifyNpmInfo.js";
|
||||
import { modifyPipResponseEventEmitter } from "./interceptors/pip/modifyPipInfo.js";
|
||||
import { cleanupCertBundle } from "../certBundle.js";
|
||||
|
||||
/** *
|
||||
|
|
@ -27,6 +28,10 @@ export function createBuiltInProxyServer() {
|
|||
emitter.emit("minPackageAgeVersionsSuppressed", ev);
|
||||
});
|
||||
|
||||
modifyPipResponseEventEmitter.addListener("versionsRemoved", (ev) => {
|
||||
emitter.emit("minPackageAgeVersionsSuppressed", ev);
|
||||
});
|
||||
|
||||
const server = http.createServer(
|
||||
// This handles direct HTTP requests (non-CONNECT requests)
|
||||
// This is normally http-only traffic, but we also handle
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { EventEmitter } from "events";
|
||||
import { ui } from "../../../../environment/userInteraction.js";
|
||||
import { clearCachingHeaders } from "../../http-utils.js";
|
||||
import { normalizePipPackageName } from "../../../../scanning/packageNameVariants.js";
|
||||
|
|
@ -6,6 +7,9 @@ export { parsePipMetadataUrl, isPipPackageInfoUrl } from "./parsePipPackageUrl.j
|
|||
import { getPipMetadataContentType, logSuppressedVersion } from "./pipMetadataResponseUtils.js";
|
||||
import { modifyPipJsonResponse } from "./modifyPipJsonResponse.js";
|
||||
|
||||
/** @type {EventEmitter<{ versionsRemoved: [{packageName: string, packageVersions: string[]}] }>} */
|
||||
export const modifyPipResponseEventEmitter = new EventEmitter();
|
||||
|
||||
/**
|
||||
* Strip conditional GET headers so PyPI always returns a full 200 response
|
||||
* with a body we can rewrite. Without this, pip sends If-None-Match /
|
||||
|
|
@ -50,33 +54,42 @@ export function modifyPipInfoResponse(
|
|||
return body;
|
||||
}
|
||||
|
||||
/** @type {{ buffer: Buffer, suppressedVersions: string[] } | undefined} */
|
||||
let result;
|
||||
if (
|
||||
contentType.includes("html") ||
|
||||
contentType.includes("application/vnd.pypi.simple.v1+html")
|
||||
) {
|
||||
return modifyHtmlSimpleResponse(
|
||||
result = modifyHtmlSimpleResponse(
|
||||
body,
|
||||
headers,
|
||||
metadataUrl,
|
||||
isNewlyReleasedPackage,
|
||||
packageName
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
} else if (
|
||||
contentType.includes("json") ||
|
||||
contentType.includes("application/vnd.pypi.simple.v1+json")
|
||||
) {
|
||||
return modifyJsonResponse(
|
||||
result = modifyJsonResponse(
|
||||
body,
|
||||
headers,
|
||||
metadataUrl,
|
||||
isNewlyReleasedPackage,
|
||||
packageName
|
||||
);
|
||||
} else {
|
||||
return body;
|
||||
}
|
||||
|
||||
return body;
|
||||
if (result.suppressedVersions.length > 0) {
|
||||
modifyPipResponseEventEmitter.emit("versionsRemoved", {
|
||||
packageName,
|
||||
packageVersions: result.suppressedVersions,
|
||||
});
|
||||
}
|
||||
|
||||
return result.buffer;
|
||||
} catch (/** @type {any} */ err) {
|
||||
ui.writeVerbose(
|
||||
`Safe-chain: PyPI package metadata not in expected format - bypassing modification. Error: ${err.message}`
|
||||
|
|
@ -91,7 +104,7 @@ export function modifyPipInfoResponse(
|
|||
* @param {string} metadataUrl
|
||||
* @param {(packageName: string | undefined, version: string | undefined) => boolean} isNewlyReleasedPackage
|
||||
* @param {string} packageName
|
||||
* @returns {Buffer}
|
||||
* @returns {{ buffer: Buffer, suppressedVersions: string[] }}
|
||||
*/
|
||||
function modifyHtmlSimpleResponse(
|
||||
body,
|
||||
|
|
@ -101,35 +114,35 @@ function modifyHtmlSimpleResponse(
|
|||
packageName
|
||||
) {
|
||||
const html = body.toString("utf8");
|
||||
let modified = false;
|
||||
const suppressedVersions = /** @type {string[]} */ ([]);
|
||||
const rewriteHtmlAnchor = createHtmlAnchorRewriter(
|
||||
metadataUrl,
|
||||
isNewlyReleasedPackage,
|
||||
packageName,
|
||||
() => {
|
||||
modified = true;
|
||||
(version) => {
|
||||
suppressedVersions.push(version);
|
||||
}
|
||||
);
|
||||
const updatedHtml = html.replace(HTML_ANCHOR_HREF_RE, rewriteHtmlAnchor);
|
||||
|
||||
if (!modified) return body;
|
||||
if (suppressedVersions.length === 0) return { buffer: body, suppressedVersions: [] };
|
||||
const modifiedBuffer = Buffer.from(updatedHtml);
|
||||
clearCachingHeaders(headers);
|
||||
return modifiedBuffer;
|
||||
return { buffer: modifiedBuffer, suppressedVersions: [...new Set(suppressedVersions)] };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} metadataUrl
|
||||
* @param {(packageName: string | undefined, version: string | undefined) => boolean} isNewlyReleasedPackage
|
||||
* @param {string} packageName
|
||||
* @param {() => void} onModified
|
||||
* @param {(version: string) => void} onVersionSuppressed
|
||||
* @returns {(anchor: string, quote: string, href: string) => string}
|
||||
*/
|
||||
function createHtmlAnchorRewriter(
|
||||
metadataUrl,
|
||||
isNewlyReleasedPackage,
|
||||
packageName,
|
||||
onModified
|
||||
onVersionSuppressed
|
||||
) {
|
||||
return (anchor, _quote, href) => {
|
||||
const resolvedHref = new URL(href, metadataUrl).toString();
|
||||
|
|
@ -145,8 +158,8 @@ function createHtmlAnchorRewriter(
|
|||
version &&
|
||||
isNewlyReleasedPackage(packageName, version)
|
||||
) {
|
||||
onModified();
|
||||
logSuppressedVersion(packageName, version);
|
||||
onVersionSuppressed(version);
|
||||
return "";
|
||||
}
|
||||
|
||||
|
|
@ -160,7 +173,7 @@ function createHtmlAnchorRewriter(
|
|||
* @param {string} metadataUrl
|
||||
* @param {(packageName: string | undefined, version: string | undefined) => boolean} isNewlyReleasedPackage
|
||||
* @param {string} packageName
|
||||
* @returns {Buffer}
|
||||
* @returns {{ buffer: Buffer, suppressedVersions: string[] }}
|
||||
*/
|
||||
function modifyJsonResponse(
|
||||
body,
|
||||
|
|
@ -170,15 +183,15 @@ function modifyJsonResponse(
|
|||
packageName
|
||||
) {
|
||||
const json = JSON.parse(body.toString("utf8"));
|
||||
const modified = modifyPipJsonResponse(
|
||||
const { suppressedVersions, wasModified } = modifyPipJsonResponse(
|
||||
json,
|
||||
metadataUrl,
|
||||
isNewlyReleasedPackage,
|
||||
packageName
|
||||
);
|
||||
|
||||
if (!modified) return body;
|
||||
if (!wasModified) return { buffer: body, suppressedVersions: [] };
|
||||
const modifiedBuffer = Buffer.from(JSON.stringify(json));
|
||||
clearCachingHeaders(headers);
|
||||
return modifiedBuffer;
|
||||
return { buffer: modifiedBuffer, suppressedVersions };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { logSuppressedVersion } from "./pipMetadataResponseUtils.js";
|
|||
* @param {string} metadataUrl
|
||||
* @param {(packageName: string | undefined, version: string | undefined) => boolean} isNewlyReleasedPackage
|
||||
* @param {string} packageName
|
||||
* @returns {boolean}
|
||||
* @returns {{ suppressedVersions: string[], wasModified: boolean }}
|
||||
*/
|
||||
export function modifyPipJsonResponse(
|
||||
json,
|
||||
|
|
@ -18,18 +18,18 @@ export function modifyPipJsonResponse(
|
|||
isNewlyReleasedPackage,
|
||||
packageName
|
||||
) {
|
||||
const filesModified = filterJsonMetadataFiles(
|
||||
const filesSuppressed = filterJsonMetadataFiles(
|
||||
json,
|
||||
metadataUrl,
|
||||
isNewlyReleasedPackage,
|
||||
packageName
|
||||
);
|
||||
const releasesModified = removeJsonMetadataReleases(
|
||||
const releasesSuppressed = removeJsonMetadataReleases(
|
||||
json,
|
||||
isNewlyReleasedPackage,
|
||||
packageName
|
||||
);
|
||||
const urlsModified = filterJsonMetadataUrls(
|
||||
const urlsSuppressed = filterJsonMetadataUrls(
|
||||
json,
|
||||
metadataUrl,
|
||||
isNewlyReleasedPackage,
|
||||
|
|
@ -37,7 +37,11 @@ export function modifyPipJsonResponse(
|
|||
);
|
||||
const versionModified = updateJsonInfoVersion(json, metadataUrl);
|
||||
|
||||
return filesModified || releasesModified || urlsModified || versionModified;
|
||||
const suppressedVersions = [
|
||||
...new Set([...filesSuppressed, ...releasesSuppressed, ...urlsSuppressed]),
|
||||
];
|
||||
|
||||
return { suppressedVersions, wasModified: suppressedVersions.length > 0 || versionModified };
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -45,7 +49,7 @@ export function modifyPipJsonResponse(
|
|||
* @param {string} metadataUrl
|
||||
* @param {(packageName: string | undefined, version: string | undefined) => boolean} isNewlyReleasedPackage
|
||||
* @param {string} packageName
|
||||
* @returns {boolean}
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function filterJsonMetadataFiles(
|
||||
json,
|
||||
|
|
@ -54,19 +58,17 @@ function filterJsonMetadataFiles(
|
|||
packageName
|
||||
) {
|
||||
if (!Array.isArray(json.files)) {
|
||||
return false;
|
||||
return [];
|
||||
}
|
||||
|
||||
let modified = false;
|
||||
const loggedVersions = new Set();
|
||||
const suppressed = new Set();
|
||||
json.files = json.files.filter((/** @type {any} */ file) => {
|
||||
const version = getPackageVersionFromMetadataFile(file, metadataUrl);
|
||||
|
||||
if (version && isNewlyReleasedPackage(packageName, version)) {
|
||||
modified = true;
|
||||
if (!loggedVersions.has(version)) {
|
||||
if (!suppressed.has(version)) {
|
||||
logSuppressedVersion(packageName, version);
|
||||
loggedVersions.add(version);
|
||||
suppressed.add(version);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
@ -74,21 +76,21 @@ function filterJsonMetadataFiles(
|
|||
return true;
|
||||
});
|
||||
|
||||
return modified;
|
||||
return [...suppressed];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} json
|
||||
* @param {(packageName: string | undefined, version: string | undefined) => boolean} isNewlyReleasedPackage
|
||||
* @param {string} packageName
|
||||
* @returns {boolean}
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function removeJsonMetadataReleases(json, isNewlyReleasedPackage, packageName) {
|
||||
if (!json.releases || typeof json.releases !== "object") {
|
||||
return false;
|
||||
return [];
|
||||
}
|
||||
|
||||
let modified = false;
|
||||
const suppressed = [];
|
||||
|
||||
for (const [version, files] of Object.entries(json.releases)) {
|
||||
if (
|
||||
|
|
@ -96,12 +98,12 @@ function removeJsonMetadataReleases(json, isNewlyReleasedPackage, packageName) {
|
|||
isNewlyReleasedPackage(packageName, version)
|
||||
) {
|
||||
delete json.releases[version];
|
||||
modified = true;
|
||||
logSuppressedVersion(packageName, version);
|
||||
suppressed.push(version);
|
||||
}
|
||||
}
|
||||
|
||||
return modified;
|
||||
return suppressed;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -109,7 +111,7 @@ function removeJsonMetadataReleases(json, isNewlyReleasedPackage, packageName) {
|
|||
* @param {string} metadataUrl
|
||||
* @param {(packageName: string | undefined, version: string | undefined) => boolean} isNewlyReleasedPackage
|
||||
* @param {string} packageName
|
||||
* @returns {boolean}
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function filterJsonMetadataUrls(
|
||||
json,
|
||||
|
|
@ -118,19 +120,17 @@ function filterJsonMetadataUrls(
|
|||
packageName
|
||||
) {
|
||||
if (!Array.isArray(json.urls)) {
|
||||
return false;
|
||||
return [];
|
||||
}
|
||||
|
||||
let modified = false;
|
||||
const loggedVersions = new Set();
|
||||
const suppressed = new Set();
|
||||
json.urls = json.urls.filter((/** @type {any} */ file) => {
|
||||
const version = getPackageVersionFromMetadataFile(file, metadataUrl);
|
||||
|
||||
if (version && isNewlyReleasedPackage(packageName, version)) {
|
||||
modified = true;
|
||||
if (!loggedVersions.has(version)) {
|
||||
if (!suppressed.has(version)) {
|
||||
logSuppressedVersion(packageName, version);
|
||||
loggedVersions.add(version);
|
||||
suppressed.add(version);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
@ -138,7 +138,7 @@ function filterJsonMetadataUrls(
|
|||
return true;
|
||||
});
|
||||
|
||||
return modified;
|
||||
return [...suppressed];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue