@extenshi/cli 0.0.8 → 0.1.0

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.
Files changed (41) hide show
  1. package/dist/cli.d.ts +2 -1
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +92 -3
  4. package/dist/cli.js.map +1 -1
  5. package/dist/html-report.d.ts +44 -0
  6. package/dist/html-report.d.ts.map +1 -0
  7. package/dist/html-report.js +527 -0
  8. package/dist/html-report.js.map +1 -0
  9. package/dist/render.d.ts +22 -0
  10. package/dist/render.d.ts.map +1 -1
  11. package/dist/render.js +82 -2
  12. package/dist/render.js.map +1 -1
  13. package/dist/report.d.ts +30 -0
  14. package/dist/report.d.ts.map +1 -0
  15. package/dist/report.js +130 -0
  16. package/dist/report.js.map +1 -0
  17. package/dist/review-archive.d.ts +29 -0
  18. package/dist/review-archive.d.ts.map +1 -0
  19. package/dist/review-archive.js +106 -0
  20. package/dist/review-archive.js.map +1 -0
  21. package/dist/review-catalog.d.ts +43 -0
  22. package/dist/review-catalog.d.ts.map +1 -0
  23. package/dist/review-catalog.js +82 -0
  24. package/dist/review-catalog.js.map +1 -0
  25. package/dist/review-checks.d.ts +139 -0
  26. package/dist/review-checks.d.ts.map +1 -0
  27. package/dist/review-checks.js +560 -0
  28. package/dist/review-checks.js.map +1 -0
  29. package/dist/review-render.d.ts +10 -0
  30. package/dist/review-render.d.ts.map +1 -0
  31. package/dist/review-render.js +94 -0
  32. package/dist/review-render.js.map +1 -0
  33. package/dist/review-risk.d.ts +29 -0
  34. package/dist/review-risk.d.ts.map +1 -0
  35. package/dist/review-risk.js +85 -0
  36. package/dist/review-risk.js.map +1 -0
  37. package/dist/scan.d.ts +82 -2
  38. package/dist/scan.d.ts.map +1 -1
  39. package/dist/scan.js +164 -17
  40. package/dist/scan.js.map +1 -1
  41. package/package.json +2 -1
