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.

168 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(
43 user,
44 repo,
45 number,
46 "dismissed",
47 reason as DismissReason,
48 String(form.get("comment") ?? "")
49 .trim()
50 .slice(0, 500),
51 );
52 return done.ok ? { ok: true } : { ok: false, error: done.error.message };
53 }
54 if (intent === "reopen") {
55 const done = await securitySuite.setCodeAlertState(user, repo, number, "open", null, "");
56 return done.ok ? { ok: true } : { ok: false, error: done.error.message };
57 }
58 return { ok: false, error: "Unknown action." };
59}
60
61export default function CodeAlertPage({ loaderData, params }: Route.ComponentProps) {
62 const { detail, can } = loaderData;
63 const { alert } = detail;
64 const base = `/${params.owner}/${params.repo}`;
65 const action = `${base}/security/code-scanning/${alert.number}`;
66 const where = alert.path ? `${alert.path}${alert.startLine ? `:${alert.startLine}` : ""}` : null;
67 return (
68 <div className="max-w-4xl space-y-6">
69 <Link to={`${base}/security/code-scanning`} className="inline-flex items-center gap-1.5 text-sm text-muted hover:text-fg">
70 <ArrowLeft size={14} /> Code scanning
71 </Link>
72 <div className="flex flex-wrap items-start justify-between gap-4">
73 <div className="min-w-0">
74 <h2 className="text-xl font-semibold tracking-tight wrap-break-word">
75 {alert.ruleName && alert.ruleName !== alert.ruleId ? alert.ruleName : shortRule(alert.ruleId)}
76 </h2>
77 <p className="mt-1.5 flex flex-wrap items-center gap-2 text-sm text-muted">
78 <span>#{alert.number}</span>
79 <SeverityBadge severity={alert.severity} />
80 <Badge tone={alert.state === "open" ? "warn" : "neutral"}>
81 {alert.state === "open" ? "Open" : alert.state === "fixed" ? "Fixed" : "Dismissed"}
82 </Badge>
83 {alert.dismissedReason && <Badge>{dismissLabel(alert.dismissedReason)}</Badge>}
84 <span>{alert.tool}</span>
85 </p>
86 </div>
87 {can.push && (
88 <div className="flex items-center gap-2">
89 {alert.state === "open" ? (
90 <DismissDialog
91 id={alert.id}
92 title={`Dismiss alert #${alert.number}`}
93 detail={`${where ?? alert.ruleId}`}
94 reasons={CODE_DISMISS_REASONS}
95 action={action}
96 />
97 ) : alert.state === "dismissed" ? (
98 <ReopenButton id={alert.id} action={action} />
99 ) : null}
100 </div>
101 )}
102 </div>
103
104 <section className={`${CARD} p-4`}>
105 <p className="text-sm">{alert.message}</p>
106 {where && (
107 <p className="mt-2 font-mono text-xs">
108 <Link to={`${base}/blob/${alert.lastCommit}/${alert.path}#L${alert.startLine ?? 1}`} className="text-fg-soft hover:underline">
109 {where}
110 </Link>
111 <span className="text-faint"> at {alert.lastCommit.slice(0, 7)}</span>
112 </p>
113 )}
114 <p className="mt-3 flex flex-wrap gap-x-4 gap-y-1 text-xs text-faint">
115 <span className="font-mono break-all">{alert.ruleId}</span>
116 <span>
117 first found <TimeAgo at={alert.createdAt} />
118 </span>
119 {alert.fixedAt && (
120 <span>
121 fixed <TimeAgo at={alert.fixedAt} />
122 </span>
123 )}
124 {alert.tags.length > 0 && <span>{alert.tags.slice(0, 6).join(", ")}</span>}
125 </p>
126 {alert.dismissedBy && (
127 <p className="mt-2 text-sm text-muted">
128 Dismissed by {alert.dismissedBy}
129 {alert.dismissedComment && <>: “{alert.dismissedComment}”</>}
130 </p>
131 )}
132 </section>
133
134 {(alert.help || alert.ruleDescription) && (
135 <section>
136 <h3 className="text-base font-semibold tracking-tight">About this rule</h3>
137 <div className="prose-g1t mt-2 text-sm text-muted [&_a]:underline [&_p]:mt-2">
138 <ReactMarkdown>{alert.help ?? alert.ruleDescription ?? ""}</ReactMarkdown>
139 </div>
140 {alert.helpUri && (
141 <a href={alert.helpUri} className="mt-2 inline-flex items-center gap-1 text-sm text-muted hover:text-fg" rel="noreferrer">
142 The rule's documentation <ExternalLink size={12} />
143 </a>
144 )}
145 </section>
146 )}
147
148 {detail.analyses.length > 0 && (
149 <section>
150 <h3 className="text-base font-semibold tracking-tight">Reported by</h3>
151 <ul className={`${CARD} mt-2 divide-y divide-line`}>
152 {detail.analyses.map((analysis) => (
153 <li key={analysis.id} className="flex flex-wrap items-center gap-x-3 gap-y-1 px-4 py-2.5 text-sm">
154 <span>{analysis.tool}</span>
155 <span className="font-mono text-xs text-muted">
156 {analysis.pull != null ? `pull request #${analysis.pull}` : analysis.gitRef.replace("refs/heads/", "")} · {analysis.commitSha.slice(0, 7)}
157 </span>
158 <span className="text-xs text-faint">
159 <TimeAgo at={analysis.createdAt} />
160 </span>
161 </li>
162 ))}
163 </ul>
164 </section>
165 )}
166 </div>
167 );
168}