flagon-io/g1t

public

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/apps/web/app/routes/workspace/security.tsx

87 lines3,961 bytesCodeBlame
1import { ChevronRight, ShieldCheck } from "lucide-react";
2import { Link, data } from "react-router";
3
4import { SEVERITIES, type SeverityCounts } from "@g1t/contracts";
5
6import type { Route } from "./+types/security";
7import { page } from "../../lib/meta";
8import { SeverityCountsGrid, SeverityCountsInline } from "../../components/security";
9import { TimeAgo } from "../../components/ui";
10import { Badge } from "../../components/ui/badge";
11import { security } from "../../lib/services.server";
12import { getViewer, roleIn, unwrap } from "../../lib/session.server";
13
14export function meta({ params, ...args }: Route.MetaArgs) {
15 return page(args, { title: `Security · ${params.owner} · g1t` });
16}
17
18export async function loader({ params, context }: Route.LoaderArgs) {
19 const viewer = getViewer(context);
20 if (!roleIn(viewer, params.owner)) throw data(null, { status: 404 });
21 const projects = unwrap(await security.workspace(params.owner, viewer));
22 const total = Object.fromEntries(SEVERITIES.map((severity) => [severity, 0])) as SeverityCounts;
23 for (const project of projects) {
24 for (const severity of SEVERITIES) total[severity] += project.counts[severity];
25 }
26 // Most to fix first.
27 projects.sort(
28 (a, b) =>
29 SEVERITIES.reduce((order, severity) => order || b.counts[severity] - a.counts[severity], 0) || a.name.localeCompare(b.name),
30 );
31 return { projects, total };
32}
33
34export default function WorkspaceSecurity({ loaderData, params }: Route.ComponentProps) {
35 const { projects, total } = loaderData;
36 return (
37 <div className="space-y-6">
38 <div>
39 <h2 className="flex items-center gap-2 text-xl font-semibold tracking-tight">
40 <ShieldCheck size={19} className="text-accent" />
41 Security across projects
42 </h2>
43 <p className="mt-1.5 max-w-2xl text-sm text-muted">
44 Open findings in every project of {params.owner}: secrets found in pushes and history, and vulnerable
45 dependencies, each with the upgrade a g1t agent is landing for it.
46 </p>
47 </div>
48 <SeverityCountsGrid counts={total} />
49 {projects.length === 0 ? (
50 <p className="rounded-xl border border-dashed border-line px-4 py-6 text-sm text-muted">
51 No project has been scanned yet. Each one is scanned on its next push to its default branch, or when its Security
52 page is first opened.
53 </p>
54 ) : (
55 <ul className="divide-y divide-line overflow-hidden rounded-xl border border-line bg-surface">
56 {projects.map((project) => (
57 <li key={project.repoId}>
58 <Link
59 to={`/${params.owner}/${project.name}/security`}
60 className="group flex flex-col gap-2 px-4 py-3 transition-colors hover:bg-raised/50 sm:flex-row sm:items-center"
61 >
62 <span className="min-w-0 grow">
63 <span className="flex flex-wrap items-center gap-2">
64 <span className="font-mono text-sm font-medium">{project.name}</span>
65 {!project.upkeep && <Badge>upkeep off</Badge>}
66 </span>
67 <span className="mt-0.5 block text-xs text-faint">
68 {project.secrets} {project.secrets === 1 ? "secret" : "secrets"} · {project.vulnerabilities}{" "}
69 {project.vulnerabilities === 1 ? "vulnerability" : "vulnerabilities"}
70 {project.dependenciesScannedAt && (
71 <>
72 {" "}
73 · read <TimeAgo at={project.dependenciesScannedAt} />
74 </>
75 )}
76 </span>
77 </span>
78 <SeverityCountsInline counts={project.counts} />
79 <ChevronRight size={15} className="hidden shrink-0 text-faint group-hover:text-fg sm:block" />
80 </Link>
81 </li>
82 ))}
83 </ul>
84 )}
85 </div>
86 );
87}