Skip to content
99 linesCodeBlameRaw
1/**
2 * Security advisories and private vulnerability reports as the pages read
3 * them: what a form posts, checked, and how states and severities show.
4 * Pure, so it is tested on its own (routes/repo/security-advisor*.tsx,
5 * security-report*.tsx).
6 */
7import type { AdvisoryInput, AdvisorySeverity, AdvisoryState, ReportState } from "@g1t/contracts";
8
9// The contracts' lists, as types only, so this file runs under `node --test`.
10export const SEVERITIES: AdvisorySeverity[] = ["critical", "high", "medium", "low"];
11export const ADVISORY_STATES: AdvisoryState[] = ["draft", "published", "withdrawn"];
12export const REPORT_STATES: ReportState[] = ["new", "accepted", "declined"];
13
14export const SEVERITY_LABEL: Record<AdvisorySeverity, string> = { critical: "Critical", high: "High", medium: "Medium", low: "Low" };
15
16export const STATE_LABEL: Record<AdvisoryState, string> = { draft: "Draft", published: "Published", withdrawn: "Withdrawn" };
17
18export const STATE_TONE: Record<AdvisoryState, "neutral" | "success" | "warn"> = { draft: "neutral", published: "success", withdrawn: "warn" };
19
20export const REPORT_STATE_LABEL: Record<ReportState, string> = { new: "New", accepted: "Accepted", declined: "Declined" };
21
22export const REPORT_STATE_TONE: Record<ReportState, "warn" | "success" | "neutral"> = { new: "warn", accepted: "success", declined: "neutral" };
23
24export function parseSeverity(value: unknown): AdvisorySeverity | null {
25 return SEVERITIES.find((severity) => severity === value) ?? null;
26}
27
28export function parseAdvisoryState(value: unknown): AdvisoryState | null {
29 return ADVISORY_STATES.find((state) => state === value) ?? null;
30}
31
32export function parseReportState(value: unknown): ReportState | null {
33 return REPORT_STATES.find((state) => state === value) ?? null;
34}
35
36const text = (form: FormData, key: string, max: number): string =>
37 String(form.get(key) ?? "")
38 .trim()
39 .slice(0, max);
40
41const optional = (form: FormData, key: string, max: number): string | null => text(form, key, max) || null;
42
43/** The advisory a form posts, or what is wrong with it. */
44export function advisoryFromForm(form: FormData): AdvisoryInput | string {
45 const title = text(form, "title", 200);
46 if (!title) return "Give the advisory a title.";
47 const severity = parseSeverity(form.get("severity"));
48 if (!severity) return "Choose a severity.";
49 const scoreText = text(form, "cvssScore", 10);
50 const cvssScore = scoreText ? Number(scoreText) : null;
51 if (cvssScore != null && (!Number.isFinite(cvssScore) || cvssScore < 0 || cvssScore > 10)) return "A CVSS score is 0 to 10.";
52 const cveId = optional(form, "cveId", 32);
53 if (cveId && !/^CVE-\d{4}-\d{4,}$/i.test(cveId)) return "A CVE id looks like CVE-2026-12345.";
54 return {
55 title,
56 description: text(form, "description", 20_000),
57 severity,
58 cvssVector: optional(form, "cvssVector", 200),
59 cvssScore,
60 cveId: cveId?.toUpperCase() ?? null,
61 ecosystem: optional(form, "ecosystem", 50),
62 package: optional(form, "package", 200),
63 affectedVersions: optional(form, "affectedVersions", 200),
64 patchedVersions: optional(form, "patchedVersions", 200),
65 credits: text(form, "credits", 2000)
66 .split(/[\n,]+/)
67 .map((credit) => credit.trim().replace(/^@/, ""))
68 .filter(Boolean)
69 .slice(0, 20),
70 };
71}
72
73export type ReportDraft = { title: string; description: string; severity: AdvisorySeverity | null; email: string | null };
74
75/** The report a form posts, or what is wrong with it. Someone signed out gives an email. */
76export function reportFromForm(form: FormData, signedIn: boolean): ReportDraft | string {
77 const title = text(form, "title", 200);
78 if (!title) return "Give the report a title: one line on what you found.";
79 const description = text(form, "description", 20_000);
80 if (!description) return "Describe the vulnerability: what is affected and how to reproduce it.";
81 const email = optional(form, "email", 254);
82 if (!signedIn && !(email && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email))) return "Give an email address the maintainers can answer at, or sign in.";
83 return { title, description, severity: parseSeverity(form.get("severity")), email: signedIn ? null : email };
84}
85
86/** "Published 3 days ago by ana", or what a draft or withdrawn advisory says. */
87export function advisoryStanding(advisory: { state: AdvisoryState; createdBy: string; publishedAt?: string | null; withdrawnAt?: string | null }): {
88 verb: string;
89 at: string | null;
90} {
91 switch (advisory.state) {
92 case "published":
93 return { verb: "Published", at: advisory.publishedAt ?? null };
94 case "withdrawn":
95 return { verb: "Withdrawn", at: advisory.withdrawnAt ?? null };
96 default:
97 return { verb: `Drafted by ${advisory.createdBy}`, at: null };
98 }
99}