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

265 lines12,238 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.

Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1import { RefreshCw, ShieldCheck } from "lucide-react";
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2import { useFetcher, useSearchParams } from "react-router";
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API3
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily4import type { AlertState, DismissReason } from "@g1t/contracts";
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API5
6import type { Route } from "./+types/security";
7import { page } from "../../lib/meta";
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily8import {
9 type PullInfo,
10 ScanSummary,
11 SecretsList,
12 SeverityCountsGrid,
13 StateFilter,
14 type UpgradeFix,
15 VersionUpdatesCard,
16 VulnerabilityList,
17} from "../../components/security";
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API18import { Switch } from "../../components/ui/switch";
19import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../components/ui/tabs";
20import { security, work } from "../../lib/services.server";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look21import { assertSameOrigin, getViewer, requireUser, unwrap } from "../../lib/session.server";
22import { refusal, requireInsider } from "../../lib/access.server";
23import { whyNot } from "../../lib/access";
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily24import { alertCapability, countByState, parseAlertState, tabOf } from "../../lib/security-alerts";
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API25
26export function meta({ params, ...args }: Route.MetaArgs) {
27 return page(args, { title: `Security · ${params.owner}/${params.repo} · g1t` });
28}
29
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily30/** Upgrade issues and security update pull requests looked up per page, at most. */
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API31const MAX_FIXES = 30;
32
33export async function loader({ params, context, request }: Route.LoaderArgs) {
34 // Git's refusal links here; someone signed out signs in first.
35 const viewer = getViewer(context) ?? requireUser(context, request);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look36 // Findings are for people who can push, public project or not: Write and up.
37 const { access } = await requireInsider(context, params, "push");
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API38 const repo = { namespace: params.owner, name: params.repo };
39 const overview = unwrap(await security.overview(repo, viewer));
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily40 const unique = (numbers: (number | null | undefined)[]) =>
41 [...new Set(numbers.filter((n): n is number => n != null))].slice(0, MAX_FIXES);
42 // Security updates' pull requests, for their live status.
43 const pullNumbers = unique(overview.vulnerabilities.map((vuln) => vuln.update?.pull));
44 // Older upgrade issues, from before g1t opened pull requests itself.
45 const issueNumbers = unique(overview.vulnerabilities.filter((vuln) => !vuln.update).map((vuln) => vuln.issue));
46 const [pullDetails, issueDetails] = await Promise.all([
47 Promise.all(pullNumbers.map((number) => work.getPull(repo, number, viewer).catch(() => null))),
48 Promise.all(issueNumbers.map((number) => work.getIssue(repo, number, viewer).catch(() => null))),
49 ]);
50 const pulls: Record<number, PullInfo> = {};
51 for (const found of pullDetails) {
52 if (!found?.ok) continue;
53 const { pull } = found.value;
54 pulls[pull.number] = { number: pull.number, status: pull.status, title: pull.title };
55 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API56 const fixes: Record<number, UpgradeFix> = {};
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily57 for (const found of issueDetails) {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API58 if (!found?.ok) continue;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily59 const { issue, pulls: issuePulls } = found.value;
60 const latest = issuePulls.at(-1) ?? null;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API61 fixes[issue.number] = {
62 number: issue.number,
63 state: issue.state,
64 resolvedBy: issue.resolvedBy,
65 pull: latest ? { number: latest.number, status: latest.status, agent: latest.runtime === "hosted" ? latest.agent : null } : null,
66 };
67 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily68 return { overview, fixes, pulls, can: access.can };
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API69}
70
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily71const DISMISS_REASONS = new Set<string>([
72 "false_positive",
73 "used_in_tests",
74 "revoked",
75 "wont_fix",
76 "fix_started",
77 "no_bandwidth",
78 "tolerable_risk",
79 "inaccurate",
80 "not_used",
81]);
82
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API83export async function action({ params, context, request }: Route.ActionArgs) {
84 assertSameOrigin(request);
85 const user = requireUser(context, request);
86 const repo = { namespace: params.owner, name: params.repo };
87 const form = await request.formData();
88 const intent = String(form.get("intent") ?? "");
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily89 const id = String(form.get("id") ?? "");
90 // Scanning spends compute (Write); security updates are a setting
91 // (Maintain); a secret alert is dismissed or reopened by Admins, who
92 // manage the repository's secrets, and a dependency alert by Write.
93 const capability =
94 intent === "rescan" ? "run" : intent === "upkeep" ? "manage_settings" : intent === "dismiss" || intent === "reopen" ? alertCapability(id) : null;
95 if (!capability) return { ok: false, error: "Unknown action." };
96 const refused = await refusal(context, params, capability);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look97 if (refused) return { ok: false, error: refused };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily98 if (intent === "dismiss") {
99 const reason = String(form.get("reason") ?? "");
100 if (!DISMISS_REASONS.has(reason)) return { ok: false, error: "Choose a reason." };
101 const comment = String(form.get("comment") ?? "").trim().slice(0, 500);
102 const dismissed = await security.dismiss(user, repo, id, reason as DismissReason, comment);
103 return dismissed.ok ? { ok: true } : { ok: false, error: dismissed.error.message };
104 }
105 if (intent === "reopen") {
106 const reopened = await security.reopen(user, repo, id);
107 return reopened.ok ? { ok: true } : { ok: false, error: reopened.error.message };
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API108 }
109 if (intent === "rescan") {
110 const scanned = await security.rescan(user, repo);
111 return scanned.ok ? { ok: true } : { ok: false, error: scanned.error.message };
112 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily113 const set = await security.setUpkeep(user, repo, form.get("enabled") === "true");
114 return set.ok ? { ok: true } : { ok: false, error: set.error.message };
115}
116
117function Count({ n }: { n: number }) {
118 return n > 0 ? <span className="ml-1.5 rounded-full bg-line px-1.5 py-px text-[0.6875rem] tabular-nums">{n}</span> : null;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API119}
120
121export default function ProjectSecurity({ loaderData, params }: Route.ComponentProps) {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily122 const { overview, fixes, pulls, can } = loaderData;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API123 const base = `/${params.owner}/${params.repo}`;
124 const action = `${base}/security`;
125 const [search, setSearch] = useSearchParams();
126 const rescan = useFetcher<{ ok: boolean; error?: string }>();
127 const upkeep = useFetcher<{ ok: boolean; error?: string }>();
128 const upkeepOn = upkeep.formData ? upkeep.formData.get("enabled") === "true" : overview.upkeep;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily129 const secretCounts = countByState(overview.secrets);
130 const vulnCounts = countByState(overview.vulnerabilities);
131 // A link to one alert (git's push refusal sends one) shows it wherever it stands.
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API132 const focus = search.get("finding");
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily133 const focused = focus
134 ? (overview.secrets.find((secret) => secret.id === focus) ?? overview.vulnerabilities.find((vuln) => vuln.id === focus))
135 : undefined;
136 const tab =
137 search.get("tab") ??
138 (focus ? tabOf(focus) : secretCounts.open > 0 && vulnCounts.open === 0 ? "secrets" : vulnCounts.open > 0 ? "dependencies" : "secrets");
139 const state: AlertState = search.get("state") ? parseAlertState(search.get("state")) : (focused?.state ?? "open");
140 const navigate = (change: (next: URLSearchParams) => void) => {
141 const next = new URLSearchParams(search);
142 change(next);
143 next.delete("finding");
144 setSearch(next, { replace: true, preventScrollReset: true });
145 };
146 const setState = (value: AlertState) =>
147 navigate((next) => (value === "open" ? next.delete("state") : next.set("state", value)));
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API148 return (
149 <div className="max-w-5xl">
150 <div className="flex flex-wrap items-start justify-between gap-4">
151 <div>
152 <h2 className="flex items-center gap-2 text-xl font-semibold tracking-tight">
153 <ShieldCheck size={19} className="text-accent" />
154 Security
155 </h2>
156 <p className="mt-1.5 max-w-2xl text-sm text-muted">
157 Pushes that add a secret are refused before they land, and the history is scanned once. Every package the
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily158 lockfiles resolve is checked for known vulnerabilities, and with security updates on, g1t opens a pull request to
159 upgrade each one that has a fix.
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API160 </p>
161 </div>
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily162 {can.run && (
163 <rescan.Form method="post" action={action}>
164 <input type="hidden" name="intent" value="rescan" />
165 <button
166 type="submit"
167 disabled={rescan.state !== "idle"}
168 className="inline-flex items-center gap-2 rounded-md border border-line px-3 py-1.5 text-sm text-fg/80 transition-colors hover:border-line-strong hover:bg-surface hover:text-fg disabled:opacity-50"
169 >
170 <RefreshCw size={14} className={rescan.state !== "idle" ? "animate-spin" : ""} />
171 {rescan.state !== "idle" ? "Scanning…" : "Re-scan now"}
172 </button>
173 {rescan.data?.error && <p className="mt-1.5 text-xs text-danger">{rescan.data.error}</p>}
174 </rescan.Form>
175 )}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API176 </div>
177
178 <div className="mt-6">
179 <SeverityCountsGrid counts={overview.counts} />
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily180 <p className="mt-2 text-xs text-faint">
181 Open alerts by severity. A secret in the history that looks real counts as critical; blocked pushes and likely test
182 values do not.
183 </p>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API184 </div>
185
186 <div className="mt-4">
187 <ScanSummary scan={overview.scan} />
188 </div>
189
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily190 <Tabs value={tab} onValueChange={(value) => navigate((next) => next.set("tab", value))} className="mt-8">
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API191 <TabsList>
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily192 <TabsTrigger value="secrets">
193 Secrets
194 <Count n={secretCounts.open} />
195 </TabsTrigger>
196 <TabsTrigger value="dependencies">
197 Dependencies
198 <Count n={vulnCounts.open} />
199 </TabsTrigger>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API200 </TabsList>
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily201 <TabsContent value="secrets" className="mt-4">
202 <StateFilter counts={secretCounts} value={state} onChange={setState} />
203 <div className="mt-3">
204 <SecretsList
205 secrets={overview.secrets.filter((secret) => secret.state === state)}
206 state={state}
207 activity={overview.activity}
208 base={base}
209 action={action}
210 focus={focus}
211 canDismiss={can.manage_integrations}
212 />
213 </div>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API214 <p className="mt-3 text-xs text-faint">
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily215 Not a real secret, such as a test fixture? Add <code>g1t:allow-secret</code> in a comment on its line, or dismiss the
216 alert here. Dismissing takes the Admin role and is recorded with your name and reason.
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API217 </p>
218 </TabsContent>
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily219 <TabsContent value="dependencies" className="mt-4">
220 <StateFilter counts={vulnCounts} value={state} onChange={setState} />
221 <div className="mt-3">
222 <VulnerabilityList
223 vulnerabilities={overview.vulnerabilities.filter((vuln) => vuln.state === state)}
224 state={state}
225 activity={overview.activity}
226 fixes={fixes}
227 pulls={pulls}
228 upkeep={overview.upkeep}
229 base={base}
230 action={action}
231 focus={focus}
232 canDismiss={can.push}
233 />
234 </div>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API235 </TabsContent>
236 </Tabs>
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily237
238 <section className="mt-10" aria-labelledby="security-settings">
239 <h3 id="security-settings" className="text-base font-semibold tracking-tight">
240 Settings
241 </h3>
242 <div className="mt-3 space-y-3">
243 <label className="flex cursor-pointer items-start justify-between gap-4 rounded-xl border border-line bg-surface p-4 transition-colors hover:border-line-strong">
244 <span className="min-w-0">
245 <span className="block text-sm font-medium">Security updates</span>
246 <span className="mt-1 block text-sm text-muted">
247 Open a pull request to upgrade each vulnerable dependency that has a fix. It lands through your branch's
248 required checks.
249 </span>
250 {upkeep.data?.error && <span className="mt-1 block text-xs text-danger">{upkeep.data.error}</span>}
251 </span>
252 <Switch
253 className="mt-0.5"
254 checked={upkeepOn}
255 disabled={upkeep.state !== "idle" || !can.manage_settings}
256 title={whyNot(can, "manage_settings")}
257 onCheckedChange={(checked) => upkeep.submit({ intent: "upkeep", enabled: String(checked) }, { method: "post", action })}
258 />
259 </label>
260 <VersionUpdatesCard state={overview.versionUpdates} />
261 </div>
262 </section>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API263 </div>
264 );
265}