From 6f306b5fcb144ba7a8b8d067b07dcd78542f2147 Mon Sep 17 00:00:00 2001 From: george-keygraph Date: Wed, 7 Oct 2026 18:09:28 -0700 Subject: [PATCH] fix(report): lay out the PDF cover like the Keygraph report design Put the wordmark and an outlined classification tag in a header row over a hair rule, set the target as a labeled line under the title, and move the metadata into Date, Status and Tester columns above the handling note. The tester column carries the "AI Pentester by Keygraph" line, linked to keygraph.io. Drops the kv helper, which only the old cover used. Co-Authored-By: Claude Opus 5.5 --- apps/worker/templates/typst/report.typ | 95 +++++++++++--------------- 1 file changed, 40 insertions(+), 55 deletions(-) diff --git a/apps/worker/templates/typst/report.typ b/apps/worker/templates/typst/report.typ index b4b7fc42..361d34d8 100644 --- a/apps/worker/templates/typst/report.typ +++ b/apps/worker/templates/typst/report.typ @@ -215,43 +215,31 @@ // Render an array of strings as a bulleted list with inline-code support. #let code-list(items) = list(..items.map(inline-code)) -#let kv(label, value) = grid( - columns: (auto, 1fr), - column-gutter: 14pt, - row-gutter: 4pt, - text(fill: muted, size: 9.5pt)[#label], - value, -) - // ---------- COVER PAGE ------------------------------------------------------ +#let cover-label(s) = text(size: 6.5pt, fill: muted, weight: "semibold", tracking: 1.2pt)[#upper(s)] +#let cover-field(label, value) = stack(spacing: 5pt, cover-label(label), value) + #page(header: none, footer: none)[ #set align(left) - #v(3.2cm) - #let brand-parts = brand.split("|").map(p => p.trim()) - // The logo is the Keygraph wordmark (about 5:1), so it is sized by height to - // sit level with the two-line brand text beside it. - #grid( - columns: (auto, 1fr), - column-gutter: 12pt, - align: (horizon, horizon), - link(keygraph-url, image("/assets/keygraph-logo.png", height: 0.5cm)), - { - set par(leading: 0.6em) - text(size: 11pt, fill: ink, weight: "semibold", tracking: 1.2pt)[ - #upper(brand-parts.at(0)) - ] - if brand-parts.len() > 1 { - linebreak() - link(keygraph-url, text(size: 9pt, fill: muted, weight: "regular")[ - #brand-parts.slice(1).join(" ") - ]) - } - } - ) - #set par(leading: 0.7em) - #v(1.6cm) + // Header: the Keygraph wordmark (about 5:1, sized by height) and the + // classification as an outlined tag, over a hair rule. + #grid( + columns: (1fr, auto), + align: (left + horizon, right + horizon), + link(keygraph-url, image("/assets/keygraph-logo.png", height: 0.45cm)), + box( + stroke: 0.8pt + ink, + radius: 1.5pt, + inset: (x: 5pt, y: 2.5pt), + text(size: 6.5pt, fill: ink, weight: "semibold", tracking: 1.4pt)[#upper(data.meta.classification)], + ), + ) + #v(0.5cm) + #line(length: 100%, stroke: 0.3pt + rule) + + #v(3.6cm) #set par(leading: 0.4em) #text(size: 46pt, weight: "bold", fill: ink)[ Security\ @@ -260,37 +248,34 @@ ] #set par(leading: 0.7em) + #v(0.9cm) + #cover-field("Target", text(size: 12pt, fill: ink)[#data.meta.target]) + #if "application" in data.meta and data.meta.application != none [ + #v(0.5cm) + #cover-field("Application", text(size: 12pt, fill: ink)[#data.meta.application]) + ] + #v(1fr) #line(length: 100%, stroke: 0.3pt + rule) - #v(0.6cm) + #v(0.9cm) #grid( - columns: (1fr, 1fr), - column-gutter: 28pt, - row-gutter: 14pt, - grid( - columns: (auto, 1fr), - column-gutter: 18pt, - row-gutter: 14pt, - text(fill: muted, size: 9pt)[Target], text(size: 10pt)[#inline-code(data.meta.target)], - text(fill: muted, size: 9pt)[Date], text(size: 10pt)[#data.meta.assessmentDate], - ..(if "application" in data.meta and data.meta.application != none { - (text(fill: muted, size: 9pt)[Application], text(size: 10pt)[#inline-code(data.meta.application)]) + columns: (1fr, 1fr, 1fr), + column-gutter: 18pt, + cover-field("Date", text(size: 9.5pt)[#data.meta.assessmentDate]), + cover-field("Status", text(size: 9.5pt)[#assessment-status]), + cover-field("Tester", stack( + spacing: 4pt, + text(size: 9.5pt)[#tester-override], + ..(if brand-parts.len() > 1 { + (link(keygraph-url, text(size: 8pt, fill: muted)[#brand-parts.slice(1).join(" ")]),) } else { () }), - ), - grid( - columns: (auto, 1fr), - column-gutter: 18pt, - row-gutter: 14pt, - text(fill: muted, size: 9pt)[Tester], text(size: 10pt)[#tester-override], - text(fill: muted, size: 9pt)[Classification], - text(size: 10pt, weight: "semibold")[#data.meta.classification], - ), + )), ) - #v(0.8cm) - #text(size: 8pt, fill: muted)[ + #v(1cm) + #text(size: 7pt, fill: muted)[ This document contains sensitive security findings. Handle in accordance with your organization's data classification policy. ]