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
2 changes: 1 addition & 1 deletion review-enrichment/src/analyzers/approval-integrity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { boundedFetchJson } from "../external-fetch.js";
import { githubHeaders } from "../github-headers.js";

const GITHUB_API = "https://github.com/ghapi";
const SLUG_RE = /^[A-Za-z0-9._-]+$/;
const SLUG_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/; // rejects `..` and other path-traversal segments
const REVIEWS_PER_PAGE = 100;
// GitHub returns PR reviews oldest-first with no reorder option, so a single `per_page=100` fetch would silently
// read only the OLDEST reviews on any PR with more — exactly backwards for "each reviewer's latest vote". Walk
Expand Down
2 changes: 1 addition & 1 deletion review-enrichment/src/analyzers/blame-link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { githubHeaders } from "../github-headers.js";
import { isHistoryUninformativePath } from "./history-path.js";

const GITHUB_API = "https://github.com/ghapi";
const SLUG_RE = /^[A-Za-z0-9._-]+$/;
const SLUG_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/; // rejects `..` and other path-traversal segments
const MAX_FILES_PROBED = 6; // bound the files we probe, matching the other history-class analyzers
const MAX_LOOKUPS = 12; // hard cap on total GitHub round-trips (each file costs up to 2: commits + pulls)
const SHA_PREFIX_LEN = 12;
Expand Down
2 changes: 1 addition & 1 deletion review-enrichment/src/analyzers/caller-impact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import { isTestPath } from "./test-ratio.js";
import { DEFAULT_MAX_FINDINGS } from "./limits.js";

const GITHUB_API = "https://github.com/ghapi";
const SLUG_RE = /^[A-Za-z0-9._-]+$/;
const SLUG_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/; // rejects `..` and other path-traversal segments
const MAX_SYMBOLS = 6; // removed symbols searched per PR (Code Search rate budget)
const MAX_SEARCHES = 6; // bounded Code Search queries per PR
const MAX_FILE_FETCHES = 12; // bounded candidate-caller content fetches per PR
Expand Down
2 changes: 1 addition & 1 deletion review-enrichment/src/analyzers/churn-hotspot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { githubHeaders } from "../github-headers.js";
import { isHistoryUninformativePath } from "./history-path.js";

const GITHUB_API = "https://github.com/ghapi";
const SLUG_RE = /^[A-Za-z0-9._-]+$/;
const SLUG_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/; // rejects `..` and other path-traversal segments
const WINDOW_DAYS = 90;
const PER_PAGE = 100; // one page; a file with a full page of commits in the window is already a clear hotspot
const MAX_FILES_PROBED = 8; // bound the GitHub round-trips, matching the other history-class analyzers
Expand Down
2 changes: 1 addition & 1 deletion review-enrichment/src/analyzers/commit-hygiene.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { githubHeaders } from "../github-headers.js";
import { DEFAULT_MAX_FINDINGS } from "./limits.js";

const GITHUB_API = "https://github.com/ghapi";
const SLUG_RE = /^[A-Za-z0-9._-]+$/;
const SLUG_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/; // rejects `..` and other path-traversal segments
const MAX_COMMITS = 100;
const MAX_FINDINGS = DEFAULT_MAX_FINDINGS;
const SHA_PREFIX_LEN = 12;
Expand Down
2 changes: 1 addition & 1 deletion review-enrichment/src/analyzers/commit-lint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { githubHeaders } from "../github-headers.js";
import { DEFAULT_MAX_FINDINGS } from "./limits.js";

const GITHUB_API = "https://github.com/ghapi";
const SLUG_RE = /^[A-Za-z0-9._-]+$/;
const SLUG_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/; // rejects `..` and other path-traversal segments
const MAX_COMMITS = 100;
const MAX_FINDINGS = DEFAULT_MAX_FINDINGS;
const SHA_PREFIX_LEN = 12;
Expand Down
2 changes: 1 addition & 1 deletion review-enrichment/src/analyzers/commit-signature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const GITHUB_API = "https://github.com/ghapi";
// analyzers cap their network round-trips.
const HISTORY_PER_PAGE = 30;
// Only repository slugs that look like real `owner/repo` segments are ever interpolated into a request URL.
const SLUG_RE = /^[A-Za-z0-9._-]+$/;
const SLUG_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/; // rejects `..` and other path-traversal segments

