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

Merged opened this

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

Merged

scout/forge:scout/9869-a-repository-s-findings-page-what-code-s → main 2 commits fork

  • Merged. scout/9869-a-repository-s-findings-page-what-code-s was merged into main as b403272.

Review

Sign in to review this change.
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
55 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.
66
77 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
1515 import { field as formField, readForm } from "../form.ts";
1616 import { html, type Html } from "../html.ts";
1717 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";
1919
2020 /** When a pass last ran, and whether it read everything: a partial pass says how many files it left out. */
2121 export function passWords(run: { at: string; skipped: number } | null): Html {
@@ -55,10 +55,14 @@ export function findingsPage(ctx: Ctx): Response {
5555 const body = html`${repoHeader(ctx, repo, "flows")}
5656 ${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." })}
5757 <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>
6266 <p class="mt">${btn("Back to flows", { variant: "secondary", href: `${base}/flows` })}</p>`;
6367 const res = page({ ctx, title: `Findings · ${repo.name}`, crumbs: crumbsFor(repo), palette: repoPalette(ctx, repo), body });
6468 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 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs