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.

g1t.sh
155 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 label="Severity" value={filters.severity ?? "all"} options={SEVERITY_OPTIONS} onChange={(value) => set("severity", value === "all" ? null : (value as Severity))} />
94 <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)} />
95 </div>
96 </div>
97 {shown.length === 0 ? (
98 <Card asChild tone="plain" className="border-dashed px-4 py-6 text-sm text-muted">
99 <p>No {filters.state} alerts match.</p>
100 </Card>
101 ) : (
102 <ul className={LIST}>
103 {shown.map((alert) => (
104 <CodeAlertItem key={alert.id} alert={alert} base={base} />
105 ))}
106 </ul>
107 )}
108 </section>
109 )}
110 {scanning.analyses.length > 0 && (
111 <section>
112 <h3 className="text-base font-semibold tracking-tight">Recent analyses</h3>
113 <div className={`${CARD} mt-2`}>
114 <Table className="min-w-[36rem]">
115 <TableHeader>
116 <TableRow>
117 <TableHead className="px-4">Tool</TableHead>
118 <TableHead>Ref</TableHead>
119 <TableHead>Commit</TableHead>
120 <TableHead className="text-right">Results</TableHead>
121 <TableHead className="text-right">New</TableHead>
122 <TableHead className="text-right">Fixed</TableHead>
123 <TableHead>When</TableHead>
124 </TableRow>
125 </TableHeader>
126 <TableBody className="tabular-nums">
127 {scanning.analyses.slice(0, 15).map((analysis) => (
128 <TableRow key={analysis.id}>
129 <TableCell className="px-4">{analysis.tool}</TableCell>
130 <TableCell className="font-mono text-xs">
131 {analysis.pull != null ? (
132 <a href={`${base}/security/pulls/${analysis.pull}`} className="hover:underline">
133 #{analysis.pull}
134 </a>
135 ) : (
136 analysis.gitRef.replace("refs/heads/", "")
137 )}
138 </TableCell>
139 <TableCell className="font-mono text-xs">{analysis.commitSha.slice(0, 7)}</TableCell>
140 <TableCell className="text-right">{analysis.results}</TableCell>
141 <TableCell className="text-right">{analysis.newAlerts}</TableCell>
142 <TableCell className="text-right">{analysis.fixedAlerts}</TableCell>
143 <TableCell className="text-xs text-muted">
144 <TimeAgo at={analysis.createdAt} />
145 </TableCell>
146 </TableRow>
147 ))}
148 </TableBody>
149 </Table>
150 </div>
151 </section>
152 )}
153 </div>
154 );
155}