Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
149 linesCodeBlameRaw
1import { ArrowLeft, ExternalLink } from "lucide-react";
2import ReactMarkdown from "react-markdown";
3import { Link } from "react-router";
4
5import { CODE_DISMISS_REASONS, type DismissReason, dismissLabel } from "@g1t/contracts";
6
7import type { Route } from "./+types/security-code-alert";
8import { page } from "../../lib/meta";
9import { DismissDialog, ReopenButton, SeverityBadge } from "../../components/security";
10import { CARD, shortRule } from "../../components/security-suite";
11import { TimeAgo } from "../../components/ui";
12import { Badge } from "../../components/ui/badge";
13import { securitySuite } from "../../lib/services.server";
14import { assertSameOrigin, getViewer, requireUser, unwrap } from "../../lib/session.server";
15import { refusal, requireInsider } from "../../lib/access.server";
16
17export function meta({ params, ...args }: Route.MetaArgs) {
18 return page(args, { title: `Code scanning alert #${params.number} · ${params.owner}/${params.repo} · g1t` });
19}
20
21export async function loader({ params, context, request }: Route.LoaderArgs) {
22 const viewer = getViewer(context) ?? requireUser(context, request);
23 const { access } = await requireInsider(context, params, "security_alerts");
24 const repo = { namespace: params.owner, name: params.repo };
25 return { detail: unwrap(await securitySuite.codeAlert(repo, Number(params.number) || 0, viewer)), can: access.can };
26}
27
28const REASONS = new Set(["false_positive", "wont_fix", "used_in_tests"]);
29
30export async function action({ params, context, request }: Route.ActionArgs) {
31 assertSameOrigin(request);
32 const user = requireUser(context, request);
33 const repo = { namespace: params.owner, name: params.repo };
34 const form = await request.formData();
35 const intent = String(form.get("intent") ?? "");
36 const refused = await refusal(context, params, "security_alerts");
37 if (refused) return { ok: false, error: refused };
38 const number = Number(params.number) || 0;
39 if (intent === "dismiss") {
40 const reason = String(form.get("reason") ?? "");
41 if (!REASONS.has(reason)) return { ok: false, error: "Choose a reason." };
42 const done = await securitySuite.setCodeAlertState(user, repo, number, "dismissed", reason as DismissReason, String(form.get("comment") ?? "").trim().slice(0, 500));
43 return done.ok ? { ok: true } : { ok: false, error: done.error.message };
44 }
45 if (intent === "reopen") {
46 const done = await securitySuite.setCodeAlertState(user, repo, number, "open", null, "");
47 return done.ok ? { ok: true } : { ok: false, error: done.error.message };
48 }
49 return { ok: false, error: "Unknown action." };
50}
51
52export default function CodeAlertPage({ loaderData, params }: Route.ComponentProps) {
53 const { detail, can } = loaderData;
54 const { alert } = detail;
55 const base = `/${params.owner}/${params.repo}`;
56 const action = `${base}/security/code-scanning/${alert.number}`;
57 const where = alert.path ? `${alert.path}${alert.startLine ? `:${alert.startLine}` : ""}` : null;
58 return (
59 <div className="max-w-4xl space-y-6">
60 <Link to={`${base}/security/code-scanning`} className="inline-flex items-center gap-1.5 text-sm text-muted hover:text-fg">
61 <ArrowLeft size={14} /> Code scanning
62 </Link>
63 <div className="flex flex-wrap items-start justify-between gap-4">
64 <div className="min-w-0">
65 <h2 className="text-xl font-semibold tracking-tight break-words">{alert.ruleName && alert.ruleName !== alert.ruleId ? alert.ruleName : shortRule(alert.ruleId)}</h2>
66 <p className="mt-1.5 flex flex-wrap items-center gap-2 text-sm text-muted">
67 <span>#{alert.number}</span>
68 <SeverityBadge severity={alert.severity} />
69 <Badge tone={alert.state === "open" ? "warn" : "neutral"}>{alert.state === "open" ? "Open" : alert.state === "fixed" ? "Fixed" : "Dismissed"}</Badge>
70 {alert.dismissedReason && <Badge>{dismissLabel(alert.dismissedReason)}</Badge>}
71 <span>{alert.tool}</span>
72 </p>
73 </div>
74 {can.push && (
75 <div className="flex items-center gap-2">
76 {alert.state === "open" ? (
77 <DismissDialog id={alert.id} title={`Dismiss alert #${alert.number}`} detail={`${where ?? alert.ruleId}`} reasons={CODE_DISMISS_REASONS} action={action} />
78 ) : alert.state === "dismissed" ? (
79 <ReopenButton id={alert.id} action={action} />
80 ) : null}
81 </div>
82 )}
83 </div>
84
85 <section className={`${CARD} p-4`}>
86 <p className="text-sm">{alert.message}</p>
87 {where && (
88 <p className="mt-2 font-mono text-xs">
89 <Link to={`${base}/blob/${alert.lastCommit}/${alert.path}#L${alert.startLine ?? 1}`} className="text-fg-soft hover:underline">
90 {where}
91 </Link>
92 <span className="text-faint"> at {alert.lastCommit.slice(0, 7)}</span>
93 </p>
94 )}
95 <p className="mt-3 flex flex-wrap gap-x-4 gap-y-1 text-xs text-faint">
96 <span className="font-mono break-all">{alert.ruleId}</span>
97 <span>
98 first found <TimeAgo at={alert.createdAt} />
99 </span>
100 {alert.fixedAt && (
101 <span>
102 fixed <TimeAgo at={alert.fixedAt} />
103 </span>
104 )}
105 {alert.tags.length > 0 && <span>{alert.tags.slice(0, 6).join(", ")}</span>}
106 </p>
107 {alert.dismissedBy && (
108 <p className="mt-2 text-sm text-muted">
109 Dismissed by {alert.dismissedBy}
110 {alert.dismissedComment && <>: “{alert.dismissedComment}”</>}
111 </p>
112 )}
113 </section>
114
115 {(alert.help || alert.ruleDescription) && (
116 <section>
117 <h3 className="text-base font-semibold tracking-tight">About this rule</h3>
118 <div className="prose-g1t mt-2 text-sm text-muted [&_a]:underline [&_p]:mt-2">
119 <ReactMarkdown>{alert.help ?? alert.ruleDescription ?? ""}</ReactMarkdown>
120 </div>
121 {alert.helpUri && (
122 <a href={alert.helpUri} className="mt-2 inline-flex items-center gap-1 text-sm text-muted hover:text-fg" rel="noreferrer">
123 The rule's documentation <ExternalLink size={12} />
124 </a>
125 )}
126 </section>
127 )}
128
129 {detail.analyses.length > 0 && (
130 <section>
131 <h3 className="text-base font-semibold tracking-tight">Reported by</h3>
132 <ul className={`${CARD} mt-2 divide-y divide-line`}>
133 {detail.analyses.map((analysis) => (
134 <li key={analysis.id} className="flex flex-wrap items-center gap-x-3 gap-y-1 px-4 py-2.5 text-sm">
135 <span>{analysis.tool}</span>
136 <span className="font-mono text-xs text-muted">
137 {analysis.pull != null ? `pull request #${analysis.pull}` : analysis.gitRef.replace("refs/heads/", "")} · {analysis.commitSha.slice(0, 7)}
138 </span>
139 <span className="text-xs text-faint">
140 <TimeAgo at={analysis.createdAt} />
141 </span>
142 </li>
143 ))}
144 </ul>
145 </section>
146 )}
147 </div>
148 );
149}