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