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 bar | 1 | import { ChevronRight, FileSearch, KeyRound, Network, PackageSearch, ShieldCheck } from "lucide-react"; |
| 2 | import type { ReactNode } from "react"; | |
| 3 | import { Link, redirect } from "react-router"; | |
| 4 | ||
| 5 | import type { Route } from "./+types/security-overview"; | |
| 6 | import { page } from "../../lib/meta"; | |
| 7 | import { ScanSummary, SeverityBadge, SeverityCountsGrid } from "../../components/security"; | |
| 8 | import { CARD, SectionHeader } from "../../components/security-suite"; | |
| 9 | import { security, securitySuite } from "../../lib/services.server"; | |
| 10 | import { getViewer, requireUser, unwrap } from "../../lib/session.server"; | |
| 11 | import { requireInsider } from "../../lib/access.server"; | |
| 12 | import { legacySecurityTarget, severityCounts, total, worstVulnerabilities } from "../../lib/security-suite"; | |
| 13 | ||
| 14 | export function meta({ params, ...args }: Route.MetaArgs) { | |
| 15 | return page(args, { title: `Security · ${params.owner}/${params.repo} · g1t` }); | |
| 16 | } | |
| 17 | ||
| 18 | export 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 | ||
| 30 | function 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 | ||
| 44 | export 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 bar | 71 | <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 bar | 85 | <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 bar | 96 | <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.