@sverka/ui 0.2.24 → 0.2.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +58 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +229 -4
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
package/dist/index.d.mts
CHANGED
|
@@ -23,5 +23,62 @@ export declare function startUiServer(options: UiServerOptions): Promise<UiServe
|
|
|
23
23
|
/** Render the dashboard page listing available SARIF files. */
|
|
24
24
|
export declare function renderDashboard(artifactsDir: string, files: readonly string[]): string;
|
|
25
25
|
//#endregion
|
|
26
|
-
|
|
26
|
+
//#region src/hub-pages.d.ts
|
|
27
|
+
/** A run summary row as the hub serves it (mirrors HubRunSummary). */
|
|
28
|
+
interface HubRunRow {
|
|
29
|
+
readonly runId: string;
|
|
30
|
+
readonly project: string;
|
|
31
|
+
readonly entry: string;
|
|
32
|
+
readonly status: string;
|
|
33
|
+
readonly startedAt: number | null;
|
|
34
|
+
readonly durationMs: number | null;
|
|
35
|
+
readonly findingCounts: {
|
|
36
|
+
readonly total: number;
|
|
37
|
+
readonly bySeverity: Record<string, number>;
|
|
38
|
+
};
|
|
39
|
+
readonly policyVerdict?: string | null;
|
|
40
|
+
}
|
|
41
|
+
/** A stored run detail (report payload + findings). */
|
|
42
|
+
interface HubRunView extends HubRunRow {
|
|
43
|
+
readonly report: Record<string, unknown>;
|
|
44
|
+
readonly findings: readonly unknown[];
|
|
45
|
+
readonly uploadedAt: number;
|
|
46
|
+
}
|
|
47
|
+
/** A flaky-step aggregate row (GET /v1/flaky). */
|
|
48
|
+
interface HubFlakyRow {
|
|
49
|
+
readonly stepId: string;
|
|
50
|
+
readonly successRate: number;
|
|
51
|
+
readonly runs: number;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Run list page — one row per stored run for a project, newest first.
|
|
55
|
+
*/
|
|
56
|
+
export declare function renderHubRunList(opts: {
|
|
57
|
+
readonly project: string;
|
|
58
|
+
readonly runs: readonly HubRunRow[];
|
|
59
|
+
}): string;
|
|
60
|
+
/**
|
|
61
|
+
* Run detail page — run metadata + per-step table + findings embedded
|
|
62
|
+
* via the SARIF report renderer (iframe to the findings sub-document).
|
|
63
|
+
*/
|
|
64
|
+
export declare function renderHubRunDetail(opts: {
|
|
65
|
+
readonly run: HubRunView;
|
|
66
|
+
}): string;
|
|
67
|
+
/**
|
|
68
|
+
* Flaky-step view — per-step success rate across the last N runs,
|
|
69
|
+
* worst first. Rows come from `GET /v1/flaky/{project}`.
|
|
70
|
+
*/
|
|
71
|
+
export declare function renderHubFlaky(opts: {
|
|
72
|
+
readonly project: string;
|
|
73
|
+
readonly rows: readonly HubFlakyRow[];
|
|
74
|
+
readonly window: number;
|
|
75
|
+
}): string;
|
|
76
|
+
/**
|
|
77
|
+
* Projects index — links to each project's run list.
|
|
78
|
+
*/
|
|
79
|
+
export declare function renderHubIndex(opts: {
|
|
80
|
+
readonly projects: readonly string[];
|
|
81
|
+
}): string;
|
|
82
|
+
//#endregion
|
|
83
|
+
export type { HubFlakyRow, HubRunRow, HubRunView, UiServer, UiServerOptions };
|
|
27
84
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/server.ts","../src/dashboard.ts"],"mappings":";UAyBiB;;WAEN;;WAEA;;WAEA;;UAGM;WACN;WACA;EACT;;;;;;;wBAkBc,cAAc,SAAS,kBAAkB,QAAQ;;;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/server.ts","../src/dashboard.ts","../src/hub-pages.ts"],"mappings":";UAyBiB;;WAEN;;WAEA;;WAEA;;UAGM;WACN;WACA;EACT;;;;;;;wBAkBc,cAAc,SAAS,kBAAkB,QAAQ;;;;wBClDjD,gBACd,sBACA;;;;UCEe;WACN;WACA;WACA;WACA;WACA;WACA;WACA;aACE;aACA,YAAY;;WAEd;;;UAIM,mBAAmB;WACzB,QAAQ;WACR;WACA;;;UAIM;WACN;WACA;WACA;;;;;wBAqCK,iBAAiB;WACtB;WACA,eAAe;;;;;;wBAgFV,mBAAmB;WAAiB,KAAK;;;;;;wBAqCzC,eAAe;WACpB;WACA,eAAe;WACf;;;;;wBAgCK,eAAe;WACpB"}
|
package/dist/index.mjs
CHANGED
|
@@ -4,6 +4,86 @@ import { existsSync, readFileSync, readdirSync, realpathSync } from "node:fs";
|
|
|
4
4
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { normalizeSarif } from "@sverka/verification";
|
|
6
6
|
import { generateSarifHtml } from "@sverka/sarif-viewer-web";
|
|
7
|
+
//#region src/escape.ts
|
|
8
|
+
function escapeHtml(text) {
|
|
9
|
+
return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
10
|
+
}
|
|
11
|
+
/** Shared dark theme — same palette as the SARIF dashboard. */
|
|
12
|
+
const PAGE_CSS = String.raw`
|
|
13
|
+
:root { color-scheme: dark; }
|
|
14
|
+
* { box-sizing: border-box; }
|
|
15
|
+
body {
|
|
16
|
+
margin: 0;
|
|
17
|
+
padding: 0;
|
|
18
|
+
background: #0d1117;
|
|
19
|
+
color: #c9d1d9;
|
|
20
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
21
|
+
line-height: 1.6;
|
|
22
|
+
min-height: 100vh;
|
|
23
|
+
display: flex;
|
|
24
|
+
flex-direction: column;
|
|
25
|
+
}
|
|
26
|
+
header {
|
|
27
|
+
padding: 1.5rem 2rem;
|
|
28
|
+
background: #161b22;
|
|
29
|
+
border-bottom: 1px solid #30363d;
|
|
30
|
+
}
|
|
31
|
+
header h1 { margin: 0 0 0.5rem 0; font-size: 1.5rem; color: #f0f6fc; }
|
|
32
|
+
header nav { font-size: 0.875rem; }
|
|
33
|
+
header nav a { color: #58a6ff; text-decoration: none; margin-right: 1rem; }
|
|
34
|
+
header nav a:hover { text-decoration: underline; }
|
|
35
|
+
.path { font-size: 0.875rem; color: #8b949e; margin: 0; }
|
|
36
|
+
main { flex: 1; padding: 2rem; }
|
|
37
|
+
h2 { font-size: 1.25rem; color: #f0f6fc; margin: 0 0 1rem 0; }
|
|
38
|
+
table { border-collapse: collapse; width: 100%; font-size: 0.9rem; }
|
|
39
|
+
th, td { text-align: left; padding: 0.5rem 0.75rem; border-bottom: 1px solid #21262d; }
|
|
40
|
+
th { color: #8b949e; font-weight: 600; }
|
|
41
|
+
tr:hover td { background: #161b22; }
|
|
42
|
+
a { color: #58a6ff; text-decoration: none; }
|
|
43
|
+
a:hover { text-decoration: underline; }
|
|
44
|
+
code { background: #21262d; padding: 0.1rem 0.3rem; border-radius: 3px; }
|
|
45
|
+
.empty { color: #8b949e; padding: 2rem; text-align: center; }
|
|
46
|
+
.ok { color: #3fb950; }
|
|
47
|
+
.fail { color: #f85149; }
|
|
48
|
+
.warn { color: #d29922; }
|
|
49
|
+
.muted { color: #8b949e; }
|
|
50
|
+
footer { padding: 1rem 2rem; border-top: 1px solid #21262d; text-align: center; }
|
|
51
|
+
footer a { color: #58a6ff; text-decoration: none; }
|
|
52
|
+
iframe.findings {
|
|
53
|
+
width: 100%;
|
|
54
|
+
min-height: 32rem;
|
|
55
|
+
border: 1px solid #30363d;
|
|
56
|
+
border-radius: 6px;
|
|
57
|
+
background: #fff;
|
|
58
|
+
}
|
|
59
|
+
`;
|
|
60
|
+
/** Wrap page body in the shared shell (doctype, head, header, footer). */
|
|
61
|
+
function pageShell(opts) {
|
|
62
|
+
const nav = (opts.nav ?? []).map((n) => `<a href="${escapeHtml(n.href)}">${escapeHtml(n.label)}</a>`).join("");
|
|
63
|
+
return `<!DOCTYPE html>
|
|
64
|
+
<html lang="en">
|
|
65
|
+
<head>
|
|
66
|
+
<meta charset="utf-8">
|
|
67
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
68
|
+
<title>${escapeHtml(opts.title)}</title>
|
|
69
|
+
<style>${PAGE_CSS}</style>
|
|
70
|
+
</head>
|
|
71
|
+
<body>
|
|
72
|
+
<header>
|
|
73
|
+
<h1>${escapeHtml(opts.title)}</h1>
|
|
74
|
+
${opts.subtitle !== void 0 ? `<p class="path">${opts.subtitle}</p>` : ""}
|
|
75
|
+
${nav === "" ? "" : `<nav>${nav}</nav>`}
|
|
76
|
+
</header>
|
|
77
|
+
<main>
|
|
78
|
+
${opts.body}
|
|
79
|
+
</main>
|
|
80
|
+
<footer>
|
|
81
|
+
<a href="https://sverka.dev">sverka.dev</a>
|
|
82
|
+
</footer>
|
|
83
|
+
</body>
|
|
84
|
+
</html>`;
|
|
85
|
+
}
|
|
86
|
+
//#endregion
|
|
7
87
|
//#region src/dashboard.ts
|
|
8
88
|
/** Render the dashboard page listing available SARIF files. */
|
|
9
89
|
function renderDashboard(artifactsDir, files) {
|
|
@@ -33,9 +113,6 @@ function renderDashboard(artifactsDir, files) {
|
|
|
33
113
|
</body>
|
|
34
114
|
</html>`;
|
|
35
115
|
}
|
|
36
|
-
function escapeHtml(text) {
|
|
37
|
-
return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
38
|
-
}
|
|
39
116
|
const CSS = String.raw`
|
|
40
117
|
:root { color-scheme: dark; }
|
|
41
118
|
* { box-sizing: border-box; }
|
|
@@ -243,6 +320,154 @@ function collectSarifFiles(root, dir, out) {
|
|
|
243
320
|
}
|
|
244
321
|
}
|
|
245
322
|
//#endregion
|
|
246
|
-
|
|
323
|
+
//#region src/hub-pages.ts
|
|
324
|
+
function formatTs(epochMs) {
|
|
325
|
+
if (epochMs === null) return "—";
|
|
326
|
+
return new Date(epochMs).toISOString().replace("T", " ").slice(0, 19) + "Z";
|
|
327
|
+
}
|
|
328
|
+
function formatDuration(ms) {
|
|
329
|
+
if (ms === null) return "—";
|
|
330
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
331
|
+
return `${(ms / 1e3).toFixed(1)}s`;
|
|
332
|
+
}
|
|
333
|
+
function statusCell(status) {
|
|
334
|
+
return `<span class="${status === "success" ? "ok" : status === "failure" ? "fail" : "warn"}">${escapeHtml(status)}</span>`;
|
|
335
|
+
}
|
|
336
|
+
function runUrl(project, runId) {
|
|
337
|
+
return `/runs/${encodeURIComponent(project)}/${encodeURIComponent(runId)}`;
|
|
338
|
+
}
|
|
339
|
+
function severityBadges(counts) {
|
|
340
|
+
return [
|
|
341
|
+
"critical",
|
|
342
|
+
"high",
|
|
343
|
+
"medium",
|
|
344
|
+
"low",
|
|
345
|
+
"info"
|
|
346
|
+
].filter((sev) => (counts[sev] ?? 0) > 0).map((sev) => `<span class="${sev === "critical" || sev === "high" ? "fail" : sev === "medium" ? "warn" : "muted"}">${sev}:${counts[sev]}</span>`).join(" ");
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Run list page — one row per stored run for a project, newest first.
|
|
350
|
+
*/
|
|
351
|
+
function renderHubRunList(opts) {
|
|
352
|
+
const rows = opts.runs.length === 0 ? `<p class="empty">No runs recorded for <code>${escapeHtml(opts.project)}</code>. Run <code>sverka run --remote</code> to upload one.</p>` : `<table>
|
|
353
|
+
<thead><tr><th>Run</th><th>Entry</th><th>Status</th><th>Started</th><th>Duration</th><th>Findings</th><th>Verdict</th></tr></thead>
|
|
354
|
+
<tbody>
|
|
355
|
+
${opts.runs.map((r) => `<tr>
|
|
356
|
+
<td><a href="${runUrl(r.project, r.runId)}"><code>${escapeHtml(r.runId)}</code></a></td>
|
|
357
|
+
<td>${escapeHtml(r.entry)}</td>
|
|
358
|
+
<td>${statusCell(r.status)}</td>
|
|
359
|
+
<td class="muted">${formatTs(r.startedAt)}</td>
|
|
360
|
+
<td class="muted">${formatDuration(r.durationMs)}</td>
|
|
361
|
+
<td>${r.findingCounts.total} ${severityBadges(r.findingCounts.bySeverity)}</td>
|
|
362
|
+
<td>${r.policyVerdict != null ? statusCell(r.policyVerdict) : "<span class=\"muted\">—</span>"}</td>
|
|
363
|
+
</tr>`).join("\n ")}
|
|
364
|
+
</tbody>
|
|
365
|
+
</table>`;
|
|
366
|
+
return pageShell({
|
|
367
|
+
title: `Runs — ${opts.project}`,
|
|
368
|
+
subtitle: `<code>${escapeHtml(opts.project)}</code>`,
|
|
369
|
+
nav: [{
|
|
370
|
+
href: "/",
|
|
371
|
+
label: "projects"
|
|
372
|
+
}, {
|
|
373
|
+
href: `/flaky/${encodeURIComponent(opts.project)}`,
|
|
374
|
+
label: "flaky steps"
|
|
375
|
+
}],
|
|
376
|
+
body: `<h2>Recent runs</h2>${rows}`
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
/** Steps table from a sverka.run/v1 report payload. */
|
|
380
|
+
function stepsTable(report) {
|
|
381
|
+
const data = report["data"];
|
|
382
|
+
const steps = typeof data === "object" && data !== null ? data.steps : void 0;
|
|
383
|
+
if (!Array.isArray(steps) || steps.length === 0) return "";
|
|
384
|
+
return `<h2>Steps</h2><table>
|
|
385
|
+
<thead><tr><th>Step</th><th>Status</th><th>Duration</th><th>Detail</th></tr></thead>
|
|
386
|
+
<tbody>
|
|
387
|
+
${steps.map((s) => {
|
|
388
|
+
const stepId = typeof s.stepId === "string" ? s.stepId : "?";
|
|
389
|
+
const status = typeof s.status === "string" ? s.status : "?";
|
|
390
|
+
const duration = typeof s.durationMs === "number" ? formatDuration(s.durationMs) : "—";
|
|
391
|
+
const detail = typeof s.error === "string" ? s.error : typeof s.cacheKey === "string" ? `cache: ${s.cacheKey}` : "";
|
|
392
|
+
return `<tr><td><code>${escapeHtml(stepId)}</code></td><td>${statusCell(status)}</td><td class="muted">${duration}</td><td class="muted">${escapeHtml(detail)}</td></tr>`;
|
|
393
|
+
}).join("\n ")}
|
|
394
|
+
</tbody>
|
|
395
|
+
</table>`;
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Run detail page — run metadata + per-step table + findings embedded
|
|
399
|
+
* via the SARIF report renderer (iframe to the findings sub-document).
|
|
400
|
+
*/
|
|
401
|
+
function renderHubRunDetail(opts) {
|
|
402
|
+
const { run } = opts;
|
|
403
|
+
const findingsBlock = run.findings.length === 0 ? `<p class="empty">No findings in this run.</p>` : `<iframe class="findings" src="${runUrl(run.project, run.runId)}/findings" title="Findings"></iframe>`;
|
|
404
|
+
return pageShell({
|
|
405
|
+
title: `Run ${run.runId}`,
|
|
406
|
+
subtitle: `<code>${escapeHtml(run.project)}</code> · ${escapeHtml(run.entry)} · ${formatTs(run.startedAt)}`,
|
|
407
|
+
nav: [{
|
|
408
|
+
href: `/?project=${encodeURIComponent(run.project)}`,
|
|
409
|
+
label: "runs"
|
|
410
|
+
}, {
|
|
411
|
+
href: `/flaky/${encodeURIComponent(run.project)}`,
|
|
412
|
+
label: "flaky steps"
|
|
413
|
+
}],
|
|
414
|
+
body: `
|
|
415
|
+
<h2>Summary</h2>
|
|
416
|
+
<table>
|
|
417
|
+
<tbody>
|
|
418
|
+
<tr><th>Status</th><td>${statusCell(run.status)}</td></tr>
|
|
419
|
+
<tr><th>Duration</th><td>${formatDuration(run.durationMs)}</td></tr>
|
|
420
|
+
<tr><th>Findings</th><td>${run.findingCounts.total} ${severityBadges(run.findingCounts.bySeverity)}</td></tr>
|
|
421
|
+
${run.policyVerdict != null ? `<tr><th>Verdict</th><td>${statusCell(run.policyVerdict)}</td></tr>` : ""}
|
|
422
|
+
<tr><th>Uploaded</th><td class="muted">${formatTs(run.uploadedAt)}</td></tr>
|
|
423
|
+
</tbody>
|
|
424
|
+
</table>
|
|
425
|
+
${stepsTable(run.report)}
|
|
426
|
+
<h2>Findings</h2>
|
|
427
|
+
${findingsBlock}`
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Flaky-step view — per-step success rate across the last N runs,
|
|
432
|
+
* worst first. Rows come from `GET /v1/flaky/{project}`.
|
|
433
|
+
*/
|
|
434
|
+
function renderHubFlaky(opts) {
|
|
435
|
+
const body = opts.rows.length === 0 ? `<p class="empty">No step data over the last ${opts.window} runs for <code>${escapeHtml(opts.project)}</code>.</p>` : `<table>
|
|
436
|
+
<thead><tr><th>Step</th><th>Success rate</th><th>Runs</th></tr></thead>
|
|
437
|
+
<tbody>
|
|
438
|
+
${opts.rows.map((r) => `<tr>
|
|
439
|
+
<td><code>${escapeHtml(r.stepId)}</code></td>
|
|
440
|
+
<td class="${r.successRate < .9 ? "fail" : r.successRate < 1 ? "warn" : "ok"}">${(r.successRate * 100).toFixed(1)}%</td>
|
|
441
|
+
<td class="muted">${r.runs}</td>
|
|
442
|
+
</tr>`).join("\n ")}
|
|
443
|
+
</tbody>
|
|
444
|
+
</table>`;
|
|
445
|
+
return pageShell({
|
|
446
|
+
title: `Flaky steps — ${opts.project}`,
|
|
447
|
+
subtitle: `last ${opts.window} runs`,
|
|
448
|
+
nav: [{
|
|
449
|
+
href: `/?project=${encodeURIComponent(opts.project)}`,
|
|
450
|
+
label: "runs"
|
|
451
|
+
}],
|
|
452
|
+
body
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Projects index — links to each project's run list.
|
|
457
|
+
*/
|
|
458
|
+
function renderHubIndex(opts) {
|
|
459
|
+
return pageShell({
|
|
460
|
+
title: "Sverka Hub",
|
|
461
|
+
subtitle: "Remote run hub — cache, history, findings",
|
|
462
|
+
body: `<h2>Projects</h2>${opts.projects.length === 0 ? `<p class="empty">No runs recorded yet. Point a checkout at this hub and run <code>sverka run --remote</code>.</p>` : `<table>
|
|
463
|
+
<thead><tr><th>Project</th></tr></thead>
|
|
464
|
+
<tbody>
|
|
465
|
+
${opts.projects.map((p) => `<tr><td><a href="/?project=${encodeURIComponent(p)}"><code>${escapeHtml(p)}</code></a></td></tr>`).join("\n ")}
|
|
466
|
+
</tbody>
|
|
467
|
+
</table>`}`
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
//#endregion
|
|
471
|
+
export { renderDashboard, renderHubFlaky, renderHubIndex, renderHubRunDetail, renderHubRunList, startUiServer };
|
|
247
472
|
|
|
248
473
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/dashboard.ts","../src/server.ts"],"sourcesContent":["// @sverka/ui — dashboard HTML generator.\n\n/** Render the dashboard page listing available SARIF files. */\nexport function renderDashboard(\n artifactsDir: string,\n files: readonly string[],\n): string {\n const fileList =\n files.length === 0\n ? '<p class=\"empty\">No SARIF files found. Run <code>sverka run --format sarif</code> to generate findings.</p>'\n : `<ul class=\"file-list\">\n ${files.map((f) => `<li><a href=\"/report/${encodeURIComponent(f)}\">${escapeHtml(f)}</a></li>`).join(\"\\n \")}\n </ul>`;\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <title>Sverka Dashboard</title>\n <style>${CSS}</style>\n</head>\n<body>\n <header>\n <h1>Sverka Dashboard</h1>\n <p class=\"path\">Artifacts: <code>${escapeHtml(artifactsDir)}</code></p>\n </header>\n <main>\n <h2>SARIF Reports</h2>\n ${fileList}\n </main>\n <footer>\n <a href=\"https://sverka.dev\">sverka.dev</a>\n </footer>\n</body>\n</html>`;\n}\n\nfunction escapeHtml(text: string): string {\n return text\n .replaceAll(\"&\", \"&\")\n .replaceAll(\"<\", \"<\")\n .replaceAll(\">\", \">\")\n .replaceAll('\"', \""\")\n .replaceAll(\"'\", \"'\");\n}\n\nconst CSS = String.raw`\n:root { color-scheme: dark; }\n* { box-sizing: border-box; }\nbody {\n margin: 0;\n padding: 0;\n background: #0d1117;\n color: #c9d1d9;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Helvetica, Arial, sans-serif;\n line-height: 1.6;\n min-height: 100vh;\n display: flex;\n flex-direction: column;\n}\nheader {\n padding: 1.5rem 2rem;\n background: #161b22;\n border-bottom: 1px solid #30363d;\n}\nheader h1 { margin: 0 0 0.5rem 0; font-size: 1.5rem; color: #f0f6fc; }\n.path { font-size: 0.875rem; color: #8b949e; margin: 0; }\npath code { color: #58a6ff; }\nmain { flex: 1; padding: 2rem; }\nh2 { font-size: 1.25rem; color: #f0f6fc; margin: 0 0 1rem 0; }\n.file-list { list-style: none; padding: 0; }\n.file-list li { margin-bottom: 0.5rem; }\n.file-list a {\n display: block;\n padding: 0.75rem 1rem;\n background: #161b22;\n border: 1px solid #30363d;\n border-radius: 6px;\n color: #58a6ff;\n text-decoration: none;\n font-family: monospace;\n font-size: 0.9rem;\n}\n.file-list a:hover { border-color: #58a6ff; background: #1c2330; }\n.empty { color: #8b949e; padding: 2rem; text-align: center; }\ncode { background: #21262d; padding: 0.1rem 0.3rem; border-radius: 3px; }\nfooter { padding: 1rem 2rem; border-top: 1px solid #21262d; text-align: center; }\nfooter a { color: #58a6ff; text-decoration: none; }\n`;\n","// @sverka/ui — local web dashboard server.\n\nimport {\n createServer,\n type Server,\n type IncomingMessage,\n type ServerResponse,\n} from \"node:http\";\nimport { createHash } from \"node:crypto\";\nimport {\n readdirSync,\n readFileSync,\n existsSync,\n realpathSync,\n type Dirent,\n} from \"node:fs\";\nimport { join, dirname, relative, resolve, sep } from \"node:path\";\nimport {\n normalizeSarif,\n type Finding,\n type SarifLog,\n} from \"@sverka/verification\";\nimport { generateSarifHtml } from \"@sverka/sarif-viewer-web\";\nimport { renderDashboard } from \"./dashboard.js\";\n\nexport interface UiServerOptions {\n /** Directory to scan for SARIF files (default: .sverka/artifacts). */\n readonly artifactsDir: string;\n /** Port to listen on (default: 3000). */\n readonly port?: number;\n /** Host to bind (default: localhost). */\n readonly host?: string;\n}\n\nexport interface UiServer {\n readonly port: number;\n readonly url: string;\n close(): void;\n}\n\n/** Security headers for all HTML responses — defense-in-depth against XSS.\n * The CSP blocks inline scripts (script-src 'none') even if escaping fails.\n * generateSarifHtml escapes all user-controlled data via escapeHtml(). */\nconst HTML_HEADERS = {\n \"content-type\": \"text/html; charset=utf-8\",\n \"content-security-policy\":\n \"default-src 'self'; script-src 'none'; style-src 'unsafe-inline'\",\n \"x-content-type-options\": \"nosniff\",\n} as const;\n\n/**\n * Start a local web dashboard server that lists SARIF files and\n * renders individual findings reports. Returns when the server is\n * listening.\n */\nexport function startUiServer(options: UiServerOptions): Promise<UiServer> {\n const port = options.port ?? 3000;\n const host = options.host ?? \"localhost\";\n const artifactsDir = resolve(options.artifactsDir);\n\n const server: Server = createServer(\n (req: IncomingMessage, res: ServerResponse) => {\n handleRequest(req, res, artifactsDir);\n },\n );\n\n return new Promise((resolve, reject) => {\n const onError = (err: Error): void => reject(err);\n server.on(\"error\", onError);\n\n server.listen(port, host, () => {\n // Get the actual port (supports port 0 = ephemeral).\n const address = server.address();\n const actualPort =\n typeof address === \"object\" && address ? address.port : port;\n\n // Keep the error listener active for errors after listen (e.g. ECONNRESET).\n server.removeListener(\"error\", onError);\n server.on(\"error\", () => {\n // Swallow post-listen errors — the server is already running.\n });\n\n resolve({\n port: actualPort,\n url: `http://${host}:${actualPort}`,\n close: () => {\n server.close((err) => {\n if (\n err &&\n (err as NodeJS.ErrnoException).code !== \"ERR_SERVER_NOT_RUNNING\"\n ) {\n // Ignore — server may already be closed.\n }\n });\n },\n });\n });\n });\n}\n\nfunction handleRequest(\n req: IncomingMessage,\n res: ServerResponse,\n artifactsDir: string,\n): void {\n const url = req.url ?? \"/\";\n\n if (url === \"/\" || url === \"/dashboard\") {\n serveDashboard(res, artifactsDir);\n return;\n }\n\n if (url.startsWith(\"/report/\")) {\n let filename: string;\n try {\n filename = decodeURIComponent(url.slice(\"/report/\".length));\n } catch {\n res.writeHead(400, { \"content-type\": \"text/plain\" });\n res.end(\"Invalid URL encoding\");\n return;\n }\n serveReport(res, artifactsDir, filename);\n return;\n }\n\n if (url === \"/health\") {\n res.writeHead(200, { \"content-type\": \"application/json\" });\n res.end(JSON.stringify({ status: \"ok\" }));\n return;\n }\n\n res.writeHead(404, { \"content-type\": \"text/plain\" });\n res.end(\"Not found\");\n}\n\n/** List SARIF files and render the dashboard page.\n * All user-controlled data (artifactsDir, filenames) is escaped via\n * escapeHtml() in renderDashboard. CSP blocks inline scripts as\n * defense-in-depth. */\nfunction serveDashboard(res: ServerResponse, artifactsDir: string): void {\n const files = listSarifFiles(artifactsDir);\n const html = renderDashboard(artifactsDir, files);\n res.writeHead(200, HTML_HEADERS);\n res.end(html); // CodeQL: stored XSS — escapeHtml + CSP mitigate\n}\n\n/** Engine run ids are `randomUUID()` values — a leading path segment in\n * this shape is the per-run artifact dir, not a step/pipeline name. */\nconst RUN_ID_SEGMENT =\n /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n/** Read a SARIF file, normalize findings, and render the HTML report. */\nfunction serveReport(\n res: ServerResponse,\n artifactsDir: string,\n filename: string,\n): void {\n // Validate the relative path — nested step directories are allowed\n // (artifacts are stored as <pipeline>/<step>/<file>.sarif) but\n // traversal, backslashes, and absolute paths are not.\n if (\n filename === \"\" ||\n filename.includes(\"..\") ||\n filename.includes(\"\\\\\") ||\n filename.startsWith(\"/\")\n ) {\n res.writeHead(400, { \"content-type\": \"text/plain\" });\n res.end(\"Invalid filename\");\n return;\n }\n\n const filePath = join(artifactsDir, filename);\n\n // Resolve real paths and verify the file is inside artifactsDir.\n // This also prevents symlink-based escapes.\n try {\n const realArtifacts = realpathSync(artifactsDir);\n const realFile = realpathSync(filePath);\n if (\n !realFile.startsWith(realArtifacts + sep) &&\n realFile !== realArtifacts\n ) {\n res.writeHead(400, { \"content-type\": \"text/plain\" });\n res.end(\"Invalid filename\");\n return;\n }\n } catch {\n res.writeHead(404, { \"content-type\": \"text/plain\" });\n res.end(\"File not found\");\n return;\n }\n\n try {\n const raw = readFileSync(filePath, \"utf8\");\n const sarif = JSON.parse(raw) as SarifLog;\n // Mirror collectFindings: checkId is prefixed with the step directory\n // relative to the artifacts root (\"\" for top-level files). Engine\n // artifacts nest under <runId> (a UUID — collectFindings scans the\n // run dir as its root), so drop a leading run segment.\n const dir = dirname(filename).split(sep).join(\"/\");\n const dirSegments = dir.split(\"/\");\n const checkIdPrefix =\n dir === \".\"\n ? \"\"\n : dirSegments[0] !== undefined && RUN_ID_SEGMENT.test(dirSegments[0])\n ? dirSegments.slice(1).join(\"/\")\n : dir;\n const findings = normalizeSarif(sarif, {\n root: artifactsDir,\n checkIdPrefix,\n defaultConfidence: 0.5,\n });\n const html = generateSarifHtml(findings);\n res.writeHead(200, reportHeaders(html));\n res.end(html); // CodeQL: stored XSS — escapeHtml + hash-locked CSP mitigate\n } catch (e) {\n res.writeHead(500, { \"content-type\": \"text/plain\" });\n res.end(\n `Error rendering report: ${e instanceof Error ? e.message : String(e)}`,\n );\n }\n}\n\n/** Response headers for report pages. The generated report ships inline\n * <script> blocks for filter/search/sort — allow exactly those scripts by\n * SHA-256 hash instead of disabling them with script-src 'none'. */\nfunction reportHeaders(html: string): Record<string, string> {\n return {\n ...HTML_HEADERS,\n \"content-security-policy\": `default-src 'self'; script-src ${inlineScriptHashes(html)}; style-src 'unsafe-inline'`,\n };\n}\n\n/** Compute a CSP script-src source list of SHA-256 hashes for every inline\n * <script> block in the HTML. Falls back to 'none' when there are none. */\nfunction inlineScriptHashes(html: string): string {\n const hashes: string[] = [];\n for (const match of html.matchAll(\n /<script\\b[^>]*>([\\s\\S]*?)<\\/script[^>]*>/gi,\n )) {\n const digest = createHash(\"sha256\")\n .update(match[1] ?? \"\")\n .digest(\"base64\");\n hashes.push(`'sha256-${digest}'`);\n }\n return hashes.length > 0 ? hashes.join(\" \") : \"'none'\";\n}\n\n/** List .sarif and .sarif.json files under the artifacts directory,\n * recursively. Artifacts are stored as <pipeline>/<step>/<file>.sarif —\n * returned paths are relative to `dir` with forward slashes. */\nfunction listSarifFiles(dir: string): string[] {\n if (!existsSync(dir)) return [];\n const files: string[] = [];\n collectSarifFiles(dir, dir, files);\n return files.sort((a, b) => a.localeCompare(b));\n}\n\nfunction collectSarifFiles(root: string, dir: string, out: string[]): void {\n let entries: Dirent[];\n try {\n entries = readdirSync(dir, { withFileTypes: true });\n } catch {\n return;\n }\n for (const entry of entries) {\n const entryPath = join(dir, entry.name);\n // Prevent traversal outside the artifacts root via ../ or links.\n const resolvedEntry = resolve(entryPath);\n if (resolvedEntry !== root && !resolvedEntry.startsWith(root + sep)) {\n continue;\n }\n if (entry.isDirectory()) {\n collectSarifFiles(root, entryPath, out);\n } else if (\n entry.name.endsWith(\".sarif\") ||\n entry.name.endsWith(\".sarif.json\")\n ) {\n out.push(relative(root, entryPath).split(sep).join(\"/\"));\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/escape.ts","../src/dashboard.ts","../src/server.ts","../src/hub-pages.ts"],"sourcesContent":["// Shared HTML escaping + page shell for server-rendered pages.\n\nexport function escapeHtml(text: string): string {\n return text\n .replaceAll(\"&\", \"&\")\n .replaceAll(\"<\", \"<\")\n .replaceAll(\">\", \">\")\n .replaceAll('\"', \""\")\n .replaceAll(\"'\", \"'\");\n}\n\n/** Shared dark theme — same palette as the SARIF dashboard. */\nexport const PAGE_CSS = String.raw`\n:root { color-scheme: dark; }\n* { box-sizing: border-box; }\nbody {\n margin: 0;\n padding: 0;\n background: #0d1117;\n color: #c9d1d9;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Helvetica, Arial, sans-serif;\n line-height: 1.6;\n min-height: 100vh;\n display: flex;\n flex-direction: column;\n}\nheader {\n padding: 1.5rem 2rem;\n background: #161b22;\n border-bottom: 1px solid #30363d;\n}\nheader h1 { margin: 0 0 0.5rem 0; font-size: 1.5rem; color: #f0f6fc; }\nheader nav { font-size: 0.875rem; }\nheader nav a { color: #58a6ff; text-decoration: none; margin-right: 1rem; }\nheader nav a:hover { text-decoration: underline; }\n.path { font-size: 0.875rem; color: #8b949e; margin: 0; }\nmain { flex: 1; padding: 2rem; }\nh2 { font-size: 1.25rem; color: #f0f6fc; margin: 0 0 1rem 0; }\ntable { border-collapse: collapse; width: 100%; font-size: 0.9rem; }\nth, td { text-align: left; padding: 0.5rem 0.75rem; border-bottom: 1px solid #21262d; }\nth { color: #8b949e; font-weight: 600; }\ntr:hover td { background: #161b22; }\na { color: #58a6ff; text-decoration: none; }\na:hover { text-decoration: underline; }\ncode { background: #21262d; padding: 0.1rem 0.3rem; border-radius: 3px; }\n.empty { color: #8b949e; padding: 2rem; text-align: center; }\n.ok { color: #3fb950; }\n.fail { color: #f85149; }\n.warn { color: #d29922; }\n.muted { color: #8b949e; }\nfooter { padding: 1rem 2rem; border-top: 1px solid #21262d; text-align: center; }\nfooter a { color: #58a6ff; text-decoration: none; }\niframe.findings {\n width: 100%;\n min-height: 32rem;\n border: 1px solid #30363d;\n border-radius: 6px;\n background: #fff;\n}\n`;\n\n/** Wrap page body in the shared shell (doctype, head, header, footer). */\nexport function pageShell(opts: {\n title: string;\n subtitle?: string;\n nav?: readonly { href: string; label: string }[];\n body: string;\n}): string {\n const nav = (opts.nav ?? [])\n .map((n) => `<a href=\"${escapeHtml(n.href)}\">${escapeHtml(n.label)}</a>`)\n .join(\"\");\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <title>${escapeHtml(opts.title)}</title>\n <style>${PAGE_CSS}</style>\n</head>\n<body>\n <header>\n <h1>${escapeHtml(opts.title)}</h1>\n ${opts.subtitle !== undefined ? `<p class=\"path\">${opts.subtitle}</p>` : \"\"}\n ${nav === \"\" ? \"\" : `<nav>${nav}</nav>`}\n </header>\n <main>\n ${opts.body}\n </main>\n <footer>\n <a href=\"https://sverka.dev\">sverka.dev</a>\n </footer>\n</body>\n</html>`;\n}\n","// @sverka/ui — dashboard HTML generator.\n\nimport { escapeHtml } from \"./escape.js\";\n\n/** Render the dashboard page listing available SARIF files. */\nexport function renderDashboard(\n artifactsDir: string,\n files: readonly string[],\n): string {\n const fileList =\n files.length === 0\n ? '<p class=\"empty\">No SARIF files found. Run <code>sverka run --format sarif</code> to generate findings.</p>'\n : `<ul class=\"file-list\">\n ${files.map((f) => `<li><a href=\"/report/${encodeURIComponent(f)}\">${escapeHtml(f)}</a></li>`).join(\"\\n \")}\n </ul>`;\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <title>Sverka Dashboard</title>\n <style>${CSS}</style>\n</head>\n<body>\n <header>\n <h1>Sverka Dashboard</h1>\n <p class=\"path\">Artifacts: <code>${escapeHtml(artifactsDir)}</code></p>\n </header>\n <main>\n <h2>SARIF Reports</h2>\n ${fileList}\n </main>\n <footer>\n <a href=\"https://sverka.dev\">sverka.dev</a>\n </footer>\n</body>\n</html>`;\n}\n\nconst CSS = String.raw`\n:root { color-scheme: dark; }\n* { box-sizing: border-box; }\nbody {\n margin: 0;\n padding: 0;\n background: #0d1117;\n color: #c9d1d9;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Helvetica, Arial, sans-serif;\n line-height: 1.6;\n min-height: 100vh;\n display: flex;\n flex-direction: column;\n}\nheader {\n padding: 1.5rem 2rem;\n background: #161b22;\n border-bottom: 1px solid #30363d;\n}\nheader h1 { margin: 0 0 0.5rem 0; font-size: 1.5rem; color: #f0f6fc; }\n.path { font-size: 0.875rem; color: #8b949e; margin: 0; }\npath code { color: #58a6ff; }\nmain { flex: 1; padding: 2rem; }\nh2 { font-size: 1.25rem; color: #f0f6fc; margin: 0 0 1rem 0; }\n.file-list { list-style: none; padding: 0; }\n.file-list li { margin-bottom: 0.5rem; }\n.file-list a {\n display: block;\n padding: 0.75rem 1rem;\n background: #161b22;\n border: 1px solid #30363d;\n border-radius: 6px;\n color: #58a6ff;\n text-decoration: none;\n font-family: monospace;\n font-size: 0.9rem;\n}\n.file-list a:hover { border-color: #58a6ff; background: #1c2330; }\n.empty { color: #8b949e; padding: 2rem; text-align: center; }\ncode { background: #21262d; padding: 0.1rem 0.3rem; border-radius: 3px; }\nfooter { padding: 1rem 2rem; border-top: 1px solid #21262d; text-align: center; }\nfooter a { color: #58a6ff; text-decoration: none; }\n`;\n","// @sverka/ui — local web dashboard server.\n\nimport {\n createServer,\n type Server,\n type IncomingMessage,\n type ServerResponse,\n} from \"node:http\";\nimport { createHash } from \"node:crypto\";\nimport {\n readdirSync,\n readFileSync,\n existsSync,\n realpathSync,\n type Dirent,\n} from \"node:fs\";\nimport { join, dirname, relative, resolve, sep } from \"node:path\";\nimport {\n normalizeSarif,\n type Finding,\n type SarifLog,\n} from \"@sverka/verification\";\nimport { generateSarifHtml } from \"@sverka/sarif-viewer-web\";\nimport { renderDashboard } from \"./dashboard.js\";\n\nexport interface UiServerOptions {\n /** Directory to scan for SARIF files (default: .sverka/artifacts). */\n readonly artifactsDir: string;\n /** Port to listen on (default: 3000). */\n readonly port?: number;\n /** Host to bind (default: localhost). */\n readonly host?: string;\n}\n\nexport interface UiServer {\n readonly port: number;\n readonly url: string;\n close(): void;\n}\n\n/** Security headers for all HTML responses — defense-in-depth against XSS.\n * The CSP blocks inline scripts (script-src 'none') even if escaping fails.\n * generateSarifHtml escapes all user-controlled data via escapeHtml(). */\nconst HTML_HEADERS = {\n \"content-type\": \"text/html; charset=utf-8\",\n \"content-security-policy\":\n \"default-src 'self'; script-src 'none'; style-src 'unsafe-inline'\",\n \"x-content-type-options\": \"nosniff\",\n} as const;\n\n/**\n * Start a local web dashboard server that lists SARIF files and\n * renders individual findings reports. Returns when the server is\n * listening.\n */\nexport function startUiServer(options: UiServerOptions): Promise<UiServer> {\n const port = options.port ?? 3000;\n const host = options.host ?? \"localhost\";\n const artifactsDir = resolve(options.artifactsDir);\n\n const server: Server = createServer(\n (req: IncomingMessage, res: ServerResponse) => {\n handleRequest(req, res, artifactsDir);\n },\n );\n\n return new Promise((resolve, reject) => {\n const onError = (err: Error): void => reject(err);\n server.on(\"error\", onError);\n\n server.listen(port, host, () => {\n // Get the actual port (supports port 0 = ephemeral).\n const address = server.address();\n const actualPort =\n typeof address === \"object\" && address ? address.port : port;\n\n // Keep the error listener active for errors after listen (e.g. ECONNRESET).\n server.removeListener(\"error\", onError);\n server.on(\"error\", () => {\n // Swallow post-listen errors — the server is already running.\n });\n\n resolve({\n port: actualPort,\n url: `http://${host}:${actualPort}`,\n close: () => {\n server.close((err) => {\n if (\n err &&\n (err as NodeJS.ErrnoException).code !== \"ERR_SERVER_NOT_RUNNING\"\n ) {\n // Ignore — server may already be closed.\n }\n });\n },\n });\n });\n });\n}\n\nfunction handleRequest(\n req: IncomingMessage,\n res: ServerResponse,\n artifactsDir: string,\n): void {\n const url = req.url ?? \"/\";\n\n if (url === \"/\" || url === \"/dashboard\") {\n serveDashboard(res, artifactsDir);\n return;\n }\n\n if (url.startsWith(\"/report/\")) {\n let filename: string;\n try {\n filename = decodeURIComponent(url.slice(\"/report/\".length));\n } catch {\n res.writeHead(400, { \"content-type\": \"text/plain\" });\n res.end(\"Invalid URL encoding\");\n return;\n }\n serveReport(res, artifactsDir, filename);\n return;\n }\n\n if (url === \"/health\") {\n res.writeHead(200, { \"content-type\": \"application/json\" });\n res.end(JSON.stringify({ status: \"ok\" }));\n return;\n }\n\n res.writeHead(404, { \"content-type\": \"text/plain\" });\n res.end(\"Not found\");\n}\n\n/** List SARIF files and render the dashboard page.\n * All user-controlled data (artifactsDir, filenames) is escaped via\n * escapeHtml() in renderDashboard. CSP blocks inline scripts as\n * defense-in-depth. */\nfunction serveDashboard(res: ServerResponse, artifactsDir: string): void {\n const files = listSarifFiles(artifactsDir);\n const html = renderDashboard(artifactsDir, files);\n res.writeHead(200, HTML_HEADERS);\n res.end(html); // CodeQL: stored XSS — escapeHtml + CSP mitigate\n}\n\n/** Engine run ids are `randomUUID()` values — a leading path segment in\n * this shape is the per-run artifact dir, not a step/pipeline name. */\nconst RUN_ID_SEGMENT =\n /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n/** Read a SARIF file, normalize findings, and render the HTML report. */\nfunction serveReport(\n res: ServerResponse,\n artifactsDir: string,\n filename: string,\n): void {\n // Validate the relative path — nested step directories are allowed\n // (artifacts are stored as <pipeline>/<step>/<file>.sarif) but\n // traversal, backslashes, and absolute paths are not.\n if (\n filename === \"\" ||\n filename.includes(\"..\") ||\n filename.includes(\"\\\\\") ||\n filename.startsWith(\"/\")\n ) {\n res.writeHead(400, { \"content-type\": \"text/plain\" });\n res.end(\"Invalid filename\");\n return;\n }\n\n const filePath = join(artifactsDir, filename);\n\n // Resolve real paths and verify the file is inside artifactsDir.\n // This also prevents symlink-based escapes.\n try {\n const realArtifacts = realpathSync(artifactsDir);\n const realFile = realpathSync(filePath);\n if (\n !realFile.startsWith(realArtifacts + sep) &&\n realFile !== realArtifacts\n ) {\n res.writeHead(400, { \"content-type\": \"text/plain\" });\n res.end(\"Invalid filename\");\n return;\n }\n } catch {\n res.writeHead(404, { \"content-type\": \"text/plain\" });\n res.end(\"File not found\");\n return;\n }\n\n try {\n const raw = readFileSync(filePath, \"utf8\");\n const sarif = JSON.parse(raw) as SarifLog;\n // Mirror collectFindings: checkId is prefixed with the step directory\n // relative to the artifacts root (\"\" for top-level files). Engine\n // artifacts nest under <runId> (a UUID — collectFindings scans the\n // run dir as its root), so drop a leading run segment.\n const dir = dirname(filename).split(sep).join(\"/\");\n const dirSegments = dir.split(\"/\");\n const checkIdPrefix =\n dir === \".\"\n ? \"\"\n : dirSegments[0] !== undefined && RUN_ID_SEGMENT.test(dirSegments[0])\n ? dirSegments.slice(1).join(\"/\")\n : dir;\n const findings = normalizeSarif(sarif, {\n root: artifactsDir,\n checkIdPrefix,\n defaultConfidence: 0.5,\n });\n const html = generateSarifHtml(findings);\n res.writeHead(200, reportHeaders(html));\n res.end(html); // CodeQL: stored XSS — escapeHtml + hash-locked CSP mitigate\n } catch (e) {\n res.writeHead(500, { \"content-type\": \"text/plain\" });\n res.end(\n `Error rendering report: ${e instanceof Error ? e.message : String(e)}`,\n );\n }\n}\n\n/** Response headers for report pages. The generated report ships inline\n * <script> blocks for filter/search/sort — allow exactly those scripts by\n * SHA-256 hash instead of disabling them with script-src 'none'. */\nfunction reportHeaders(html: string): Record<string, string> {\n return {\n ...HTML_HEADERS,\n \"content-security-policy\": `default-src 'self'; script-src ${inlineScriptHashes(html)}; style-src 'unsafe-inline'`,\n };\n}\n\n/** Compute a CSP script-src source list of SHA-256 hashes for every inline\n * <script> block in the HTML. Falls back to 'none' when there are none. */\nfunction inlineScriptHashes(html: string): string {\n const hashes: string[] = [];\n for (const match of html.matchAll(\n /<script\\b[^>]*>([\\s\\S]*?)<\\/script[^>]*>/gi,\n )) {\n const digest = createHash(\"sha256\")\n .update(match[1] ?? \"\")\n .digest(\"base64\");\n hashes.push(`'sha256-${digest}'`);\n }\n return hashes.length > 0 ? hashes.join(\" \") : \"'none'\";\n}\n\n/** List .sarif and .sarif.json files under the artifacts directory,\n * recursively. Artifacts are stored as <pipeline>/<step>/<file>.sarif —\n * returned paths are relative to `dir` with forward slashes. */\nfunction listSarifFiles(dir: string): string[] {\n if (!existsSync(dir)) return [];\n const files: string[] = [];\n collectSarifFiles(dir, dir, files);\n return files.sort((a, b) => a.localeCompare(b));\n}\n\nfunction collectSarifFiles(root: string, dir: string, out: string[]): void {\n let entries: Dirent[];\n try {\n entries = readdirSync(dir, { withFileTypes: true });\n } catch {\n return;\n }\n for (const entry of entries) {\n const entryPath = join(dir, entry.name);\n // Prevent traversal outside the artifacts root via ../ or links.\n const resolvedEntry = resolve(entryPath);\n if (resolvedEntry !== root && !resolvedEntry.startsWith(root + sep)) {\n continue;\n }\n if (entry.isDirectory()) {\n collectSarifFiles(root, entryPath, out);\n } else if (\n entry.name.endsWith(\".sarif\") ||\n entry.name.endsWith(\".sarif.json\")\n ) {\n out.push(relative(root, entryPath).split(sep).join(\"/\"));\n }\n }\n}\n","// @sverka/ui — hub dashboard pages (Spec 55): run list, run detail,\n// findings trend, flaky-step view. Server-rendered static HTML reusing\n// the dashboard's dark theme; findings render via\n// @sverka/sarif-viewer-web's generateSarifHtml (served as a sub-document\n// and embedded via <iframe>).\n\nimport { escapeHtml, pageShell } from \"./escape.js\";\n\n/** A run summary row as the hub serves it (mirrors HubRunSummary). */\nexport interface HubRunRow {\n readonly runId: string;\n readonly project: string;\n readonly entry: string;\n readonly status: string;\n readonly startedAt: number | null;\n readonly durationMs: number | null;\n readonly findingCounts: {\n readonly total: number;\n readonly bySeverity: Record<string, number>;\n };\n readonly policyVerdict?: string | null;\n}\n\n/** A stored run detail (report payload + findings). */\nexport interface HubRunView extends HubRunRow {\n readonly report: Record<string, unknown>;\n readonly findings: readonly unknown[];\n readonly uploadedAt: number;\n}\n\n/** A flaky-step aggregate row (GET /v1/flaky). */\nexport interface HubFlakyRow {\n readonly stepId: string;\n readonly successRate: number;\n readonly runs: number;\n}\n\nfunction formatTs(epochMs: number | null): string {\n if (epochMs === null) return \"—\";\n return new Date(epochMs).toISOString().replace(\"T\", \" \").slice(0, 19) + \"Z\";\n}\n\nfunction formatDuration(ms: number | null): string {\n if (ms === null) return \"—\";\n if (ms < 1000) return `${ms}ms`;\n return `${(ms / 1000).toFixed(1)}s`;\n}\n\nfunction statusCell(status: string): string {\n const cls =\n status === \"success\" ? \"ok\" : status === \"failure\" ? \"fail\" : \"warn\";\n return `<span class=\"${cls}\">${escapeHtml(status)}</span>`;\n}\n\nfunction runUrl(project: string, runId: string): string {\n return `/runs/${encodeURIComponent(project)}/${encodeURIComponent(runId)}`;\n}\n\nfunction severityBadges(counts: Record<string, number>): string {\n return ([\"critical\", \"high\", \"medium\", \"low\", \"info\"] as const)\n .filter((sev) => (counts[sev] ?? 0) > 0)\n .map(\n (sev) =>\n `<span class=\"${sev === \"critical\" || sev === \"high\" ? \"fail\" : sev === \"medium\" ? \"warn\" : \"muted\"}\">${sev}:${counts[sev]}</span>`,\n )\n .join(\" \");\n}\n\n/**\n * Run list page — one row per stored run for a project, newest first.\n */\nexport function renderHubRunList(opts: {\n readonly project: string;\n readonly runs: readonly HubRunRow[];\n}): string {\n const rows =\n opts.runs.length === 0\n ? `<p class=\"empty\">No runs recorded for <code>${escapeHtml(opts.project)}</code>. Run <code>sverka run --remote</code> to upload one.</p>`\n : `<table>\n <thead><tr><th>Run</th><th>Entry</th><th>Status</th><th>Started</th><th>Duration</th><th>Findings</th><th>Verdict</th></tr></thead>\n <tbody>\n ${opts.runs\n .map(\n (r) => `<tr>\n <td><a href=\"${runUrl(r.project, r.runId)}\"><code>${escapeHtml(r.runId)}</code></a></td>\n <td>${escapeHtml(r.entry)}</td>\n <td>${statusCell(r.status)}</td>\n <td class=\"muted\">${formatTs(r.startedAt)}</td>\n <td class=\"muted\">${formatDuration(r.durationMs)}</td>\n <td>${r.findingCounts.total} ${severityBadges(r.findingCounts.bySeverity)}</td>\n <td>${r.policyVerdict != null ? statusCell(r.policyVerdict) : '<span class=\"muted\">—</span>'}</td>\n </tr>`,\n )\n .join(\"\\n \")}\n </tbody>\n</table>`;\n return pageShell({\n title: `Runs — ${opts.project}`,\n subtitle: `<code>${escapeHtml(opts.project)}</code>`,\n nav: [\n { href: \"/\", label: \"projects\" },\n {\n href: `/flaky/${encodeURIComponent(opts.project)}`,\n label: \"flaky steps\",\n },\n ],\n body: `<h2>Recent runs</h2>${rows}`,\n });\n}\n\ninterface ReportStep {\n readonly stepId?: unknown;\n readonly status?: unknown;\n readonly durationMs?: unknown;\n readonly error?: unknown;\n readonly cacheKey?: unknown;\n}\n\n/** Steps table from a sverka.run/v1 report payload. */\nfunction stepsTable(report: Record<string, unknown>): string {\n const data = report[\"data\"];\n const steps =\n typeof data === \"object\" && data !== null\n ? (data as { steps?: readonly ReportStep[] }).steps\n : undefined;\n if (!Array.isArray(steps) || steps.length === 0) return \"\";\n const rows = steps\n .map((s) => {\n const stepId = typeof s.stepId === \"string\" ? s.stepId : \"?\";\n const status = typeof s.status === \"string\" ? s.status : \"?\";\n const duration =\n typeof s.durationMs === \"number\" ? formatDuration(s.durationMs) : \"—\";\n const detail =\n typeof s.error === \"string\"\n ? s.error\n : typeof s.cacheKey === \"string\"\n ? `cache: ${s.cacheKey}`\n : \"\";\n return `<tr><td><code>${escapeHtml(stepId)}</code></td><td>${statusCell(status)}</td><td class=\"muted\">${duration}</td><td class=\"muted\">${escapeHtml(detail)}</td></tr>`;\n })\n .join(\"\\n \");\n return `<h2>Steps</h2><table>\n <thead><tr><th>Step</th><th>Status</th><th>Duration</th><th>Detail</th></tr></thead>\n <tbody>\n ${rows}\n </tbody>\n</table>`;\n}\n\n/**\n * Run detail page — run metadata + per-step table + findings embedded\n * via the SARIF report renderer (iframe to the findings sub-document).\n */\nexport function renderHubRunDetail(opts: { readonly run: HubRunView }): string {\n const { run } = opts;\n const findingsBlock =\n run.findings.length === 0\n ? `<p class=\"empty\">No findings in this run.</p>`\n : `<iframe class=\"findings\" src=\"${runUrl(run.project, run.runId)}/findings\" title=\"Findings\"></iframe>`;\n return pageShell({\n title: `Run ${run.runId}`,\n subtitle: `<code>${escapeHtml(run.project)}</code> · ${escapeHtml(run.entry)} · ${formatTs(run.startedAt)}`,\n nav: [\n { href: `/?project=${encodeURIComponent(run.project)}`, label: \"runs\" },\n {\n href: `/flaky/${encodeURIComponent(run.project)}`,\n label: \"flaky steps\",\n },\n ],\n body: `\n<h2>Summary</h2>\n<table>\n <tbody>\n <tr><th>Status</th><td>${statusCell(run.status)}</td></tr>\n <tr><th>Duration</th><td>${formatDuration(run.durationMs)}</td></tr>\n <tr><th>Findings</th><td>${run.findingCounts.total} ${severityBadges(run.findingCounts.bySeverity)}</td></tr>\n ${run.policyVerdict != null ? `<tr><th>Verdict</th><td>${statusCell(run.policyVerdict)}</td></tr>` : \"\"}\n <tr><th>Uploaded</th><td class=\"muted\">${formatTs(run.uploadedAt)}</td></tr>\n </tbody>\n</table>\n${stepsTable(run.report)}\n<h2>Findings</h2>\n${findingsBlock}`,\n });\n}\n\n/**\n * Flaky-step view — per-step success rate across the last N runs,\n * worst first. Rows come from `GET /v1/flaky/{project}`.\n */\nexport function renderHubFlaky(opts: {\n readonly project: string;\n readonly rows: readonly HubFlakyRow[];\n readonly window: number;\n}): string {\n const body =\n opts.rows.length === 0\n ? `<p class=\"empty\">No step data over the last ${opts.window} runs for <code>${escapeHtml(opts.project)}</code>.</p>`\n : `<table>\n <thead><tr><th>Step</th><th>Success rate</th><th>Runs</th></tr></thead>\n <tbody>\n ${opts.rows\n .map(\n (r) => `<tr>\n <td><code>${escapeHtml(r.stepId)}</code></td>\n <td class=\"${r.successRate < 0.9 ? \"fail\" : r.successRate < 1 ? \"warn\" : \"ok\"}\">${(r.successRate * 100).toFixed(1)}%</td>\n <td class=\"muted\">${r.runs}</td>\n </tr>`,\n )\n .join(\"\\n \")}\n </tbody>\n</table>`;\n return pageShell({\n title: `Flaky steps — ${opts.project}`,\n subtitle: `last ${opts.window} runs`,\n nav: [\n { href: `/?project=${encodeURIComponent(opts.project)}`, label: \"runs\" },\n ],\n body,\n });\n}\n\n/**\n * Projects index — links to each project's run list.\n */\nexport function renderHubIndex(opts: {\n readonly projects: readonly string[];\n}): string {\n const body =\n opts.projects.length === 0\n ? `<p class=\"empty\">No runs recorded yet. Point a checkout at this hub and run <code>sverka run --remote</code>.</p>`\n : `<table>\n <thead><tr><th>Project</th></tr></thead>\n <tbody>\n ${opts.projects\n .map(\n (p) =>\n `<tr><td><a href=\"/?project=${encodeURIComponent(p)}\"><code>${escapeHtml(p)}</code></a></td></tr>`,\n )\n .join(\"\\n \")}\n </tbody>\n</table>`;\n return pageShell({\n title: \"Sverka Hub\",\n subtitle: \"Remote run hub — cache, history, findings\",\n body: `<h2>Projects</h2>${body}`,\n });\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sverka/ui",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.26",
|
|
4
4
|
"description": "Sverka UI — local web dashboard for viewing SARIF findings and run results",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
"typecheck": "tsc --noEmit"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@sverka/verification": "^0.2.
|
|
38
|
-
"@sverka/sarif-viewer-web": "^0.2.
|
|
37
|
+
"@sverka/verification": "^0.2.26",
|
|
38
|
+
"@sverka/sarif-viewer-web": "^0.2.26"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"tsdown": "^0.23.0",
|