g1t/apps/web/app/components/security.tsx

950 lines35,584 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 API1/**
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2 * Security, as a project's page shows it: open alerts by severity, the
3 * secrets found in pushes and history, vulnerable dependencies with the
4 * security update g1t opened for each, and what happened to every alert.
5 * The page posts the intents in `routes/repo/security.tsx`'s action.
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API6 */
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily7import {
8 Bot,
9 CircleAlert,
10 CircleCheck,
11 CircleDot,
12 CircleSlash,
13 ExternalLink,
14 FileWarning,
15 GitBranch,
16 GitMerge,
17 GitPullRequest,
18 GitPullRequestClosed,
19 History,
20 KeyRound,
21 Loader,
22 Package,
23 ShieldAlert,
24 ShieldCheck,
25} from "lucide-react";
26import { type ReactNode, useEffect, useRef, useState } from "react";
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API27import { Link, useFetcher } from "react-router";
28
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily29import {
30 type AlertActivity,
31 type AlertState,
32 DEPENDENCY_DISMISS_REASONS,
33 type DismissReason,
34 SECRET_DISMISS_REASONS,
35 SEVERITIES,
36 type PullStatus,
37 type SecretFinding,
38 type SecurityUpdate,
39 type Severity,
40 type SeverityCounts,
41 type VersionUpdatesState,
42 type Vulnerability,
43 dismissLabel,
44} from "@g1t/contracts";
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API45
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily46import {
47 type ActivityEntry,
48 type PackageGroup,
49 UPDATE_STATES,
50 alertActivity,
51 groupByPackage,
52 highestFix,
53 latestUpdate,
54 splitSecrets,
55 worstSeverity,
56} from "../lib/security-alerts";
57import { Avatar, TimeAgo } from "./ui";
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API58import { Badge, type BadgeTone } from "./ui/badge";
59import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "./ui/dialog";
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily60import { RadioGroup, RadioOption } from "./ui/radio-group";
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API61
62type Done = { ok: boolean; error?: string } | undefined;
63
64const SEVERITY: Record<Severity, { label: string; tone: BadgeTone }> = {
65 critical: { label: "Critical", tone: "danger" },
66 high: { label: "High", tone: "warn" },
67 medium: { label: "Medium", tone: "merged" },
68 low: { label: "Low", tone: "info" },
69 unknown: { label: "Unrated", tone: "neutral" },
70};
71
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily72/** Where the docs explain `.g1t/dependencies.yml`. */
73export const VERSION_UPDATES_DOCS = "https://docs.g1t.sh/guides/security/#version-updates";
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API74
75export function SeverityBadge({ severity }: { severity: Severity }) {
76 return <Badge tone={SEVERITY[severity].tone}>{SEVERITY[severity].label}</Badge>;
77}
78
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily79/** Open alerts by severity, one tile each. */
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API80export function SeverityCountsGrid({ counts }: { counts: SeverityCounts }) {
81 return (
82 <div className="grid grid-cols-2 gap-3 sm:grid-cols-5">
83 {SEVERITIES.map((severity) => (
84 <div key={severity} className="rounded-xl border border-line bg-surface px-4 py-3">
85 <p className="text-xs text-muted">{SEVERITY[severity].label}</p>
86 <p className={`mt-1 text-2xl font-semibold tabular-nums ${counts[severity] > 0 && severity === "critical" ? "text-danger" : ""}`}>
87 {counts[severity]}
88 </p>
89 </div>
90 ))}
91 </div>
92 );
93}
94
95/** A compact row of severity counts, for a list of projects. */
96export function SeverityCountsInline({ counts }: { counts: SeverityCounts }) {
97 const shown = SEVERITIES.filter((severity) => counts[severity] > 0);
98 if (shown.length === 0) {
99 return (
100 <span className="inline-flex items-center gap-1 text-xs text-accent">
101 <ShieldCheck size={13} />
102 Nothing open
103 </span>
104 );
105 }
106 return (
107 <span className="flex flex-wrap gap-1.5">
108 {shown.map((severity) => (
109 <Badge key={severity} tone={SEVERITY[severity].tone}>
110 {counts[severity]} {SEVERITY[severity].label.toLowerCase()}
111 </Badge>
112 ))}
113 </span>
114 );
115}
116
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily117const STATE_FILTERS: { state: AlertState; label: string; icon: ReactNode }[] = [
118 { state: "open", label: "Open", icon: <CircleDot size={14} /> },
119 { state: "dismissed", label: "Dismissed", icon: <CircleSlash size={14} /> },
120 { state: "fixed", label: "Fixed", icon: <CircleCheck size={14} /> },
121];
122
123/** Open, Dismissed and Fixed, with how many alerts each holds. */
124export function StateFilter({
125 counts,
126 value,
127 onChange,
128}: {
129 counts: Record<AlertState, number>;
130 value: AlertState;
131 onChange: (state: AlertState) => void;
132}) {
133 return (
134 <div role="group" aria-label="Filter alerts" className="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm">
135 {STATE_FILTERS.map(({ state, label, icon }) => (
136 <button
137 key={state}
138 type="button"
139 aria-pressed={value === state}
140 onClick={() => onChange(state)}
141 className={`inline-flex items-center gap-1.5 transition-colors ${
142 value === state ? "font-medium text-fg" : "text-muted hover:text-fg"
143 }`}
144 >
145 {icon}
146 {counts[state]} {label}
147 </button>
148 ))}
149 </div>
150 );
151}
152
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API153const TEXTAREA =
154 "w-full rounded-md border border-line bg-bg px-3 py-2 text-sm outline-none transition-colors placeholder:text-faint hover:border-line-strong focus:border-accent-dim";
155
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily156const SMALL_BUTTON =
157 "rounded-md border border-line px-2.5 py-1 text-xs font-medium text-muted transition-colors hover:border-line-strong hover:text-fg disabled:opacity-50";
158
159/** Dismiss an alert with one of `reasons` and an optional comment. */
160function DismissDialog({
161 id,
162 title,
163 detail,
164 reasons,
165 note,
166 action,
167 defaultReason,
168 trigger = "Dismiss",
169}: {
170 id: string;
171 title: string;
172 /** What the alert is, in a line of code type. */
173 detail: string;
174 reasons: { reason: DismissReason; label: string; about: string }[];
175 note?: string;
176 action: string;
177 defaultReason?: DismissReason;
178 trigger?: string;
179}) {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API180 const fetcher = useFetcher<Done>();
181 const [open, setOpen] = useState(false);
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily182 const [reason, setReason] = useState<string>(defaultReason ?? "");
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API183 useEffect(() => {
184 if (fetcher.state === "idle" && fetcher.data?.ok) setOpen(false);
185 }, [fetcher.state, fetcher.data]);
186 return (
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily187 <Dialog
188 open={open}
189 onOpenChange={(next) => {
190 setOpen(next);
191 if (next) setReason(defaultReason ?? "");
192 }}
193 >
194 <DialogTrigger className={SMALL_BUTTON}>{trigger}</DialogTrigger>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API195 <DialogContent>
196 <DialogHeader>
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily197 <DialogTitle>{title}</DialogTitle>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API198 <DialogDescription>
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily199 Say why it can stay. The alert keeps your name, the reason and your comment, and anyone with access can reopen it.
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API200 </DialogDescription>
201 </DialogHeader>
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily202 <fetcher.Form method="post" action={action} className="space-y-4">
203 <input type="hidden" name="intent" value="dismiss" />
204 <input type="hidden" name="id" value={id} />
205 <p className="font-mono text-xs break-all text-muted">{detail}</p>
206 <fieldset>
207 <legend className="text-xs font-medium text-muted">Reason</legend>
208 <RadioGroup name="reason" value={reason} onValueChange={setReason} required className="mt-2 gap-3">
209 {reasons.map((option) => (
210 <RadioOption key={option.reason} value={option.reason} label={option.label} description={option.about} />
211 ))}
212 </RadioGroup>
213 </fieldset>
214 {note && <p className="rounded-md border border-warn/30 bg-warn/5 px-3 py-2 text-xs text-warn">{note}</p>}
215 <label className="block">
216 <span className="mb-1.5 block text-xs font-medium text-muted">Comment (optional)</span>
217 <textarea name="comment" rows={3} maxLength={500} placeholder="What someone reading this later should know." className={TEXTAREA} />
218 </label>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API219 <div className="flex justify-end">
220 <button
221 type="submit"
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily222 disabled={fetcher.state !== "idle" || !reason}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API223 className="rounded-md bg-fg px-3.5 py-2 text-sm font-medium text-bg hover:bg-white disabled:opacity-50"
224 >
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily225 {fetcher.state !== "idle" ? "Dismissing…" : "Dismiss alert"}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API226 </button>
227 </div>
228 {fetcher.data?.error && <p className="text-sm text-danger">{fetcher.data.error}</p>}
229 </fetcher.Form>
230 </DialogContent>
231 </Dialog>
232 );
233}
234
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily235function ReopenButton({ id, action }: { id: string; action: string }) {
236 const fetcher = useFetcher<Done>();
237 return (
238 <span className="flex flex-col items-end gap-1">
239 <button
240 type="button"
241 disabled={fetcher.state !== "idle"}
242 onClick={() => fetcher.submit({ intent: "reopen", id }, { method: "post", action })}
243 className={SMALL_BUTTON}
244 >
245 {fetcher.state !== "idle" ? "Reopening…" : "Reopen"}
246 </button>
247 {fetcher.data?.error && <span className="text-xs text-danger">{fetcher.data.error}</span>}
248 </span>
249 );
250}
251
252/** What happened to an alert, oldest first, folded away until asked for. */
253function ActivityLog({ entries, base, open }: { entries: ActivityEntry[]; base: string; open?: boolean }) {
254 if (entries.length === 0) return null;
255 return (
256 <details className="group mt-2" open={open}>
257 <summary className="inline-flex cursor-pointer list-none items-center gap-1.5 text-xs text-muted hover:text-fg [&::-webkit-details-marker]:hidden">
258 <History size={12} />
259 <span className="group-open:hidden">Show activity ({entries.length})</span>
260 <span className="hidden group-open:inline">Hide activity</span>
261 </summary>
262 <ol className="mt-2 space-y-2 border-l border-line pl-3">
263 {entries.map((entry) => (
264 <li key={entry.key} className="text-xs">
265 <p className="flex flex-wrap items-center gap-x-1.5 gap-y-1 text-muted">
266 {entry.actor ? (
267 <>
268 <Avatar name={entry.actor} size={16} />
269 <span className="font-medium text-fg">{entry.actor}</span>
270 </>
271 ) : (
272 <CircleDot size={12} className="text-faint" />
273 )}
274 <span>{entry.text}</span>
275 {entry.ref && (
276 <Link
277 to={`${base}/${entry.ref.kind === "pull" ? "pull" : "issues"}/${entry.ref.number}`}
278 className="font-medium text-fg-soft hover:text-fg hover:underline"
279 >
280 #{entry.ref.number}
281 </Link>
282 )}
283 {entry.reason && <Badge>{dismissLabel(entry.reason)}</Badge>}
284 <span className="text-faint">
285 <TimeAgo at={entry.at} />
286 </span>
287 </p>
288 {entry.comment && <p className="mt-1 text-fg-soft wrap-anywhere">“{entry.comment}”</p>}
289 </li>
290 ))}
291 </ol>
292 </details>
293 );
294}
295
296function useFocus<T extends HTMLElement>(focused: boolean) {
297 const ref = useRef<T>(null);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API298 useEffect(() => {
299 if (focused) ref.current?.scrollIntoView({ block: "center" });
300 }, [focused]);
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily301 return ref;
302}
303
304const FOCUSED = "bg-accent/5 ring-1 ring-accent/40 ring-inset";
305
306function secretBadge(finding: SecretFinding): { label: string; tone: BadgeTone; about: string } {
307 if (finding.state === "dismissed") {
308 return {
309 label: "Dismissed",
310 tone: "neutral",
311 about: finding.status === "allowed" ? "Pushes carrying it go through." : "Dismissed.",
312 };
313 }
314 if (finding.state === "fixed") return { label: "Revoked", tone: "accent", about: "Revoked or rotated." };
315 if (finding.status === "blocked") {
316 return { label: "Push blocked", tone: finding.testValue ? "neutral" : "warn", about: "A push carrying it was refused, so it never landed." };
317 }
318 return {
319 label: "In history",
320 tone: finding.testValue ? "neutral" : "danger",
321 about: "In the repository's history. Rotate it with whoever issued it, then dismiss it as revoked.",
322 };
323}
324
325function SecretItem({
326 finding,
327 activity,
328 base,
329 action,
330 focused,
331 canDismiss,
332}: {
333 finding: SecretFinding;
334 activity: AlertActivity[];
335 base: string;
336 action: string;
337 focused: boolean;
338 canDismiss: boolean;
339}) {
340 const ref = useFocus<HTMLLIElement>(focused);
341 const badge = secretBadge(finding);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API342 const landed = finding.source === "history" || finding.status === "open";
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily343 const where = `${finding.path}:${finding.line}`;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API344 return (
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily345 <li ref={ref} id={finding.id} className={`flex flex-col gap-3 px-4 py-3 sm:flex-row sm:items-start ${focused ? FOCUSED : ""}`}>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API346 <KeyRound size={15} className="mt-0.5 hidden shrink-0 text-muted sm:block" />
347 <div className="min-w-0 grow">
348 <div className="flex flex-wrap items-center gap-2">
349 <span className="text-sm font-medium first-letter:uppercase">{finding.label}</span>
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily350 <Badge tone={badge.tone} title={badge.about}>
351 {badge.label}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API352 </Badge>
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily353 {finding.testValue && <Badge title={finding.testValue}>Likely test value</Badge>}
354 {finding.state === "dismissed" && finding.dismissedReason && <Badge>{dismissLabel(finding.dismissedReason)}</Badge>}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API355 </div>
356 <p className="mt-1 truncate font-mono text-xs">
357 {landed ? (
358 <Link to={`${base}/blob/${finding.commit}/${finding.path}#L${finding.line}`} className="text-fg-soft hover:text-fg hover:underline">
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily359 {where}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API360 </Link>
361 ) : (
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily362 <span className="text-fg-soft">{where}</span>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API363 )}
364 <span className="text-faint"> · {finding.preview}</span>
365 </p>
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily366 {finding.testValue && <p className="mt-1 text-xs text-muted">{finding.testValue}</p>}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API367 <p className="mt-1.5 flex flex-wrap gap-x-3 gap-y-1 text-xs text-faint">
368 <span>
369 {finding.source === "push" ? "in a push" : "in history"}
370 {finding.foundBy && <> by {finding.foundBy}</>}, commit{" "}
371 {landed ? (
372 <Link to={`${base}/commit/${finding.commit}`} className="font-mono hover:text-fg">
373 {finding.commit.slice(0, 7)}
374 </Link>
375 ) : (
376 <span className="font-mono">{finding.commit.slice(0, 7)}</span>
377 )}
378 </span>
379 <span>
380 found <TimeAgo at={finding.foundAt} />
381 </span>
382 </p>
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily383 <ActivityLog entries={alertActivity(finding, activity)} base={base} open={focused && finding.state !== "open"} />
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API384 </div>
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily385 {canDismiss && (
386 <div className="flex shrink-0 items-center gap-1.5">
387 {finding.state === "open" ? (
388 <DismissDialog
389 id={finding.id}
390 title={`Dismiss ${finding.label}`}
391 detail={`${where} · ${finding.preview}`}
392 reasons={SECRET_DISMISS_REASONS}
393 note={
394 finding.status === "blocked" && !finding.testValue
395 ? "Dismissing it lets the same push through, unless you dismiss it as revoked."
396 : undefined
397 }
398 action={action}
399 />
400 ) : (
401 <ReopenButton id={finding.id} action={action} />
402 )}
403 </div>
404 )}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API405 </li>
406 );
407}
408
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily409function Empty({ title, children }: { title: string; children: ReactNode }) {
410 return (
411 <div className="rounded-xl border border-dashed border-line px-6 py-10 text-center">
412 <ShieldCheck size={22} className="mx-auto text-accent" />
413 <p className="mt-2 font-medium">{title}</p>
414 <p className="mt-1 text-sm text-muted">{children}</p>
415 </div>
416 );
417}
418
419const LIST = "divide-y divide-line overflow-hidden rounded-xl border border-line bg-surface";
420
421/** The secret alerts in one state: on Open, real ones first, then likely test values. */
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API422export function SecretsList({
423 secrets,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily424 state,
425 activity,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API426 base,
427 action,
428 focus,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily429 canDismiss,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API430}: {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily431 /** Already filtered to `state`. */
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API432 secrets: SecretFinding[];
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily433 state: AlertState;
434 activity: AlertActivity[];
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API435 base: string;
436 action: string;
437 focus: string | null;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily438 /** Whether the viewer may dismiss and reopen secret alerts (Admin). */
439 canDismiss: boolean;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API440}) {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily441 const item = (finding: SecretFinding) => (
442 <SecretItem
443 key={finding.id}
444 finding={finding}
445 activity={activity}
446 base={base}
447 action={action}
448 focused={finding.id === focus}
449 canDismiss={canDismiss}
450 />
451 );
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API452 if (secrets.length === 0) {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily453 return state === "open" ? (
454 <Empty title="No open secret alerts">
455 Pushes that add a key or a token are refused before they land, and the history is scanned once in the background.
456 </Empty>
457 ) : (
458 <Empty title={state === "dismissed" ? "No dismissed secret alerts" : "No revoked secrets"}>
459 {state === "dismissed"
460 ? "Alerts someone dismissed as a false positive, a test value or accepted are listed here."
461 : "Secrets dismissed as revoked are listed here."}
462 </Empty>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API463 );
464 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily465 if (state !== "open") return <ul className={LIST}>{secrets.map(item)}</ul>;
466 const { real, tests } = splitSecrets(secrets);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API467 return (
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily468 <div className="space-y-5">
469 {real.length > 0 ? (
470 <ul className={LIST}>{real.map(item)}</ul>
471 ) : (
472 <Empty title="No secrets that look real">Only likely test values are open, and they never block a push.</Empty>
473 )}
474 {tests.length > 0 && (
475 <section>
476 <h4 className="text-sm font-medium">Likely test values</h4>
477 <p className="mt-0.5 mb-2 text-xs text-muted">
478 These look made for tests or documentation. They never block a push and are not counted as critical; dismiss them
479 to clear the list.
480 </p>
481 <ul className={LIST}>{tests.map(item)}</ul>
482 </section>
483 )}
484 </div>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API485 );
486}
487
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily488/** Where the legacy upgrade issue for a package stands, as the page loads it. */
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API489export type UpgradeFix = {
490 number: number;
491 state: "open" | "closed";
492 /** The newest pull request for the issue, if any. */
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily493 pull: { number: number; status: PullStatus; agent: string | null } | null;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API494 resolvedBy: number | null;
495};
496
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily497/** A security update's pull request as the page loads it. */
498export type PullInfo = { number: number; status: PullStatus; title: string };
499
500function FixLink({ issue, fix, base }: { issue: number; fix: UpgradeFix | undefined; base: string }) {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API501 const pull = fix?.pull;
502 return (
503 <span className="flex flex-wrap items-center gap-x-2 gap-y-1 text-xs">
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily504 <span className="text-muted">Upgrade issue</span>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API505 <Link to={`${base}/issues/${issue}`} className="inline-flex items-center gap-1 text-fg-soft hover:text-fg">
506 {fix?.state === "closed" ? <CircleCheck size={12} className="text-merged" /> : <CircleDot size={12} className="text-accent" />}#{issue}
507 </Link>
508 {pull && (
509 <Link to={`${base}/pull/${pull.number}`} className="inline-flex items-center gap-1 text-muted hover:text-fg">
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily510 {pull.agent ? <Bot size={12} /> : <GitPullRequest size={12} />}#{pull.number} {pull.status === "draft" ? "in progress" : pull.status}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API511 </Link>
512 )}
513 </span>
514 );
515}
516
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily517const PULL_ICON: Record<PullStatus, ReactNode> = {
518 draft: <GitPullRequest size={13} className="text-muted" />,
519 open: <GitPullRequest size={13} className="text-accent" />,
520 merged: <GitMerge size={13} className="text-merged" />,
521 closed: <GitPullRequestClosed size={13} className="text-danger" />,
522};
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API523
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily524/** Where g1t's security update for a package stands. */
525function UpdateStatus({ update, name, pulls, base }: { update: SecurityUpdate; name: string; pulls: Record<number, PullInfo>; base: string }) {
526 const meta = UPDATE_STATES[update.state];
527 const pull = update.pull != null ? pulls[update.pull] : undefined;
528 const about: Record<SecurityUpdate["state"], ReactNode> = {
529 requested: <>A sandbox is raising {name} to {update.target}.</>,
530 open: <>It raises {name} to {update.target} and lands through your branch's required checks.</>,
531 merged: <>{name} was raised to {update.target}.</>,
532 closed: <>The pull request was closed without merging.</>,
533 superseded: <>A newer update replaced it, or the package is no longer vulnerable, so g1t closed it.</>,
534 needs_code: (
535 <>
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent536 Raising {name} to {update.target} needs code changes, so g1t opened an issue to make them
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily537 {update.issue != null && (
538 <>
539 :{" "}
540 <Link to={`${base}/issues/${update.issue}`} className="font-medium text-fg-soft hover:text-fg hover:underline">
541 #{update.issue}
542 </Link>
543 </>
544 )}
545 .
546 </>
547 ),
548 failed: <>{update.error ?? "g1t could not make the change."}</>,
549 };
550 return (
551 <div className="mt-2.5 rounded-lg border border-line bg-bg/40 px-3 py-2 text-xs sm:ml-7">
552 <div className="flex flex-wrap items-center gap-x-2.5 gap-y-1.5">
553 {update.state === "requested" ? (
554 <Loader size={13} className="animate-spin text-info motion-reduce:animate-none" />
555 ) : update.state === "failed" || update.state === "needs_code" ? (
556 <CircleAlert size={13} className={update.state === "failed" ? "text-danger" : "text-warn"} />
557 ) : (
558 PULL_ICON[pull?.status ?? (update.state === "merged" ? "merged" : update.state === "open" ? "open" : "closed")]
559 )}
560 <Badge tone={meta.tone}>{meta.label}</Badge>
561 {update.pull != null && (
562 <Link to={`${base}/pull/${update.pull}`} className="font-medium text-fg-soft hover:text-fg hover:underline">
563 #{update.pull}
564 {pull && <span className="font-normal text-muted"> {pull.status === "draft" ? "draft" : pull.status}</span>}
565 </Link>
566 )}
567 {update.branch && (
568 <span className="inline-flex min-w-0 items-center gap-1 font-mono text-muted">
569 <GitBranch size={12} className="shrink-0" />
570 <span className="truncate">{update.branch}</span>
571 </span>
572 )}
573 <span className="text-faint">
574 <TimeAgo at={update.updatedAt} />
575 </span>
576 </div>
577 <p className={`mt-1 ${update.state === "failed" ? "text-danger" : "text-muted"} wrap-anywhere`}>{about[update.state]}</p>
578 </div>
579 );
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API580}
581
582export function VulnerabilityList({
583 vulnerabilities,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily584 state,
585 activity,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API586 fixes,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily587 pulls,
588 upkeep,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API589 base,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily590 action,
591 focus,
592 canDismiss,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API593}: {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily594 /** Already filtered to `state`. */
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API595 vulnerabilities: Vulnerability[];
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily596 state: AlertState;
597 activity: AlertActivity[];
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API598 fixes: Record<number, UpgradeFix>;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily599 pulls: Record<number, PullInfo>;
600 /** Whether security updates are on. */
601 upkeep: boolean;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API602 base: string;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily603 action: string;
604 focus: string | null;
605 /** Whether the viewer may dismiss and reopen dependency alerts (Write). */
606 canDismiss: boolean;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API607}) {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily608 const packages = groupByPackage(vulnerabilities);
609 if (packages.length === 0) {
610 return state === "open" ? (
611 <Empty title="No known vulnerabilities">
612 Every package the lockfiles resolve is checked against the OSV database on each push to the default branch, and daily.
613 </Empty>
614 ) : (
615 <Empty title={state === "dismissed" ? "No dismissed dependency alerts" : "Nothing fixed yet"}>
616 {state === "dismissed"
617 ? "Alerts someone dismissed, with their reason, are listed here."
618 : "Alerts whose package was upgraded, or is no longer vulnerable, are listed here."}
619 </Empty>
620 );
621 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API622 return (
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily623 <ul className={LIST}>
624 {packages.map((group) => (
625 <PackageItem
626 key={group.key}
627 group={group}
628 activity={activity}
629 fixes={fixes}
630 pulls={pulls}
631 upkeep={upkeep}
632 base={base}
633 action={action}
634 focus={focus}
635 canDismiss={canDismiss}
636 />
637 ))}
638 </ul>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API639 );
640}
641
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily642function PackageItem({
643 group,
644 activity,
645 fixes,
646 pulls,
647 upkeep,
648 base,
649 action,
650 focus,
651 canDismiss,
652}: {
653 group: PackageGroup;
654 activity: AlertActivity[];
655 fixes: Record<number, UpgradeFix>;
656 pulls: Record<number, PullInfo>;
657 upkeep: boolean;
658 base: string;
659 action: string;
660 focus: string | null;
661 canDismiss: boolean;
662}) {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API663 const versions = [...new Set(group.vulns.map((vuln) => vuln.version))];
664 const manifests = [...new Set(group.vulns.map((vuln) => vuln.manifest))];
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily665 const target = highestFix(group.vulns);
666 const update = latestUpdate(group.vulns);
667 const issue = update ? null : (group.vulns.find((vuln) => vuln.issue != null)?.issue ?? null);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API668 return (
669 <li className="px-4 py-3">
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily670 <div className="flex items-start gap-3">
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API671 <Package size={15} className="mt-0.5 hidden shrink-0 text-muted sm:block" />
672 <div className="min-w-0 grow">
673 <div className="flex flex-wrap items-center gap-2">
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily674 <span className="font-mono text-sm font-medium break-all">{group.name}</span>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API675 <span className="font-mono text-xs text-muted">{versions.join(", ")}</span>
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily676 <Badge>{group.ecosystem}</Badge>
677 <SeverityBadge severity={worstSeverity(group.vulns)} />
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API678 </div>
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily679 <p className="mt-1 text-xs text-faint wrap-anywhere">
680 {target ? <>Fixed in {target}</> : "No patched version"} · locked in{" "}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API681 <span className="font-mono">{manifests.join(", ")}</span>
682 </p>
683 </div>
684 </div>
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily685 {update && <UpdateStatus update={update} name={group.name} pulls={pulls} base={base} />}
686 {issue != null && (
687 <div className="mt-2 sm:ml-7">
688 <FixLink issue={issue} fix={fixes[issue]} base={base} />
689 </div>
690 )}
691 <ul className="mt-2.5 space-y-px overflow-hidden rounded-lg border border-line sm:ml-7">
692 {group.vulns.map((vuln) => (
693 <AdvisoryItem
694 key={vuln.id}
695 vuln={vuln}
696 showManifest={manifests.length > 1}
697 activity={activity}
698 upkeep={upkeep}
699 base={base}
700 action={action}
701 focused={vuln.id === focus}
702 canDismiss={canDismiss}
703 />
704 ))}
705 </ul>
706 </li>
707 );
708}
709
710function AdvisoryItem({
711 vuln,
712 showManifest,
713 activity,
714 upkeep,
715 base,
716 action,
717 focused,
718 canDismiss,
719}: {
720 vuln: Vulnerability;
721 showManifest: boolean;
722 activity: AlertActivity[];
723 upkeep: boolean;
724 base: string;
725 action: string;
726 focused: boolean;
727 canDismiss: boolean;
728}) {
729 const ref = useFocus<HTMLLIElement>(focused);
730 const osv = `https://osv.dev/vulnerability/${vuln.osvId}`;
731 const dismiss = (reason?: DismissReason, trigger?: string) => (
732 <DismissDialog
733 id={vuln.id}
734 title={`Dismiss ${vuln.advisory}`}
735 detail={`${vuln.package} ${vuln.version} · ${vuln.manifest}`}
736 reasons={DEPENDENCY_DISMISS_REASONS}
737 action={action}
738 defaultReason={reason}
739 trigger={trigger}
740 />
741 );
742 return (
743 <li ref={ref} id={vuln.id} className={`bg-bg/30 px-3 py-2.5 not-first:border-t not-first:border-line ${focused ? FOCUSED : ""}`}>
744 <div className="flex flex-col gap-2 sm:flex-row sm:items-start">
745 <div className="min-w-0 grow text-xs">
746 <p className="flex flex-wrap items-center gap-x-2 gap-y-1">
747 <a href={osv} target="_blank" rel="noreferrer" className="inline-flex items-center gap-1 font-mono text-fg-soft hover:text-fg">
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API748 {vuln.advisory}
749 <ExternalLink size={10} />
750 </a>
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily751 <SeverityBadge severity={vuln.severity} />
752 {vuln.fixedVersion && <span className="text-faint">fixed in {vuln.fixedVersion}</span>}
753 {showManifest && <span className="font-mono text-faint">{vuln.manifest}</span>}
754 {vuln.state === "dismissed" && vuln.dismissedReason && <Badge>{dismissLabel(vuln.dismissedReason)}</Badge>}
755 </p>
756 <p className="mt-1 text-muted wrap-anywhere">{vuln.summary}</p>
757 </div>
758 {canDismiss && vuln.state !== "fixed" && (
759 <div className="flex shrink-0 items-center gap-1.5">
760 {vuln.state === "open" ? dismiss() : <ReopenButton id={vuln.id} action={action} />}
761 </div>
762 )}
763 </div>
764 {vuln.state === "open" && !vuln.fixedVersion && (
765 <div className="mt-2 rounded-md border border-warn/30 bg-warn/5 px-3 py-2 text-xs">
766 <p className="flex items-center gap-1.5 font-medium text-warn">
767 <ShieldAlert size={13} />
768 No patched version available
769 </p>
770 <p className="mt-1 text-muted">
771 No release fixes this yet. Dependencies are checked again daily
772 {upkeep
773 ? ", and g1t opens a pull request when a fix is published."
774 : "; turn on security updates and g1t opens a pull request when a fix is published."}
775 </p>
776 <div className="mt-2 flex flex-wrap items-center gap-2">
777 <a href={osv} target="_blank" rel="noreferrer" className="inline-flex items-center gap-1 text-fg-soft hover:text-fg hover:underline">
778 Read the advisory
779 <ExternalLink size={10} />
780 </a>
781 {canDismiss && dismiss("tolerable_risk", "Dismiss as tolerable risk")}
782 </div>
783 </div>
784 )}
785 <ActivityLog entries={alertActivity(vuln, activity)} base={base} open={focused && vuln.state !== "open"} />
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API786 </li>
787 );
788}
789
790export function ScanSummary({
791 scan,
792}: {
793 scan: { history: string; commitsScanned: number; historyFinishedAt: string | null; dependenciesScannedAt: string | null; dependenciesError: string | null; lockfiles: string[] };
794}) {
795 const history =
796 scan.history === "done" ? (
797 <>
798 History scanned: {scan.commitsScanned.toLocaleString()} commits
799 {scan.historyFinishedAt && (
800 <>
801 , <TimeAgo at={scan.historyFinishedAt} />
802 </>
803 )}
804 </>
805 ) : scan.history === "stopped" ? (
806 "History scan paused: the workspace reached its spending limit"
807 ) : scan.history === "running" ? (
808 `Scanning history: ${scan.commitsScanned.toLocaleString()} commits so far`
809 ) : (
810 "History scan queued"
811 );
812 return (
813 <div className="flex flex-wrap gap-x-5 gap-y-1 text-xs text-muted">
814 <span className="inline-flex items-center gap-1.5">
815 <KeyRound size={12} />
816 {history}
817 </span>
818 <span className="inline-flex items-center gap-1.5">
819 <Package size={12} />
820 {scan.dependenciesScannedAt ? (
821 <>
822 Dependencies read <TimeAgo at={scan.dependenciesScannedAt} />
823 {scan.lockfiles.length > 0 ? <> from {scan.lockfiles.join(", ")}</> : " (no lockfiles found)"}
824 </>
825 ) : (
826 "Dependencies not read yet"
827 )}
828 </span>
829 {scan.dependenciesError && (
830 <span className="inline-flex items-center gap-1.5 text-warn">
831 <ShieldAlert size={12} />
832 {scan.dependenciesError}
833 </span>
834 )}
835 </div>
836 );
837}
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily838
839function list(values: string[]): string {
840 return values.length > 0 ? values.join(", ") : "—";
841}
842
843/** What `.g1t/dependencies.yml` asks for, and that acting on it is still to come. */
844export function VersionUpdatesCard({ state }: { state: VersionUpdatesState }) {
845 const rows = state.updates.map((entry) => ({
846 key: `${entry.ecosystem}:${entry.directory}`,
847 ecosystem: entry.ecosystem,
848 directory: entry.directory,
849 interval: entry.interval,
850 groups: list(entry.groups.map((group) => `${group.name} (${group.patterns.join(", ")})`)),
851 ignore: list(entry.ignore.map((rule) => (rule.versions.length > 0 ? `${rule.dependency} ${rule.versions.join(", ")}` : rule.dependency))),
852 limit: String(entry.openPullRequestsLimit),
853 }));
854 return (
855 <div className="rounded-xl border border-line bg-surface p-4">
856 <div className="flex flex-wrap items-center gap-2">
857 <span className="text-sm font-medium">Version updates</span>
858 <Badge tone="merged">Coming soon</Badge>
859 </div>
860 <p className="mt-1 text-sm text-muted">
861 Ask for pull requests that raise your dependencies to new versions on a schedule, in{" "}
862 <code className="text-fg-soft">.g1t/dependencies.yml</code>. g1t reads and checks this file now; pull requests for new
863 versions are coming.
864 </p>
865 <p className="mt-3 flex items-center gap-1.5 text-xs text-muted">
866 {state.error ? (
867 <>
868 <FileWarning size={13} className="shrink-0 text-danger" />
869 <span className="text-danger wrap-anywhere">.g1t/dependencies.yml has a problem: {state.error}</span>
870 </>
871 ) : state.found ? (
872 <>
873 <CircleCheck size={13} className="shrink-0 text-accent" />
874 <span>
875 Read .g1t/dependencies.yml
876 {state.readAt && (
877 <>
878 {" "}
879 <TimeAgo at={state.readAt} />
880 </>
881 )}
882 : {rows.length} {rows.length === 1 ? "entry" : "entries"}
883 </span>
884 </>
885 ) : (
886 <>
887 <CircleDot size={13} className="shrink-0 text-faint" />
888 <span>No .g1t/dependencies.yml on the default branch.</span>
889 </>
890 )}
891 </p>
892 {rows.length > 0 && (
893 <>
894 <table className="mt-3 hidden w-full text-left text-xs sm:table">
895 <thead className="text-faint">
896 <tr className="border-b border-line">
897 <th className="py-1.5 pr-3 font-medium">Ecosystem</th>
898 <th className="py-1.5 pr-3 font-medium">Directory</th>
899 <th className="py-1.5 pr-3 font-medium">Interval</th>
900 <th className="py-1.5 pr-3 font-medium">Groups</th>
901 <th className="py-1.5 pr-3 font-medium">Ignored</th>
902 <th className="py-1.5 text-right font-medium">Limit</th>
903 </tr>
904 </thead>
905 <tbody className="text-muted">
906 {rows.map((row) => (
907 <tr key={row.key} className="border-b border-line/60 align-top last:border-0">
908 <td className="py-1.5 pr-3 font-mono text-fg-soft">{row.ecosystem}</td>
909 <td className="py-1.5 pr-3 font-mono">{row.directory}</td>
910 <td className="py-1.5 pr-3">{row.interval}</td>
911 <td className="py-1.5 pr-3 font-mono wrap-anywhere">{row.groups}</td>
912 <td className="py-1.5 pr-3 font-mono wrap-anywhere">{row.ignore}</td>
913 <td className="py-1.5 text-right tabular-nums">{row.limit}</td>
914 </tr>
915 ))}
916 </tbody>
917 </table>
918 <ul className="mt-3 space-y-2 sm:hidden">
919 {rows.map((row) => (
920 <li key={row.key} className="rounded-lg border border-line px-3 py-2 text-xs">
921 <p className="font-mono text-fg-soft">
922 {row.ecosystem} <span className="text-muted">{row.directory}</span>
923 </p>
924 <dl className="mt-1 grid grid-cols-[auto_1fr] gap-x-3 gap-y-0.5 text-muted">
925 <dt className="text-faint">Interval</dt>
926 <dd>{row.interval}</dd>
927 <dt className="text-faint">Groups</dt>
928 <dd className="font-mono wrap-anywhere">{row.groups}</dd>
929 <dt className="text-faint">Ignored</dt>
930 <dd className="font-mono wrap-anywhere">{row.ignore}</dd>
931 <dt className="text-faint">Limit</dt>
932 <dd className="tabular-nums">{row.limit}</dd>
933 </dl>
934 </li>
935 ))}
936 </ul>
937 </>
938 )}
939 <a
940 href={VERSION_UPDATES_DOCS}
941 target="_blank"
942 rel="noreferrer"
943 className="mt-3 inline-flex items-center gap-1 text-xs text-fg-soft hover:text-fg hover:underline"
944 >
945 How to write .g1t/dependencies.yml
946 <ExternalLink size={10} />
947 </a>
948 </div>
949 );
950}