g1t/apps/web/app/components/security.tsx
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 API | 1 | /** |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 2 | * 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 API | 6 | */ |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 7 | import { |
| 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"; | |
| 26 | import { 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 API | 27 | import { Link, useFetcher } from "react-router"; |
| 28 | ||
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 29 | import { |
| 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 API | 45 | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 46 | import { |
| 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"; | |
| 57 | import { Avatar, TimeAgo } from "./ui"; | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 58 | import { Badge, type BadgeTone } from "./ui/badge"; |
| 59 | import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "./ui/dialog"; | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 60 | import { 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 API | 61 | |
| 62 | type Done = { ok: boolean; error?: string } | undefined; | |
| 63 | ||
| 64 | const 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 daily | 72 | /** Where the docs explain `.g1t/dependencies.yml`. */ |
| 73 | export 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 API | 74 | |
| 75 | export 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 daily | 79 | /** 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 API | 80 | export 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. */ | |
| 96 | export 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 daily | 117 | const 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. */ | |
| 124 | export 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 API | 153 | const 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 daily | 156 | const 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. */ | |
| 160 | function 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 API | 180 | const fetcher = useFetcher<Done>(); |
| 181 | const [open, setOpen] = useState(false); | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 182 | 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 API | 183 | 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 daily | 187 | <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 API | 195 | <DialogContent> |
| 196 | <DialogHeader> | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 197 | <DialogTitle>{title}</DialogTitle> |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 198 | <DialogDescription> |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 199 | 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 API | 200 | </DialogDescription> |
| 201 | </DialogHeader> | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 202 | <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 API | 219 | <div className="flex justify-end"> |
| 220 | <button | |
| 221 | type="submit" | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 222 | disabled={fetcher.state !== "idle" || !reason} |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 223 | 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 daily | 225 | {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 API | 226 | </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 daily | 235 | function 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. */ | |
| 253 | function 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 | ||
| 296 | function 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 API | 298 | 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 daily | 301 | return ref; |
| 302 | } | |
| 303 | ||
| 304 | const FOCUSED = "bg-accent/5 ring-1 ring-accent/40 ring-inset"; | |
| 305 | ||
| 306 | function 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 | ||
| 325 | function 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 API | 342 | const landed = finding.source === "history" || finding.status === "open"; |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 343 | 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 API | 344 | return ( |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 345 | <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 API | 346 | <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 daily | 350 | <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 API | 352 | </Badge> |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 353 | {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 API | 355 | </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 daily | 359 | {where} |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 360 | </Link> |
| 361 | ) : ( | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 362 | <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 API | 363 | )} |
| 364 | <span className="text-faint"> · {finding.preview}</span> | |
| 365 | </p> | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 366 | {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 API | 367 | <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 daily | 383 | <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 API | 384 | </div> |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 385 | {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 API | 405 | </li> |
| 406 | ); | |
| 407 | } | |
| 408 | ||
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 409 | function 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 | ||
| 419 | const 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 API | 422 | export function SecretsList({ |
| 423 | secrets, | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 424 | state, |
| 425 | activity, | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 426 | base, |
| 427 | action, | |
| 428 | focus, | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 429 | canDismiss, |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 430 | }: { |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 431 | /** Already filtered to `state`. */ |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 432 | secrets: SecretFinding[]; |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 433 | state: AlertState; |
| 434 | activity: AlertActivity[]; | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 435 | base: string; |
| 436 | action: string; | |
| 437 | focus: string | null; | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 438 | /** 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 API | 440 | }) { |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 441 | 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 API | 452 | if (secrets.length === 0) { |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 453 | 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 API | 463 | ); |
| 464 | } | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 465 | 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 API | 467 | return ( |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 468 | <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 API | 485 | ); |
| 486 | } | |
| 487 | ||
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 488 | /** 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 API | 489 | export 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 daily | 493 | 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 API | 494 | resolvedBy: number | null; |
| 495 | }; | |
| 496 | ||
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 497 | /** A security update's pull request as the page loads it. */ |
| 498 | export type PullInfo = { number: number; status: PullStatus; title: string }; | |
| 499 | ||
| 500 | function 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 API | 501 | 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 daily | 504 | <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 API | 505 | <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 daily | 510 | {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 API | 511 | </Link> |
| 512 | )} | |
| 513 | </span> | |
| 514 | ); | |
| 515 | } | |
| 516 | ||
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 517 | const 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 API | 523 | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 524 | /** Where g1t's security update for a package stands. */ |
| 525 | function 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 | <> | |
| 536 | Raising {name} to {update.target} needs code changes, so g1t opened an issue for g1t-agent | |
| 537 | {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 API | 580 | } |
| 581 | ||
| 582 | export function VulnerabilityList({ | |
| 583 | vulnerabilities, | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 584 | state, |
| 585 | activity, | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 586 | fixes, |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 587 | pulls, |
| 588 | upkeep, | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 589 | base, |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 590 | action, |
| 591 | focus, | |
| 592 | canDismiss, | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 593 | }: { |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 594 | /** Already filtered to `state`. */ |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 595 | vulnerabilities: Vulnerability[]; |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 596 | state: AlertState; |
| 597 | activity: AlertActivity[]; | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 598 | fixes: Record<number, UpgradeFix>; |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 599 | 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 API | 602 | base: string; |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 603 | 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 API | 607 | }) { |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 608 | 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 API | 622 | return ( |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 623 | <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 API | 639 | ); |
| 640 | } | |
| 641 | ||
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 642 | function 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 API | 663 | 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 daily | 665 | 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 API | 668 | return ( |
| 669 | <li className="px-4 py-3"> | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 670 | <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 API | 671 | <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 daily | 674 | <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 API | 675 | <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 daily | 676 | <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 API | 678 | </div> |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 679 | <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 API | 681 | <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 daily | 685 | {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 | ||
| 710 | function 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 API | 748 | {vuln.advisory} |
| 749 | <ExternalLink size={10} /> | |
| 750 | </a> | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 751 | <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 API | 786 | </li> |
| 787 | ); | |
| 788 | } | |
| 789 | ||
| 790 | export 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 daily | 838 | |
| 839 | function 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. */ | |
| 844 | export 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 | } |