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.

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