Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion review-enrichment/src/analyzers/eol-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ interface VersionPin {
version: string;
}

const MAX_EOL_FILES = 40;
const MAX_EOL_PATCH_LINES = 1_000;
const MAX_EOL_PINS = 80;

// Leading numeric version from a tag/value: "3.8-slim" → "3.8", "18" → "18", "latest" → null.
function leadingVersion(value: string): string | null {
return /^v?(\d+(?:\.\d+)*)/.exec(value.trim())?.[1] ?? null;
Expand All @@ -36,10 +40,17 @@ export function extractVersionPins(
files: NonNullable<EnrichRequest["files"]>,
): VersionPin[] {
const pins: VersionPin[] = [];
let filesScanned = 0;
let linesScanned = 0;
for (const file of files) {
if (!file.patch) continue;
if (filesScanned >= MAX_EOL_FILES) break;
filesScanned += 1;
const base = file.path.split("/").pop() ?? file.path;
for (const raw of file.patch.split("\n")) {
if (linesScanned >= MAX_EOL_PATCH_LINES || pins.length >= MAX_EOL_PINS)
return pins;
linesScanned += 1;
if (raw[0] !== "+" || raw.startsWith("+++")) continue;
const line = raw.slice(1).trim();
if (isDockerfile(file.path)) {
Expand Down Expand Up @@ -115,11 +126,14 @@ export async function scanEol(
): Promise<EolFinding[]> {
const findings: EolFinding[] = [];
const seen = new Set<string>();
const cyclesByProduct = new Map<string, Cycle[] | null>();
for (const pin of extractVersionPins(req.files ?? [])) {
const key = `${pin.product}:${pin.version}`;
if (seen.has(key)) continue;
seen.add(key);
const cycles = await fetchCycles(pin.product, fetchImpl);
if (!cyclesByProduct.has(pin.product))
cyclesByProduct.set(pin.product, await fetchCycles(pin.product, fetchImpl));
const cycles = cyclesByProduct.get(pin.product);
if (!cycles) continue;
const cycle = matchCycle(cycles, pin.version);
if (!cycle) continue;
Expand Down
60 changes: 60 additions & 0 deletions review-enrichment/test/enrichment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,66 @@ test("extractVersionPins: Dockerfile FROM + .nvmrc + go.mod; latest skipped", ()
); // node:latest skipped
});

test("extractVersionPins: caps attacker-controlled EOL scan input", () => {
const pins = extractVersionPins([
{
path: "Dockerfile",
patch:
"@@ -1,0 +1,100 @@\n" +
Array.from(
{ length: 100 },
(_, index) => `+FROM node:18.0.${index}`,
).join("\n"),
},
]);

assert.equal(pins.length, 80);
assert.equal(pins[0].version, "18.0.0");
assert.equal(pins.at(-1).version, "18.0.79");
});

test("scanEol: caches endoflife.date cycles per product", async () => {
const requested: string[] = [];
const findings = await scanEol(
{
repoFullName: "o/r",
prNumber: 1,
files: [
{
path: "Dockerfile",
patch: [
"@@ -1,0 +1,4 @@",
"+FROM node:18.0.0",
"+FROM node:18.0.1",
"+FROM python:3.8",
"+FROM node:20.0.0",
].join("\n"),
},
],
},
async (url) => {
requested.push(String(url));
return {
ok: true,
json: async () =>
String(url).includes("python")
? [{ cycle: "3.8", eol: "2024-10-07" }]
: [
{ cycle: "18", eol: "2023-06-01" },
{ cycle: "20", eol: "2026-07-01" },
],
};
},
NOW,
);

assert.deepEqual(requested, [
"https://endoflife.date/api/nodejs.json",
"https://endoflife.date/api/python.json",
]);
assert.equal(findings.length, 4);
});

test("scanEol: flags EOL + EOL-soon, skips current + fetch-fail (injected now)", async () => {
const cycles = [
{ cycle: "18", eol: "2023-06-01" },
Expand Down