| 615 | } |
| 616 | |
| 617 | function parseVerdict(text: string): ScanVerdict { |
| 618 | const fenced = text.match(/```json\s*([\s\S]*?)```/gi); |
| 619 | let candidate: string | null = null; |
| 620 | |
| 621 | if (fenced && fenced.length > 0) { |
| 622 | const last = fenced[fenced.length - 1]; |
| 623 | candidate = last |
| 624 | .replace(/```json\s*/i, "") |
| 625 | .replace(/```\s*$/, "") |
| 626 | .trim(); |
| 627 | } else { |
| 628 | const match = text.match(/\{[\s\S]*\}\s*$/); |
| 629 | if (match) candidate = match[0]; |
| 630 | } |
| 631 | |
| 632 | if (!candidate) { |
| 633 | throw new FatalScanError("Scan output did not contain a JSON verdict."); |
| 634 | } |
| 635 | |
| 636 | let parsed: unknown; |
| 637 | try { |
| 638 | parsed = JSON.parse(candidate); |
| 639 | } catch { |
| 640 | throw new FatalScanError("Scan output JSON could not be parsed."); |
| 641 | } |
| 642 | |
| 643 | const validated = verdictSchema.safeParse(parsed); |
| 644 | if (!validated.success) { |
| 645 | throw new FatalScanError( |
| 646 | `Scan verdict failed schema validation: ${validated.error.message}`, |
| 647 | ); |
| 648 | } |
| 649 | |
| 650 | return { |
| 651 | verdict: validated.data.verdict, |
| 652 | severity: validated.data.severity, |
| 653 | categories: validated.data.categories, |
| 654 | reasons: validated.data.reasons, |
| 655 | summary: validated.data.summary, |
| 656 | }; |
| 657 | } |
| 658 | |
| 659 | async function applyVerdict( |
| 660 | pluginId: string, |