Skip to content

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

121 lines6,372 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1import { ChevronRight, FileSearch, KeyRound, Network, PackageSearch, ShieldCheck } from "lucide-react";
2import type { ReactNode } from "react";
3import { Link, redirect } from "react-router";
4
5import type { Route } from "./+types/security-overview";
6import { page } from "../../lib/meta";
7import { ScanSummary, SeverityBadge, SeverityCountsGrid } from "../../components/security";
8import { CARD, SectionHeader } from "../../components/security-suite";
9import { security, securitySuite } from "../../lib/services.server";
10import { getViewer, requireUser, unwrap } from "../../lib/session.server";
11import { requireInsider } from "../../lib/access.server";
12import { legacySecurityTarget, severityCounts, total, worstVulnerabilities } from "../../lib/security-suite";
13
14export function meta({ params, ...args }: Route.MetaArgs) {
15 return page(args, { title: `Security · ${params.owner}/${params.repo} · g1t` });
16}
17
18export async function loader({ params, context, request }: Route.LoaderArgs) {
19 const base = `/${params.owner}/${params.repo}`;
20 // Links from before the sections (git's push refusals among them).
21 const moved = legacySecurityTarget(base, new URL(request.url).searchParams);
22 if (moved) throw redirect(moved);
23 const viewer = getViewer(context) ?? requireUser(context, request);
24 await requireInsider(context, params, "push");
25 const repo = { namespace: params.owner, name: params.repo };
26 const [overview, code] = await Promise.all([security.overview(repo, viewer), securitySuite.codeScanning(repo, viewer)]);
27 return { overview: unwrap(overview), code: code.ok ? code.value : null };
28}
29
30function Card({ to, icon, title, children, footer }: { to: string; icon: ReactNode; title: string; children: ReactNode; footer?: ReactNode }) {
31 return (
32 <Link to={to} className={`${CARD} group flex flex-col p-4 transition-colors hover:border-line-strong`}>
33 <span className="flex items-center gap-2 text-sm font-medium">
34 <span className="text-accent">{icon}</span>
35 {title}
36 <ChevronRight size={14} className="ml-auto text-faint group-hover:text-fg" />
37 </span>
38 <div className="mt-2 grow text-sm text-muted">{children}</div>
39 {footer && <div className="mt-3 text-xs text-faint">{footer}</div>}
40 </Link>
41 );
42}
43
44export default function SecurityOverview({ loaderData, params }: Route.ComponentProps) {
45 const { overview, code } = loaderData;
46 const base = `/${params.owner}/${params.repo}`;
47 const secrets = overview.secretCounts;
48 const codeOpen = code ? severityCounts(code.alerts) : null;
49 // Open alerts of every kind, by severity.
50 const counts = { ...overview.counts };
51 if (codeOpen) for (const key of Object.keys(counts) as (keyof typeof counts)[]) counts[key] += codeOpen[key];
52 const worst = worstVulnerabilities(overview.vulnerabilities, 3);
53 const openVulns = overview.vulnerabilities.filter((vuln) => vuln.state === "open").length;
54 return (
55 <div className="max-w-5xl space-y-8">
56 <SectionHeader
57 title="Security"
58 about="Pushes that add a secret are refused before they land, and the history is scanned. Code scanning reads your tools' SARIF results, every package the lockfiles resolve is checked for known vulnerabilities, and g1t can fix what it finds."
59 />
60 <div>
61 <SeverityCountsGrid counts={counts} />
62 <p className="mt-2 text-xs text-faint">
63 Open alerts of every kind by severity. A secret in the history that looks real counts as critical; blocked pushes and
64 likely test values do not.
65 </p>
66 </div>
67 <div className="grid gap-3 sm:grid-cols-2">
68 <Card to={`${base}/security/secret-scanning`} icon={<KeyRound size={15} />} title="Secret scanning" footer="Push protection is on for every push.">
69 {secrets.open + secrets.blocked === 0 ? (
Merge branch 'worktree-agent-ad7c6d88d93adc817'70 <span className="inline-flex items-center gap-1.5 text-success">
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar71 <ShieldCheck size={14} /> No open secrets
72 </span>
73 ) : (
74 <>
75 <span className="font-medium text-fg">{secrets.open}</span> in the history to rotate,{" "}
76 <span className="font-medium text-fg">{secrets.blocked}</span> stopped at a push
77 </>
78 )}
79 </Card>
80 <Card to={`${base}/security/code-scanning`} icon={<FileSearch size={15} />} title="Code scanning" footer={code?.analyses[0] ? `Last analysis by ${code.analyses[0].tool}` : undefined}>
81 {!code || (code.analyses.length === 0 && code.alerts.length === 0) ? (
82 "Not set up. Add a workflow that uploads SARIF, and results show here and on pull requests."
83 ) : total(codeOpen!) === 0 ? (
Merge branch 'worktree-agent-ad7c6d88d93adc817'84 <span className="inline-flex items-center gap-1.5 text-success">
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar85 <ShieldCheck size={14} /> No open alerts
86 </span>
87 ) : (
88 <>
89 <span className="font-medium text-fg">{total(codeOpen!)}</span> open {total(codeOpen!) === 1 ? "alert" : "alerts"}
90 </>
91 )}
92 </Card>
93 <Card to={`${base}/security/vulnerabilities`} icon={<PackageSearch size={15} />} title="Vulnerabilities">
94 {openVulns === 0 ? (
Merge branch 'worktree-agent-ad7c6d88d93adc817'95 <span className="inline-flex items-center gap-1.5 text-success">
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar96 <ShieldCheck size={14} /> No vulnerable dependencies
97 </span>
98 ) : (
99 <ul className="space-y-1">
100 {worst.map((vuln) => (
101 <li key={vuln.id} className="flex items-center gap-2">
102 <SeverityBadge severity={vuln.severity} />
103 <span className="truncate font-mono text-xs text-fg-soft">
104 {vuln.package}@{vuln.version}
105 </span>
106 </li>
107 ))}
108 {openVulns > worst.length && <li className="text-xs">and {openVulns - worst.length} more</li>}
109 </ul>
110 )}
111 </Card>
112 <Card to={`${base}/security/dependency-graph`} icon={<Network size={15} />} title="Dependency graph">
113 {overview.scan.lockfiles.length === 0
114 ? "No lockfiles found on the default branch."
115 : `${overview.scan.lockfiles.length} ${overview.scan.lockfiles.length === 1 ? "lockfile" : "lockfiles"}, with an SPDX SBOM to download.`}
116 </Card>
117 </div>
118 <ScanSummary scan={overview.scan} />
119 </div>
120 );
121}

This file's history is long; its oldest lines are credited to the oldest commit read.