supasec 1.0.2 → 1.0.3
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/COMPLETION_REPORT.md +324 -0
- package/FIXES_SUMMARY.md +224 -0
- package/IMPLEMENTATION_NOTES.md +305 -0
- package/QUICK_REFERENCE.md +185 -0
- package/README.md +1 -1
- package/REPORTING.md +217 -0
- package/STATUS.md +269 -0
- package/dist/commands/scan.d.ts +1 -0
- package/dist/commands/scan.d.ts.map +1 -1
- package/dist/commands/scan.js +186 -15
- package/dist/commands/scan.js.map +1 -1
- package/dist/models/scan-result.d.ts +8 -0
- package/dist/models/scan-result.d.ts.map +1 -1
- package/dist/models/scan-result.js.map +1 -1
- package/dist/reporters/html.d.ts +18 -0
- package/dist/reporters/html.d.ts.map +1 -0
- package/dist/reporters/html.js +946 -0
- package/dist/reporters/html.js.map +1 -0
- package/dist/reporters/index.d.ts +2 -0
- package/dist/reporters/index.d.ts.map +1 -1
- package/dist/reporters/index.js +2 -0
- package/dist/reporters/index.js.map +1 -1
- package/dist/reporters/terminal.d.ts.map +1 -1
- package/dist/reporters/terminal.js +9 -0
- package/dist/reporters/terminal.js.map +1 -1
- package/dist/scanners/secrets/detector.d.ts.map +1 -1
- package/dist/scanners/secrets/detector.js +6 -2
- package/dist/scanners/secrets/detector.js.map +1 -1
- package/package.json +1 -1
- package/reports/supasec---------app-2026-01-28-16-58-47.html +804 -0
- package/reports/supasec---------app-2026-01-28-17-06-43.html +722 -0
- package/reports/supasec---------app-2026-01-28-17-07-23.html +722 -0
- package/reports/supasec---------app-2026-01-28-17-08-00.html +722 -0
- package/reports/supasec---------app-2026-01-28-17-08-20.html +722 -0
- package/reports/supasec---------app-2026-01-28-17-08-41.html +722 -0
- package/reports/supasec-au---your-app-2026-01-28-17-14-57.html +715 -0
- package/reports/supasec-au---your-app-2026-01-28-17-19-03.html +715 -0
- package/reports/supasec-audityour-app-2026-01-28-17-09-24.html +722 -0
- package/reports/supasec-ex-mple-com-2026-01-28-17-14-52.json +229 -0
- package/reports/supasec-ex-mple-com-2026-01-28-17-15-39.html +715 -0
- package/reports/supasec-ex-mple-com-2026-01-28-17-17-22.html +715 -0
- package/reports/supasec-example-com-2026-01-28-17-15-06.html +715 -0
- package/reports/supasec-my--------------name-com-2026-01-28-17-15-02.html +715 -0
- package/reports/supasec-st-ging-com-2026-01-28-17-16-17.html +715 -0
- package/PUBLISHING.md +0 -51
|
@@ -0,0 +1,946 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* HTML Reporter
|
|
4
|
+
* Generates detailed HTML reports matching the Supascan.io style
|
|
5
|
+
*/
|
|
6
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
7
|
+
if (k2 === undefined) k2 = k;
|
|
8
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
9
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
10
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
11
|
+
}
|
|
12
|
+
Object.defineProperty(o, k2, desc);
|
|
13
|
+
}) : (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
o[k2] = m[k];
|
|
16
|
+
}));
|
|
17
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
18
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
19
|
+
}) : function(o, v) {
|
|
20
|
+
o["default"] = v;
|
|
21
|
+
});
|
|
22
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
23
|
+
var ownKeys = function(o) {
|
|
24
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
25
|
+
var ar = [];
|
|
26
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
27
|
+
return ar;
|
|
28
|
+
};
|
|
29
|
+
return ownKeys(o);
|
|
30
|
+
};
|
|
31
|
+
return function (mod) {
|
|
32
|
+
if (mod && mod.__esModule) return mod;
|
|
33
|
+
var result = {};
|
|
34
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
35
|
+
__setModuleDefault(result, mod);
|
|
36
|
+
return result;
|
|
37
|
+
};
|
|
38
|
+
})();
|
|
39
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
+
exports.generateHTMLReport = generateHTMLReport;
|
|
41
|
+
exports.saveHTMLReport = saveHTMLReport;
|
|
42
|
+
const finding_js_1 = require("../models/finding.js");
|
|
43
|
+
/**
|
|
44
|
+
* Generate HTML report from scan result
|
|
45
|
+
*/
|
|
46
|
+
function generateHTMLReport(result, options = {}) {
|
|
47
|
+
const { title = 'SupaSec Security Audit Report', includeDetails = true } = options;
|
|
48
|
+
const counts = (0, finding_js_1.countFindingsBySeverity)(result.findings);
|
|
49
|
+
const sortedFindings = (0, finding_js_1.sortFindingsBySeverity)(result.findings);
|
|
50
|
+
// Group findings by severity
|
|
51
|
+
const findingsBySeverity = {
|
|
52
|
+
CRITICAL: [],
|
|
53
|
+
HIGH: [],
|
|
54
|
+
MEDIUM: [],
|
|
55
|
+
LOW: [],
|
|
56
|
+
INFO: []
|
|
57
|
+
};
|
|
58
|
+
for (const finding of sortedFindings) {
|
|
59
|
+
findingsBySeverity[finding.severity].push(finding);
|
|
60
|
+
}
|
|
61
|
+
const scanDate = new Date(result.scan_metadata.scan_date).toLocaleString();
|
|
62
|
+
return `<!DOCTYPE html>
|
|
63
|
+
<html lang="en">
|
|
64
|
+
<head>
|
|
65
|
+
<meta charset="UTF-8">
|
|
66
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
67
|
+
<title>${escapeHtml(title)}</title>
|
|
68
|
+
<style>
|
|
69
|
+
* {
|
|
70
|
+
margin: 0;
|
|
71
|
+
padding: 0;
|
|
72
|
+
box-sizing: border-box;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
body {
|
|
76
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
77
|
+
background: #f8fafc;
|
|
78
|
+
min-height: 100vh;
|
|
79
|
+
color: #334155;
|
|
80
|
+
line-height: 1.6;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.container {
|
|
84
|
+
max-width: 1000px;
|
|
85
|
+
margin: 0 auto;
|
|
86
|
+
padding: 20px;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* Header */
|
|
90
|
+
.header {
|
|
91
|
+
background: white;
|
|
92
|
+
padding: 16px 24px;
|
|
93
|
+
border-bottom: 1px solid #e2e8f0;
|
|
94
|
+
display: flex;
|
|
95
|
+
align-items: center;
|
|
96
|
+
gap: 12px;
|
|
97
|
+
margin: -20px -20px 20px -20px;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.logo {
|
|
101
|
+
width: 32px;
|
|
102
|
+
height: 32px;
|
|
103
|
+
background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
|
|
104
|
+
border-radius: 8px;
|
|
105
|
+
display: flex;
|
|
106
|
+
align-items: center;
|
|
107
|
+
justify-content: center;
|
|
108
|
+
color: white;
|
|
109
|
+
font-weight: bold;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.header h1 {
|
|
113
|
+
font-size: 20px;
|
|
114
|
+
color: #1e293b;
|
|
115
|
+
font-weight: 600;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* Info Banner */
|
|
119
|
+
.info-banner {
|
|
120
|
+
background: #eff6ff;
|
|
121
|
+
border: 1px solid #bfdbfe;
|
|
122
|
+
border-radius: 8px;
|
|
123
|
+
padding: 16px 20px;
|
|
124
|
+
margin-bottom: 24px;
|
|
125
|
+
display: flex;
|
|
126
|
+
align-items: flex-start;
|
|
127
|
+
gap: 12px;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.info-banner .icon {
|
|
131
|
+
color: #3b82f6;
|
|
132
|
+
font-size: 18px;
|
|
133
|
+
margin-top: 2px;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.info-banner h2 {
|
|
137
|
+
font-size: 16px;
|
|
138
|
+
color: #1e40af;
|
|
139
|
+
margin-bottom: 4px;
|
|
140
|
+
font-weight: 600;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.info-banner p {
|
|
144
|
+
font-size: 14px;
|
|
145
|
+
color: #3b82f6;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* Success Card */
|
|
149
|
+
.success-card {
|
|
150
|
+
background: white;
|
|
151
|
+
border: 1px solid #e2e8f0;
|
|
152
|
+
border-radius: 12px;
|
|
153
|
+
padding: 24px;
|
|
154
|
+
margin-bottom: 24px;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.success-header {
|
|
158
|
+
display: flex;
|
|
159
|
+
align-items: center;
|
|
160
|
+
gap: 12px;
|
|
161
|
+
margin-bottom: 16px;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.success-icon {
|
|
165
|
+
width: 40px;
|
|
166
|
+
height: 40px;
|
|
167
|
+
background: #dcfce7;
|
|
168
|
+
border-radius: 50%;
|
|
169
|
+
display: flex;
|
|
170
|
+
align-items: center;
|
|
171
|
+
justify-content: center;
|
|
172
|
+
color: #16a34a;
|
|
173
|
+
font-size: 20px;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.success-title {
|
|
177
|
+
font-size: 18px;
|
|
178
|
+
font-weight: 600;
|
|
179
|
+
color: #1e293b;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.success-title a {
|
|
183
|
+
color: #3b82f6;
|
|
184
|
+
text-decoration: none;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.success-title a:hover {
|
|
188
|
+
text-decoration: underline;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/* Scan Info Grid */
|
|
192
|
+
.scan-info-grid {
|
|
193
|
+
display: grid;
|
|
194
|
+
grid-template-columns: repeat(2, 1fr);
|
|
195
|
+
gap: 24px;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.info-group h4 {
|
|
199
|
+
font-size: 11px;
|
|
200
|
+
text-transform: uppercase;
|
|
201
|
+
color: #64748b;
|
|
202
|
+
font-weight: 600;
|
|
203
|
+
letter-spacing: 0.5px;
|
|
204
|
+
margin-bottom: 6px;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.info-group p {
|
|
208
|
+
font-size: 14px;
|
|
209
|
+
color: #334155;
|
|
210
|
+
font-weight: 500;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/* Stats Grid */
|
|
214
|
+
.stats-grid {
|
|
215
|
+
display: grid;
|
|
216
|
+
grid-template-columns: repeat(4, 1fr);
|
|
217
|
+
gap: 16px;
|
|
218
|
+
margin-bottom: 24px;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.stat-card {
|
|
222
|
+
background: white;
|
|
223
|
+
border: 1px solid #e2e8f0;
|
|
224
|
+
border-radius: 12px;
|
|
225
|
+
padding: 20px;
|
|
226
|
+
text-align: center;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.stat-label {
|
|
230
|
+
font-size: 11px;
|
|
231
|
+
text-transform: uppercase;
|
|
232
|
+
color: #64748b;
|
|
233
|
+
font-weight: 600;
|
|
234
|
+
letter-spacing: 0.5px;
|
|
235
|
+
margin-bottom: 8px;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.stat-value {
|
|
239
|
+
font-size: 32px;
|
|
240
|
+
font-weight: 700;
|
|
241
|
+
color: #1e293b;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/* Key Findings Section */
|
|
245
|
+
.section {
|
|
246
|
+
margin-bottom: 24px;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.section-header {
|
|
250
|
+
margin-bottom: 16px;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.section-header h2 {
|
|
254
|
+
font-size: 20px;
|
|
255
|
+
font-weight: 600;
|
|
256
|
+
color: #1e293b;
|
|
257
|
+
margin-bottom: 4px;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.section-header p {
|
|
261
|
+
font-size: 14px;
|
|
262
|
+
color: #64748b;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/* Accordion Cards */
|
|
266
|
+
.accordion-card {
|
|
267
|
+
background: white;
|
|
268
|
+
border: 1px solid #e2e8f0;
|
|
269
|
+
border-radius: 12px;
|
|
270
|
+
margin-bottom: 12px;
|
|
271
|
+
overflow: hidden;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.accordion-header {
|
|
275
|
+
padding: 16px 20px;
|
|
276
|
+
cursor: pointer;
|
|
277
|
+
display: flex;
|
|
278
|
+
align-items: center;
|
|
279
|
+
justify-content: space-between;
|
|
280
|
+
gap: 12px;
|
|
281
|
+
transition: background 0.2s;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.accordion-header:hover {
|
|
285
|
+
background: #f8fafc;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.accordion-title {
|
|
289
|
+
display: flex;
|
|
290
|
+
align-items: center;
|
|
291
|
+
gap: 12px;
|
|
292
|
+
flex: 1;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.accordion-title h3 {
|
|
296
|
+
font-size: 15px;
|
|
297
|
+
font-weight: 600;
|
|
298
|
+
color: #1e293b;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.accordion-header p {
|
|
302
|
+
font-size: 13px;
|
|
303
|
+
color: #64748b;
|
|
304
|
+
margin-top: 2px;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.badge {
|
|
308
|
+
display: inline-flex;
|
|
309
|
+
align-items: center;
|
|
310
|
+
gap: 4px;
|
|
311
|
+
padding: 4px 10px;
|
|
312
|
+
border-radius: 20px;
|
|
313
|
+
font-size: 11px;
|
|
314
|
+
font-weight: 600;
|
|
315
|
+
text-transform: uppercase;
|
|
316
|
+
letter-spacing: 0.5px;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.badge-critical {
|
|
320
|
+
background: #fee2e2;
|
|
321
|
+
color: #dc2626;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.badge-high {
|
|
325
|
+
background: #fef3c7;
|
|
326
|
+
color: #d97706;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.badge-medium {
|
|
330
|
+
background: #fef9c3;
|
|
331
|
+
color: #a16207;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.badge-low {
|
|
335
|
+
background: #dbeafe;
|
|
336
|
+
color: #2563eb;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.badge-info {
|
|
340
|
+
background: #dcfce7;
|
|
341
|
+
color: #16a34a;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
.badge-risk {
|
|
345
|
+
background: #fee2e2;
|
|
346
|
+
color: #dc2626;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.badge-concern {
|
|
350
|
+
background: #fef3c7;
|
|
351
|
+
color: #d97706;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.accordion-icon {
|
|
355
|
+
color: #94a3b8;
|
|
356
|
+
font-size: 12px;
|
|
357
|
+
transition: transform 0.2s;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.accordion-content {
|
|
361
|
+
display: none;
|
|
362
|
+
padding: 0 20px 20px 20px;
|
|
363
|
+
border-top: 1px solid #f1f5f9;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
.accordion-content.active {
|
|
367
|
+
display: block;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.content-section {
|
|
371
|
+
margin-bottom: 20px;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
.content-section:last-child {
|
|
375
|
+
margin-bottom: 0;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
.content-section h4 {
|
|
379
|
+
font-size: 14px;
|
|
380
|
+
font-weight: 600;
|
|
381
|
+
color: #1e293b;
|
|
382
|
+
margin-bottom: 8px;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.content-section p {
|
|
386
|
+
font-size: 14px;
|
|
387
|
+
color: #475569;
|
|
388
|
+
line-height: 1.6;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
.content-section ul {
|
|
392
|
+
list-style: none;
|
|
393
|
+
padding: 0;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.content-section li {
|
|
397
|
+
font-size: 14px;
|
|
398
|
+
color: #475569;
|
|
399
|
+
padding: 4px 0;
|
|
400
|
+
padding-left: 16px;
|
|
401
|
+
position: relative;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
.content-section li::before {
|
|
405
|
+
content: "•";
|
|
406
|
+
position: absolute;
|
|
407
|
+
left: 0;
|
|
408
|
+
color: #94a3b8;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
.affected-assets {
|
|
412
|
+
background: #f8fafc;
|
|
413
|
+
border-radius: 8px;
|
|
414
|
+
padding: 12px 16px;
|
|
415
|
+
margin-top: 12px;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
.affected-assets li {
|
|
419
|
+
font-family: 'Monaco', 'Consolas', monospace;
|
|
420
|
+
font-size: 13px;
|
|
421
|
+
color: #475569;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
.tech-details-btn {
|
|
425
|
+
display: inline-flex;
|
|
426
|
+
align-items: center;
|
|
427
|
+
gap: 8px;
|
|
428
|
+
margin-top: 16px;
|
|
429
|
+
padding: 8px 16px;
|
|
430
|
+
background: white;
|
|
431
|
+
border: 1px solid #e2e8f0;
|
|
432
|
+
border-radius: 6px;
|
|
433
|
+
font-size: 13px;
|
|
434
|
+
color: #64748b;
|
|
435
|
+
cursor: pointer;
|
|
436
|
+
transition: all 0.2s;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
.tech-details-btn:hover {
|
|
440
|
+
background: #f8fafc;
|
|
441
|
+
border-color: #cbd5e1;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/* Endpoints Section */
|
|
445
|
+
.endpoints-section {
|
|
446
|
+
background: white;
|
|
447
|
+
border: 1px solid #e2e8f0;
|
|
448
|
+
border-radius: 12px;
|
|
449
|
+
overflow: hidden;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
.endpoints-header {
|
|
453
|
+
padding: 20px;
|
|
454
|
+
border-bottom: 1px solid #f1f5f9;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
.endpoints-header h2 {
|
|
458
|
+
display: flex;
|
|
459
|
+
align-items: center;
|
|
460
|
+
gap: 10px;
|
|
461
|
+
font-size: 16px;
|
|
462
|
+
font-weight: 600;
|
|
463
|
+
color: #1e293b;
|
|
464
|
+
margin-bottom: 4px;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
.endpoints-header p {
|
|
468
|
+
font-size: 13px;
|
|
469
|
+
color: #64748b;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
.endpoints-table {
|
|
473
|
+
width: 100%;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
.table-header {
|
|
477
|
+
display: grid;
|
|
478
|
+
grid-template-columns: 2fr 1fr 1fr 1fr 1fr;
|
|
479
|
+
gap: 12px;
|
|
480
|
+
padding: 12px 20px;
|
|
481
|
+
background: #f8fafc;
|
|
482
|
+
border-bottom: 1px solid #e2e8f0;
|
|
483
|
+
font-size: 11px;
|
|
484
|
+
font-weight: 600;
|
|
485
|
+
color: #64748b;
|
|
486
|
+
text-transform: uppercase;
|
|
487
|
+
letter-spacing: 0.5px;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
.table-row {
|
|
491
|
+
display: grid;
|
|
492
|
+
grid-template-columns: 2fr 1fr 1fr 1fr 1fr;
|
|
493
|
+
gap: 12px;
|
|
494
|
+
padding: 14px 20px;
|
|
495
|
+
border-bottom: 1px solid #f1f5f9;
|
|
496
|
+
align-items: center;
|
|
497
|
+
font-size: 13px;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
.table-row:last-child {
|
|
501
|
+
border-bottom: none;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
.table-row:hover {
|
|
505
|
+
background: #f8fafc;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
.endpoint-path {
|
|
509
|
+
display: flex;
|
|
510
|
+
align-items: center;
|
|
511
|
+
gap: 8px;
|
|
512
|
+
font-family: 'Monaco', 'Consolas', monospace;
|
|
513
|
+
color: #475569;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
.expand-icon {
|
|
517
|
+
color: #94a3b8;
|
|
518
|
+
font-size: 10px;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
.status-badge {
|
|
522
|
+
display: inline-flex;
|
|
523
|
+
padding: 4px 10px;
|
|
524
|
+
border-radius: 20px;
|
|
525
|
+
font-size: 11px;
|
|
526
|
+
font-weight: 600;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
.status-at-risk {
|
|
530
|
+
background: #fee2e2;
|
|
531
|
+
color: #dc2626;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
.status-review {
|
|
535
|
+
background: #fef3c7;
|
|
536
|
+
color: #d97706;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
.status-secure {
|
|
540
|
+
background: #dcfce7;
|
|
541
|
+
color: #16a34a;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
.icon-warning {
|
|
545
|
+
color: #f59e0b;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
.icon-check {
|
|
549
|
+
color: #22c55e;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
.sensitive-badge {
|
|
553
|
+
display: inline-flex;
|
|
554
|
+
padding: 4px 10px;
|
|
555
|
+
background: #fef3c7;
|
|
556
|
+
color: #92400e;
|
|
557
|
+
border-radius: 20px;
|
|
558
|
+
font-size: 11px;
|
|
559
|
+
font-weight: 600;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
.sensitive-none {
|
|
563
|
+
color: #64748b;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/* Footer */
|
|
567
|
+
.footer {
|
|
568
|
+
text-align: center;
|
|
569
|
+
padding: 40px 20px;
|
|
570
|
+
font-size: 12px;
|
|
571
|
+
color: #94a3b8;
|
|
572
|
+
line-height: 1.8;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
@media print {
|
|
576
|
+
body { background: white; }
|
|
577
|
+
.accordion-content { display: block !important; }
|
|
578
|
+
}
|
|
579
|
+
</style>
|
|
580
|
+
</head>
|
|
581
|
+
<body>
|
|
582
|
+
<div class="container">
|
|
583
|
+
<!-- Header -->
|
|
584
|
+
<div class="header">
|
|
585
|
+
<div class="logo">S</div>
|
|
586
|
+
<h1>supasec</h1>
|
|
587
|
+
</div>
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
<!-- Success Card -->
|
|
592
|
+
<div class="success-card">
|
|
593
|
+
<div class="success-header">
|
|
594
|
+
<div class="success-icon">✓</div>
|
|
595
|
+
<div>
|
|
596
|
+
<div class="success-title">Scan completed successfully in ${result.scan_metadata.scan_duration_seconds.toFixed(0)} seconds</div>
|
|
597
|
+
<p style="font-size: 14px; color: #3b82f6; margin-top: 4px;">We found <a href="#findings">${result.summary.total_issues} issues to review</a></p>
|
|
598
|
+
</div>
|
|
599
|
+
</div>
|
|
600
|
+
|
|
601
|
+
<div class="scan-info-grid">
|
|
602
|
+
<div class="info-group">
|
|
603
|
+
<h4>Target</h4>
|
|
604
|
+
<p>${escapeHtml(result.scan_metadata.target_url)}</p>
|
|
605
|
+
</div>
|
|
606
|
+
<div class="info-group">
|
|
607
|
+
<h4>Scan Method</h4>
|
|
608
|
+
<p>${result.scan_metadata.scanner_mode === 'url' ? 'URL Scan' : 'Project Scan'}</p>
|
|
609
|
+
</div>
|
|
610
|
+
<div class="info-group">
|
|
611
|
+
<h4>Duration</h4>
|
|
612
|
+
<p>${result.scan_metadata.scan_duration_seconds.toFixed(2)} seconds</p>
|
|
613
|
+
</div>
|
|
614
|
+
<div class="info-group">
|
|
615
|
+
<h4>Scan Date</h4>
|
|
616
|
+
<p>${scanDate}</p>
|
|
617
|
+
</div>
|
|
618
|
+
</div>
|
|
619
|
+
</div>
|
|
620
|
+
|
|
621
|
+
<!-- Stats Grid -->
|
|
622
|
+
<div class="stats-grid">
|
|
623
|
+
<div class="stat-card">
|
|
624
|
+
<div class="stat-label">Critical</div>
|
|
625
|
+
<div class="stat-value" style="color: #dc2626;">${counts.CRITICAL}</div>
|
|
626
|
+
</div>
|
|
627
|
+
<div class="stat-card">
|
|
628
|
+
<div class="stat-label">High</div>
|
|
629
|
+
<div class="stat-value" style="color: #d97706;">${counts.HIGH}</div>
|
|
630
|
+
</div>
|
|
631
|
+
<div class="stat-card">
|
|
632
|
+
<div class="stat-label">Medium</div>
|
|
633
|
+
<div class="stat-value" style="color: #a16207;">${counts.MEDIUM}</div>
|
|
634
|
+
</div>
|
|
635
|
+
<div class="stat-card">
|
|
636
|
+
<div class="stat-label">Low</div>
|
|
637
|
+
<div class="stat-value" style="color: #2563eb;">${counts.LOW}</div>
|
|
638
|
+
</div>
|
|
639
|
+
</div>
|
|
640
|
+
|
|
641
|
+
${includeDetails ? generateKeyFindings(findingsBySeverity) : ''}
|
|
642
|
+
|
|
643
|
+
${generateEndpointsSection(result)}
|
|
644
|
+
|
|
645
|
+
<!-- Footer -->
|
|
646
|
+
<div class="footer">
|
|
647
|
+
<p>Supasec is an independent service and is not affiliated, associated, authorized, endorsed by, or in any way officially connected with Supabase Inc.</p>
|
|
648
|
+
<p>"Supabase" and related marks are trademarks of Supabase Inc. Any mention is for descriptive purposes only and does not imply any partnership.</p>
|
|
649
|
+
<p style="margin-top: 16px; color: #64748b;">Generated by Supasec • Report ID: ${escapeHtml(result.scan_metadata.scan_id)}</p>
|
|
650
|
+
</div>
|
|
651
|
+
</div>
|
|
652
|
+
|
|
653
|
+
<script>
|
|
654
|
+
// Accordion functionality
|
|
655
|
+
document.querySelectorAll('.accordion-header').forEach(header => {
|
|
656
|
+
header.addEventListener('click', () => {
|
|
657
|
+
const content = header.nextElementSibling;
|
|
658
|
+
const icon = header.querySelector('.accordion-icon');
|
|
659
|
+
content.classList.toggle('active');
|
|
660
|
+
icon.style.transform = content.classList.contains('active') ? 'rotate(180deg)' : 'rotate(0deg)';
|
|
661
|
+
});
|
|
662
|
+
});
|
|
663
|
+
</script>
|
|
664
|
+
</body>
|
|
665
|
+
</html>`;
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Generate Key Findings section with accordions
|
|
669
|
+
*/
|
|
670
|
+
function generateKeyFindings(findingsBySeverity) {
|
|
671
|
+
const severities = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'];
|
|
672
|
+
const allFindings = [];
|
|
673
|
+
for (const severity of severities) {
|
|
674
|
+
allFindings.push(...findingsBySeverity[severity]);
|
|
675
|
+
}
|
|
676
|
+
if (allFindings.length === 0) {
|
|
677
|
+
return '';
|
|
678
|
+
}
|
|
679
|
+
let html = `
|
|
680
|
+
<!-- Key Findings -->
|
|
681
|
+
<div class="section" id="findings">
|
|
682
|
+
<div class="section-header">
|
|
683
|
+
<h2>Key Findings</h2>
|
|
684
|
+
<p>High-level security assessment summary for your application</p>
|
|
685
|
+
</div>
|
|
686
|
+
`;
|
|
687
|
+
for (const finding of allFindings) {
|
|
688
|
+
html += generateFindingAccordion(finding);
|
|
689
|
+
}
|
|
690
|
+
html += '</div>';
|
|
691
|
+
return html;
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Generate a single finding accordion
|
|
695
|
+
*/
|
|
696
|
+
function generateFindingAccordion(finding) {
|
|
697
|
+
const badgeClass = getBadgeClass(finding.severity);
|
|
698
|
+
const badgeText = getBadgeText(finding.severity);
|
|
699
|
+
let contentHtml = '';
|
|
700
|
+
// What we found
|
|
701
|
+
contentHtml += `
|
|
702
|
+
<div class="content-section">
|
|
703
|
+
<h4>What we found</h4>
|
|
704
|
+
<p>${escapeHtml(finding.description)}</p>
|
|
705
|
+
</div>`;
|
|
706
|
+
// Impact
|
|
707
|
+
if (finding.impact) {
|
|
708
|
+
contentHtml += `
|
|
709
|
+
<div class="content-section">
|
|
710
|
+
<h4>Impact</h4>
|
|
711
|
+
<p>${escapeHtml(finding.impact.description)}</p>
|
|
712
|
+
</div>`;
|
|
713
|
+
}
|
|
714
|
+
// Remediation
|
|
715
|
+
if (finding.remediation) {
|
|
716
|
+
contentHtml += `
|
|
717
|
+
<div class="content-section">
|
|
718
|
+
<h4>Our recommendation</h4>
|
|
719
|
+
<ul>`;
|
|
720
|
+
for (const step of finding.remediation.steps || []) {
|
|
721
|
+
contentHtml += `<li>${escapeHtml(step.action)}</li>`;
|
|
722
|
+
}
|
|
723
|
+
if (finding.remediation.sql) {
|
|
724
|
+
contentHtml += `<li>Apply the SQL fix provided below</li>`;
|
|
725
|
+
}
|
|
726
|
+
contentHtml += `</ul></div>`;
|
|
727
|
+
}
|
|
728
|
+
// Affected assets
|
|
729
|
+
if (finding.location?.table || finding.location?.file) {
|
|
730
|
+
contentHtml += `
|
|
731
|
+
<div class="content-section">
|
|
732
|
+
<h4>Affected assets</h4>
|
|
733
|
+
<ul class="affected-assets">`;
|
|
734
|
+
if (finding.location.table) {
|
|
735
|
+
contentHtml += `<li>/rest/v1/${escapeHtml(finding.location.table)}</li>`;
|
|
736
|
+
}
|
|
737
|
+
if (finding.location.file) {
|
|
738
|
+
contentHtml += `<li>${escapeHtml(finding.location.file)}${finding.location.line ? ':' + finding.location.line : ''}</li>`;
|
|
739
|
+
}
|
|
740
|
+
contentHtml += `</ul></div>`;
|
|
741
|
+
}
|
|
742
|
+
// Technical details section (initially hidden, toggled by button)
|
|
743
|
+
contentHtml += generateTechnicalDetails(finding);
|
|
744
|
+
return `
|
|
745
|
+
<div class="accordion-card">
|
|
746
|
+
<div class="accordion-header">
|
|
747
|
+
<div style="flex: 1;">
|
|
748
|
+
<div class="accordion-title">
|
|
749
|
+
<h3>${escapeHtml(finding.title)}</h3>
|
|
750
|
+
<span class="badge ${badgeClass}">${badgeText}</span>
|
|
751
|
+
</div>
|
|
752
|
+
<p>${escapeHtml(finding.description.substring(0, 100))}${finding.description.length > 100 ? '...' : ''}</p>
|
|
753
|
+
</div>
|
|
754
|
+
<span class="accordion-icon">▼</span>
|
|
755
|
+
</div>
|
|
756
|
+
<div class="accordion-content">
|
|
757
|
+
${contentHtml}
|
|
758
|
+
</div>
|
|
759
|
+
</div>`;
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Generate technical details section for a finding
|
|
763
|
+
* Shows exposed key (masked), file path, line number, and code snippet
|
|
764
|
+
*/
|
|
765
|
+
function generateTechnicalDetails(finding) {
|
|
766
|
+
const hasEvidence = finding.evidence && (finding.evidence.code_snippet ||
|
|
767
|
+
finding.evidence.sample_data ||
|
|
768
|
+
finding.evidence.matched_pattern);
|
|
769
|
+
const hasLocation = finding.location && (finding.location.file ||
|
|
770
|
+
finding.location.line ||
|
|
771
|
+
finding.location.column ||
|
|
772
|
+
finding.location.url);
|
|
773
|
+
if (!hasEvidence && !hasLocation) {
|
|
774
|
+
return '';
|
|
775
|
+
}
|
|
776
|
+
let detailsHtml = `
|
|
777
|
+
<div class="tech-details-section" style="margin-top: 16px; padding: 16px; background: #f8fafc; border-radius: 8px; border: 1px solid #e2e8f0;">
|
|
778
|
+
<h4 style="font-size: 13px; font-weight: 600; color: #1e293b; margin-bottom: 12px; text-transform: uppercase; letter-spacing: 0.5px;">Technical Details</h4>`;
|
|
779
|
+
// Exposed Key (masked)
|
|
780
|
+
if (finding.evidence?.sample_data?.masked) {
|
|
781
|
+
detailsHtml += `
|
|
782
|
+
<div style="margin-bottom: 12px;">
|
|
783
|
+
<span style="font-size: 12px; color: #64748b; display: block; margin-bottom: 4px;">Exposed Key (masked):</span>
|
|
784
|
+
<code style="font-family: 'Monaco', 'Consolas', monospace; font-size: 13px; background: #1e293b; color: #e2e8f0; padding: 8px 12px; border-radius: 6px; display: block; word-break: break-all;">${escapeHtml(finding.evidence.sample_data.masked)}</code>
|
|
785
|
+
</div>`;
|
|
786
|
+
}
|
|
787
|
+
// Key Type / Pattern
|
|
788
|
+
if (finding.evidence?.matched_pattern || finding.subcategory) {
|
|
789
|
+
const keyType = finding.evidence?.matched_pattern || finding.subcategory;
|
|
790
|
+
detailsHtml += `
|
|
791
|
+
<div style="margin-bottom: 12px;">
|
|
792
|
+
<span style="font-size: 12px; color: #64748b; display: block; margin-bottom: 4px;">Key Type:</span>
|
|
793
|
+
<span style="font-size: 13px; color: #334155; font-weight: 500;">${escapeHtml(keyType || 'Unknown')}</span>
|
|
794
|
+
</div>`;
|
|
795
|
+
}
|
|
796
|
+
// Location (File path and line number)
|
|
797
|
+
if (hasLocation) {
|
|
798
|
+
detailsHtml += `
|
|
799
|
+
<div style="margin-bottom: 12px;">
|
|
800
|
+
<span style="font-size: 12px; color: #64748b; display: block; margin-bottom: 4px;">Location:</span>`;
|
|
801
|
+
if (finding.location?.file) {
|
|
802
|
+
detailsHtml += `<div style="font-family: 'Monaco', 'Consolas', monospace; font-size: 13px; color: #334155;">${escapeHtml(finding.location.file)}`;
|
|
803
|
+
if (finding.location?.line) {
|
|
804
|
+
detailsHtml += `<span style="color: #64748b;">:${finding.location.line}</span>`;
|
|
805
|
+
if (finding.location?.column) {
|
|
806
|
+
detailsHtml += `<span style="color: #94a3b8;">:${finding.location.column}</span>`;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
detailsHtml += `</div>`;
|
|
810
|
+
}
|
|
811
|
+
if (finding.location?.url) {
|
|
812
|
+
detailsHtml += `<div style="font-family: 'Monaco', 'Consolas', monospace; font-size: 13px; color: #334155;">${escapeHtml(finding.location.url)}</div>`;
|
|
813
|
+
}
|
|
814
|
+
if (finding.location?.table) {
|
|
815
|
+
detailsHtml += `<div style="font-size: 13px; color: #334155;">Table: <span style="font-family: monospace;">${escapeHtml(finding.location.table)}</span></div>`;
|
|
816
|
+
}
|
|
817
|
+
detailsHtml += `</div>`;
|
|
818
|
+
}
|
|
819
|
+
// Code Snippet
|
|
820
|
+
if (finding.evidence?.code_snippet) {
|
|
821
|
+
detailsHtml += `
|
|
822
|
+
<div>
|
|
823
|
+
<span style="font-size: 12px; color: #64748b; display: block; margin-bottom: 4px;">Code Snippet:</span>
|
|
824
|
+
<pre style="font-family: 'Monaco', 'Consolas', monospace; font-size: 12px; background: #1e293b; color: #e2e8f0; padding: 12px; border-radius: 6px; overflow-x: auto; margin: 0; line-height: 1.5;"><code>${escapeHtml(finding.evidence.code_snippet)}</code></pre>
|
|
825
|
+
</div>`;
|
|
826
|
+
}
|
|
827
|
+
detailsHtml += `</div>`;
|
|
828
|
+
return detailsHtml;
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* Get badge class based on severity
|
|
832
|
+
*/
|
|
833
|
+
function getBadgeClass(severity) {
|
|
834
|
+
switch (severity) {
|
|
835
|
+
case 'CRITICAL': return 'badge-risk';
|
|
836
|
+
case 'HIGH': return 'badge-risk';
|
|
837
|
+
case 'MEDIUM': return 'badge-concern';
|
|
838
|
+
case 'LOW': return 'badge-info';
|
|
839
|
+
default: return 'badge-info';
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* Get badge text based on severity
|
|
844
|
+
*/
|
|
845
|
+
function getBadgeText(severity) {
|
|
846
|
+
switch (severity) {
|
|
847
|
+
case 'CRITICAL': return '⊘ Confirmed Risk';
|
|
848
|
+
case 'HIGH': return '⊘ Confirmed Risk';
|
|
849
|
+
case 'MEDIUM': return '⚠ Potential Concern';
|
|
850
|
+
case 'LOW': return 'ⓘ Info';
|
|
851
|
+
default: return 'ⓘ Info';
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Generate Endpoints section
|
|
856
|
+
* Shows real endpoints from scan results or a message if none detected
|
|
857
|
+
*/
|
|
858
|
+
function generateEndpointsSection(result) {
|
|
859
|
+
// Check if we have real endpoint data from the scan
|
|
860
|
+
const endpoints = result.endpoints || [];
|
|
861
|
+
// If no endpoints detected, show a message or return empty
|
|
862
|
+
if (endpoints.length === 0) {
|
|
863
|
+
return `
|
|
864
|
+
<!-- Endpoints Section -->
|
|
865
|
+
<div class="endpoints-section">
|
|
866
|
+
<div class="endpoints-header">
|
|
867
|
+
<h2>
|
|
868
|
+
<span style="font-size: 20px;">🗄</span>
|
|
869
|
+
Endpoints
|
|
870
|
+
</h2>
|
|
871
|
+
<p>No API endpoints were detected during this scan.</p>
|
|
872
|
+
</div>
|
|
873
|
+
<div style="padding: 40px 20px; text-align: center; color: #64748b;">
|
|
874
|
+
<p style="font-size: 14px;">Endpoints are detected when scanning Supabase projects with accessible REST API.</p>
|
|
875
|
+
<p style="font-size: 13px; margin-top: 8px;">Try scanning with --project-url and --anon-key options for deeper analysis.</p>
|
|
876
|
+
</div>
|
|
877
|
+
</div>`;
|
|
878
|
+
}
|
|
879
|
+
// Generate rows from real endpoint data
|
|
880
|
+
let rowsHtml = '';
|
|
881
|
+
for (const endpoint of endpoints) {
|
|
882
|
+
const statusClass = endpoint.status === 'At Risk' ? 'status-at-risk' :
|
|
883
|
+
endpoint.status === 'Review' ? 'status-review' : 'status-secure';
|
|
884
|
+
const readableIcon = endpoint.readable === 'warning' ? '⚠' : '✓';
|
|
885
|
+
const writableIcon = endpoint.writable === 'warning' ? '⚠' : '✓';
|
|
886
|
+
const readableClass = endpoint.readable === 'warning' ? 'icon-warning' : 'icon-check';
|
|
887
|
+
const writableClass = endpoint.writable === 'warning' ? 'icon-warning' : 'icon-check';
|
|
888
|
+
const sensitiveDisplay = endpoint.sensitive && endpoint.sensitive !== 'None'
|
|
889
|
+
? `<span class="sensitive-badge">${escapeHtml(endpoint.sensitive)}</span>`
|
|
890
|
+
: '<span class="sensitive-none">None</span>';
|
|
891
|
+
rowsHtml += `
|
|
892
|
+
<div class="table-row">
|
|
893
|
+
<div class="endpoint-path">
|
|
894
|
+
<span class="expand-icon">›</span>
|
|
895
|
+
<span>${escapeHtml(endpoint.path)}</span>
|
|
896
|
+
</div>
|
|
897
|
+
<div><span class="status-badge ${statusClass}">${endpoint.status}</span></div>
|
|
898
|
+
<div><span class="${readableClass}">${readableIcon}</span></div>
|
|
899
|
+
<div><span class="${writableClass}">${writableIcon}</span></div>
|
|
900
|
+
<div>${sensitiveDisplay}</div>
|
|
901
|
+
</div>`;
|
|
902
|
+
}
|
|
903
|
+
return `
|
|
904
|
+
<!-- Endpoints Section -->
|
|
905
|
+
<div class="endpoints-section">
|
|
906
|
+
<div class="endpoints-header">
|
|
907
|
+
<h2>
|
|
908
|
+
<span style="font-size: 20px;">🗄</span>
|
|
909
|
+
Endpoints
|
|
910
|
+
</h2>
|
|
911
|
+
<p>A list of all API endpoints discovered and analyzed.</p>
|
|
912
|
+
</div>
|
|
913
|
+
<div class="endpoints-table">
|
|
914
|
+
<div class="table-header">
|
|
915
|
+
<div>Path</div>
|
|
916
|
+
<div>Status</div>
|
|
917
|
+
<div>Readable</div>
|
|
918
|
+
<div>Writable</div>
|
|
919
|
+
<div>Sensitive Data</div>
|
|
920
|
+
</div>
|
|
921
|
+
${rowsHtml}
|
|
922
|
+
</div>
|
|
923
|
+
</div>`;
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Escape HTML special characters
|
|
927
|
+
*/
|
|
928
|
+
function escapeHtml(text) {
|
|
929
|
+
if (!text)
|
|
930
|
+
return '';
|
|
931
|
+
return text
|
|
932
|
+
.replace(/&/g, '&')
|
|
933
|
+
.replace(/</g, '<')
|
|
934
|
+
.replace(/>/g, '>')
|
|
935
|
+
.replace(/"/g, '"')
|
|
936
|
+
.replace(/'/g, ''');
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* Save HTML report to file
|
|
940
|
+
*/
|
|
941
|
+
async function saveHTMLReport(result, filePath, options) {
|
|
942
|
+
const fs = await Promise.resolve().then(() => __importStar(require('fs/promises')));
|
|
943
|
+
const html = generateHTMLReport(result, options);
|
|
944
|
+
await fs.writeFile(filePath, html, 'utf-8');
|
|
945
|
+
}
|
|
946
|
+
//# sourceMappingURL=html.js.map
|