Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified docs/images/webui-docs.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/webui-emitter-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/webui-emitter.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/webui-run.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/webui-terminal.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 16 additions & 1 deletion scripts/capture-webui-screenshots.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,21 @@ def button_by_text(text: str) -> str:
)


def button_starting_with(prefix: str) -> str:
"""Match on a stable prefix rather than a whole label.

The run button used to read "Start run". It now names its destination, so it
is "Run and send to 10.0.20.125:514", "Run and write to file" or "Run without
sending" depending on where the events are going. An exact match silently
stopped working and this script only failed the next time someone ran it,
which was months later.
"""
return (
"[...document.querySelectorAll('button')]"
f".find(b => b.textContent.trim().startsWith({json.dumps(prefix)}))"
)


def shot_name(view: str, theme: str) -> str:
"""`webui-emitter.png` for dark, `webui-emitter-light.png` for light.

Expand Down Expand Up @@ -285,7 +300,7 @@ async def capture_all(url: str, theme: str, views: set[str]) -> None:
await asyncio.sleep(0.8)
# No collector and no output file: the run emits to the browser
# stream and writes nothing to disk.
await page.click(button_by_text("Start run"))
await page.click(button_starting_with("Run "))
# Capture just after the plan drains. REP-004's default is 108000
# events and the useful window is narrow: at 2.2s the readout still
# showed single digits, and a looser "wait until the rate is high"
Expand Down
8 changes: 4 additions & 4 deletions webui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ export default function App() {
<button
onClick={() => setTab(id)}
className={cn(
"py-[17px] text-[13px] font-medium transition-colors",
"py-[17px] text-body font-medium transition-colors",
tab === id
? "text-foreground shadow-[inset_0_-2px_0_hsl(var(--foreground))]"
: "text-muted-foreground hover:text-foreground",
Expand All @@ -184,7 +184,7 @@ export default function App() {
<span className="text-sm font-semibold tracking-tight">Replicant</span>
{/* The environment chip is orientation, not state. It is the first thing
to go when the bar runs out of room. */}
<span className="ml-1 hidden border-l pl-2.5 font-mono text-[11px] text-text-3 lg:inline">
<span className="ml-1 hidden border-l pl-2.5 font-mono text-micro text-text-3 lg:inline">
lab · 10.20.0.0/16
</span>
</div>
Expand All @@ -200,7 +200,7 @@ export default function App() {
{config.terminal_enabled && navItem("terminal", "Terminal")}
</nav>
<div className="flex items-center gap-2.5 sm:gap-4">
<span className="flex items-center gap-2 font-mono text-[11.5px] text-text-3">
<span className="flex items-center gap-2 font-mono text-micro text-text-3">
{collector ? (
<>
<span className="h-1.5 w-1.5 flex-none rounded-full bg-signal" />
Expand Down Expand Up @@ -242,7 +242,7 @@ export default function App() {
className="flex items-center justify-between gap-3 border-b px-4 py-3 text-left transition-colors hover:bg-secondary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring lg:hidden"
>
<span className="u-label">Collector and techniques</span>
<span className="flex items-center gap-2 font-mono text-[11px] text-text-3">
<span className="flex items-center gap-2 font-mono text-micro text-text-3">
{/* Naming the selection keeps the collapsed state informative: what
is armed is the one thing you lose by closing this. */}
{selected?.id ?? "none selected"}
Expand Down
20 changes: 10 additions & 10 deletions webui/src/components/CatalogTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ export function CatalogTable({ techniques, selectedId, onSelect }: Props) {
return (
<section className="flex min-h-0 flex-col">
<div className="mb-3 flex items-baseline justify-between">
<span className="text-[13px] font-semibold">Techniques</span>
<span className="font-mono text-[10.5px] text-text-3">
<span className="text-body font-semibold">Techniques</span>
<span className="font-mono text-label text-text-3">
{filtering ? `${shown} of ${techniques.length}` : `${techniques.length} · ATT&CK`}
</span>
</div>
Expand All @@ -73,7 +73,7 @@ export function CatalogTable({ techniques, selectedId, onSelect }: Props) {
placeholder="id, name, use case, ATT&CK"
value={query}
onChange={(e) => setQuery(e.target.value)}
className="h-8 w-full rounded-md border bg-transparent pl-8 pr-2.5 font-mono text-[11.5px] placeholder:text-text-3 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
className="h-8 w-full rounded-md border bg-transparent pl-8 pr-2.5 font-mono text-micro placeholder:text-text-3 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
/>
</div>

Expand All @@ -86,7 +86,7 @@ export function CatalogTable({ techniques, selectedId, onSelect }: Props) {
onClick={() => toggleLogType(value)}
aria-pressed={on}
className={cn(
"rounded border px-1.5 py-0.5 font-mono text-[9.5px] transition-colors",
"rounded border px-1.5 py-0.5 font-mono text-micro transition-colors",
on
? "border-signal/50 bg-signal/10 text-signal"
: "text-text-3 hover:text-muted-foreground",
Expand All @@ -100,7 +100,7 @@ export function CatalogTable({ techniques, selectedId, onSelect }: Props) {

<div className="-mx-2 flex min-h-0 flex-col overflow-y-auto scroll-thin">
{groups.length === 0 && (
<p className="px-2.5 py-4 text-[12px] text-muted-foreground">
<p className="px-2.5 py-4 text-body text-muted-foreground">
No techniques match that filter.
</p>
)}
Expand All @@ -118,7 +118,7 @@ export function CatalogTable({ techniques, selectedId, onSelect }: Props) {
className={cn("h-3 w-3 text-text-4 transition-transform", open && "rotate-90")}
/>
<span className="u-label flex-1">{tacticLabel(group.tactic)}</span>
<span className="font-mono text-[9.5px] text-text-3">
<span className="font-mono text-micro text-text-3">
{group.techniques.length}
</span>
</button>
Expand All @@ -139,17 +139,17 @@ export function CatalogTable({ techniques, selectedId, onSelect }: Props) {
{sel && (
<span className="absolute bottom-2 left-0 top-2 w-0.5 rounded bg-foreground" />
)}
<span className="col-start-1 row-start-1 text-[13px] font-medium">
<span className="col-start-1 row-start-1 text-body font-medium">
{t.name}
</span>
<span className="col-start-2 row-start-1 justify-self-end font-mono text-[9.5px] text-text-3">
<span className="col-start-2 row-start-1 justify-self-end font-mono text-micro text-text-3">
{t.attack[0] ?? ""}
</span>
<span className="col-start-1 row-start-2 font-mono text-[10.5px] text-text-3">
<span className="col-start-1 row-start-2 font-mono text-label text-text-3">
{t.id} · {logTypeOf(t)}
</span>
{!t.implemented && (
<span className="col-start-2 row-start-2 justify-self-end font-mono text-[9px] font-semibold uppercase tracking-wide text-signal">
<span className="col-start-2 row-start-2 justify-self-end font-mono text-micro font-semibold uppercase tracking-wide text-signal">
soon
</span>
)}
Expand Down
28 changes: 14 additions & 14 deletions webui/src/components/ConnectionCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,8 @@ export function ConnectionCard({
return (
<section className="rounded-lg border bg-card p-4">
<div className="mb-3.5 flex items-baseline justify-between">
<span className="text-[13px] font-semibold">Collector</span>
<span className={cn("font-mono text-[10.5px]", BADGE[badge].tone)}>{BADGE[badge].label}</span>
<span className="text-body font-semibold">Collector</span>
<span className={cn("font-mono text-label", BADGE[badge].tone)}>{BADGE[badge].label}</span>
</div>

<div className="u-label mb-1.5">Vendor profile</div>
Expand All @@ -145,7 +145,7 @@ export function ConnectionCard({
aria-checked={v === vendor}
onClick={() => onVendorChange(v)}
className={cn(
"h-7 flex-1 rounded-[5px] text-[12px] font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
"h-7 flex-1 whitespace-nowrap rounded-[5px] text-label font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
v === vendor
? "bg-elev text-foreground shadow-sm"
: "text-text-3 hover:text-foreground",
Expand All @@ -163,7 +163,7 @@ export function ConnectionCard({
</label>
<Input
id="host"
className="h-9 font-mono text-[13px]"
className="h-9 font-mono text-data"
value={host}
onChange={(e) => setHost(e.target.value)}
/>
Expand All @@ -174,15 +174,15 @@ export function ConnectionCard({
</label>
<Input
id="port"
className="h-9 font-mono text-[13px]"
className="h-9 font-mono text-data"
value={port}
onChange={(e) => setPort(e.target.value)}
/>
</div>
<div>
<label className="u-label mb-1.5 block">Trans</label>
<Select value={transport} onValueChange={setTransport}>
<SelectTrigger className="h-9 font-mono text-[13px]">
<SelectTrigger className="h-9 font-mono text-data">
<SelectValue />
</SelectTrigger>
<SelectContent>
Expand All @@ -198,7 +198,7 @@ export function ConnectionCard({
<div className="mt-3 grid grid-cols-[auto_1fr] items-end gap-3">
<div className="flex items-center gap-2 pb-2">
<Switch id="tlsverify" checked={tlsVerify} onCheckedChange={setTlsVerify} />
<label htmlFor="tlsverify" className="whitespace-nowrap text-[12.5px] text-muted-foreground">
<label htmlFor="tlsverify" className="whitespace-nowrap text-body text-muted-foreground">
Verify cert
</label>
</div>
Expand All @@ -208,7 +208,7 @@ export function ConnectionCard({
</label>
<Input
id="cafile"
className="h-9 font-mono text-[12px]"
className="h-9 font-mono text-data"
value={tlsCafile}
onChange={(e) => setTlsCafile(e.target.value)}
placeholder="/path/to/ca.pem"
Expand All @@ -221,15 +221,15 @@ export function ConnectionCard({
<button
onClick={handleTest}
disabled={busy || !host}
className="h-8 rounded-md border px-3 text-[12.5px] font-medium text-muted-foreground transition-colors hover:text-foreground disabled:opacity-40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
className="h-8 rounded-md border px-3 text-body font-medium text-muted-foreground transition-colors hover:text-foreground disabled:opacity-40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
{busy ? "Sending…" : "Send test log"}
</button>
<span className="font-mono text-[11px] text-text-3">cap {epsCap} eps</span>
<span className="font-mono text-micro text-text-3">cap {epsCap} eps</span>
</div>

{error && (
<div className="mt-3 flex items-start gap-2 rounded-md border border-destructive/40 bg-destructive/10 p-2.5 text-[11.5px] leading-relaxed text-destructive">
<div className="mt-3 flex items-start gap-2 rounded-md border border-destructive/40 bg-destructive/10 p-2.5 text-label leading-relaxed text-destructive">
<X className="mt-0.5 h-3.5 w-3.5 shrink-0" />
<span>{error}</span>
</div>
Expand All @@ -239,7 +239,7 @@ export function ConnectionCard({
<div
data-testid="connect-report"
className={cn(
"mt-3 space-y-2 rounded-md border p-2.5 text-[11.5px] leading-relaxed",
"mt-3 space-y-2 rounded-md border p-2.5 text-label leading-relaxed",
report.verdict === "refused" ||
report.verdict === "failed" ||
report.verdict === "name_not_resolved"
Expand All @@ -253,7 +253,7 @@ export function ConnectionCard({
as perfectly ordinary, and it survived two lab sessions and a dozen
log lines. Beside its own source address it does not. */}
{report.source && (
<div className="font-mono text-[11px]">
<div className="font-mono text-micro">
{report.source} <span className="text-text-3">-&gt;</span> {report.host}:
{report.port}
{report.interface ? (
Expand Down Expand Up @@ -284,7 +284,7 @@ export function ConnectionCard({
)}

{stale && (
<div className="mt-3 rounded-md border p-2.5 text-[11.5px] leading-relaxed text-text-3">
<div className="mt-3 rounded-md border p-2.5 text-label leading-relaxed text-text-3">
The target changed since the last test. Send a test log to describe this one.
</div>
)}
Expand Down
4 changes: 2 additions & 2 deletions webui/src/components/DocsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ export function DocsView() {
disabled={!page.available}
aria-current={page.id === current ? "true" : undefined}
className={cn(
"whitespace-nowrap rounded-md px-2.5 py-2 text-left text-[12.5px] transition-colors hover:bg-secondary disabled:opacity-40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
"whitespace-nowrap rounded-md px-2.5 py-2 text-left text-body transition-colors hover:bg-secondary disabled:opacity-40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
page.id === current && "bg-secondary font-medium",
)}
>
Expand All @@ -148,7 +148,7 @@ export function DocsView() {

<article className="min-h-0 flex-1 overflow-y-auto scroll-thin px-4 py-5 sm:px-8 sm:py-6">
{error && (
<div className="rounded-md border border-destructive/40 bg-destructive/10 p-2.5 text-[12px] text-destructive">
<div className="rounded-md border border-destructive/40 bg-destructive/10 p-2.5 text-body text-destructive">
{error}
</div>
)}
Expand Down
10 changes: 5 additions & 5 deletions webui/src/components/LogsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ export function LogsView() {
title={option.hint}
aria-pressed={level === option.id}
className={cn(
"rounded-sm border px-2.5 py-1 text-[12px] transition-colors",
"rounded-sm border px-2.5 py-1 text-body transition-colors",
level === option.id
? "border-signal/60 bg-signal/10 text-foreground"
: "text-muted-foreground hover:text-foreground",
Expand All @@ -159,7 +159,7 @@ export function LogsView() {
</div>

<div className="ml-auto flex items-center gap-2">
<span className="font-mono text-[11.5px] text-text-3">
<span className="font-mono text-micro text-text-3">
{counts.total} line{counts.total === 1 ? "" : "s"}
{counts.warnings > 0 && (
<span className="text-signal"> · {counts.warnings} warning</span>
Expand All @@ -186,7 +186,7 @@ export function LogsView() {
</div>

{error && (
<p className="rounded-sm border border-destructive/40 px-2.5 py-1.5 text-[12px] text-destructive">
<p className="rounded-sm border border-destructive/40 px-2.5 py-1.5 text-body text-destructive">
{error}
</p>
)}
Expand All @@ -196,7 +196,7 @@ export function LogsView() {
onScroll={onScroll}
// min-w-0 so one long line scrolls inside this box instead of stretching
// the page. See docs/webui-reskin-design.md section 5.
className="min-w-0 flex-1 overflow-auto rounded-sm border bg-card p-2.5 font-mono text-[12px] leading-[1.55]"
className="min-w-0 flex-1 overflow-auto rounded-sm border bg-card p-2.5 font-mono text-data leading-[1.55]"
>
{entries.length === 0 ? (
<p className="text-text-3">
Expand Down Expand Up @@ -226,7 +226,7 @@ export function LogsView() {
const node = bodyRef.current;
if (node) node.scrollTop = node.scrollHeight;
}}
className="self-center rounded-sm border px-2.5 py-1 text-[12px] text-muted-foreground hover:text-foreground"
className="self-center rounded-sm border px-2.5 py-1 text-body text-muted-foreground hover:text-foreground"
>
Jump to latest
</button>
Expand Down
Loading
Loading