Public: anyone can read this; the crew can change it.
A repository's Findings page, what code scanning found, speaks the home page's language #9871
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/9869-a-repository-s-findings-page-what-code-s → main 2 commits fork
3 files
changed +101
−5
docs/design/areas/flows.md +2
| @@ -5,3 +5,5 @@ No new component class. A flow line is the existing `row` with one `badge`: its | ||
| 5 | 5 | The page draws them as a picture (#9595): `.flow-map` is a three-column grid, sources (`.flow-in`) on the left, the repository's name (`.flow-hub`, with `.flow-arrow` and `.flow-name`) between, followers (`.flow-out`) on the right. The columns stack below 900 px. All server-rendered, no script. |
| 6 | 6 | |
| 7 | 7 | The hub uses `--muted` for the arrows and `--fg` for the name; no accent is used on it, so the one-accent rule holds. The arrows are text glyphs and decoration only: the hub is `aria-hidden` because `repoHeader` already names the repository. Stacked, the reading order is *Flows in*, *Packages*, *Flows out*. The name wraps and the hub column is capped at 24ch so a long name cannot squeeze the sides. |
| 8 | ||
| 9 | Findings (`findingsPage()` in `src/web/pages/findings.ts`, #9869), reached from the flows view's "Code scanning" line, adds no class and has no card: the list is a `<section>` under one `<h2>`, each finding a `row()` in `<ul class="list">`, open high first, with the accept-with-reason form under it for a writer only; with none, one plain sentence, "Nothing found." The matched text is never drawn. Pinned by `test/findings-language.test.ts`. |
src/web/pages/findings.ts +9 −5
| @@ -15,7 +15,7 @@ import { crumbsFor, loadRepo, notFoundPage, pageHead, repoBase, repoHeader, repo | ||
| 15 | 15 | import { field as formField, readForm } from "../form.ts"; |
| 16 | 16 | import { html, type Html } from "../html.ts"; |
| 17 | 17 | import { flashTo, page } from "../layout.ts"; |
| 18 | import { badge, btn, card, field, listCard, row, time } from "../ui.ts"; | |
| 18 | import { badge, btn, field, row, time } from "../ui.ts"; | |
| 19 | 19 | |
| 20 | 20 | /** When a pass last ran, and whether it read everything: a partial pass says how many files it left out. */ |
| 21 | 21 | export function passWords(run: { at: string; skipped: number } | null): Html { |
| @@ -55,10 +55,14 @@ export function findingsPage(ctx: Ctx): Response { | ||
| 55 | 55 | const body = html`${repoHeader(ctx, repo, "flows")} |
| 56 | 56 | ${pageHead("Findings", { sub: "Every push is scanned for secrets and advisories; each night adds static analysis. An open high finding holds a release until a person accepts it." })} |
| 57 | 57 | <p class="muted">Last push scan ${passWords(push)} · last nightly scan ${passWords(nightly)}</p> |
| 58 | ${card(rows.length === 0 ? html`<p class="muted">Nothing found.</p>` : listCard(rows.map((f) => findingRow(f, base, writer))), { | |
| 59 | head: html`Findings`, | |
| 60 | pad: rows.length === 0, | |
| 61 | })} | |
| 58 | <section class="mt"> | |
| 59 | <h2>Findings</h2> | |
| 60 | ${rows.length === 0 | |
| 61 | ? html`<p class="muted">Nothing found.</p>` | |
| 62 | : html`<ul class="list"> | |
| 63 | ${rows.map((f) => findingRow(f, base, writer))} | |
| 64 | </ul>`} | |
| 65 | </section> | |
| 62 | 66 | <p class="mt">${btn("Back to flows", { variant: "secondary", href: `${base}/flows` })}</p>`; |
| 63 | 67 | const res = page({ ctx, title: `Findings · ${repo.name}`, crumbs: crumbsFor(repo), palette: repoPalette(ctx, repo), body }); |
| 64 | 68 | notePhase(ctx.request, "render", performance.now() - rendering); |
test/findings-language.test.ts added +90
| @@ -0,0 +1,90 @@ | ||
| 1 | /** | |
| 2 | * A repository's Findings page speaks the home page's language (#9869): the | |
| 3 | * list is a plain `<section>` under one `<h2>`, no card; with none, one plain | |
| 4 | * sentence. Accept-with-reason is there for a writer and absent for a reader, | |
| 5 | * and the matched text is never drawn. | |
| 6 | */ | |
| 7 | import { afterAll, beforeAll, describe, expect, test } from "bun:test"; | |
| 8 | import { mkdtemp, rm } from "node:fs/promises"; | |
| 9 | import { tmpdir } from "node:os"; | |
| 10 | import { join } from "node:path"; | |
| 11 | import { createRepo } from "../src/actions/repos.ts"; | |
| 12 | import { createApp, type App } from "../src/app.ts"; | |
| 13 | import { secretFindings } from "../src/checks/scan_rules.ts"; | |
| 14 | import { recordFindings } from "../src/checks/scan_store.ts"; | |
| 15 | import { createUser, type User } from "../src/core/users.ts"; | |
| 16 | import { serve, type Servers } from "../src/http/server.ts"; | |
| 17 | import { quietLogs } from "./helpers/globals.ts"; | |
| 18 | ||
| 19 | quietLogs(); | |
| 20 | ||
| 21 | const AWS = "AKIA" + "QX7Z".repeat(4); | |
| 22 | ||
| 23 | describe("findings page shape", () => { | |
| 24 | let dir: string; | |
| 25 | let forge: App; | |
| 26 | let servers: Servers; | |
| 27 | let adaCookie = ""; | |
| 28 | let eveCookie = ""; | |
| 29 | ||
| 30 | const signIn = async (username: string): Promise<string> => { | |
| 31 | const res = await fetch(`${servers.url}/signin`, { | |
| 32 | method: "POST", | |
| 33 | body: new URLSearchParams({ username, password: "a-good-passphrase" }), | |
| 34 | redirect: "manual", | |
| 35 | }); | |
| 36 | return (res.headers.get("set-cookie") ?? "").split(";")[0]!; | |
| 37 | }; | |
| 38 | const get = async (path: string, cookie: string): Promise<string> => | |
| 39 | (await fetch(`${servers.url}${path}`, { headers: cookie ? { cookie } : {} })).text(); | |
| 40 | const main = (page: string): string => page.slice(page.indexOf("<main"), page.indexOf("</main>")); | |
| 41 | ||
| 42 | beforeAll(async () => { | |
| 43 | dir = await mkdtemp(join(tmpdir(), "forge-findings-language-")); | |
| 44 | forge = createApp({ dataDir: dir, port: 0 }); | |
| 45 | servers = await serve(forge); | |
| 46 | const make = async (username: string, adminIfFirst: boolean): Promise<User> => { | |
| 47 | const made = await createUser(forge.db, { origin: "seed", trusted: true, username, email: `${username}@forge.local`, password: "a-good-passphrase", adminIfFirst }); | |
| 48 | if (!made.ok) throw new Error(made.error); | |
| 49 | return made.value; | |
| 50 | }; | |
| 51 | const ada = await make("ada", true); | |
| 52 | await make("eve", false); | |
| 53 | for (const name of ["arm", "calm"]) { | |
| 54 | const made = await createRepo(forge, ada, { name, owner: "ada", visibility: "public", description: "" }); | |
| 55 | if (!made.ok) throw new Error(JSON.stringify(made.error)); | |
| 56 | if (name === "arm") recordFindings(forge.db, made.value.id, "a".repeat(40), secretFindings("deploy.ts", `const k = "${AWS}";\n`)); | |
| 57 | } | |
| 58 | adaCookie = await signIn("ada"); | |
| 59 | eveCookie = await signIn("eve"); | |
| 60 | }, 60_000); | |
| 61 | afterAll(async () => { | |
| 62 | servers?.stop(); | |
| 63 | forge?.close(); | |
| 64 | await rm(dir, { recursive: true, force: true }); | |
| 65 | }); | |
| 66 | ||
| 67 | test("the list is a section under one h2, with no card", async () => { | |
| 68 | const body = main(await get("/ada/arm/findings", adaCookie)); | |
| 69 | expect(body).toContain("deploy.ts:1"); | |
| 70 | expect(body).not.toContain('class="card'); | |
| 71 | expect(body.match(/<h2[\s>]/g)?.length).toBe(1); | |
| 72 | expect(body).toMatch(/<section[^>]*>\s*<h2>Findings<\/h2>/); | |
| 73 | expect(body).not.toContain(AWS.slice(4)); | |
| 74 | }); | |
| 75 | ||
| 76 | test("a writer sees accept-with-reason; a reader does not", async () => { | |
| 77 | expect(main(await get("/ada/arm/findings", adaCookie))).toContain("/ada/arm/findings/"); | |
| 78 | const reader = main(await get("/ada/arm/findings", eveCookie)); | |
| 79 | expect(reader).toContain("deploy.ts:1"); | |
| 80 | expect(reader).not.toContain("/accept"); | |
| 81 | expect(reader).not.toContain("Why it may stand"); | |
| 82 | }); | |
| 83 | ||
| 84 | test("with none, one plain sentence and no box", async () => { | |
| 85 | const body = main(await get("/ada/calm/findings", adaCookie)); | |
| 86 | expect(body).toContain("Nothing found."); | |
| 87 | expect(body).not.toContain('class="card'); | |
| 88 | expect(body).not.toContain('<ul class="list"'); | |
| 89 | }); | |
| 90 | }); |