interface ScanOptions {
signal?: AbortSignal;
Expand Down
2 changes: 1 addition & 1 deletion review-enrichment/src/analyzers/complexity-delta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import { isJsTsPath, scanContentForComplexity } from "./complexity.js";
import { DEFAULT_MAX_FINDINGS } from "./limits.js";

const GITHUB_API = "https://github.com/ghapi";
const SLUG_RE = /^[A-Za-z0-9._-]+$/;
const SLUG_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/; // rejects `..` and other path-traversal segments
const MAX_FILES = 20;
const MAX_FINDINGS = DEFAULT_MAX_FINDINGS;
const MAX_FETCH_BYTES = 1_000_000;
Expand Down
2 changes: 1 addition & 1 deletion review-enrichment/src/analyzers/coverage-delta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { boundedFetchJson } from "../external-fetch.js";
import { githubHeaders } from "../github-headers.js";

const GITHUB_API = "https://github.com/ghapi";
const SLUG_RE = /^[A-Za-z0-9._-]+$/;
const SLUG_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/; // rejects `..` and other path-traversal segments
const MAX_RUNS_PROBED = 5; // recent successful runs to search for a coverage artifact
const MAX_ARTIFACT_BYTES = 8 * 1024 * 1024; // skip an artifact zip larger than this (bounded download)
const MAX_ENTRY_BYTES = 4 * 1024 * 1024; // skip a single uncompressed zip entry larger than this
Expand Down
2 changes: 1 addition & 1 deletion review-enrichment/src/analyzers/doc-comment-drift.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const MAX_SIGNATURE_LINES = 40;
const MAX_FETCH_BYTES = 1_000_000;
const SOURCE_RE = /\.(?:ts|tsx|mts|cts|js|jsx|mjs|cjs)$/;
const SKIP_RE = /(?:\.d\.ts$|\.min\.|\.test\.|\.spec\.|__tests__\/|(?:^|\/)tests?\/)/;
const SLUG_RE = /^[A-Za-z0-9._-]+$/;
const SLUG_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/; // rejects `..` and other path-traversal segments
// Matches a named `function` declaration up to its parameter `(`. A single, non-nested generic clause is allowed;
// a nested-generic declaration (e.g. `function f<T extends Record<string, string>>(x)`) simply does not match and
// the function is skipped — a deliberate recall/precision trade-off, never a false positive.
Expand Down
2 changes: 1 addition & 1 deletion review-enrichment/src/analyzers/exhaustiveness-drift.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { isTestPath } from "./test-ratio.js";
import { DEFAULT_MAX_FINDINGS } from "./limits.js";

const GITHUB_API = "https://github.com/ghapi";
const SLUG_RE = /^[A-Za-z0-9._-]+$/;
const SLUG_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/; // rejects `..` and other path-traversal segments
const MAX_FILES = 10;
const MAX_FETCHES = 10;
const MAX_FINDINGS = DEFAULT_MAX_FINDINGS;
Expand Down
2 changes: 1 addition & 1 deletion review-enrichment/src/analyzers/flaky-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { isTestPath } from "./test-ratio.js";
import { DEFAULT_MAX_FINDINGS } from "./limits.js";

const GITHUB_API = "https://github.com/ghapi";
const SLUG_RE = /^[A-Za-z0-9._-]+$/;
const SLUG_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/; // rejects `..` and other path-traversal segments
const WINDOW_DAYS = 30;
const WINDOW_LABEL = "30d";
const MAX_FILES_PROBED = 6;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { boundedFetchJson } from "../external-fetch.js";
import { githubHeaders } from "../github-headers.js";

const GITHUB_API = "https://github.com/ghapi";
const SLUG_RE = /^[A-Za-z0-9._-]+$/;
const SLUG_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/; // rejects `..` and other path-traversal segments
const TIMELINE_PER_PAGE = 100;
const MAX_TIMELINE_PAGES = 5;
const STALE_THRESHOLD_MS = 48 * 60 * 60 * 1000; // 48 hours
Expand Down
2 changes: 1 addition & 1 deletion review-enrichment/src/analyzers/revert-recurrence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { isHistoryUninformativePath } from "./history-path.js";
import { DEFAULT_MAX_FINDINGS } from "./limits.js";

const GITHUB_API = "https://github.com/ghapi";
const SLUG_RE = /^[A-Za-z0-9._-]+$/;
const SLUG_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/; // rejects `..` and other path-traversal segments
const MAX_FILES_PROBED = 5; // bound the per-file commit-history fan-out, matching the other history-class analyzers
const COMMITS_PER_FILE = 15; // recent commits to inspect per probed file when looking for a revert
const MAX_REVERT_LOOKUPS = 10; // global cap on revert-commit detail fetches across all probed files
Expand Down
2 changes: 1 addition & 1 deletion review-enrichment/src/analyzers/stale-branch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { boundedFetchJson } from "../external-fetch.js";
import { githubHeaders } from "../github-headers.js";

const GITHUB_API = "https://github.com/ghapi";
const SLUG_RE = /^[A-Za-z0-9._-]+$/;
const SLUG_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/; // rejects `..` and other path-traversal segments
// Below this many commits behind, drifting from the default branch is normal PR life, not a staleness risk.
const BEHIND_THRESHOLD = 100;

Expand Down
2 changes: 1 addition & 1 deletion review-enrichment/src/analyzers/undocumented-export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const GITHUB_API = "https://github.com/ghapi";
const MAX_FILES = 10;
const MAX_FINDINGS = 30;
const MAX_FETCH_BYTES = 1_000_000;
const SLUG_RE = /^[A-Za-z0-9._-]+$/;
const SLUG_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/; // rejects `..` and other path-traversal segments
// A public entrypoint barrel — an `index.<js/ts>` source file. Declaration (.d.ts), test, and generated output are
// excluded: they are not the hand-authored public surface this scan is about.
const ENTRYPOINT_RE = /(?:^|\/)index\.(?:ts|tsx|mts|cts|js|jsx|mjs|cjs)$/;
Expand Down
2 changes: 1 addition & 1 deletion review-enrichment/src/analyzers/unused-export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { isTestPath } from "./test-ratio.js";
import { DEFAULT_MAX_FINDINGS } from "./limits.js";

const GITHUB_API = "https://github.com/ghapi";
const SLUG_RE = /^[A-Za-z0-9._-]+$/;
const SLUG_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/; // rejects `..` and other path-traversal segments
const MAX_SYMBOLS = 10;
const MAX_SEARCHES = 10;
const MAX_FILE_FETCHES = 10;
Expand Down
9 changes: 9 additions & 0 deletions review-enrichment/test/approval-integrity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,15 @@ test("scanApprovalIntegrity: a malformed repoFullName is skipped, not thrown", a
assert.deepEqual(findings, []);
});

