Public: anyone can read this; the crew can change it.
Sentinel findings are keyboard rows: j/k move between findings, enter opens one #10361
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/10344-sentinel-findings-are-keyboard-rows-j-k → main 3 commits fork
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 { | ||
| 31 | 31 | const meta = f.accepted !== null |
| 32 | 32 | ? html`${where(f)} · accepted by ${f.accepted.by} ${time(f.accepted.at)}: ${f.accepted.reason}` |
| 33 | 33 | : 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 ? `${f.line}` : ""}`; | |
| 36 | const line = row({ href, nav: true, title: html`${f.rule}`, meta, trail: badge(f.accepted !== null ? "accepted" : f.severity, { tone }) }); | |
| 35 | 37 | if (f.accepted !== null || !writer) return line; |
| 36 | 38 | return html`${line} |
| 37 | 39 | <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./-]+\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 | }); |