import { createServerFn } from "@tanstack/react-start"; import { z } from "zod"; import { requireViewer, viewerMiddleware } from "./middleware"; import { badRequest, forbidden } from "~/server/errors"; import { limitBrowse, limitWrite } from "~/server/limits"; import { requireRepoAccess } from "~/server/repos"; import { addAdvisoryComment, advisoryDetail, canManageAdvisories, createAdvisory, listAdvisories, setAdvisoryCollaborator, setAdvisoryState, updateAdvisory, } from "~/server/security/advisories"; import { loadSecuritySettings } from "~/server/security/settings"; import { accountName, markdownBody, repoRef, title } from "~/lib/validation"; const optionalText = (max: number) => z .string() .trim() .max(max) .transform((value) => value || null) .nullable(); const advisoryFields = z.object({ title, description: markdownBody.min(1, "Describe the vulnerability"), severity: z.enum(["low", "moderate", "high", "critical"]), cveId: optionalText(32).refine((value) => value === null || /^CVE-\d{4}-\d{4,}$/.test(value), "Use CVE-YYYY-NNNN"), cweIds: optionalText(200), ecosystem: z.enum(["npm", "pip", "go", "cargo", "rubygems", "composer", "other"]).nullable(), packageName: optionalText(214), affectedVersions: optionalText(200), patchedVersions: optionalText(200), }); const ghsaId = z.string().regex(/^GITFOR-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}$/); export const getAdvisories = createServerFn({ method: "GET" }) .middleware([viewerMiddleware]) .validator(repoRef) .handler(async ({ data, context }) => { await limitBrowse(context.viewer); const { repo, permission } = await requireRepoAccess(data.owner, data.repo, context.viewer, "read"); const settings = await loadSecuritySettings(repo.id); return { advisories: await listAdvisories(repo, context.viewer, permission), canManage: canManageAdvisories(permission), canReport: context.viewer !== null && settings.privateReporting, }; }); export const getAdvisory = createServerFn({ method: "GET" }) .middleware([viewerMiddleware]) .validator(repoRef.extend({ ghsaId })) .handler(async ({ data, context }) => { await limitBrowse(context.viewer); const { repo, permission } = await requireRepoAccess(data.owner, data.repo, context.viewer, "read"); return advisoryDetail(repo, data.ghsaId, context.viewer, permission); }); export const submitAdvisory = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator(repoRef.extend({ advisory: advisoryFields, mode: z.enum(["draft", "report"]) })) .handler(async ({ data, context }) => { const viewer = requireViewer(context.viewer); await limitWrite(viewer); const { repo, permission } = await requireRepoAccess(data.owner, data.repo, viewer, "read"); if (data.mode === "draft") { if (!canManageAdvisories(permission)) throw forbidden(); } else if (!(await loadSecuritySettings(repo.id)).privateReporting) { throw badRequest("Private vulnerability reporting is not enabled for this repository"); } const id = await createAdvisory(repo, viewer, data.advisory, data.mode === "draft" ? "maintainer" : "report"); return { ghsaId: id }; }); export const editAdvisory = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator(repoRef.extend({ ghsaId, advisory: advisoryFields })) .handler(async ({ data, context }) => { const viewer = requireViewer(context.viewer); await limitWrite(viewer); const { repo } = await requireRepoAccess(data.owner, data.repo, viewer, "admin"); await updateAdvisory(repo, data.ghsaId, data.advisory); return { ok: true }; }); export const changeAdvisoryState = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator(repoRef.extend({ ghsaId, state: z.enum(["draft", "published", "closed"]) })) .handler(async ({ data, context }) => { const viewer = requireViewer(context.viewer); await limitWrite(viewer); const { repo } = await requireRepoAccess(data.owner, data.repo, viewer, "admin"); await setAdvisoryState(repo, data.ghsaId, data.state, viewer.id); return { ok: true }; }); export const commentOnAdvisory = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator(repoRef.extend({ ghsaId, body: markdownBody.min(1) })) .handler(async ({ data, context }) => { const viewer = requireViewer(context.viewer); await limitWrite(viewer); const { repo, permission } = await requireRepoAccess(data.owner, data.repo, viewer, "read"); await addAdvisoryComment(repo, data.ghsaId, viewer, permission, data.body); return { ok: true }; }); export const updateAdvisoryCollaborator = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator(repoRef.extend({ ghsaId, username: accountName, add: z.boolean() })) .handler(async ({ data, context }) => { const viewer = requireViewer(context.viewer); await limitWrite(viewer); const { repo } = await requireRepoAccess(data.owner, data.repo, viewer, "admin"); await setAdvisoryCollaborator(repo, data.ghsaId, data.username, data.add); return { ok: true }; });