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
67 changes: 67 additions & 0 deletions src/lib/__tests__/brinApi.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { scanContributor, scanPr } from "../brinApi.js";

vi.mock("../logger.js", () => ({
childLogger: () => ({
error: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
}),
}));

describe("scanPr", () => {
beforeEach(() => {
process.env.BRIN_API_BASE = "https://brin.example";
vi.unstubAllGlobals();
});

it("forwards a GitHub token for private PR access", async () => {
const fetchMock = vi.fn().mockResolvedValue(
new Response(JSON.stringify({ score: 90, verdict: "safe" }), {
status: 200,
headers: { "content-type": "application/json" },
}),
);
vi.stubGlobal("fetch", fetchMock);

await scanPr("acme", "private-repo", 123, {
tolerance: "aggressive",
githubToken: "ghs_installation_token",
});

expect(fetchMock).toHaveBeenCalledWith(
"https://brin.example/pr/acme/private-repo/123?details=true&mode=full&tolerance=aggressive",
expect.objectContaining({
headers: { "x-github-token": "ghs_installation_token" },
}),
);
});
});

describe("scanContributor", () => {
beforeEach(() => {
process.env.BRIN_API_BASE = "https://brin.example";
vi.unstubAllGlobals();
});

it("forwards a GitHub token for private contributor access", async () => {
const fetchMock = vi.fn().mockResolvedValue(
new Response(JSON.stringify({ score: 90, verdict: "safe" }), {
status: 200,
headers: { "content-type": "application/json" },
}),
);
vi.stubGlobal("fetch", fetchMock);

await scanContributor("octocat", {
githubToken: "ghs_installation_token",
});

expect(fetchMock).toHaveBeenCalledWith(
"https://brin.example/contributor/octocat?details=true&mode=full",
expect.objectContaining({
headers: { "x-github-token": "ghs_installation_token" },
}),
);
});
});
18 changes: 18 additions & 0 deletions src/lib/__tests__/policy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,24 @@ describe("evaluatePrScan", () => {
});
});

it("returns inconclusive when Brin reports an internal scan error", () => {
const result: PrScanResult = {
score: 0,
verdict: "dangerous",
threats: [
{
type: "scan_error",
detail: "failed to fetch PR: GitHub API returned 404",
},
],
};

expect(evaluatePrScan(result, config)).toEqual({
status: "inconclusive",
shouldFail: false,
});
});

it("respects custom blockBelowScore threshold", () => {
const custom: RepoConfig = {
...config,
Expand Down
24 changes: 20 additions & 4 deletions src/lib/brinApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,20 @@ export async function scanPr(
owner: string,
repo: string,
prNumber: number,
tolerance = "conservative",
options: { tolerance?: string; githubToken?: string } = {},
): Promise<PrScanResult> {
const tolerance = options.tolerance ?? "conservative";
const url = `${env.brinApiBase}/pr/${owner}/${repo}/${prNumber}?details=true&mode=full&tolerance=${tolerance}`;
const log = childLogger({ service: "brin-api", endpoint: "pr", owner, repo, prNumber });
const headers = options.githubToken
? { "x-github-token": options.githubToken }
: undefined;

try {
const res = await fetch(url, { signal: AbortSignal.timeout(300_000) });
const res = await fetch(url, {
headers,
signal: AbortSignal.timeout(300_000),
});
if (!res.ok) {
log.warn({ status: res.status }, "Brin PR API returned non-OK status");
return {};
Expand All @@ -26,12 +33,21 @@ export async function scanPr(
}
}

export async function scanContributor(login: string): Promise<ContributorResult> {
export async function scanContributor(
login: string,
options: { githubToken?: string } = {},
): Promise<ContributorResult> {
const url = `${env.brinApiBase}/contributor/${login}?details=true&mode=full`;
const log = childLogger({ service: "brin-api", endpoint: "contributor", login });
const headers = options.githubToken
? { "x-github-token": options.githubToken }
: undefined;

try {
const res = await fetch(url, { signal: AbortSignal.timeout(30_000) });
const res = await fetch(url, {
headers,
signal: AbortSignal.timeout(30_000),
});
if (!res.ok) {
log.warn({ status: res.status }, "Brin contributor API returned non-OK status");
return {};
Expand Down
7 changes: 7 additions & 0 deletions src/lib/policy.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import type { PrScanResult, ContributorResult, RepoConfig, PrStatus } from "./types.js";

export function hasPrScanError(result: PrScanResult): boolean {
return result.threats?.some((threat) => threat.type === "scan_error") ?? false;
}

export function evaluatePrScan(
result: PrScanResult,
config: RepoConfig,
): { status: PrStatus; shouldFail: boolean } {
if (hasPrScanError(result)) {
return { status: "inconclusive", shouldFail: false };
}
if (result.score == null) {
return { status: "inconclusive", shouldFail: false };
}
Expand Down
28 changes: 27 additions & 1 deletion src/services/__tests__/labels.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect, vi } from "vitest";
import { ensureLabels, setLabel } from "../labels.js";
import { clearLabels, ensureLabels, setLabel } from "../labels.js";

function mockOctokit(overrides: Record<string, any> = {}) {
return {
Expand Down Expand Up @@ -122,3 +122,29 @@ describe("setLabel", () => {
});
});
});

describe("clearLabels", () => {
it("removes brin labels and preserves unrelated labels", async () => {
const octokit = mockOctokit({
listLabelsOnIssue: vi.fn().mockResolvedValue({
data: [
{ name: "bug" },
{ name: "pr:flagged" },
{ name: "enhancement" },
],
}),
});

await clearLabels(octokit, "owner", "repo", 42, [
"pr:verified",
"pr:flagged",
]);

expect(octokit.rest.issues.setLabels).toHaveBeenCalledWith({
owner: "owner",
repo: "repo",
issue_number: 42,
labels: ["bug", "enhancement"],
});
});
});
4 changes: 3 additions & 1 deletion src/services/contributorTrust.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
deleteMarkerComment,
renderContributorTrustComment,
} from "./comments.js";
import { getGitHubToken } from "./githubToken.js";
import { ensureLabels, setLabel } from "./labels.js";
import { childLogger } from "../lib/logger.js";

Expand Down Expand Up @@ -53,7 +54,8 @@ export async function runContributorTrust(
CHECK_NAMES.CONTRIBUTOR_TRUST,
);

const result = await scanContributor(authorLogin);
const githubToken = await getGitHubToken(octokit);
const result = await scanContributor(authorLogin, { githubToken });
const { isSafe } = evaluateContributor(result, config);

log.info(
Expand Down
9 changes: 9 additions & 0 deletions src/services/githubToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { Octokit } from "octokit";

export async function getGitHubToken(octokit: Octokit): Promise<string | undefined> {
const auth = await octokit.auth({ type: "installation" });
if (auth && typeof auth === "object" && "token" in auth && typeof auth.token === "string") {
return auth.token;
}
return undefined;
}
25 changes: 25 additions & 0 deletions src/services/labels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,28 @@ export async function setLabel(
labels: [...preserved, nextLabel],
});
}

export async function clearLabels(
octokit: Octokit,
owner: string,
repo: string,
issueNumber: number,
brinLabelNames: readonly string[],
): Promise<void> {
const { data: current } = await octokit.rest.issues.listLabelsOnIssue({
owner,
repo,
issue_number: issueNumber,
});

const preserved = current
.map((l) => l.name)
.filter((name) => !brinLabelNames.includes(name));

await octokit.rest.issues.setLabels({
owner,
repo,
issue_number: issueNumber,
labels: preserved,
});
}
11 changes: 9 additions & 2 deletions src/services/prScan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import {
deleteMarkerComment,
renderPrScanComment,
} from "./comments.js";
import { ensureLabels, setLabel } from "./labels.js";
import { getGitHubToken } from "./githubToken.js";
import { clearLabels, ensureLabels, setLabel } from "./labels.js";
import { childLogger } from "../lib/logger.js";

const PR_LABELS = [LABEL_DEFS.PR_VERIFIED, LABEL_DEFS.PR_FLAGGED];
Expand Down Expand Up @@ -41,7 +42,11 @@ export async function runPrScan(
CHECK_NAMES.PR_SCAN,
);

const result = await scanPr(owner, repo, prNumber, config.prScan.tolerance);
const githubToken = await getGitHubToken(octokit);
const result = await scanPr(owner, repo, prNumber, {
tolerance: config.prScan.tolerance,
githubToken,
});
const { status, shouldFail } = evaluatePrScan(result, config);

log.info(
Expand Down Expand Up @@ -91,6 +96,8 @@ export async function runPrScan(
? "A deep scan is in progress. Results will update automatically."
: "Unable to determine scan results at this time.",
});
await clearLabels(octokit, owner, repo, prNumber, PR_LABEL_NAMES);
await deleteMarkerComment(octokit, owner, repo, prNumber, MARKERS.PR_SCAN);
break;
}
}
Expand Down
Loading