Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

advisories.tsx

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

advisories.tsxBlame372 lines · 2 contributors
f60ccdeClaude1/**
2 * Block J2 — Security advisory / dependabot-style alert routes.
3 *
4 * GET /:owner/:repo/security/advisories — list open alerts
5 * GET /:owner/:repo/security/advisories/all — dismissed + fixed too
6 * POST /:owner/:repo/security/advisories/scan — owner-only; re-scan
7 * POST /:owner/:repo/security/advisories/:id/dismiss
8 * POST /:owner/:repo/security/advisories/:id/reopen
9 */
10
11import { Hono } from "hono";
12import { and, eq } from "drizzle-orm";
13import { db } from "../db";
14import { repositories, users } from "../db/schema";
15import { Layout } from "../views/layout";
16import { RepoHeader, RepoNav } from "../views/components";
17import { softAuth, requireAuth } from "../middleware/auth";
18import type { AuthEnv } from "../middleware/auth";
19import { audit } from "../lib/notify";
20import {
21 dismissAlert,
22 listAlertsForRepo,
23 reopenAlert,
24 scanRepositoryForAlerts,
25 seedAdvisories,
26} from "../lib/advisories";
27
28const advisories = new Hono<AuthEnv>();
29advisories.use("*", softAuth);
30
31async function loadRepo(ownerName: string, repoName: string) {
32 const [owner] = await db
33 .select()
34 .from(users)
35 .where(eq(users.username, ownerName))
36 .limit(1);
37 if (!owner) return null;
38 const [repo] = await db
39 .select()
40 .from(repositories)
41 .where(
42 and(eq(repositories.ownerId, owner.id), eq(repositories.name, repoName))
43 )
44 .limit(1);
45 if (!repo) return null;
46 return { owner, repo };
47}
48
49function severityColor(sev: string): string {
50 switch (sev) {
51 case "critical":
52 return "var(--red)";
53 case "high":
54 return "var(--red)";
55 case "moderate":
56 return "var(--yellow)";
57 default:
58 return "var(--text-muted)";
59 }
60}
61
62// ---------- List ----------
63
64async function renderList(
65 c: any,
66 ownerName: string,
67 repoName: string,
68 status: "open" | "all"
69) {
70 const ctx = await loadRepo(ownerName, repoName);
71 if (!ctx) return c.notFound();
72 const { repo } = ctx;
73 const user = c.get("user");
74 if (repo.isPrivate && (!user || user.id !== repo.ownerId)) {
75 return c.notFound();
76 }
77
78 const isOwner = !!user && user.id === repo.ownerId;
79 const alerts = await listAlertsForRepo(repo.id, status);
80 const message = c.req.query("message");
81 const error = c.req.query("error");
82
83 return c.html(
84 <Layout
85 title={`Security advisories — ${ownerName}/${repoName}`}
86 user={user}
87 >
88 <RepoHeader owner={ownerName} repo={repoName} />
89 <RepoNav owner={ownerName} repo={repoName} active="code" />
90 <div class="settings-container">
91 <div style="display:flex;justify-content:space-between;align-items:center">
92 <h2 style="margin:0">Security advisories</h2>
93 {isOwner && (
94 <form
b9968e3Claude95 method="post"
f60ccdeClaude96 action={`/${ownerName}/${repoName}/security/advisories/scan`}
97 >
98 <button type="submit" class="btn btn-primary btn-sm">
99 Re-scan
100 </button>
101 </form>
102 )}
103 </div>
104 <p style="color:var(--text-muted);margin-top:8px">
105 Cross-references this repo's parsed dependency graph against a
106 curated advisory database. Run <em>Reindex</em> on{" "}
107 <a href={`/${ownerName}/${repoName}/dependencies`}>Dependencies</a>{" "}
108 first if no alerts show up.
109 </p>
110 {message && (
111 <div class="auth-success" style="margin-top:12px">
112 {decodeURIComponent(message)}
113 </div>
114 )}
115 {error && (
116 <div class="auth-error" style="margin-top:12px">
117 {decodeURIComponent(error)}
118 </div>
119 )}
120
121 <div style="display:flex;gap:8px;margin:16px 0">
122 <a
123 href={`/${ownerName}/${repoName}/security/advisories`}
124 class={`btn ${status === "open" ? "btn-primary" : ""}`}
125 >
126 Open
127 </a>
128 <a
129 href={`/${ownerName}/${repoName}/security/advisories/all`}
130 class={`btn ${status === "all" ? "btn-primary" : ""}`}
131 >
132 All
133 </a>
134 </div>
135
136 {alerts.length === 0 ? (
137 <div class="panel-empty" style="padding:24px">
138 No {status === "open" ? "open " : ""}advisories.
139 {isOwner &&
140 status === "open" &&
141 " Click Re-scan to check against the advisory database."}
142 </div>
143 ) : (
144 <div class="panel">
145 {alerts.map((a) => (
146 <div
147 class="panel-item"
148 style="flex-direction:column;align-items:stretch;gap:6px"
149 >
150 <div
151 style="display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap"
152 >
153 <div style="min-width:0">
154 <span
155 style={`font-size:10px;padding:2px 6px;border-radius:3px;background:${severityColor(
156 a.advisory.severity
157 )};color:#fff;text-transform:uppercase;margin-right:6px`}
158 >
159 {a.advisory.severity}
160 </span>
161 <span style="font-weight:600">
162 {a.advisory.summary}
163 </span>
164 </div>
165 <div
166 style="font-size:10px;padding:2px 6px;border-radius:3px;background:var(--bg-subtle);text-transform:uppercase"
167 >
168 {a.status}
169 </div>
170 </div>
171 <div
172 style="font-size:12px;color:var(--text-muted);display:flex;gap:12px;flex-wrap:wrap"
173 >
174 <span style="font-family:var(--font-mono)">
175 {a.advisory.ecosystem} · {a.dependencyName}
176 {a.dependencyVersion
177 ? ` ${a.dependencyVersion}`
178 : ""}
179 </span>
180 <span>affected: {a.advisory.affectedRange}</span>
181 {a.advisory.fixedVersion && (
182 <span>fixed in ≥ {a.advisory.fixedVersion}</span>
183 )}
184 <a
185 href={`/${ownerName}/${repoName}/blob/HEAD/${a.manifestPath}`}
186 >
187 {a.manifestPath}
188 </a>
189 {a.advisory.referenceUrl && (
190 <a
191 href={a.advisory.referenceUrl}
192 rel="noreferrer"
193 target="_blank"
194 >
195 {a.advisory.ghsaId || a.advisory.cveId || "ref"}
196 </a>
197 )}
198 </div>
199 {a.status === "dismissed" && a.dismissedReason && (
200 <div
201 style="font-size:12px;color:var(--text-muted);font-style:italic"
202 >
203 Dismissed: {a.dismissedReason}
204 </div>
205 )}
206 {isOwner && (
207 <div style="display:flex;gap:6px">
208 {a.status === "open" && (
209 <form
b9968e3Claude210 method="post"
f60ccdeClaude211 action={`/${ownerName}/${repoName}/security/advisories/${a.id}/dismiss`}
212 style="display:flex;gap:4px;align-items:center"
213 >
214 <input
215 type="text"
216 name="reason"
217 placeholder="reason (optional)"
218 maxLength={280}
2c3ba6ecopilot-swe-agent[bot]219 aria-label="Dismiss reason"
f60ccdeClaude220 style="font-size:12px;padding:4px 6px"
221 />
222 <button
223 type="submit"
224 class="btn btn-sm"
225 style="font-size:11px"
226 >
227 Dismiss
228 </button>
229 </form>
230 )}
231 {a.status === "dismissed" && (
232 <form
b9968e3Claude233 method="post"
f60ccdeClaude234 action={`/${ownerName}/${repoName}/security/advisories/${a.id}/reopen`}
235 >
236 <button
237 type="submit"
238 class="btn btn-sm"
239 style="font-size:11px"
240 >
241 Reopen
242 </button>
243 </form>
244 )}
245 </div>
246 )}
247 </div>
248 ))}
249 </div>
250 )}
251 </div>
252 </Layout>
253 );
254}
255
256advisories.get("/:owner/:repo/security/advisories", async (c) => {
257 const { owner: ownerName, repo: repoName } = c.req.param();
258 return renderList(c, ownerName, repoName, "open");
259});
260
261advisories.get("/:owner/:repo/security/advisories/all", async (c) => {
262 const { owner: ownerName, repo: repoName } = c.req.param();
263 return renderList(c, ownerName, repoName, "all");
264});
265
266// ---------- Re-scan (owner-only) ----------
267
268advisories.post(
269 "/:owner/:repo/security/advisories/scan",
270 requireAuth,
271 async (c) => {
272 const user = c.get("user")!;
273 const { owner: ownerName, repo: repoName } = c.req.param();
274 const ctx = await loadRepo(ownerName, repoName);
275 if (!ctx) return c.notFound();
276 const { repo } = ctx;
277 if (user.id !== repo.ownerId) {
278 return c.redirect(
279 `/${ownerName}/${repoName}/security/advisories?error=${encodeURIComponent(
280 "Only the repo owner can scan"
281 )}`
282 );
283 }
284 await seedAdvisories().catch(() => {});
285 const result = await scanRepositoryForAlerts(repo.id);
286 await audit({
287 userId: user.id,
288 repositoryId: repo.id,
289 action: "advisories.scan",
290 metadata: result || {},
291 });
292 const to = `/${ownerName}/${repoName}/security/advisories`;
293 if (!result) {
294 return c.redirect(
295 `${to}?error=${encodeURIComponent("Scan failed")}`
296 );
297 }
298 const msg = `Scan complete — ${result.opened} new, ${result.closed} closed, ${result.matched} total matches.`;
299 return c.redirect(`${to}?message=${encodeURIComponent(msg)}`);
300 }
301);
302
303// ---------- Dismiss / reopen (owner-only) ----------
304
305advisories.post(
306 "/:owner/:repo/security/advisories/:id/dismiss",
307 requireAuth,
308 async (c) => {
309 const user = c.get("user")!;
310 const { owner: ownerName, repo: repoName, id } = c.req.param();
311 const ctx = await loadRepo(ownerName, repoName);
312 if (!ctx) return c.notFound();
313 const { repo } = ctx;
314 if (user.id !== repo.ownerId) {
315 return c.redirect(
316 `/${ownerName}/${repoName}/security/advisories?error=${encodeURIComponent(
317 "Only the repo owner can dismiss"
318 )}`
319 );
320 }
321 const body = await c.req.parseBody();
322 const reason = String(body.reason || "").trim();
323 const ok = await dismissAlert(id, repo.id, reason);
324 await audit({
325 userId: user.id,
326 repositoryId: repo.id,
327 action: "advisories.dismiss",
328 targetId: id,
329 metadata: { reason: reason || null },
330 });
331 const to = `/${ownerName}/${repoName}/security/advisories`;
332 return c.redirect(
333 `${to}?${ok ? "message" : "error"}=${encodeURIComponent(
334 ok ? "Alert dismissed." : "Dismiss failed"
335 )}`
336 );
337 }
338);
339
340advisories.post(
341 "/:owner/:repo/security/advisories/:id/reopen",
342 requireAuth,
343 async (c) => {
344 const user = c.get("user")!;
345 const { owner: ownerName, repo: repoName, id } = c.req.param();
346 const ctx = await loadRepo(ownerName, repoName);
347 if (!ctx) return c.notFound();
348 const { repo } = ctx;
349 if (user.id !== repo.ownerId) {
350 return c.redirect(
351 `/${ownerName}/${repoName}/security/advisories?error=${encodeURIComponent(
352 "Only the repo owner can reopen"
353 )}`
354 );
355 }
356 const ok = await reopenAlert(id, repo.id);
357 await audit({
358 userId: user.id,
359 repositoryId: repo.id,
360 action: "advisories.reopen",
361 targetId: id,
362 });
363 const to = `/${ownerName}/${repoName}/security/advisories`;
364 return c.redirect(
365 `${to}?${ok ? "message" : "error"}=${encodeURIComponent(
366 ok ? "Alert reopened." : "Reopen failed"
367 )}`
368 );
369 }
370);
371
372export default advisories;