Public: anyone can read this; the crew can change it.

Sentinel findings are keyboard rows: j/k move between findings, enter opens one #10361

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/10344-sentinel-findings-are-keyboard-rows-j-k → main 3 commits fork

  • Merged. scout/10344-sentinel-findings-are-keyboard-rows-j-k was merged into main as 27d4d47.

Review

Sign in to review this change.
2 files changed +56 −1
src/web/pages/findings.ts +3 −1
@@ -31,7 +31,9 @@ function findingRow(f: FindingRow, base: string, writer: boolean): Html {
3131 const meta = f.accepted !== null
3232 ? html`${where(f)} · accepted by ${f.accepted.by} ${time(f.accepted.at)}: ${f.accepted.reason}`
3333 : html`${where(f)} · ${f.kind} · found ${time(f.foundAt)} at ${f.oid.slice(0, 12)}`;
34 const line = row({ title: html`${f.rule}`, meta, trail: badge(f.accepted !== null ? "accepted" : f.severity, { tone }) });
34 // A finding has no page of its own: a row opens the file at the commit that found it, on the line.
35 const href = `${base}/blob/${f.oid}/${f.path.split("/").map(encodeURIComponent).join("/")}${f.line > 0 ? `#L${f.line}` : ""}`;
36 const line = row({ href, nav: true, title: html`${f.rule}`, meta, trail: badge(f.accepted !== null ? "accepted" : f.severity, { tone }) });
3537 if (f.accepted !== null || !writer) return line;
3638 return html`${line}
3739 <form method="post" action="${base}/findings/${String(f.id)}/accept" class="mt">
test/keys-findings.test.ts added +53
@@ -0,0 +1,53 @@
1 /** The findings page joins the shell's j/k row traversal (#10344): one data-nav per row, none on the empty page. */
2 import { afterAll, beforeAll, expect, test } from "bun:test";
3 import { mkdtemp, rm } from "node:fs/promises";
4 import { tmpdir } from "node:os";
5 import { join } from "node:path";
6 import { createRepo } from "../src/actions/repos.ts";
7 import { createApp, type App } from "../src/app.ts";
8 import { secretFindings } from "../src/checks/scan_rules.ts";
9 import { recordFindings } from "../src/checks/scan_store.ts";
10 import { createUser } from "../src/core/users.ts";
11 import { serve, type Servers } from "../src/http/server.ts";
12
13 const AWS = "AKIA" + "ABCDEFGHIJKLMNOP";
14 let dataDir: string;
15 let app: App;
16 let servers: Servers;
17
18 beforeAll(async () => {
19 dataDir = await mkdtemp(join(tmpdir(), "forge-keys-findings-"));
20 app = createApp({ dataDir, port: 0 });
21 servers = await serve(app);
22 const made = await createUser(app.db, {
23 origin: "seed", trusted: true, username: "alice", email: "[email protected]",
24 password: "a-good-passphrase", adminIfFirst: true,
25 });
26 if (!made.ok) throw new Error(made.error);
27 const full = await createRepo(app, made.value, { name: "full", visibility: "public" });
28 const bare = await createRepo(app, made.value, { name: "bare", visibility: "public" });
29 if (!full.ok || !bare.ok) throw new Error("repo");
30 recordFindings(app.db, full.value.id, "a".repeat(40), [
31 ...secretFindings("deploy.ts", `const k = "${AWS}";\n`),
32 ...secretFindings("src/env.ts", `\n\nconst k = "${AWS}x";\n`),
33 ]);
34 });
35
36 afterAll(async () => {
37 await servers.stop();
38 await rm(dataDir, { recursive: true, force: true });
39 });
40
41 test("each finding row is one data-nav link to its file", async () => {
42 const text = await (await fetch(`${servers.url}/alice/full/findings`)).text();
43 const navs = [...text.matchAll(/<a href="([^"]+)"\s+class="row"[^>]*data-nav tabindex="-1"/g)].map((m) => m[1]);
44 expect(navs.length).toBeGreaterThan(0);
45 expect(text.match(/data-nav/g)!.length).toBe(navs.length);
46 for (const n of navs) expect(n).toMatch(/^\/alice\/full\/blob\/a{40}\/[\w./-]+#L\d+$/);
47 });
48
49 test("the empty page draws no data-nav", async () => {
50 const res = await fetch(`${servers.url}/alice/bare/findings`);
51 expect(res.status).toBe(200);
52 expect(await res.text()).not.toContain("data-nav");
53 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs