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