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.

134 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
55 title="Thank you"
56 about="Your report reached the maintainers, and only them. They will look into it and answer you when there is news."
57 />
58 <ButtonLink to={`${base}/security`} variant="outline">
59 Back to Security
60 </ButtonLink>
61 </div>
62 );
63 }
64 if (!reporting) {
65 return (
66 <div className="max-w-3xl space-y-6">
67 <SectionHeader title="Report a vulnerability" about="This repository does not take private vulnerability reports here." />
68 <Alert tone="info">
69 <AlertTitle>Reach the maintainers another way</AlertTitle>
70 <AlertDescription>
71 Its{" "}
72 <Link to={`${base}/security`} className="underline underline-offset-2">
73 Security page
74 </Link>{" "}
75 shows its security policy, if it has one, which says how to report. Please do not open a public work item about a vulnerability.
76 </AlertDescription>
77 </Alert>
78 </div>
79 );
80 }
81 return (
82 <div className="max-w-3xl space-y-6">
83 <SectionHeader
84 title="Report a vulnerability"
85 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.`}
86 />
87 <Form method="post" className="space-y-4">
88 <Card className="space-y-4 p-4">
89 <Field label="Title" hint="One line: what you found.">
90 <Input name="title" required maxLength={200} placeholder="Path traversal in the archive reader" {...notACredential()} />
91 </Field>
92 <Field label="Description" hint="What is affected, how to reproduce it, and what you think it allows. Markdown is fine.">
93 <Textarea name="description" required rows={10} maxLength={20_000} />
94 </Field>
95 <div className="grid gap-4 sm:grid-cols-2">
96 <Field label="Severity" hint="How bad you think it is, if you have a view.">
97 <SelectField
98 name="severity"
99 defaultValue="medium"
100 options={SEVERITIES.map((severity: AdvisorySeverity) => ({ value: severity, label: SEVERITY_LABEL[severity] }))}
101 aria-label="Severity"
102 />
103 </Field>
104 {signedIn ? (
105 <Field label="Reported as" hint="The maintainers answer you on g1t.">
106 <Input value={username ?? ""} readOnly className="font-mono" />
107 </Field>
108 ) : (
109 <Field label="Your email" hint="Where the maintainers can answer you. Never published.">
110 <Input name="email" type="email" required maxLength={254} autoComplete="email" placeholder="you@example.com" />
111 </Field>
112 )}
113 </div>
114 </Card>
115 <div className="flex flex-wrap items-center gap-3">
116 <SubmitButton pending="Sending…">Send report</SubmitButton>
117 <ButtonLink to={`${base}/security`} variant="outline">
118 Cancel
119 </ButtonLink>
120 {navigation.state === "idle" && <ErrorText>{actionData?.error}</ErrorText>}
121 </div>
122 {!signedIn && (
123 <p className="text-xs text-faint">
124 Have an account?{" "}
125 <Link to={`/login?next=${encodeURIComponent(`${base}/security/report`)}`} className="underline underline-offset-2 hover:text-fg">
126 Sign in
127 </Link>{" "}
128 to report under your name.
129 </p>
130 )}
131 </Form>
132 </div>
133 );
134}