Skip to content

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

146 lines7,945 bytesCodeBlame
1import { FileSearch } from "lucide-react";
2import { Form, useNavigation, useSearchParams } from "react-router";
3
4import type { AlertState, Severity } from "@g1t/contracts";
5
6import type { Route } from "./+types/security-code";
7import { page } from "../../lib/meta";
8import { StateFilter } from "../../components/security";
9import { ActivationPrompt, CARD, CodeAlertItem, FilterSelect, LIST, SectionHeader } from "../../components/security-suite";
10import { TimeAgo } from "../../components/ui";
11import { securitySuite } from "../../lib/services.server";
12import { getViewer, managesSecurity, requireUser, unwrap } from "../../lib/session.server";
13import { requireInsider } from "../../lib/access.server";
14import { activationPrice } from "../../lib/security-suite.server";
15import { codeFilters, countStates, keepCode } from "../../lib/security-suite";
16
17export function meta({ params, ...args }: Route.MetaArgs) {
18 return page(args, { title: `Code scanning · ${params.owner}/${params.repo} · g1t` });
19}
20
21export async function loader({ params, context, request }: Route.LoaderArgs) {
22 const viewer = getViewer(context) ?? requireUser(context, request);
23 const { access } = await requireInsider(context, params, "security_alerts");
24 const repo = { namespace: params.owner, name: params.repo };
25 const [scanning, price] = await Promise.all([securitySuite.codeScanning(repo, viewer), activationPrice(params.owner, viewer)]);
26 return { scanning: unwrap(scanning), price, can: access.can, owner: managesSecurity(viewer, params.owner) };
27}
28
29const SEVERITY_OPTIONS: [string, string][] = [
30 ["all", "Any severity"],
31 ["critical", "Critical"],
32 ["high", "High"],
33 ["medium", "Medium"],
34 ["low", "Low"],
35];
36
37export default function CodeScanning({ loaderData, params }: Route.ComponentProps) {
38 const { scanning, price, can, owner } = loaderData;
39 const base = `/${params.owner}/${params.repo}`;
40 const [search, setSearch] = useSearchParams();
41 const navigation = useNavigation();
42 const filters = codeFilters(search);
43 const tools = [...new Set(scanning.alerts.map((alert) => alert.tool))].sort();
44 const counts = countStates(scanning.alerts.filter((alert) => keepCode(alert, { ...filters, state: alert.state })));
45 const shown = scanning.alerts.filter((alert) => keepCode(alert, filters));
46 const set = (key: string, value: string | null) => {
47 const next = new URLSearchParams(search);
48 if (value) next.set(key, value);
49 else next.delete(key);
50 setSearch(next, { replace: true, preventScrollReset: true });
51 };
52 const empty = scanning.alerts.length === 0 && scanning.analyses.length === 0;
53 const settingUp = navigation.state !== "idle" && navigation.formAction?.endsWith("/code-scanning/setup");
54 return (
55 <div className="max-w-5xl space-y-8">
56 <SectionHeader
57 title="Code scanning"
58 about="Results from your static analysis tools, uploaded as SARIF. On the default branch each problem is an alert, fixed when a later analysis no longer reports it. On a pull request, new results on the lines it changes become review comments and the Code scanning check."
59 actions={
60 scanning.entitled && can.manage_settings && !scanning.configured ? (
61 <Form method="post" action={`${base}/security/code-scanning/setup`}>
62 <button type="submit" disabled={settingUp} className="rounded-md bg-fg px-3.5 py-2 text-sm font-medium text-bg hover:bg-white disabled:opacity-50">
63 {settingUp ? "Opening a pull request…" : "Set up code scanning"}
64 </button>
65 </Form>
66 ) : null
67 }
68 />
69 {!scanning.entitled ? (
70 <ActivationPrompt workspace={params.owner} feature="Code scanning" monthlyCents={price} isOwner={owner} />
71 ) : empty ? (
72 <div className={`${CARD} px-6 py-10 text-center`}>
73 <FileSearch size={22} className="mx-auto text-accent" />
74 <p className="mt-2 font-medium">No analyses yet</p>
75 <p className="mx-auto mt-1 max-w-xl text-sm text-muted">
76 {scanning.configured
77 ? "The code scanning workflow is on the default branch; its first run's results show here."
78 : "Set up code scanning to open a pull request adding a workflow that scans each language the repository has (Bandit, gosec, ESLint, Clippy) and uploads the results, or upload SARIF from any tool with POST /repos/{owner}/{name}/code-scanning/sarifs."}
79 </p>
80 </div>
81 ) : (
82 <section className="space-y-3">
83 <div className="flex flex-wrap items-end justify-between gap-3">
84 <StateFilter counts={counts} value={filters.state} onChange={(state: AlertState) => set("state", state === "open" ? null : state)} />
85 <div className="flex flex-wrap gap-2">
86 <FilterSelect label="Severity" value={filters.severity ?? "all"} options={SEVERITY_OPTIONS} onChange={(value) => set("severity", value === "all" ? null : (value as Severity))} />
87 <FilterSelect label="Tool" value={filters.tool ?? "all"} options={[["all", "Every tool"], ...tools.map((tool): [string, string] => [tool, tool])]} onChange={(value) => set("tool", value === "all" ? null : value)} />
88 </div>
89 </div>
90 {shown.length === 0 ? (
91 <p className="rounded-xl border border-dashed border-line px-4 py-6 text-sm text-muted">No {filters.state} alerts match.</p>
92 ) : (
93 <ul className={LIST}>
94 {shown.map((alert) => (
95 <CodeAlertItem key={alert.id} alert={alert} base={base} />
96 ))}
97 </ul>
98 )}
99 </section>
100 )}
101 {scanning.analyses.length > 0 && (
102 <section>
103 <h3 className="text-base font-semibold tracking-tight">Recent analyses</h3>
104 <div className={`${CARD} mt-2 overflow-x-auto`}>
105 <table className="w-full min-w-[36rem] text-sm">
106 <thead className="text-left text-xs text-muted">
107 <tr className="border-b border-line">
108 <th className="px-4 py-2 font-medium">Tool</th>
109 <th className="px-3 py-2 font-medium">Ref</th>
110 <th className="px-3 py-2 font-medium">Commit</th>
111 <th className="px-3 py-2 text-right font-medium">Results</th>
112 <th className="px-3 py-2 text-right font-medium">New</th>
113 <th className="px-3 py-2 text-right font-medium">Fixed</th>
114 <th className="px-3 py-2 font-medium">When</th>
115 </tr>
116 </thead>
117 <tbody className="tabular-nums">
118 {scanning.analyses.slice(0, 15).map((analysis) => (
119 <tr key={analysis.id} className="border-b border-line last:border-0">
120 <td className="px-4 py-2">{analysis.tool}</td>
121 <td className="px-3 py-2 font-mono text-xs">
122 {analysis.pull != null ? (
123 <a href={`${base}/security/pulls/${analysis.pull}`} className="hover:underline">
124 #{analysis.pull}
125 </a>
126 ) : (
127 analysis.gitRef.replace("refs/heads/", "")
128 )}
129 </td>
130 <td className="px-3 py-2 font-mono text-xs">{analysis.commitSha.slice(0, 7)}</td>
131 <td className="px-3 py-2 text-right">{analysis.results}</td>
132 <td className="px-3 py-2 text-right">{analysis.newAlerts}</td>
133 <td className="px-3 py-2 text-right">{analysis.fixedAlerts}</td>
134 <td className="px-3 py-2 text-xs text-muted">
135 <TimeAgo at={analysis.createdAt} />
136 </td>
137 </tr>
138 ))}
139 </tbody>
140 </table>
141 </div>
142 </section>
143 )}
144 </div>
145 );
146}