Skip to content

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

842 lines30,875 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 GitBranch,
15 GitMerge,
16 GitPullRequest,
17 GitPullRequestClosed,
18 History,
19 KeyRound,
20 Loader,
21 Package,
22 ShieldAlert,
23 ShieldCheck,
24} from "lucide-react";
25import { 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 API26import { Link, useFetcher } from "react-router";
27
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily28import {
29 type AlertActivity,
30 type AlertState,
31 DEPENDENCY_DISMISS_REASONS,
32 type DismissReason,
33 SECRET_DISMISS_REASONS,
34 SEVERITIES,
35 type PullStatus,
36 type SecretFinding,
37 type SecurityUpdate,
38 type Severity,
39 type SeverityCounts,
40 type Vulnerability,
41 dismissLabel,
42} from "@g1t/contracts";
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API43
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily44import {
45 type ActivityEntry,
46 type PackageGroup,
47 UPDATE_STATES,
48 alertActivity,
49 groupByPackage,
50 highestFix,
51 latestUpdate,
52 splitSecrets,
53 worstSeverity,
54} from "../lib/security-alerts";
55import { Avatar, TimeAgo } from "./ui";
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API56import { Badge, type BadgeTone } from "./ui/badge";
57import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "./ui/dialog";
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily58import { 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 API59
60type Done = { ok: boolean; error?: string } | undefined;
61
62const SEVERITY: Record<Severity, { label: string; tone: BadgeTone }> = {
63 critical: { label: "Critical", tone: "danger" },
64 high: { label: "High", tone: "warn" },
65 medium: { label: "Medium", tone: "merged" },
66 low: { label: "Low", tone: "info" },
67 unknown: { label: "Unrated", tone: "neutral" },
68};
69
70
71export function SeverityBadge({ severity }: { severity: Severity }) {
72 return <Badge tone={SEVERITY[severity].tone}>{SEVERITY[severity].label}</Badge>;
73}
74
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily75/** 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 API76export function SeverityCountsGrid({ counts }: { counts: SeverityCounts }) {
77 return (
78 <div className="grid grid-cols-2 gap-3 sm:grid-cols-5">
79 {SEVERITIES.map((severity) => (
80 <div key={severity} className="rounded-xl border border-line bg-surface px-4 py-3">
81 <p className="text-xs text-muted">{SEVERITY[severity].label}</p>
82 <p className={`mt-1 text-2xl font-semibold tabular-nums ${counts[severity] > 0 && severity === "critical" ? "text-danger" : ""}`}>
83 {counts[severity]}
84 </p>
85 </div>
86 ))}
87 </div>
88 );
89}
90
91/** A compact row of severity counts, for a list of projects. */
92export function SeverityCountsInline({ counts }: { counts: SeverityCounts }) {
93 const shown = SEVERITIES.filter((severity) => counts[severity] > 0);
94 if (shown.length === 0) {
95 return (
96 <span className="inline-flex items-center gap-1 text-xs text-accent">
97 <ShieldCheck size={13} />
98 Nothing open
99 </span>
100 );
101 }
102 return (
103 <span className="flex flex-wrap gap-1.5">
104 {shown.map((severity) => (
105 <Badge key={severity} tone={SEVERITY[severity].tone}>
106 {counts[severity]} {SEVERITY[severity].label.toLowerCase()}
107 </Badge>
108 ))}
109 </span>
110 );
111}
112
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily113const STATE_FILTERS: { state: AlertState; label: string; icon: ReactNode }[] = [
114 { state: "open", label: "Open", icon: <CircleDot size={14} /> },
115 { state: "dismissed", label: "Dismissed", icon: <CircleSlash size={14} /> },
116 { state: "fixed", label: "Fixed", icon: <CircleCheck size={14} /> },
117];
118
119/** Open, Dismissed and Fixed, with how many alerts each holds. */
120export function StateFilter({
121 counts,
122 value,
123 onChange,
124}: {
125 counts: Record<AlertState, number>;
126 value: AlertState;
127 onChange: (state: AlertState) => void;
128}) {
129 return (
130 <div role="group" aria-label="Filter alerts" className="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm">
131 {STATE_FILTERS.map(({ state, label, icon }) => (
132 <button
133 key={state}
134 type="button"
135 aria-pressed={value === state}
136 onClick={() => onChange(state)}
137 className={`inline-flex items-center gap-1.5 transition-colors ${
138 value === state ? "font-medium text-fg" : "text-muted hover:text-fg"
139 }`}
140 >
141 {icon}
142 {counts[state]} {label}
143 </button>
144 ))}
145 </div>
146 );
147}
148
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API149const TEXTAREA =
150 "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";
151
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily152const SMALL_BUTTON =
153 "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";
154
155/** Dismiss an alert with one of `reasons` and an optional comment. */
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar156export function DismissDialog({
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily157 id,
158 title,
159 detail,
160 reasons,
161 note,
162 action,
163 defaultReason,
164 trigger = "Dismiss",
165}: {
166 id: string;
167 title: string;
168 /** What the alert is, in a line of code type. */
169 detail: string;
170 reasons: { reason: DismissReason; label: string; about: string }[];
171 note?: string;
172 action: string;
173 defaultReason?: DismissReason;
174 trigger?: string;
175}) {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API176 const fetcher = useFetcher<Done>();
177 const [open, setOpen] = useState(false);
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily178 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 API179 useEffect(() => {
180 if (fetcher.state === "idle" && fetcher.data?.ok) setOpen(false);
181 }, [fetcher.state, fetcher.data]);
182 return (
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily183 <Dialog
184 open={open}
185 onOpenChange={(next) => {
186 setOpen(next);
187 if (next) setReason(defaultReason ?? "");
188 }}
189 >
190 <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 API191 <DialogContent>
192 <DialogHeader>
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily193 <DialogTitle>{title}</DialogTitle>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API194 <DialogDescription>
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily195 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 API196 </DialogDescription>
197 </DialogHeader>
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily198 <fetcher.Form method="post" action={action} className="space-y-4">
199 <input type="hidden" name="intent" value="dismiss" />
200 <input type="hidden" name="id" value={id} />
201 <p className="font-mono text-xs break-all text-muted">{detail}</p>
202 <fieldset>
203 <legend className="text-xs font-medium text-muted">Reason</legend>
204 <RadioGroup name="reason" value={reason} onValueChange={setReason} required className="mt-2 gap-3">
205 {reasons.map((option) => (
206 <RadioOption key={option.reason} value={option.reason} label={option.label} description={option.about} />
207 ))}
208 </RadioGroup>
209 </fieldset>
210 {note && <p className="rounded-md border border-warn/30 bg-warn/5 px-3 py-2 text-xs text-warn">{note}</p>}
211 <label className="block">
212 <span className="mb-1.5 block text-xs font-medium text-muted">Comment (optional)</span>
213 <textarea name="comment" rows={3} maxLength={500} placeholder="What someone reading this later should know." className={TEXTAREA} />
214 </label>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API215 <div className="flex justify-end">
216 <button
217 type="submit"
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily218 disabled={fetcher.state !== "idle" || !reason}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API219 className="rounded-md bg-fg px-3.5 py-2 text-sm font-medium text-bg hover:bg-white disabled:opacity-50"
220 >
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily221 {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 API222 </button>
223 </div>
224 {fetcher.data?.error && <p className="text-sm text-danger">{fetcher.data.error}</p>}
225 </fetcher.Form>
226 </DialogContent>
227 </Dialog>
228 );
229}
230
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar231export function ReopenButton({ id, action }: { id: string; action: string }) {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily232 const fetcher = useFetcher<Done>();
233 return (
234 <span className="flex flex-col items-end gap-1">
235 <button
236 type="button"
237 disabled={fetcher.state !== "idle"}
238 onClick={() => fetcher.submit({ intent: "reopen", id }, { method: "post", action })}
239 className={SMALL_BUTTON}
240 >
241 {fetcher.state !== "idle" ? "Reopening…" : "Reopen"}
242 </button>
243 {fetcher.data?.error && <span className="text-xs text-danger">{fetcher.data.error}</span>}
244 </span>
245 );
246}
247
248/** What happened to an alert, oldest first, folded away until asked for. */
249function ActivityLog({ entries, base, open }: { entries: ActivityEntry[]; base: string; open?: boolean }) {
250 if (entries.length === 0) return null;
251 return (
252 <details className="group mt-2" open={open}>
253 <summary className="inline-flex cursor-pointer list-none items-center gap-1.5 text-xs text-muted hover:text-fg [&::-webkit-details-marker]:hidden">
254 <History size={12} />
255 <span className="group-open:hidden">Show activity ({entries.length})</span>
256 <span className="hidden group-open:inline">Hide activity</span>
257 </summary>
258 <ol className="mt-2 space-y-2 border-l border-line pl-3">
259 {entries.map((entry) => (
260 <li key={entry.key} className="text-xs">
261 <p className="flex flex-wrap items-center gap-x-1.5 gap-y-1 text-muted">
262 {entry.actor ? (
263 <>
264 <Avatar name={entry.actor} size={16} />
265 <span className="font-medium text-fg">{entry.actor}</span>
266 </>
267 ) : (
268 <CircleDot size={12} className="text-faint" />
269 )}
270 <span>{entry.text}</span>
271 {entry.ref && (
272 <Link
273 to={`${base}/${entry.ref.kind === "pull" ? "pull" : "issues"}/${entry.ref.number}`}
274 className="font-medium text-fg-soft hover:text-fg hover:underline"
275 >
276 #{entry.ref.number}
277 </Link>
278 )}
279 {entry.reason && <Badge>{dismissLabel(entry.reason)}</Badge>}
280 <span className="text-faint">
281 <TimeAgo at={entry.at} />
282 </span>
283 </p>
284 {entry.comment && <p className="mt-1 text-fg-soft wrap-anywhere">“{entry.comment}”</p>}
285 </li>
286 ))}
287 </ol>
288 </details>
289 );
290}
291
292function useFocus<T extends HTMLElement>(focused: boolean) {
293 const ref = useRef<T>(null);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API294 useEffect(() => {
295 if (focused) ref.current?.scrollIntoView({ block: "center" });
296 }, [focused]);
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily297 return ref;
298}
299
300const FOCUSED = "bg-accent/5 ring-1 ring-accent/40 ring-inset";
301
302function secretBadge(finding: SecretFinding): { label: string; tone: BadgeTone; about: string } {
303 if (finding.state === "dismissed") {
304 return {
305 label: "Dismissed",
306 tone: "neutral",
307 about: finding.status === "allowed" ? "Pushes carrying it go through." : "Dismissed.",
308 };
309 }
310 if (finding.state === "fixed") return { label: "Revoked", tone: "accent", about: "Revoked or rotated." };
311 if (finding.status === "blocked") {
312 return { label: "Push blocked", tone: finding.testValue ? "neutral" : "warn", about: "A push carrying it was refused, so it never landed." };
313 }
314 return {
315 label: "In history",
316 tone: finding.testValue ? "neutral" : "danger",
317 about: "In the repository's history. Rotate it with whoever issued it, then dismiss it as revoked.",
318 };
319}
320
321function SecretItem({
322 finding,
323 activity,
324 base,
325 action,
326 focused,
327 canDismiss,
328}: {
329 finding: SecretFinding;
330 activity: AlertActivity[];
331 base: string;
332 action: string;
333 focused: boolean;
334 canDismiss: boolean;
335}) {
336 const ref = useFocus<HTMLLIElement>(focused);
337 const badge = secretBadge(finding);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API338 const landed = finding.source === "history" || finding.status === "open";
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily339 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 API340 return (
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily341 <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 API342 <KeyRound size={15} className="mt-0.5 hidden shrink-0 text-muted sm:block" />
343 <div className="min-w-0 grow">
344 <div className="flex flex-wrap items-center gap-2">
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar345 <Link to={`${base}/security/secret-scanning/${finding.id}`} className="text-sm font-medium first-letter:uppercase hover:underline">
346 {finding.label}
347 </Link>
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily348 <Badge tone={badge.tone} title={badge.about}>
349 {badge.label}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API350 </Badge>
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily351 {finding.testValue && <Badge title={finding.testValue}>Likely test value</Badge>}
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar352 {finding.validity === "active" && (
353 <Badge tone="danger" title="Its issuer says it still works">
354 Active
355 </Badge>
356 )}
357 {finding.validity === "inactive" && <Badge title="Its issuer refused it: revoked or expired">Inactive</Badge>}
358 {finding.bypass && <Badge tone="warn" title={`Bypassed by ${finding.bypass.by}`}>Bypassed</Badge>}
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily359 {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 API360 </div>
361 <p className="mt-1 truncate font-mono text-xs">
362 {landed ? (
363 <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 daily364 {where}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API365 </Link>
366 ) : (
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily367 <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 API368 )}
369 <span className="text-faint"> · {finding.preview}</span>
370 </p>
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily371 {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 API372 <p className="mt-1.5 flex flex-wrap gap-x-3 gap-y-1 text-xs text-faint">
373 <span>
374 {finding.source === "push" ? "in a push" : "in history"}
375 {finding.foundBy && <> by {finding.foundBy}</>}, commit{" "}
376 {landed ? (
377 <Link to={`${base}/commit/${finding.commit}`} className="font-mono hover:text-fg">
378 {finding.commit.slice(0, 7)}
379 </Link>
380 ) : (
381 <span className="font-mono">{finding.commit.slice(0, 7)}</span>
382 )}
383 </span>
384 <span>
385 found <TimeAgo at={finding.foundAt} />
386 </span>
387 </p>
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily388 <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 API389 </div>
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily390 {canDismiss && (
391 <div className="flex shrink-0 items-center gap-1.5">
392 {finding.state === "open" ? (
393 <DismissDialog
394 id={finding.id}
395 title={`Dismiss ${finding.label}`}
396 detail={`${where} · ${finding.preview}`}
397 reasons={SECRET_DISMISS_REASONS}
398 note={
399 finding.status === "blocked" && !finding.testValue
400 ? "Dismissing it lets the same push through, unless you dismiss it as revoked."
401 : undefined
402 }
403 action={action}
404 />
405 ) : (
406 <ReopenButton id={finding.id} action={action} />
407 )}
408 </div>
409 )}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API410 </li>
411 );
412}
413
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily414function Empty({ title, children }: { title: string; children: ReactNode }) {
415 return (
416 <div className="rounded-xl border border-dashed border-line px-6 py-10 text-center">
417 <ShieldCheck size={22} className="mx-auto text-accent" />
418 <p className="mt-2 font-medium">{title}</p>
419 <p className="mt-1 text-sm text-muted">{children}</p>
420 </div>
421 );
422}
423
424const LIST = "divide-y divide-line overflow-hidden rounded-xl border border-line bg-surface";
425
426/** 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 API427export function SecretsList({
428 secrets,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily429 state,
430 activity,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API431 base,
432 action,
433 focus,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily434 canDismiss,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API435}: {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily436 /** Already filtered to `state`. */
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API437 secrets: SecretFinding[];
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily438 state: AlertState;
439 activity: AlertActivity[];
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API440 base: string;
441 action: string;
442 focus: string | null;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily443 /** Whether the viewer may dismiss and reopen secret alerts (Admin). */
444 canDismiss: boolean;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API445}) {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily446 const item = (finding: SecretFinding) => (
447 <SecretItem
448 key={finding.id}
449 finding={finding}
450 activity={activity}
451 base={base}
452 action={action}
453 focused={finding.id === focus}
454 canDismiss={canDismiss}
455 />
456 );
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API457 if (secrets.length === 0) {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily458 return state === "open" ? (
459 <Empty title="No open secret alerts">
460 Pushes that add a key or a token are refused before they land, and the history is scanned once in the background.
461 </Empty>
462 ) : (
463 <Empty title={state === "dismissed" ? "No dismissed secret alerts" : "No revoked secrets"}>
464 {state === "dismissed"
465 ? "Alerts someone dismissed as a false positive, a test value or accepted are listed here."
466 : "Secrets dismissed as revoked are listed here."}
467 </Empty>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API468 );
469 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily470 if (state !== "open") return <ul className={LIST}>{secrets.map(item)}</ul>;
471 const { real, tests } = splitSecrets(secrets);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API472 return (
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily473 <div className="space-y-5">
474 {real.length > 0 ? (
475 <ul className={LIST}>{real.map(item)}</ul>
476 ) : (
477 <Empty title="No secrets that look real">Only likely test values are open, and they never block a push.</Empty>
478 )}
479 {tests.length > 0 && (
480 <section>
481 <h4 className="text-sm font-medium">Likely test values</h4>
482 <p className="mt-0.5 mb-2 text-xs text-muted">
483 These look made for tests or documentation. They never block a push and are not counted as critical; dismiss them
484 to clear the list.
485 </p>
486 <ul className={LIST}>{tests.map(item)}</ul>
487 </section>
488 )}
489 </div>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API490 );
491}
492
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily493/** 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 API494export type UpgradeFix = {
495 number: number;
496 state: "open" | "closed";
497 /** The newest pull request for the issue, if any. */
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily498 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 API499 resolvedBy: number | null;
500};
501
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily502/** A security update's pull request as the page loads it. */
503export type PullInfo = { number: number; status: PullStatus; title: string };
504
505function 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 API506 const pull = fix?.pull;
507 return (
508 <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 daily509 <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 API510 <Link to={`${base}/issues/${issue}`} className="inline-flex items-center gap-1 text-fg-soft hover:text-fg">
511 {fix?.state === "closed" ? <CircleCheck size={12} className="text-merged" /> : <CircleDot size={12} className="text-accent" />}#{issue}
512 </Link>
513 {pull && (
514 <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 daily515 {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 API516 </Link>
517 )}
518 </span>
519 );
520}
521
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily522const PULL_ICON: Record<PullStatus, ReactNode> = {
523 draft: <GitPullRequest size={13} className="text-muted" />,
524 open: <GitPullRequest size={13} className="text-accent" />,
525 merged: <GitMerge size={13} className="text-merged" />,
526 closed: <GitPullRequestClosed size={13} className="text-danger" />,
527};
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API528
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily529/** Where g1t's security update for a package stands. */
530function UpdateStatus({ update, name, pulls, base }: { update: SecurityUpdate; name: string; pulls: Record<number, PullInfo>; base: string }) {
531 const meta = UPDATE_STATES[update.state];
532 const pull = update.pull != null ? pulls[update.pull] : undefined;
533 const about: Record<SecurityUpdate["state"], ReactNode> = {
534 requested: <>A sandbox is raising {name} to {update.target}.</>,
535 open: <>It raises {name} to {update.target} and lands through your branch's required checks.</>,
536 merged: <>{name} was raised to {update.target}.</>,
537 closed: <>The pull request was closed without merging.</>,
538 superseded: <>A newer update replaced it, or the package is no longer vulnerable, so g1t closed it.</>,
539 needs_code: (
540 <>
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent541 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 daily542 {update.issue != null && (
543 <>
544 :{" "}
545 <Link to={`${base}/issues/${update.issue}`} className="font-medium text-fg-soft hover:text-fg hover:underline">
546 #{update.issue}
547 </Link>
548 </>
549 )}
550 .
551 </>
552 ),
553 failed: <>{update.error ?? "g1t could not make the change."}</>,
554 };
555 return (
556 <div className="mt-2.5 rounded-lg border border-line bg-bg/40 px-3 py-2 text-xs sm:ml-7">
557 <div className="flex flex-wrap items-center gap-x-2.5 gap-y-1.5">
558 {update.state === "requested" ? (
559 <Loader size={13} className="animate-spin text-info motion-reduce:animate-none" />
560 ) : update.state === "failed" || update.state === "needs_code" ? (
561 <CircleAlert size={13} className={update.state === "failed" ? "text-danger" : "text-warn"} />
562 ) : (
563 PULL_ICON[pull?.status ?? (update.state === "merged" ? "merged" : update.state === "open" ? "open" : "closed")]
564 )}
565 <Badge tone={meta.tone}>{meta.label}</Badge>
566 {update.pull != null && (
567 <Link to={`${base}/pull/${update.pull}`} className="font-medium text-fg-soft hover:text-fg hover:underline">
568 #{update.pull}
569 {pull && <span className="font-normal text-muted"> {pull.status === "draft" ? "draft" : pull.status}</span>}
570 </Link>
571 )}
572 {update.branch && (
573 <span className="inline-flex min-w-0 items-center gap-1 font-mono text-muted">
574 <GitBranch size={12} className="shrink-0" />
575 <span className="truncate">{update.branch}</span>
576 </span>
577 )}
578 <span className="text-faint">
579 <TimeAgo at={update.updatedAt} />
580 </span>
581 </div>
582 <p className={`mt-1 ${update.state === "failed" ? "text-danger" : "text-muted"} wrap-anywhere`}>{about[update.state]}</p>
583 </div>
584 );
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API585}
586
587export function VulnerabilityList({
588 vulnerabilities,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily589 state,
590 activity,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API591 fixes,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily592 pulls,
593 upkeep,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API594 base,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily595 action,
596 focus,
597 canDismiss,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API598}: {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily599 /** Already filtered to `state`. */
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API600 vulnerabilities: Vulnerability[];
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily601 state: AlertState;
602 activity: AlertActivity[];
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API603 fixes: Record<number, UpgradeFix>;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily604 pulls: Record<number, PullInfo>;
605 /** Whether security updates are on. */
606 upkeep: boolean;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API607 base: string;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily608 action: string;
609 focus: string | null;
610 /** Whether the viewer may dismiss and reopen dependency alerts (Write). */
611 canDismiss: boolean;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API612}) {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily613 const packages = groupByPackage(vulnerabilities);
614 if (packages.length === 0) {
615 return state === "open" ? (
616 <Empty title="No known vulnerabilities">
617 Every package the lockfiles resolve is checked against the OSV database on each push to the default branch, and daily.
618 </Empty>
619 ) : (
620 <Empty title={state === "dismissed" ? "No dismissed dependency alerts" : "Nothing fixed yet"}>
621 {state === "dismissed"
622 ? "Alerts someone dismissed, with their reason, are listed here."
623 : "Alerts whose package was upgraded, or is no longer vulnerable, are listed here."}
624 </Empty>
625 );
626 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API627 return (
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily628 <ul className={LIST}>
629 {packages.map((group) => (
630 <PackageItem
631 key={group.key}
632 group={group}
633 activity={activity}
634 fixes={fixes}
635 pulls={pulls}
636 upkeep={upkeep}
637 base={base}
638 action={action}
639 focus={focus}
640 canDismiss={canDismiss}
641 />
642 ))}
643 </ul>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API644 );
645}
646
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily647function PackageItem({
648 group,
649 activity,
650 fixes,
651 pulls,
652 upkeep,
653 base,
654 action,
655 focus,
656 canDismiss,
657}: {
658 group: PackageGroup;
659 activity: AlertActivity[];
660 fixes: Record<number, UpgradeFix>;
661 pulls: Record<number, PullInfo>;
662 upkeep: boolean;
663 base: string;
664 action: string;
665 focus: string | null;
666 canDismiss: boolean;
667}) {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API668 const versions = [...new Set(group.vulns.map((vuln) => vuln.version))];
669 const manifests = [...new Set(group.vulns.map((vuln) => vuln.manifest))];
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily670 const target = highestFix(group.vulns);
671 const update = latestUpdate(group.vulns);
672 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 API673 return (
674 <li className="px-4 py-3">
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily675 <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 API676 <Package size={15} className="mt-0.5 hidden shrink-0 text-muted sm:block" />
677 <div className="min-w-0 grow">
678 <div className="flex flex-wrap items-center gap-2">
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily679 <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 API680 <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 daily681 <Badge>{group.ecosystem}</Badge>
682 <SeverityBadge severity={worstSeverity(group.vulns)} />
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API683 </div>
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily684 <p className="mt-1 text-xs text-faint wrap-anywhere">
685 {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 API686 <span className="font-mono">{manifests.join(", ")}</span>
687 </p>
688 </div>
689 </div>
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily690 {update && <UpdateStatus update={update} name={group.name} pulls={pulls} base={base} />}
691 {issue != null && (
692 <div className="mt-2 sm:ml-7">
693 <FixLink issue={issue} fix={fixes[issue]} base={base} />
694 </div>
695 )}
696 <ul className="mt-2.5 space-y-px overflow-hidden rounded-lg border border-line sm:ml-7">
697 {group.vulns.map((vuln) => (
698 <AdvisoryItem
699 key={vuln.id}
700 vuln={vuln}
701 showManifest={manifests.length > 1}
702 activity={activity}
703 upkeep={upkeep}
704 base={base}
705 action={action}
706 focused={vuln.id === focus}
707 canDismiss={canDismiss}
708 />
709 ))}
710 </ul>
711 </li>
712 );
713}
714
715function AdvisoryItem({
716 vuln,
717 showManifest,
718 activity,
719 upkeep,
720 base,
721 action,
722 focused,
723 canDismiss,
724}: {
725 vuln: Vulnerability;
726 showManifest: boolean;
727 activity: AlertActivity[];
728 upkeep: boolean;
729 base: string;
730 action: string;
731 focused: boolean;
732 canDismiss: boolean;
733}) {
734 const ref = useFocus<HTMLLIElement>(focused);
735 const osv = `https://osv.dev/vulnerability/${vuln.osvId}`;
736 const dismiss = (reason?: DismissReason, trigger?: string) => (
737 <DismissDialog
738 id={vuln.id}
739 title={`Dismiss ${vuln.advisory}`}
740 detail={`${vuln.package} ${vuln.version} · ${vuln.manifest}`}
741 reasons={DEPENDENCY_DISMISS_REASONS}
742 action={action}
743 defaultReason={reason}
744 trigger={trigger}
745 />
746 );
747 return (
748 <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 : ""}`}>
749 <div className="flex flex-col gap-2 sm:flex-row sm:items-start">
750 <div className="min-w-0 grow text-xs">
751 <p className="flex flex-wrap items-center gap-x-2 gap-y-1">
752 <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 API753 {vuln.advisory}
754 <ExternalLink size={10} />
755 </a>
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily756 <SeverityBadge severity={vuln.severity} />
757 {vuln.fixedVersion && <span className="text-faint">fixed in {vuln.fixedVersion}</span>}
758 {showManifest && <span className="font-mono text-faint">{vuln.manifest}</span>}
759 {vuln.state === "dismissed" && vuln.dismissedReason && <Badge>{dismissLabel(vuln.dismissedReason)}</Badge>}
760 </p>
761 <p className="mt-1 text-muted wrap-anywhere">{vuln.summary}</p>
762 </div>
763 {canDismiss && vuln.state !== "fixed" && (
764 <div className="flex shrink-0 items-center gap-1.5">
765 {vuln.state === "open" ? dismiss() : <ReopenButton id={vuln.id} action={action} />}
766 </div>
767 )}
768 </div>
769 {vuln.state === "open" && !vuln.fixedVersion && (
770 <div className="mt-2 rounded-md border border-warn/30 bg-warn/5 px-3 py-2 text-xs">
771 <p className="flex items-center gap-1.5 font-medium text-warn">
772 <ShieldAlert size={13} />
773 No patched version available
774 </p>
775 <p className="mt-1 text-muted">
776 No release fixes this yet. Dependencies are checked again daily
777 {upkeep
778 ? ", and g1t opens a pull request when a fix is published."
779 : "; turn on security updates and g1t opens a pull request when a fix is published."}
780 </p>
781 <div className="mt-2 flex flex-wrap items-center gap-2">
782 <a href={osv} target="_blank" rel="noreferrer" className="inline-flex items-center gap-1 text-fg-soft hover:text-fg hover:underline">
783 Read the advisory
784 <ExternalLink size={10} />
785 </a>
786 {canDismiss && dismiss("tolerable_risk", "Dismiss as tolerable risk")}
787 </div>
788 </div>
789 )}
790 <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 API791 </li>
792 );
793}
794
795export function ScanSummary({
796 scan,
797}: {
798 scan: { history: string; commitsScanned: number; historyFinishedAt: string | null; dependenciesScannedAt: string | null; dependenciesError: string | null; lockfiles: string[] };
799}) {
800 const history =
801 scan.history === "done" ? (
802 <>
803 History scanned: {scan.commitsScanned.toLocaleString()} commits
804 {scan.historyFinishedAt && (
805 <>
806 , <TimeAgo at={scan.historyFinishedAt} />
807 </>
808 )}
809 </>
810 ) : scan.history === "stopped" ? (
811 "History scan paused: the workspace reached its spending limit"
812 ) : scan.history === "running" ? (
813 `Scanning history: ${scan.commitsScanned.toLocaleString()} commits so far`
814 ) : (
815 "History scan queued"
816 );
817 return (
818 <div className="flex flex-wrap gap-x-5 gap-y-1 text-xs text-muted">
819 <span className="inline-flex items-center gap-1.5">
820 <KeyRound size={12} />
821 {history}
822 </span>
823 <span className="inline-flex items-center gap-1.5">
824 <Package size={12} />
825 {scan.dependenciesScannedAt ? (
826 <>
827 Dependencies read <TimeAgo at={scan.dependenciesScannedAt} />
828 {scan.lockfiles.length > 0 ? <> from {scan.lockfiles.join(", ")}</> : " (no lockfiles found)"}
829 </>
830 ) : (
831 "Dependencies not read yet"
832 )}
833 </span>
834 {scan.dependenciesError && (
835 <span className="inline-flex items-center gap-1.5 text-warn">
836 <ShieldAlert size={12} />
837 {scan.dependenciesError}
838 </span>
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily839 )}
840 </div>
841 );
842}