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.

153 linesCodeBlameRaw
1import { ArrowLeft, CircleAlert, CircleCheck, RotateCw } from "lucide-react";
2import { Form, Link } from "react-router";
3
4import type { Route } from "./+types/security-pull";
5import { page } from "../../lib/meta";
6import { SeverityBadge } from "../../components/security";
7import { CARD, LIST, SectionHeader, shortRule } from "../../components/security-suite";
8import { ErrorText, SubmitButton } from "../../components/ui";
9import { Badge } from "../../components/ui/badge";
10import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../../components/ui/table";
11import { cn } from "../../lib/cn";
12import { securitySuite } from "../../lib/services.server";
13import { assertSameOrigin, getViewer, requireUser, unwrap } from "../../lib/session.server";
14import { refusal, requireInsider } from "../../lib/access.server";
15
16export function meta({ params, ...args }: Route.MetaArgs) {
17 return page(args, { title: `Security checks · #${params.number} · ${params.owner}/${params.repo} · g1t` });
18}
19
20export async function loader({ params, context, request }: Route.LoaderArgs) {
21 const viewer = getViewer(context) ?? requireUser(context, request);
22 await requireInsider(context, params, "security_alerts");
23 const repo = { namespace: params.owner, name: params.repo };
24 return { scanning: unwrap(await securitySuite.pullScanning(repo, Number(params.number) || 0, viewer)) };
25}
26
27/** The Dependency review check made again on the pull request's head; its comment changes in place. */
28export async function action({ params, context, request }: Route.ActionArgs) {
29 assertSameOrigin(request);
30 const user = requireUser(context, request);
31 const refused = await refusal(context, params, "run");
32 if (refused) return { error: refused };
33 const repo = { namespace: params.owner, name: params.repo };
34 const rerun = await securitySuite.rerunDependencyReview(user, repo, Number(params.number) || 0);
35 return rerun.ok ? null : { error: rerun.error.message };
36}
37
38function Verdict({ passed, text }: { passed: boolean; text: string }) {
39 return (
40 <p className={`flex items-center gap-1.5 text-sm ${passed ? "text-success" : "text-danger"}`}>
41 {passed ? <CircleCheck size={15} /> : <CircleAlert size={15} />}
42 {text}
43 </p>
44 );
45}
46
47/** What the Code scanning and Dependency review checks found on a pull request: the page their statuses link to. */
48export default function PullSecurity({ loaderData, actionData, params }: Route.ComponentProps) {
49 const { scanning } = loaderData;
50 const base = `/${params.owner}/${params.repo}`;
51 const onLines = scanning.results.filter((result) => result.new && result.onChangedLine);
52 const others = scanning.results.filter((result) => !(result.new && result.onChangedLine));
53 return (
54 <div className="max-w-4xl space-y-8">
55 <Link to={`${base}/pull/${params.number}`} className="inline-flex items-center gap-1.5 text-sm text-muted hover:text-fg">
56 <ArrowLeft size={14} /> Pull request #{params.number}
57 </Link>
58 <SectionHeader
59 title={`Security checks for #${params.number}`}
60 about={`What code scanning and dependency review found on this pull request${scanning.commit ? ` at ${scanning.commit.slice(0, 7)}` : ""}. Require the checks in branch protection to block merges on them.`}
61 />
62 <section className="space-y-3">
63 <h3 className="text-base font-semibold tracking-tight">Code scanning</h3>
64 {scanning.codeStatus ? (
65 <Verdict passed={scanning.codeStatus === "success"} text={scanning.codeDescription ?? ""} />
66 ) : (
67 <p className="text-sm text-muted">No code scanning results for this pull request yet.</p>
68 )}
69 {onLines.length > 0 && (
70 <ul className={LIST}>
71 {onLines.map((result, at) => (
72 <li key={at} className="px-4 py-3">
73 <div className="flex flex-wrap items-center gap-2">
74 <span className="text-sm font-medium">{shortRule(result.ruleId)}</span>
75 <SeverityBadge severity={result.severity} />
76 {result.failing && <Badge tone="danger">Fails the check</Badge>}
77 <span className="text-xs text-faint">{result.tool}</span>
78 </div>
79 <p className="mt-1 text-xs text-muted">{result.message}</p>
80 {result.path && (
81 <p className="mt-1 font-mono text-xs text-fg-soft">
82 {result.path}
83 {result.line ? `:${result.line}` : ""}
84 </p>
85 )}
86 </li>
87 ))}
88 </ul>
89 )}
90 {others.length > 0 && (
91 <p className="text-xs text-faint">
92 {others.length} other {others.length === 1 ? "result" : "results"}: already open on the default branch, or on lines this pull request does not change.
93 </p>
94 )}
95 </section>
96 <section className="space-y-3">
97 <div className="flex flex-wrap items-center justify-between gap-2">
98 <h3 className="text-base font-semibold tracking-tight">Dependency review</h3>
99 <Form method="post">
100 <SubmitButton variant="outline" pending="Re-running…">
101 <RotateCw size={13} />
102 Re-run
103 </SubmitButton>
104 </Form>
105 </div>
106 {actionData?.error && <ErrorText>{actionData.error}</ErrorText>}
107 {!scanning.review ? (
108 <p className="text-sm text-muted">Not reviewed yet: it runs when a pull request opens and each time its head moves.</p>
109 ) : (
110 <>
111 <Verdict passed={scanning.review.passed} text={scanning.review.headline} />
112 {scanning.review.changes.length > 0 && (
113 <div className={CARD}>
114 <Table className="min-w-xl">
115 <TableHeader>
116 <TableRow>
117 <TableHead className="px-4">Change</TableHead>
118 <TableHead>Package</TableHead>
119 <TableHead>License</TableHead>
120 <TableHead>Vulnerabilities</TableHead>
121 </TableRow>
122 </TableHeader>
123 <TableBody>
124 {scanning.review.changes.map((change) => (
125 <TableRow key={`${change.changeType}:${change.manifest}:${change.name}@${change.version}`}>
126 <TableCell className="px-4 text-xs">{change.changeType === "added" ? "Added" : "Removed"}</TableCell>
127 <TableCell className="font-mono text-xs">
128 {change.name}@{change.version}
129 </TableCell>
130 <TableCell className={cn("text-xs", change.deniedLicense ? "text-danger" : "text-muted")}>{change.license ?? "—"}</TableCell>
131 <TableCell className="text-xs">
132 {change.vulnerabilities.length === 0 ? (
133 <span className="text-faint">none known</span>
134 ) : (
135 change.vulnerabilities.map((vuln) => (
136 <a key={vuln.osvId} href={vuln.url} className="mr-2 inline-flex items-center gap-1 hover:underline" rel="noreferrer">
137 <SeverityBadge severity={vuln.severity} /> {vuln.advisory}
138 </a>
139 ))
140 )}
141 </TableCell>
142 </TableRow>
143 ))}
144 </TableBody>
145 </Table>
146 </div>
147 )}
148 </>
149 )}
150 </section>
151 </div>
152 );
153}