diff --git a/src/services/__tests__/prScan.test.ts b/src/services/__tests__/prScan.test.ts index 5e0c101..9683f0b 100644 --- a/src/services/__tests__/prScan.test.ts +++ b/src/services/__tests__/prScan.test.ts @@ -1,6 +1,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { CHECK_NAMES, DEFAULT_CONFIG } from "../../lib/types.js"; +import { CHECK_NAMES, DEFAULT_CONFIG, MARKERS } from "../../lib/types.js"; import { runPrScan } from "../prScan.js"; +import { fingerprintPrFindingCommentBody } from "../prFindings.js"; const isPrFindingFingerprintDismissedMock = vi.hoisted(() => vi.fn()); @@ -97,8 +98,21 @@ describe("runPrScan", () => { expect(body).toContain("A new postinstall hook executes a remote script."); expect(body).toContain("Remove the lifecycle hook"); expect(body).toContain("**P1:** Suspicious lifecycle hook"); + expect(body).toContain("AI prompt"); + expect(body).toContain("Check if this security scanner issue is valid."); + expect(body).toContain(''); + expect(body).toContain(''); + expect(body).toContain("Suspicious lifecycle hook"); expect(body).not.toContain("Fix:"); expect(body).not.toContain("Recommended fix"); + expect(fingerprintPrFindingCommentBody(body)).toBe( + fingerprintPrFindingCommentBody(`${MARKERS.PR_FINDING} +**P1:** Suspicious lifecycle hook + +A new postinstall hook executes a remote script. + +Remove the lifecycle hook or replace it with reviewed local setup code.`), + ); }); it("does not recreate comments for previously dismissed findings", async () => { diff --git a/src/services/prScan.ts b/src/services/prScan.ts index bf50a94..4e48a04 100644 --- a/src/services/prScan.ts +++ b/src/services/prScan.ts @@ -137,7 +137,7 @@ function isFindingDismissed( } function fingerprintPrFinding(finding: PrFinding): string { - return fingerprintPrFindingCommentBody(renderInlineFindingCommentBody(finding))!; + return fingerprintPrFindingCommentBody(renderInlineFindingFingerprintBody(finding))!; } function renderFindingsSummary(findings: PrFinding[]): string { @@ -209,10 +209,16 @@ async function deleteInlineFindingComments( function renderInlineFindingComment(finding: PrFinding): string { const body = renderInlineFindingCommentBody(finding); - return appendPrFindingFingerprint(body, fingerprintPrFindingCommentBody(body)!); + return appendPrFindingFingerprint(body, fingerprintPrFinding(finding)); } function renderInlineFindingCommentBody(finding: PrFinding): string { + return `${renderInlineFindingFingerprintBody(finding)} + +${renderAiPrompt(finding)}`; +} + +function renderInlineFindingFingerprintBody(finding: PrFinding): string { const evidence = compactSentence(finding.short_evidence ?? finding.evidence, 180); const recommendation = compactSentence( finding.short_recommendation ?? finding.recommendation, @@ -227,6 +233,59 @@ ${evidence} ${recommendation}`; } +function renderAiPrompt(finding: PrFinding): string { + return `
+AI prompt + +${markdownCodeFence(renderAiPromptText(finding))} + +
`; +} + +function renderAiPromptText(finding: PrFinding): string { + const file = finding.file ?? "unknown"; + const location = finding.file + ? `${finding.file}${finding.line ? `:${finding.line}` : ""}` + : finding.line + ? `line ${finding.line}` + : "unknown"; + + return `Check if this security scanner issue is valid. If so, understand the root cause and fix it. If appropriate, update or add tests. Keep the change focused and preserve intended behavior. + + + +${escapeXmlText(formatFindingPriority(finding.severity))} +${escapeXmlText(finding.title)} +${escapeXmlText(finding.evidence)} +${escapeXmlText(finding.recommendation)} + +`; +} + +function markdownCodeFence(value: string): string { + const fenceLength = Math.max( + 3, + ...Array.from(value.matchAll(/`+/g), (match) => match[0].length + 1), + ); + const fence = "`".repeat(fenceLength); + return `${fence}text +${value} +${fence}`; +} + +function escapeXmlText(value: string): string { + return value + .replace(/&/g, "&") + .replace(//g, ">"); +} + +function escapeXmlAttribute(value: string): string { + return escapeXmlText(value) + .replace(/"/g, """) + .replace(/'/g, "'"); +} + function compactSentence(value: string, maxLength: number): string { const compact = value.replace(/\s+/g, " ").trim(); if (compact.length <= maxLength) return compact;