Skip to content

g1t/apps/web/app/routes/repo/security-pull.tsx

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