Skip to content

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

136 lines6,904 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 { Download } from "lucide-react";
2import { Link, useSearchParams } from "react-router";
3
4import type { Route } from "./+types/security-graph";
5import { page } from "../../lib/meta";
6import { CARD, FilterSelect, SectionHeader } from "../../components/security-suite";
7import { Badge } from "../../components/ui/badge";
8import { securitySuite } from "../../lib/services.server";
9import { getViewer, requireUser, unwrap } from "../../lib/session.server";
10import { requireInsider } from "../../lib/access.server";
11
12export function meta({ params, ...args }: Route.MetaArgs) {
13 return page(args, { title: `Dependency graph · ${params.owner}/${params.repo} · g1t` });
14}
15
16export async function loader({ params, context, request }: Route.LoaderArgs) {
17 const viewer = getViewer(context) ?? requireUser(context, request);
18 await requireInsider(context, params, "push");
19 return { graph: unwrap(await securitySuite.dependencyGraph({ namespace: params.owner, name: params.repo }, viewer)) };
20}
21
22/** Rows shown at once; the SBOM has them all. */
23const SHOWN = 500;
24
25export default function DependencyGraph({ loaderData, params }: Route.ComponentProps) {
26 const { graph } = loaderData;
27 const base = `/${params.owner}/${params.repo}`;
28 const [search, setSearch] = useSearchParams();
29 const manifest = search.get("manifest") ?? "all";
30 const relationship = search.get("relationship") ?? "all";
31 const query = (search.get("q") ?? "").toLowerCase();
32 const set = (key: string, value: string) => {
33 const next = new URLSearchParams(search);
34 if (value === "all" || !value) next.delete(key);
35 else next.set(key, value);
36 setSearch(next, { replace: true, preventScrollReset: true });
37 };
38 const shown = graph.dependencies.filter(
39 (dep) =>
40 (manifest === "all" || dep.manifest === manifest) &&
41 (relationship === "all" || dep.relationship === relationship) &&
42 (!query || dep.name.toLowerCase().includes(query)),
43 );
44 return (
45 <div className="max-w-5xl space-y-6">
46 <SectionHeader
47 title="Dependency graph"
48 about="Every package the lockfiles on the default branch resolve: direct or transitive where the lockfile says, for development or not, and its license where the lockfile records one. Read again on every push to the default branch, and daily."
49 actions={
50 graph.dependencies.length > 0 ? (
51 <a href={`${base}/security/dependency-graph/sbom.json`} download className="inline-flex items-center gap-1.5 rounded-md border border-line px-3 py-1.5 text-sm hover:border-line-strong">
52 <Download size={14} /> Export SBOM (SPDX)
53 </a>
54 ) : null
55 }
56 />
57 {graph.manifests.length === 0 ? (
58 <p className="rounded-xl border border-dashed border-line px-4 py-6 text-sm text-muted">
59 No lockfiles on the default branch. g1t reads package-lock.json, pnpm-lock.yaml, yarn.lock, Cargo.lock, go.mod, go.sum,
60 requirements.txt and poetry.lock.
61 </p>
62 ) : (
63 <>
64 <ul className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
65 {graph.manifests.map((item) => (
66 <li key={item.path} className={`${CARD} p-3`}>
67 <p className="truncate font-mono text-sm">{item.path}</p>
68 <p className="mt-1 text-xs text-muted">
69 {item.ecosystem} · {item.dependencies} packages{item.direct ? `, ${item.direct} direct` : ""}
70 </p>
71 </li>
72 ))}
73 </ul>
74 <div className="flex flex-wrap items-end gap-2">
75 <label className="flex min-w-0 grow flex-col gap-1 text-xs text-muted sm:max-w-xs">
76 Package
77 <input
78 defaultValue={search.get("q") ?? ""}
79 onChange={(event) => set("q", event.target.value)}
80 placeholder="Filter by name"
81 className="h-8 rounded-md border border-line bg-bg px-2.5 text-[0.8125rem] text-fg outline-none hover:border-line-strong focus:border-accent-dim"
82 />
83 </label>
84 <FilterSelect label="Lockfile" value={manifest} options={[["all", "Every lockfile"], ...graph.manifests.map((item): [string, string] => [item.path, item.path])]} onChange={(value) => set("manifest", value)} />
85 <FilterSelect
86 label="Relationship"
87 value={relationship}
88 options={[["all", "Any"], ["direct", "Direct"], ["transitive", "Transitive"], ["unknown", "Not said"]]}
89 onChange={(value) => set("relationship", value)}
90 />
91 </div>
92 <div className={`${CARD} overflow-x-auto`}>
93 <table className="w-full min-w-[40rem] text-sm">
94 <thead className="text-left text-xs text-muted">
95 <tr className="border-b border-line">
96 <th className="px-4 py-2 font-medium">Package</th>
97 <th className="px-3 py-2 font-medium">Version</th>
98 <th className="px-3 py-2 font-medium">Relationship</th>
99 <th className="px-3 py-2 font-medium">License</th>
100 <th className="px-3 py-2 font-medium">Lockfile</th>
101 </tr>
102 </thead>
103 <tbody>
104 {shown.slice(0, SHOWN).map((dep) => (
105 <tr key={`${dep.manifest}:${dep.name}@${dep.version}`} className="border-b border-line last:border-0">
106 <td className="px-4 py-2">
107 <span className="font-mono text-xs">{dep.name}</span>
108 {dep.vulnerabilities > 0 && (
109 <Link to={`${base}/security/vulnerabilities`} className="ml-2">
110 <Badge tone="danger">
111 {dep.vulnerabilities} {dep.vulnerabilities === 1 ? "vulnerability" : "vulnerabilities"}
112 </Badge>
113 </Link>
114 )}
115 </td>
116 <td className="px-3 py-2 font-mono text-xs">{dep.version}</td>
117 <td className="px-3 py-2 text-xs text-muted">
118 {dep.relationship === "unknown" ? "not said" : dep.relationship}
119 {dep.development ? ", dev" : ""}
120 </td>
121 <td className="px-3 py-2 text-xs text-muted">{dep.license ?? "—"}</td>
122 <td className="px-3 py-2 font-mono text-xs text-faint">{dep.manifest}</td>
123 </tr>
124 ))}
125 </tbody>
126 </table>
127 </div>
128 <p className="text-xs text-faint">
129 {shown.length > SHOWN ? `The first ${SHOWN} of ${shown.length} shown; the SBOM has every package.` : `${shown.length} packages.`}
130 {graph.commit && <> Read at {graph.commit.slice(0, 7)}.</>}
131 </p>
132 </>
133 )}
134 </div>
135 );
136}