supasec 1.0.1 → 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.
Files changed (45) hide show
  1. package/COMPLETION_REPORT.md +324 -0
  2. package/FIXES_SUMMARY.md +224 -0
  3. package/IMPLEMENTATION_NOTES.md +305 -0
  4. package/QUICK_REFERENCE.md +185 -0
  5. package/README.md +1 -1
  6. package/REPORTING.md +217 -0
  7. package/STATUS.md +269 -0
  8. package/dist/commands/scan.d.ts +1 -0
  9. package/dist/commands/scan.d.ts.map +1 -1
  10. package/dist/commands/scan.js +186 -15
  11. package/dist/commands/scan.js.map +1 -1
  12. package/dist/models/scan-result.d.ts +8 -0
  13. package/dist/models/scan-result.d.ts.map +1 -1
  14. package/dist/models/scan-result.js.map +1 -1
  15. package/dist/reporters/html.d.ts +18 -0
  16. package/dist/reporters/html.d.ts.map +1 -0
  17. package/dist/reporters/html.js +946 -0
  18. package/dist/reporters/html.js.map +1 -0
  19. package/dist/reporters/index.d.ts +2 -0
  20. package/dist/reporters/index.d.ts.map +1 -1
  21. package/dist/reporters/index.js +2 -0
  22. package/dist/reporters/index.js.map +1 -1
  23. package/dist/reporters/terminal.d.ts.map +1 -1
  24. package/dist/reporters/terminal.js +9 -0
  25. package/dist/reporters/terminal.js.map +1 -1
  26. package/dist/scanners/secrets/detector.d.ts.map +1 -1
  27. package/dist/scanners/secrets/detector.js +6 -2
  28. package/dist/scanners/secrets/detector.js.map +1 -1
  29. package/package.json +1 -1
  30. package/reports/supasec---------app-2026-01-28-16-58-47.html +804 -0
  31. package/reports/supasec---------app-2026-01-28-17-06-43.html +722 -0
  32. package/reports/supasec---------app-2026-01-28-17-07-23.html +722 -0
  33. package/reports/supasec---------app-2026-01-28-17-08-00.html +722 -0
  34. package/reports/supasec---------app-2026-01-28-17-08-20.html +722 -0
  35. package/reports/supasec---------app-2026-01-28-17-08-41.html +722 -0
  36. package/reports/supasec-au---your-app-2026-01-28-17-14-57.html +715 -0
  37. package/reports/supasec-au---your-app-2026-01-28-17-19-03.html +715 -0
  38. package/reports/supasec-audityour-app-2026-01-28-17-09-24.html +722 -0
  39. package/reports/supasec-ex-mple-com-2026-01-28-17-14-52.json +229 -0
  40. package/reports/supasec-ex-mple-com-2026-01-28-17-15-39.html +715 -0
  41. package/reports/supasec-ex-mple-com-2026-01-28-17-17-22.html +715 -0
  42. package/reports/supasec-example-com-2026-01-28-17-15-06.html +715 -0
  43. package/reports/supasec-my--------------name-com-2026-01-28-17-15-02.html +715 -0
  44. package/reports/supasec-st-ging-com-2026-01-28-17-16-17.html +715 -0
  45. 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, '&#039;');
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