@@ -0,0 +1,527 @@
1
+ /**
2
+ * Branded, self-contained HTML report renderer for `extenshi scan --format html`.
3
+ *
4
+ * Pure (no I/O) so it is snapshot-testable — returns a complete single-file
5
+ * HTML document with inline CSS, inline JS, and the inline Extenshi logo. No
6
+ * network access, no external assets: the file opens straight from disk
7
+ * (file://) and works fully offline.
8
+ *
9
+ * Why this exists
10
+ * ---------------
11
+ * The terminal report repeats the same rule across dozens of files, producing a
12
+ * wall of near-identical lines. The HTML report DEDUPLICATES: security findings
13
+ * are grouped by (severity, scanner, rule) into one collapsible row carrying a
14
+ * count and the list of locations. On top of that it adds client-side filtering
15
+ * (by severity, by scanner, free-text), a sticky table of contents, and a
16
+ * light/dark theme toggle in the real extenshi.io palette.
17
+ *
18
+ * Schema tolerance is inherited wholesale from `summarizeReport` /
19
+ * `summarizeCompliance` in render.ts — this module never reads the raw scan
20
+ * shape directly, so it stays correct across scanner-output drift.
21
+ */
22
+ import { STORE_IMPACT_SHORT, severityRank, summarizeCompliance, summarizeReport, } from './render.js';
23
+ import { computeVerdict } from './report.js';
24
+ // ── HTML escaping ───────────────────────────────────────────────────────────
25
+ const ESCAPE_MAP = {
26
+ '&': '&',
27
+ '<': '&lt;',
28
+ '>': '&gt;',
29
+ '"': '&quot;',
30
+ "'": '&#39;',
31
+ };
32
+ /** Escape a string for safe interpolation into text or double-quoted attributes. */
33
+ function esc(s) {
34
+ return s.replace(/[&<>"']/g, (c) => ESCAPE_MAP[c] ?? c);
35
+ }
36
+ /**
37
+ * Sanitise an untrusted value used as a CSS class-name fragment (e.g.
38
+ * `sev-${...}`). `esc()` neutralises HTML but not whitespace, which would split
39
+ * one class into several tokens; this keeps only class-safe characters so a
40
+ * future severity/storeImpact value from the API can never inject extra classes.
41
+ */
42
+ function cssToken(s) {
43
+ return s.replace(/[^a-zA-Z0-9_-]/g, '_');
44
+ }
45
+ /** Hard cap on locations rendered per group — a noisy rule can fire thousands of times. */
46
+ const MAX_LOCATIONS_PER_GROUP = 200;
47
+ /**
48
+ * Collapse identical findings (same severity + scanner + rule headline) into one
49
+ * group with a count and the de-duplicated list of locations. Sorted by
50
+ * severity (critical first), then by count (loudest first), then headline.
51
+ *
52
+ * Dedup uses a `Set` (insertion-ordered) rather than `Array.includes` so a rule
53
+ * that fires thousands of times stays O(n) instead of O(n²).
54
+ */
55
+ export function groupFindings(findings) {
56
+ const map = new Map();
57
+ for (const f of findings) {
58
+ const key = `${f.severity}\n${f.scanner}\n${f.headline}`;
59
+ let g = map.get(key);
60
+ if (!g) {
61
+ g = {
62
+ severity: f.severity,
63
+ scanner: f.scanner,
64
+ headline: f.headline,
65
+ message: f.message,
66
+ count: 0,
67
+ locations: new Set(),
68
+ extras: new Set(),
69
+ };
70
+ map.set(key, g);
71
+ }
72
+ g.count++;
73
+ if (!g.message && f.message)
74
+ g.message = f.message;
75
+ if (f.location)
76
+ g.locations.add(f.location);
77
+ if (f.extras)
78
+ g.extras.add(f.extras);
79
+ }
80
+ // Materialise the Sets to arrays (Set preserves first-seen order).
81
+ return [...map.values()]
82
+ .map((g) => ({
83
+ severity: g.severity,
84
+ scanner: g.scanner,
85
+ headline: g.headline,
86
+ message: g.message,
87
+ count: g.count,
88
+ locations: [...g.locations],
89
+ extras: [...g.extras],
90
+ }))
91
+ .sort((a, b) => severityRank(a.severity) - severityRank(b.severity) ||
92
+ b.count - a.count ||
93
+ a.headline.localeCompare(b.headline));
94
+ }
95
+ // ── Static brand assets ──────────────────────────────────────────────────────
96
+ /**
97
+ * The Extenshi mark, lifted verbatim from catalog-frontend/src/components/logo.tsx.
98
+ * `fill="currentColor"` lets it pick up the brand colour from CSS in either theme.
99
+ */
100
+ const LOGO_SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400" width="28" height="28" aria-hidden="true">' +
101
+ '<g transform="translate(200, 200) scale(1.1) translate(-219, -169)">' +
102
+ '<path fill="currentColor" fill-rule="evenodd" d="M 43 29.961 C 43 32.246, 47.125 48.247, 49.888 56.680 C 55.762 74.610, 62.198 83.944, 72.096 88.890 C 78.191 91.936, 78.614 92, 92.660 92 L 107 92 106.994 96.250 C 106.991 98.588, 106.697 104.233, 106.342 108.795 L 105.697 117.091 92.098 116.996 C 84.619 116.943, 77.938 116.923, 77.250 116.950 C 76.300 116.988, 76 121.193, 76 134.500 L 76 152 90.487 152 L 104.974 152 104.449 159.250 C 104.161 163.237, 103.503 174.150, 102.987 183.500 C 102.472 192.850, 101.387 211.300, 100.578 224.500 C 98.475 258.794, 96.671 304.700, 97.356 306.487 C 97.875 307.839, 111.424 307.980, 219.220 307.754 L 340.500 307.500 340.255 287.609 C 340.096 274.661, 339.617 267.243, 338.883 266.359 C 338.004 265.299, 333.491 265, 318.397 265 L 299.038 265 298.411 267.500 C 298.066 268.875, 298.066 271.125, 298.411 272.500 L 299.038 275 222.519 275 L 146 275 146 213.500 L 146 152 218.300 152 C 273.200 152, 290.889 152.289, 291.800 153.200 C 293.565 154.965, 293.269 163.776, 291.257 169.391 C 289.026 175.614, 283.255 182.192, 277.770 184.762 C 265.607 190.461, 266.274 190.383, 224.500 190.961 L 185.500 191.500 185.229 211.226 L 184.958 230.951 187.533 231.598 C 191.687 232.640, 262.673 231.320, 272.308 230.021 C 306.918 225.355, 331.983 202.503, 332.006 175.593 C 332.009 171.692, 332.300 164.799, 332.652 160.277 L 333.292 152.053 347.396 151.777 L 361.500 151.500 361.500 134.500 L 361.500 117.500 347.219 117.222 L 332.939 116.945 333.219 104.722 L 333.500 92.500 347.500 91.947 C 357.308 91.560, 362.626 90.900, 365.261 89.744 C 371.234 87.122, 379.834 78.279, 383.489 71 C 388.795 60.434, 397.405 31.072, 395.699 29.365 C 395.464 29.130, 391.723 30.083, 387.386 31.483 C 379.737 33.952, 356.811 38.101, 340.500 39.969 C 320.691 42.238, 283.099 43.185, 214.975 43.131 C 145.730 43.077, 109.789 42.106, 94.500 39.878 C 90.650 39.316, 84.575 38.430, 81 37.907 C 65.459 35.634, 56.790 33.817, 50.373 31.490 C 42.436 28.611, 43 28.728, 43 29.961 M 146.673 92.660 C 146.303 93.030, 146 98.658, 146 105.167 L 146 117 219.531 117 L 293.062 117 292.781 104.750 L 292.500 92.500 219.923 92.244 C 180.006 92.102, 147.043 92.290, 146.673 92.660"/>' +
103
+ '</g></svg>';
104
+ /**
105
+ * Brand palette mirrors catalog-frontend globals.css (the real site tokens):
106
+ * a warm terracotta accent on a paper background in light mode, a green accent
107
+ * on near-black in dark mode. Fonts reference the site faces by name with a
108
+ * graceful system fallback (no webfont is bundled — keeps the file offline).
109
+ */
110
+ const STYLE = `
111
+ :root {
112
+ --bg: oklch(0.9711 0.0074 80.7211);
113
+ --surface: oklch(0.99 0.005 80);
114
+ --surface-2: oklch(0.937 0.0142 74.4218);
115
+ --fg: oklch(0.3 0.0358 30.2042);
116
+ --muted-fg: oklch(0.4495 0.0486 39.211);
117
+ --border: oklch(0.8805 0.0208 74.6428);
118
+ --primary: oklch(0.7234 0.1783 41.1468);
119
+ --primary-fg: oklch(1 0 0);
120
+ --shadow: 0 1px 3px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
121
+ --font-sans: 'Space Grotesk', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
122
+ --font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
123
+ --sev-critical: #b42318;
124
+ --sev-high: #d92d20;
125
+ --sev-medium: #dc6803;
126
+ --sev-low: #1570ef;
127
+ --sev-info: #667085;
128
+ --sev-unknown: #98a2b3;
129
+ --pass: #157f3d;
130
+ --review: #dc6803;
131
+ --fail: #b42318;
132
+ }
133
+ html.dark {
134
+ --bg: oklch(0.1377 0.0167 150.7681);
135
+ --surface: oklch(0.1961 0.0163 146.9867);
136
+ --surface-2: oklch(0.2335 0.0184 146.9867);
137
+ --fg: oklch(0.9423 0.0097 72.6595);
138
+ --muted-fg: oklch(0.79 0.0174 76.0955);
139
+ --border: oklch(0.2644 0.0159 142.9926);
140
+ --primary: oklch(0.6731 0.1624 144.2083);
141
+ --primary-fg: oklch(0.2157 0.0453 145.7256);
142
+ --shadow: 0 1px 3px hsl(0 0% 0% / 0.4), 0 1px 2px -1px hsl(0 0% 0% / 0.4);
143
+ --sev-critical: #f97066;
144
+ --sev-high: #fda29b;
145
+ --sev-medium: #fdb022;
146
+ --sev-low: #53b1fd;
147
+ --sev-info: #98a2b3;
148
+ --sev-unknown: #667085;
149
+ --pass: #47cd89;
150
+ --review: #fdb022;
151
+ --fail: #f97066;
152
+ }
153
+ * { box-sizing: border-box; }
154
+ html, body { margin: 0; padding: 0; }
155
+ body {
156
+ background: var(--bg);
157
+ color: var(--fg);
158
+ font-family: var(--font-sans);
159
+ font-feature-settings: "ss01", "ss02", "cv11";
160
+ line-height: 1.5;
161
+ -webkit-font-smoothing: antialiased;
162
+ }
163
+ a { color: var(--primary); }
164
+ code, .mono { font-family: var(--font-mono); font-size: 0.86em; }
165
+
166
+ .topbar {
167
+ position: sticky; top: 0; z-index: 20;
168
+ display: flex; align-items: center; gap: 12px;
169
+ padding: 14px 24px;
170
+ background: color-mix(in oklab, var(--bg) 85%, transparent);
171
+ backdrop-filter: blur(8px);
172
+ border-bottom: 1px solid var(--border);
173
+ }
174
+ .topbar .logo { color: var(--primary); display: inline-flex; }
175
+ .topbar .brand { font-weight: 700; font-size: 1.05rem; letter-spacing: -0.01em; }
176
+ .topbar .sep { color: var(--muted-fg); }
177
+ .topbar .subtitle { color: var(--muted-fg); font-size: 0.9rem; }
178
+ .topbar .spacer { flex: 1; }
179
+ .toggle {
180
+ display: inline-flex; align-items: center; gap: 6px;
181
+ border: 1px solid var(--border); background: var(--surface);
182
+ color: var(--fg); border-radius: 999px; padding: 6px 12px;
183
+ font: inherit; font-size: 0.85rem; cursor: pointer;
184
+ }
185
+ .toggle:hover { border-color: var(--primary); }
186
+
187
+ .layout { display: grid; grid-template-columns: 230px minmax(0, 1fr); gap: 32px; max-width: 1180px; margin: 0 auto; padding: 28px 24px 80px; }
188
+ @media (max-width: 860px) { .layout { grid-template-columns: 1fr; } .nav { display: none; } }
189
+
190
+ .nav { position: sticky; top: 76px; align-self: start; font-size: 0.9rem; }
191
+ .nav a { display: flex; justify-content: space-between; gap: 8px; padding: 7px 10px; border-radius: 8px; color: var(--fg); text-decoration: none; }
192
+ .nav a:hover { background: var(--surface-2); }
193
+ .nav a .n { color: var(--muted-fg); font-variant-numeric: tabular-nums; }
194
+
195
+ .meta { color: var(--muted-fg); font-size: 0.85rem; margin-bottom: 18px; }
196
+ .meta .mono { color: var(--fg); }
197
+
198
+ section.card { background: var(--surface); border: 1px solid var(--border); border-radius: 14px; padding: 20px 22px; margin-bottom: 22px; box-shadow: var(--shadow); }
199
+ h2 { font-size: 1.15rem; margin: 0 0 14px; letter-spacing: -0.01em; }
200
+ h2 .count { color: var(--muted-fg); font-weight: 500; font-size: 0.9rem; }
201
+
202
+ .verdict { border-left: 5px solid var(--review); }
203
+ .verdict.fail { border-left-color: var(--fail); }
204
+ .verdict.review { border-left-color: var(--review); }
205
+ .verdict.pass { border-left-color: var(--pass); }
206
+ .verdict .headline { font-size: 1.3rem; font-weight: 700; margin: 0 0 6px; }
207
+ .verdict.fail .headline { color: var(--fail); }
208
+ .verdict.review .headline { color: var(--review); }
209
+ .verdict.pass .headline { color: var(--pass); }
210
+ .verdict .detail { color: var(--muted-fg); margin: 0; max-width: 70ch; }
211
+
212
+ .stats { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 16px; }
213
+ .stat { background: var(--surface-2); border-radius: 10px; padding: 10px 14px; min-width: 92px; }
214
+ .stat .v { font-size: 1.4rem; font-weight: 700; font-variant-numeric: tabular-nums; }
215
+ .stat .l { color: var(--muted-fg); font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.04em; }
216
+
217
+ .gauge { margin-top: 16px; }
218
+ .gauge .track { height: 10px; border-radius: 999px; background: var(--surface-2); overflow: hidden; }
219
+ .gauge .fill { height: 100%; border-radius: 999px; }
220
+ .gauge .label { display: flex; justify-content: space-between; font-size: 0.82rem; color: var(--muted-fg); margin-bottom: 6px; }
221
+
222
+ .badge { display: inline-block; padding: 2px 9px; border-radius: 999px; font-size: 0.72rem; font-weight: 700; letter-spacing: 0.03em; color: #fff; white-space: nowrap; }
223
+ .badge.sev-CRITICAL { background: var(--sev-critical); }
224
+ .badge.sev-HIGH { background: var(--sev-high); }
225
+ .badge.sev-MEDIUM { background: var(--sev-medium); }
226
+ .badge.sev-LOW { background: var(--sev-low); }
227
+ .badge.sev-INFO { background: var(--sev-info); }
228
+ .badge.sev-UNKNOWN { background: var(--sev-unknown); }
229
+ html.dark .badge { color: #0b0f0c; }
230
+ .badge.impact { background: var(--surface-2); color: var(--fg); border: 1px solid var(--border); }
231
+ .badge.impact.REJECTION_LIKELY { background: var(--fail); color: #fff; border: none; }
232
+ .badge.impact.REVIEW_FLAG { background: var(--review); color: #fff; border: none; }
233
+ html.dark .badge.impact.REJECTION_LIKELY, html.dark .badge.impact.REVIEW_FLAG { color: #0b0f0c; }
234
+
235
+ .compliance-item { border: 1px solid var(--border); border-radius: 12px; padding: 14px 16px; margin-bottom: 12px; }
236
+ .compliance-item .head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
237
+ .compliance-item .title { font-weight: 700; }
238
+ .compliance-item .stores { color: var(--muted-fg); font-size: 0.8rem; }
239
+ .compliance-item .why { color: var(--muted-fg); margin: 8px 0 0; }
240
+ .compliance-item .fix { margin: 8px 0 0; }
241
+ .compliance-item .evidence { margin: 10px 0 0; background: var(--surface-2); border-radius: 8px; padding: 8px 10px; overflow-x: auto; }
242
+ .compliance-item .evidence pre { margin: 0; white-space: pre-wrap; word-break: break-word; }
243
+
244
+ .filters { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-bottom: 16px; }
245
+ .sev-chip { cursor: pointer; border: 1px solid transparent; opacity: 1; user-select: none; }
246
+ .sev-chip.off { opacity: 0.32; filter: grayscale(0.6); }
247
+ .filters input[type=search], .filters select {
248
+ font: inherit; font-size: 0.88rem; padding: 7px 11px; border-radius: 8px;
249
+ border: 1px solid var(--border); background: var(--surface); color: var(--fg);
250
+ }
251
+ .filters input[type=search] { flex: 1; min-width: 160px; }
252
+ .filters .spacer { flex: 1; }
253
+ .filters .lnk { background: none; border: none; color: var(--primary); cursor: pointer; font: inherit; font-size: 0.85rem; padding: 4px; }
254
+
255
+ .group { border: 1px solid var(--border); border-radius: 12px; margin-bottom: 10px; overflow: hidden; }
256
+ .group[hidden] { display: none; }
257
+ .group > summary { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; padding: 12px 14px; cursor: pointer; list-style: none; }
258
+ .group > summary::-webkit-details-marker { display: none; }
259
+ .group > summary::before { content: "▸"; color: var(--muted-fg); transition: transform 0.15s; }
260
+ .group[open] > summary::before { transform: rotate(90deg); }
261
+ .group .rule { font-family: var(--font-mono); font-size: 0.9rem; font-weight: 600; }
262
+ .group .scanner { color: var(--muted-fg); font-size: 0.8rem; }
263
+ .group .times { margin-left: auto; background: var(--surface-2); border-radius: 999px; padding: 2px 10px; font-size: 0.78rem; font-variant-numeric: tabular-nums; color: var(--muted-fg); }
264
+ .group .body { padding: 0 14px 14px 36px; }
265
+ .group .msg { color: var(--muted-fg); margin: 0 0 10px; max-width: 80ch; }
266
+ .group .locs { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 3px; }
267
+ .group .locs li { font-family: var(--font-mono); font-size: 0.82rem; color: var(--fg); word-break: break-all; }
268
+ .group .more { color: var(--muted-fg); font-size: 0.8rem; margin-top: 6px; }
269
+
270
+ .no-matches { color: var(--muted-fg); text-align: center; padding: 24px; }
271
+
272
+ table.scanners { width: 100%; border-collapse: collapse; font-size: 0.88rem; }
273
+ table.scanners th, table.scanners td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--border); }
274
+ table.scanners th { color: var(--muted-fg); font-weight: 600; font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.04em; }
275
+ table.scanners .ok { color: var(--pass); }
276
+ table.scanners .bad { color: var(--fail); }
277
+
278
+ .disclaimer { color: var(--muted-fg); font-size: 0.82rem; max-width: 80ch; }
279
+ .disclaimer a { color: var(--primary); }
280
+ .empty-good { color: var(--pass); font-weight: 600; }
281
+ `;
282
+ /** Client-side behaviour: theme persistence, filtering, expand/collapse. No template literals / no `${` so it survives interpolation verbatim. */
283
+ const SCRIPT = `
284
+ (function () {
285
+ var root = document.documentElement;
286
+ var btn = document.getElementById('theme-toggle');
287
+ function syncLabel() {
288
+ if (!btn) return;
289
+ btn.textContent = root.classList.contains('dark') ? '☀ Light' : '☾ Dark';
290
+ }
291
+ syncLabel();
292
+ if (btn) btn.addEventListener('click', function () {
293
+ var dark = root.classList.toggle('dark');
294
+ try { localStorage.setItem('extenshi-report-theme', dark ? 'dark' : 'light'); } catch (e) {}
295
+ syncLabel();
296
+ });
297
+
298
+ var groups = Array.prototype.slice.call(document.querySelectorAll('.group'));
299
+ var chips = Array.prototype.slice.call(document.querySelectorAll('.sev-chip'));
300
+ var scannerSel = document.getElementById('scanner-filter');
301
+ var search = document.getElementById('search');
302
+ var empty = document.getElementById('no-matches');
303
+ var counter = document.getElementById('visible-count');
304
+ var active = {};
305
+ chips.forEach(function (c) { active[c.getAttribute('data-sev')] = true; });
306
+
307
+ function refresh() {
308
+ var scanner = scannerSel ? scannerSel.value : 'all';
309
+ var q = (search ? search.value : '').trim().toLowerCase();
310
+ var shown = 0;
311
+ groups.forEach(function (g) {
312
+ var sev = g.getAttribute('data-severity');
313
+ var sc = g.getAttribute('data-scanner');
314
+ var text = g.getAttribute('data-text') || '';
315
+ var ok = active[sev] && (scanner === 'all' || scanner === sc) && (q === '' || text.indexOf(q) > -1);
316
+ if (ok) { g.removeAttribute('hidden'); shown++; } else { g.setAttribute('hidden', ''); }
317
+ });
318
+ if (counter) counter.textContent = String(shown);
319
+ if (empty) { if (shown === 0) empty.removeAttribute('hidden'); else empty.setAttribute('hidden', ''); }
320
+ }
321
+
322
+ chips.forEach(function (c) {
323
+ c.addEventListener('click', function () {
324
+ var s = c.getAttribute('data-sev');
325
+ active[s] = !active[s];
326
+ c.classList.toggle('off', !active[s]);
327
+ refresh();
328
+ });
329
+ });
330
+ if (scannerSel) scannerSel.addEventListener('change', refresh);
331
+ if (search) search.addEventListener('input', refresh);
332
+
333
+ var expandBtn = document.getElementById('expand-all');
334
+ var collapseBtn = document.getElementById('collapse-all');
335
+ if (expandBtn) expandBtn.addEventListener('click', function () {
336
+ groups.forEach(function (g) { if (!g.hasAttribute('hidden')) g.setAttribute('open', ''); });
337
+ });
338
+ if (collapseBtn) collapseBtn.addEventListener('click', function () {
339
+ groups.forEach(function (g) { g.removeAttribute('open'); });
340
+ });
341
+
342
+ refresh();
343
+ })();
344
+ `;
345
+ // ── Section renderers ────────────────────────────────────────────────────────
346
+ const SEVERITIES = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW', 'INFO', 'UNKNOWN'];
347
+ function renderStats(summary, groupCount) {
348
+ const stat = (v, l) => `<div class="stat"><div class="v">${esc(String(v))}</div><div class="l">${esc(l)}</div></div>`;
349
+ return ('<div class="stats">' +
350
+ stat(summary.total, 'Findings') +
351
+ stat(summary.critHigh, 'Critical / high') +
352
+ stat(groupCount, 'Distinct rules') +
353
+ stat(`${summary.scannersOk}/${summary.scannersRun}`, 'Scanners OK') +
354
+ '</div>');
355
+ }
356
+ function renderGauge(score) {
357
+ const clamped = Math.max(0, Math.min(100, Math.round(score)));
358
+ const color = clamped >= 70 ? 'var(--fail)' : clamped >= 35 ? 'var(--review)' : 'var(--pass)';
359
+ return ('<div class="gauge">' +
360
+ `<div class="label"><span>Manifest risk score</span><span>${clamped}/100</span></div>` +
361
+ `<div class="track"><div class="fill" style="width:${clamped}%;background:${color}"></div></div>` +
362
+ '</div>');
363
+ }
364
+ function renderComplianceSection(compliance) {
365
+ const parts = [`<section class="card" id="compliance"><h2>Store compliance</h2>`];
366
+ if (!compliance.present) {
367
+ parts.push(`<p class="disclaimer">Store-compliance analysis is unavailable — the scan server has not been updated yet. Re-run after the next server rollout (no CLI action needed).</p>`);
368
+ return `${parts.join('')}</section>`;
369
+ }
370
+ if (compliance.findings.length === 0) {
371
+ parts.push(`<p class="empty-good">✓ No store-compliance signals detected in the manifest.</p>`);
372
+ parts.push(renderGauge(compliance.manifestRiskScore));
373
+ return `${parts.join('')}</section>`;
374
+ }
375
+ for (const f of compliance.findings) {
376
+ const impactShort = STORE_IMPACT_SHORT[f.storeImpact] ?? f.storeImpact;
377
+ const stores = f.stores.length ? `<span class="stores">${esc(f.stores.join(', '))}</span>` : '';
378
+ const evidence = f.evidence.length
379
+ ? `<div class="evidence"><pre>${esc(f.evidence.slice(0, 10).join('\n'))}</pre></div>`
380
+ : '';
381
+ parts.push('<div class="compliance-item">' +
382
+ '<div class="head">' +
383
+ `<span class="badge impact ${cssToken(f.storeImpact)}">${esc(impactShort)}</span>` +
384
+ `<span class="badge sev-${cssToken(f.severity)}">${esc(f.severity)}</span>` +
385
+ `<span class="title">${esc(f.label)}</span>` +
386
+ stores +
387
+ '</div>' +
388
+ (f.summary ? `<p class="why"><strong>Why stores care:</strong> ${esc(f.summary)}</p>` : '') +
389
+ evidence +
390
+ (f.remediation ? `<p class="fix"><strong>How to fix:</strong> ${esc(f.remediation)}</p>` : '') +
391
+ '</div>');
392
+ }
393
+ parts.push(renderGauge(compliance.manifestRiskScore));
394
+ if (compliance.flags.length > 0) {
395
+ parts.push(`<p class="meta">Flags set: ${compliance.flags.map((fl) => `<code>${esc(fl)}</code>`).join(', ')}</p>`);
396
+ }
397
+ return `${parts.join('')}</section>`;
398
+ }
399
+ function renderSecuritySection(groups) {
400
+ const parts = [
401
+ `<section class="card" id="security"><h2>Security findings <span class="count">(<span id="visible-count">${groups.length}</span> shown)</span></h2>`,
402
+ ];
403
+ if (groups.length === 0) {
404
+ parts.push(`<p class="empty-good">✓ No code-analysis findings raised by the automated scanners.</p>`);
405
+ return `${parts.join('')}</section>`;
406
+ }
407
+ // Severity filter chips (only the severities actually present).
408
+ const presentSevs = SEVERITIES.filter((s) => groups.some((g) => g.severity === s));
409
+ const chips = presentSevs
410
+ .map((s) => {
411
+ const n = groups.filter((g) => g.severity === s).reduce((acc, g) => acc + g.count, 0);
412
+ return `<span class="badge sev-${s} sev-chip" data-sev="${s}" role="button" tabindex="0">${s} ${n}</span>`;
413
+ })
414
+ .join('');
415
+ // Scanner dropdown (unique scanners across groups).
416
+ const scanners = [...new Set(groups.map((g) => g.scanner))].sort((a, b) => a.localeCompare(b));
417
+ const scannerOpts = ['<option value="all">All scanners</option>']
418
+ .concat(scanners.map((s) => `<option value="${esc(s)}">${esc(s)}</option>`))
419
+ .join('');
420
+ parts.push('<div class="filters">' +
421
+ chips +
422
+ '<span class="spacer"></span>' +
423
+ `<select id="scanner-filter" aria-label="Filter by scanner">${scannerOpts}</select>` +
424
+ '<input id="search" type="search" placeholder="Filter findings…" aria-label="Filter findings by text">' +
425
+ '<button class="lnk" id="expand-all" type="button">Expand all</button>' +
426
+ '<button class="lnk" id="collapse-all" type="button">Collapse all</button>' +
427
+ '</div>');
428
+ for (const g of groups) {
429
+ const searchText = esc(`${g.headline} ${g.scanner} ${g.message} ${g.locations.join(' ')} ${g.extras.join(' ')}`.toLowerCase());
430
+ const shownLocs = g.locations.slice(0, MAX_LOCATIONS_PER_GROUP);
431
+ const locsHtml = shownLocs.length
432
+ ? `<ul class="locs">${shownLocs.map((l) => `<li>${esc(l)}</li>`).join('')}</ul>`
433
+ : '';
434
+ const moreLocs = g.locations.length > MAX_LOCATIONS_PER_GROUP
435
+ ? `<p class="more">…and ${g.locations.length - MAX_LOCATIONS_PER_GROUP} more location(s)</p>`
436
+ : '';
437
+ const extrasHtml = g.extras.length ? `<p class="more">${esc(g.extras.slice(0, 5).join(' • '))}</p>` : '';
438
+ const times = g.count > 1 ? `<span class="times">×${g.count}</span>` : '';
439
+ parts.push(`<details class="group" data-severity="${esc(g.severity)}" data-scanner="${esc(g.scanner)}" data-text="${searchText}">` +
440
+ '<summary>' +
441
+ `<span class="badge sev-${cssToken(g.severity)}">${esc(g.severity)}</span>` +
442
+ `<span class="rule">${esc(g.headline)}</span>` +
443
+ `<span class="scanner">${esc(g.scanner)}</span>` +
444
+ times +
445
+ '</summary>' +
446
+ '<div class="body">' +
447
+ (g.message ? `<p class="msg">${esc(g.message)}</p>` : '') +
448
+ locsHtml +
449
+ moreLocs +
450
+ extrasHtml +
451
+ '</div>' +
452
+ '</details>');
453
+ }
454
+ parts.push(`<p class="no-matches" id="no-matches" hidden>No findings match the current filters.</p>`);
455
+ return `${parts.join('')}</section>`;
456
+ }
457
+ function renderScannersSection(summary) {
458
+ const parts = [
459
+ `<section class="card" id="scanners"><h2>Scanner status <span class="count">(${summary.scannersOk}/${summary.scannersRun} succeeded)</span></h2>`,
460
+ ];
461
+ if (summary.issues.length === 0) {
462
+ parts.push(`<p class="empty-good">✓ All ${summary.scannersRun} scanners completed successfully.</p>`);
463
+ return `${parts.join('')}</section>`;
464
+ }
465
+ parts.push('<table class="scanners"><thead><tr><th>Scanner</th><th>Status</th><th>Detail</th></tr></thead><tbody>');
466
+ for (const i of summary.issues) {
467
+ parts.push(`<tr><td class="mono">${esc(i.scanner)}</td><td class="bad">${esc(i.status)}</td><td>${esc(i.error ?? '')}</td></tr>`);
468
+ }
469
+ parts.push('</tbody></table>');
470
+ return `${parts.join('')}</section>`;
471
+ }
472
+ // ── Document assembly ─────────────────────────────────────────────────────────
473
+ export function renderHtmlReport(body, meta) {
474
+ const summary = summarizeReport(body);
475
+ const compliance = summarizeCompliance(body);
476
+ const verdict = computeVerdict(compliance, summary);
477
+ const groups = groupFindings(summary.findings);
478
+ const title = `Extenshi scan report — ${meta.artifactName}`;
479
+ const job = summary.jobId ? ` · Job <span class="mono">${esc(summary.jobId)}</span>` : '';
480
+ const nav = '<nav class="nav">' +
481
+ '<a href="#verdict"><span>Verdict</span></a>' +
482
+ `<a href="#compliance"><span>Store compliance</span><span class="n">${compliance.present ? compliance.findings.length : '–'}</span></a>` +
483
+ `<a href="#security"><span>Security findings</span><span class="n">${summary.total}</span></a>` +
484
+ `<a href="#scanners"><span>Scanners</span><span class="n">${summary.scannersOk}/${summary.scannersRun}</span></a>` +
485
+ '</nav>';
486
+ const verdictSection = `<section class="card verdict ${verdict.level}" id="verdict">` +
487
+ `<p class="headline">${esc(verdict.headline)}</p>` +
488
+ `<p class="detail">${esc(verdict.detail)}</p>` +
489
+ renderStats(summary, groups.length) +
490
+ '</section>';
491
+ const metaLine = '<p class="meta">' +
492
+ `<span class="mono">${esc(meta.artifactName)}</span> · ` +
493
+ `@extenshi/cli v${esc(meta.cliVersion)} · ${esc(meta.generatedAt.toISOString())}${job}` +
494
+ '</p>';
495
+ const footer = '<section class="card"><p class="disclaimer">' +
496
+ 'This scan is automated. The scanners are heuristics that may miss real issues or over-flag benign ' +
497
+ 'code — a clean result is not a guarantee of safety or store approval. ' +
498
+ 'See the <a href="https://catalog.extenshi.io/disclaimers/security-risk" target="_blank" rel="noreferrer">security-risk disclaimer</a>. ' +
499
+ 'Manage claims and respond to findings at <a href="https://dojo.extenshi.io" target="_blank" rel="noreferrer">dojo.extenshi.io</a>.' +
500
+ '</p></section>';
501
+ const head = '<!doctype html><html lang="en"><head><meta charset="utf-8">' +
502
+ '<meta name="viewport" content="width=device-width, initial-scale=1">' +
503
+ `<title>${esc(title)}</title>` +
504
+ // Set the theme before paint to avoid a flash of the wrong palette.
505
+ "<script>(function(){try{var t=localStorage.getItem('extenshi-report-theme');if(!t)t=(window.matchMedia&&window.matchMedia('(prefers-color-scheme: dark)').matches)?'dark':'light';if(t==='dark')document.documentElement.classList.add('dark');}catch(e){}})();</script>" +
506
+ `<style>${STYLE}</style></head>`;
507
+ const topbar = '<header class="topbar">' +
508
+ `<span class="logo">${LOGO_SVG}</span>` +
509
+ '<span class="brand">extenshi</span>' +
510
+ '<span class="sep">·</span>' +
511
+ '<span class="subtitle">Security scan report</span>' +
512
+ '<span class="spacer"></span>' +
513
+ '<button class="toggle" id="theme-toggle" type="button" aria-label="Toggle colour theme"></button>' +
514
+ '</header>';
515
+ const main = '<div class="layout">' +
516
+ nav +
517
+ '<div class="content">' +
518
+ metaLine +
519
+ verdictSection +
520
+ renderComplianceSection(compliance) +
521
+ renderSecuritySection(groups) +
522
+ renderScannersSection(summary) +
523
+ footer +
524
+ '</div></div>';
525
+ return `${head}<body>${topbar}${main}<script>${SCRIPT}</script></body></html>\n`;
526
+ }
527
+ //# sourceMappingURL=html-report.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html-report.js","sourceRoot":"","sources":["../src/html-report.ts"],"names":[],"mappings":"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"}
package/dist/render.d.ts CHANGED
@@ -43,6 +43,7 @@ export declare function isScannerOk(status: unknown): boolean;
43
43
  export declare function scannerLabel(s: ScannerResult): string;
44
44
  /** Headline = identifier, falling back to category, never empty (no more "(no rule)"). */
45
45
  export declare function findingHeadline(f: Finding): string;
46
+ export declare function severityRank(s: string | undefined): number;
46
47
  export interface FindingView {
47
48
  scanner: string;
48
49
  severity: string;
@@ -71,6 +72,27 @@ export interface ReportSummary {
71
72
  * fixture without spying on console.log.
72
73
  */
73
74
  export declare function summarizeReport(body: Record<string, unknown>): ReportSummary;
75
+ export interface ComplianceFindingView {
76
+ ruleId: string;
77
+ severity: string;
78
+ label: string;
79
+ summary: string;
80
+ remediation: string;
81
+ storeImpact: string;
82
+ stores: string[];
83
+ evidence: string[];
84
+ }
85
+ export interface ComplianceSummary {
86
+ /** false when the scan server has not been updated to emit `compliance` yet. */
87
+ present: boolean;
88
+ status: string;
89
+ manifestRiskScore: number;
90
+ findings: ComplianceFindingView[];
91
+ flags: string[];
92
+ }
93
+ export declare const STORE_IMPACT_SHORT: Record<string, string>;
94
+ /** Reduce the additive `compliance` payload key to a render-ready summary. Pure. */
95
+ export declare function summarizeCompliance(body: Record<string, unknown>): ComplianceSummary;
74
96
  export declare function renderReport(body: Record<string, unknown>): void;
75
97
  export {};
76
98
  //# sourceMappingURL=render.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAMH,UAAU,OAAO;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACtB;AAED,UAAU,aAAa;IACtB,iBAAiB;IACjB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,8BAA8B;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAA;IACpB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACtB;AAaD,wBAAgB,WAAW,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,CAEpD;AAED,sFAAsF;AACtF,wBAAgB,YAAY,CAAC,CAAC,EAAE,aAAa,GAAG,MAAM,CAIrD;AAiBD,0FAA0F;AAC1F,wBAAgB,eAAe,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,CAElD;AAyED,MAAM,WAAW,WAAW;IAC3B,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,YAAY;IAC5B,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,aAAa;IAC7B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,WAAW,EAAE,CAAA;IACvB,MAAM,EAAE,YAAY,EAAE,CAAA;CACtB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,aAAa,CA6C5E;AAID,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAwDhE"}
1
+ {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAMH,UAAU,OAAO;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACtB;AAED,UAAU,aAAa;IACtB,iBAAiB;IACjB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,8BAA8B;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAA;IACpB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACtB;AAaD,wBAAgB,WAAW,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,CAEpD;AAED,sFAAsF;AACtF,wBAAgB,YAAY,CAAC,CAAC,EAAE,aAAa,GAAG,MAAM,CAIrD;AAiBD,0FAA0F;AAC1F,wBAAgB,eAAe,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,CAElD;AAkDD,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAE1D;AAqBD,MAAM,WAAW,WAAW;IAC3B,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,YAAY;IAC5B,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,aAAa;IAC7B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,WAAW,EAAE,CAAA;IACvB,MAAM,EAAE,YAAY,EAAE,CAAA;CACtB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,aAAa,CA6C5E;AAID,MAAM,WAAW,qBAAqB;IACrC,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,MAAM,CAAA;IACf,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,QAAQ,EAAE,MAAM,EAAE,CAAA;CAClB;AAED,MAAM,WAAW,iBAAiB;IACjC,gFAAgF;IAChF,OAAO,EAAE,OAAO,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,iBAAiB,EAAE,MAAM,CAAA;IACzB,QAAQ,EAAE,qBAAqB,EAAE,CAAA;IACjC,KAAK,EAAE,MAAM,EAAE,CAAA;CACf;AASD,eAAO,MAAM,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAKrD,CAAA;AAMD,oFAAoF;AACpF,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,iBAAiB,CAuCpF;AAID,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAuFhE"}