test("scanApprovalIntegrity: a dot-segment owner (path-traversal shape) is rejected, not thrown", async () => {
// ".." individually satisfies a bare `[A-Za-z0-9._-]+` class; only a first-character requirement catches it.
const findings = await scanApprovalIntegrity(
req({ repoFullName: "../evil" }),
reviewsFetch([review("alice", "APPROVED", "old-sha", "2026-01-01T00:00:00Z")]),
);
assert.deepEqual(findings, []);
});

test("scanApprovalIntegrity: a fetch failure yields no finding", async () => {
const findings = await scanApprovalIntegrity(req(), async () => jsonResponse({ message: "bad" }, 500));
assert.deepEqual(findings, []);
Expand Down
9 changes: 9 additions & 0 deletions review-enrichment/test/blame-link.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,3 +175,12 @@ test("scanBlameLink: no GitHub token → skipped (no finding, no throw)", async
);
assert.deepEqual(findings, []);
});

test("scanBlameLink: a dot-segment owner (path-traversal shape) is rejected, not thrown", async () => {
// ".." individually satisfies a bare `[A-Za-z0-9._-]+` class; only a first-character requirement catches it.
const findings = await scanBlameLink(
req([{ path: "src/app.ts", status: "modified", patch: modifyPatch(2) }], { repoFullName: "../evil" }),
routedFetch({ commitSha: "abcdef1234567890", prNumber: 1 }),
);
assert.deepEqual(findings, []);
});
5 changes: 5 additions & 0 deletions review-enrichment/test/caller-impact.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,11 @@ test("scanCallerImpact: no token / no headSha / invalid slug / no removed export
await scanCallerImpact(req(files, { repoFullName: "octo/re po" }), failFetch),
[],
);
// ".." individually satisfies a bare `[A-Za-z0-9._-]+` class; only a first-character requirement catches it.
assert.deepEqual(
await scanCallerImpact(req(files, { repoFullName: "../evil" }), failFetch),
[],
);
assert.deepEqual(
await scanCallerImpact(
req([{ path: "src/utils.ts", patch: ["@@ -0,0 +1,1 @@", "+export function added() {}"].join("\n") }]),
Expand Down
2 changes: 2 additions & 0 deletions review-enrichment/test/churn-hotspot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ test("scanChurnHotspot: skips lockfiles, binaries, and newly-added files without
test("scanChurnHotspot: requires a github token and a valid repo slug", async () => {
assert.deepEqual(await scanChurnHotspot({ repoFullName: "octo/repo", prNumber: 1, files: [{ path: "src/a.ts" }] }, async () => jsonResponse(commits(20, 2))), []);
assert.deepEqual(await scanChurnHotspot({ repoFullName: "bad slug/x!", prNumber: 1, githubToken: "t", files: [{ path: "src/a.ts" }] }, async () => jsonResponse(commits(20, 2))), []);
// ".." individually satisfies a bare `[A-Za-z0-9._-]+` class; only a first-character requirement catches it.
assert.deepEqual(await scanChurnHotspot({ repoFullName: "../evil", prNumber: 1, githubToken: "t", files: [{ path: "src/a.ts" }] }, async () => jsonResponse(commits(20, 2))), []);
});

test("scanChurnHotspot: rejects multi-segment repo slugs without fetching", async () => {
Expand Down
9 changes: 9 additions & 0 deletions review-enrichment/test/commit-hygiene.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,15 @@ test("scanCommitHygiene: a malformed repoFullName is skipped, not thrown", async
assert.deepEqual(findings, []);
});

test("scanCommitHygiene: a dot-segment owner (path-traversal shape) is rejected, not thrown", async () => {
// ".." individually satisfies a bare `[A-Za-z0-9._-]+` class; only a first-character requirement catches it.
const findings = await scanCommitHygiene(
req({ repoFullName: "../evil" }),
commitsFetch([commit(SHA_A, "fixup! x")]),
);
assert.deepEqual(findings, []);
});

test("scanCommitHygiene: a fetch failure yields no finding", async () => {
const findings = await scanCommitHygiene(req(), async () => jsonResponse({ message: "bad" }, 500));
assert.deepEqual(findings, []);
Expand Down
2 changes: 2 additions & 0 deletions review-enrichment/test/commit-lint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ test("scanCommitLint: fail-safe — no token, a bad repo slug, or a fetch error
assert.deepEqual(await scanCommitLint(req({ githubToken: undefined }), good), []);
assert.deepEqual(await scanCommitLint(req({ repoFullName: "octo/repo/extra" }), good), []);
assert.deepEqual(await scanCommitLint(req({ repoFullName: "bad slug!/x" }), good), []);
// ".." individually satisfies a bare `[A-Za-z0-9._-]+` class; only a first-character requirement catches it.
assert.deepEqual(await scanCommitLint(req({ repoFullName: "../evil" }), good), []);
const err = async () => new Response("nope", { status: 500 });
assert.deepEqual(await scanCommitLint(req(), err), []);
});
4 changes: 3 additions & 1 deletion review-enrichment/test/commit-signature.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,9 @@ test("scanCommitSignature fails closed on a malformed repo slug WITHOUT any netw
// A spy that records invocation: a malformed slug must be rejected BEFORE any GitHub request, so the guard
// can never query the wrong repository. (A throwing fetch would be swallowed by the analyzer's fail-safe
// try/catch and could mask a slug that slipped through, so assert the call never happens instead.)
for (const repoFullName of ["not-a-slug", "o/r/extra", "/r", "o/", "a/b/c/d"]) {
// "../evil"/"o/.."/"o/." each individually satisfy a bare `[A-Za-z0-9._-]+` class (every char in ".."/"." is
// allowed); only a first-character requirement on owner AND repo independently catches them.
for (const repoFullName of ["not-a-slug", "o/r/extra", "/r", "o/", "a/b/c/d", "../evil", "o/..", "o/."]) {
let called = false;
const spyFetch: typeof fetch = async () => {
called = true;
Expand Down
20 changes: 20 additions & 0 deletions review-enrichment/test/complexity-delta.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,26 @@ test("scanComplexityDelta: rejects multi-segment repo slugs without fetching", a
assert.equal(called, false);
});

test("scanComplexityDelta: rejects a dot-segment owner/repo slug without fetching", async () => {
// ".." individually satisfies a bare `[A-Za-z0-9._-]+` class; only a first-character requirement catches it.
let called = false;
const out = await scanComplexityDelta(
{
repoFullName: "../evil",
prNumber: 1,
headSha: "abc123",
githubToken: "ght",
files: [{ path: "src/a.ts", patch: CALC_PATCH }],
},
async () => {
called = true;
return fileWith(HEAD_CONTENT)();
},
);
assert.deepEqual(out, []);
assert.equal(called, false);
});

test("scanComplexityDelta: skips non-source, test, and patch-less files without fetching", async () => {
let called = false;
const out = await scanComplexityDelta(
Expand Down
2 changes: 2 additions & 0 deletions review-enrichment/test/coverage-delta.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,8 @@ test("scanCoverageDelta: requires a github token, a head sha, and a single valid
assert.deepEqual(await scanCoverageDelta(req(files, { headSha: undefined }), call), []);
assert.deepEqual(await scanCoverageDelta(req(files, { repoFullName: "octo/repo/extra" }), call), []);
assert.deepEqual(await scanCoverageDelta(req(files, { repoFullName: "bad slug/x!" }), call), []);
// ".." individually satisfies a bare `[A-Za-z0-9._-]+` class; only a first-character requirement catches it.
assert.deepEqual(await scanCoverageDelta(req(files, { repoFullName: "../evil" }), call), []);
});

test("scanCoverageDelta: a PR that adds no lines never touches the network", async () => {
Expand Down
14 changes: 14 additions & 0 deletions review-enrichment/test/doc-comment-drift.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,20 @@ test("scanDocCommentDrift: rejects multi-segment repo slugs without fetching", a
assert.equal(called, false);
});

test("scanDocCommentDrift: rejects a dot-segment owner/repo slug without fetching", async () => {
// ".." individually satisfies a bare `[A-Za-z0-9._-]+` class; only a first-character requirement catches it.
let called = false;
const out = await scanDocCommentDrift(
{ repoFullName: "../evil", prNumber: 1, headSha: "abc123", githubToken: "ght", files: [{ path: "src/a.ts", patch: DRIFT_PATCH }] },
async () => {
called = true;
return fileWith(DRIFTED)();
},
);
assert.deepEqual(out, []);
assert.equal(called, false);
});

test("scanDocCommentDrift: skips non-source and test files without fetching", async () => {
let called = false;
const out = await scanDocCommentDrift(
Expand Down
Loading