Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

131 linesCodeBlameRaw
1/**
2 * "Report a vulnerability": tells a repository's maintainers privately,
3 * when the repository takes such reports. Anyone who can see the
4 * repository may; on a public one someone signed out may too, with an
5 * email address the maintainers can answer at. The front door limits how
6 * fast one address can post here (lib/front-door-limits.ts), and the
7 * service takes five reports a day from one reporter.
8 */
9import { Form, Link, data, useNavigation } from "react-router";
10
11import type { AdvisorySeverity } from "@g1t/contracts";
12
13import type { Route } from "./+types/security-report";
14import { page } from "../../lib/meta";
15import { SectionHeader } from "../../components/security-suite";
16import { ButtonLink, ErrorText, Field, Input, SubmitButton, Textarea, notACredential } from "../../components/ui";
17import { Alert, AlertDescription, AlertTitle } from "../../components/ui/alert";
18import { Card } from "../../components/ui/card";
19import { SelectField } from "../../components/ui/select";
20import { advisories } from "../../lib/services.server";
21import { assertSameOrigin, getViewer } from "../../lib/session.server";
22import { requireRepo } from "../../lib/access.server";
23import { SEVERITIES, SEVERITY_LABEL, reportFromForm } from "../../lib/advisories";
24
25export function meta({ params, ...args }: Route.MetaArgs) {
26 return page(args, { title: `Report a vulnerability · ${params.owner}/${params.repo} · g1t` });
27}
28
29export async function loader({ params, context }: Route.LoaderArgs) {
30 const viewer = getViewer(context);
31 await requireRepo(context, params, "read");
32 const settings = await advisories.reportSettings({ namespace: params.owner, name: params.repo }, viewer);
33 return { reporting: settings.ok && settings.value.privateVulnerabilityReporting, signedIn: Boolean(viewer), username: viewer?.username ?? null };
34}
35
36export async function action({ params, context, request }: Route.ActionArgs) {
37 assertSameOrigin(request);
38 const viewer = getViewer(context);
39 await requireRepo(context, params, "read");
40 const draft = reportFromForm(await request.formData(), Boolean(viewer));
41 if (typeof draft === "string") return data({ error: draft, sent: null }, { status: 400 });
42 const sent = await advisories.report(viewer, { namespace: params.owner, name: params.repo }, draft);
43 if (!sent.ok) return data({ error: sent.error.message, sent: null }, { status: sent.error.code === "limit" ? 429 : 400 });
44 return { error: null, sent: sent.value.id };
45}
46
47export default function SecurityReport({ loaderData, actionData, params }: Route.ComponentProps) {
48 const { reporting, signedIn, username } = loaderData;
49 const base = `/${params.owner}/${params.repo}`;
50 const navigation = useNavigation();
51 if (actionData?.sent) {
52 return (
53 <div className="max-w-3xl space-y-6">
54 <SectionHeader title="Thank you" about="Your report reached the maintainers, and only them. They will look into it and answer you when there is news." />
55 <ButtonLink to={`${base}/security`} variant="outline">
56 Back to Security
57 </ButtonLink>
58 </div>
59 );
60 }
61 if (!reporting) {
62 return (
63 <div className="max-w-3xl space-y-6">
64 <SectionHeader title="Report a vulnerability" about="This repository does not take private vulnerability reports here." />
65 <Alert tone="info">
66 <AlertTitle>Reach the maintainers another way</AlertTitle>
67 <AlertDescription>
68 Its{" "}
69 <Link to={`${base}/security`} className="underline underline-offset-2">
70 Security page
71 </Link>{" "}
72 shows its security policy, if it has one, which says how to report. Please do not open a public issue about a vulnerability.
73 </AlertDescription>
74 </Alert>
75 </div>
76 );
77 }
78 return (
79 <div className="max-w-3xl space-y-6">
80 <SectionHeader
81 title="Report a vulnerability"
82 about={`Tell the maintainers of ${params.owner}/${params.repo} privately. Only they see what you write here; nothing is published until they publish an advisory.`}
83 />
84 <Form method="post" className="space-y-4">
85 <Card className="space-y-4 p-4">
86 <Field label="Title" hint="One line: what you found.">
87 <Input name="title" required maxLength={200} placeholder="Path traversal in the archive reader" {...notACredential()} />
88 </Field>
89 <Field label="Description" hint="What is affected, how to reproduce it, and what you think it allows. Markdown is fine.">
90 <Textarea name="description" required rows={10} maxLength={20_000} />
91 </Field>
92 <div className="grid gap-4 sm:grid-cols-2">
93 <Field label="Severity" hint="How bad you think it is, if you have a view.">
94 <SelectField
95 name="severity"
96 defaultValue="medium"
97 options={SEVERITIES.map((severity: AdvisorySeverity) => ({ value: severity, label: SEVERITY_LABEL[severity] }))}
98 aria-label="Severity"
99 />
100 </Field>
101 {signedIn ? (
102 <Field label="Reported as" hint="The maintainers answer you on g1t.">
103 <Input value={username ?? ""} readOnly className="font-mono" />
104 </Field>
105 ) : (
106 <Field label="Your email" hint="Where the maintainers can answer you. Never published.">
107 <Input name="email" type="email" required maxLength={254} autoComplete="email" placeholder="you@example.com" />
108 </Field>
109 )}
110 </div>
111 </Card>
112 <div className="flex flex-wrap items-center gap-3">
113 <SubmitButton pending="Sending…">Send report</SubmitButton>
114 <ButtonLink to={`${base}/security`} variant="outline">
115 Cancel
116 </ButtonLink>
117 {navigation.state === "idle" && <ErrorText>{actionData?.error}</ErrorText>}
118 </div>
119 {!signedIn && (
120 <p className="text-xs text-faint">
121 Have an account?{" "}
122 <Link to={`/login?next=${encodeURIComponent(`${base}/security/report`)}`} className="underline underline-offset-2 hover:text-fg">
123 Sign in
124 </Link>{" "}
125 to report under your name.
126 </p>
127 )}
128 </Form>
129 </div>
130 );
131}