supasec 1.0.3 → 1.0.5

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 (117) hide show
  1. package/Feature-List.md +233 -0
  2. package/README.md +53 -12
  3. package/dist/cli.js +2 -0
  4. package/dist/cli.js.map +1 -1
  5. package/dist/commands/index.d.ts +1 -0
  6. package/dist/commands/index.d.ts.map +1 -1
  7. package/dist/commands/index.js +1 -0
  8. package/dist/commands/index.js.map +1 -1
  9. package/dist/commands/scan.d.ts.map +1 -1
  10. package/dist/commands/scan.js +82 -26
  11. package/dist/commands/scan.js.map +1 -1
  12. package/dist/commands/snapshot.d.ts +32 -0
  13. package/dist/commands/snapshot.d.ts.map +1 -0
  14. package/dist/commands/snapshot.js +282 -0
  15. package/dist/commands/snapshot.js.map +1 -0
  16. package/dist/reporters/html.d.ts +3 -2
  17. package/dist/reporters/html.d.ts.map +1 -1
  18. package/dist/reporters/html.js +844 -538
  19. package/dist/reporters/html.js.map +1 -1
  20. package/dist/reporters/terminal.d.ts +38 -2
  21. package/dist/reporters/terminal.d.ts.map +1 -1
  22. package/dist/reporters/terminal.js +292 -131
  23. package/dist/reporters/terminal.js.map +1 -1
  24. package/dist/scanners/auth/analyzer.d.ts +40 -0
  25. package/dist/scanners/auth/analyzer.d.ts.map +1 -0
  26. package/dist/scanners/auth/analyzer.js +673 -0
  27. package/dist/scanners/auth/analyzer.js.map +1 -0
  28. package/dist/scanners/auth/index.d.ts +6 -0
  29. package/dist/scanners/auth/index.d.ts.map +1 -0
  30. package/dist/scanners/auth/index.js +22 -0
  31. package/dist/scanners/auth/index.js.map +1 -0
  32. package/dist/scanners/edge/analyzer.d.ts +35 -0
  33. package/dist/scanners/edge/analyzer.d.ts.map +1 -0
  34. package/dist/scanners/edge/analyzer.js +614 -0
  35. package/dist/scanners/edge/analyzer.js.map +1 -0
  36. package/dist/scanners/edge/index.d.ts +6 -0
  37. package/dist/scanners/edge/index.d.ts.map +1 -0
  38. package/dist/scanners/edge/index.js +22 -0
  39. package/dist/scanners/edge/index.js.map +1 -0
  40. package/dist/scanners/functions/analyzer.d.ts +41 -0
  41. package/dist/scanners/functions/analyzer.d.ts.map +1 -0
  42. package/dist/scanners/functions/analyzer.js +378 -0
  43. package/dist/scanners/functions/analyzer.js.map +1 -0
  44. package/dist/scanners/functions/index.d.ts +6 -0
  45. package/dist/scanners/functions/index.d.ts.map +1 -0
  46. package/dist/scanners/functions/index.js +22 -0
  47. package/dist/scanners/functions/index.js.map +1 -0
  48. package/dist/scanners/git/index.d.ts +6 -0
  49. package/dist/scanners/git/index.d.ts.map +1 -0
  50. package/dist/scanners/git/index.js +22 -0
  51. package/dist/scanners/git/index.js.map +1 -0
  52. package/dist/scanners/git/scanner.d.ts +22 -0
  53. package/dist/scanners/git/scanner.d.ts.map +1 -0
  54. package/dist/scanners/git/scanner.js +531 -0
  55. package/dist/scanners/git/scanner.js.map +1 -0
  56. package/dist/scanners/https/analyzer.d.ts +42 -0
  57. package/dist/scanners/https/analyzer.d.ts.map +1 -0
  58. package/dist/scanners/https/analyzer.js +470 -0
  59. package/dist/scanners/https/analyzer.js.map +1 -0
  60. package/dist/scanners/https/index.d.ts +8 -0
  61. package/dist/scanners/https/index.d.ts.map +1 -0
  62. package/dist/scanners/https/index.js +17 -0
  63. package/dist/scanners/https/index.js.map +1 -0
  64. package/dist/scanners/index.d.ts +6 -0
  65. package/dist/scanners/index.d.ts.map +1 -1
  66. package/dist/scanners/index.js +6 -0
  67. package/dist/scanners/index.js.map +1 -1
  68. package/dist/scanners/rls/fuzzer.d.ts +40 -0
  69. package/dist/scanners/rls/fuzzer.d.ts.map +1 -0
  70. package/dist/scanners/rls/fuzzer.js +360 -0
  71. package/dist/scanners/rls/fuzzer.js.map +1 -0
  72. package/dist/scanners/rls/index.d.ts +1 -0
  73. package/dist/scanners/rls/index.d.ts.map +1 -1
  74. package/dist/scanners/rls/index.js +1 -0
  75. package/dist/scanners/rls/index.js.map +1 -1
  76. package/dist/scanners/secrets/detector.d.ts.map +1 -1
  77. package/dist/scanners/secrets/detector.js +44 -12
  78. package/dist/scanners/secrets/detector.js.map +1 -1
  79. package/dist/scanners/secrets/index.d.ts +1 -0
  80. package/dist/scanners/secrets/index.d.ts.map +1 -1
  81. package/dist/scanners/secrets/index.js +4 -0
  82. package/dist/scanners/secrets/index.js.map +1 -1
  83. package/dist/scanners/secrets/patterns.d.ts +25 -0
  84. package/dist/scanners/secrets/patterns.d.ts.map +1 -1
  85. package/dist/scanners/secrets/patterns.js +138 -27
  86. package/dist/scanners/secrets/patterns.js.map +1 -1
  87. package/dist/scanners/storage/analyzer.d.ts +49 -0
  88. package/dist/scanners/storage/analyzer.d.ts.map +1 -0
  89. package/dist/scanners/storage/analyzer.js +438 -0
  90. package/dist/scanners/storage/analyzer.js.map +1 -0
  91. package/dist/scanners/storage/index.d.ts +6 -0
  92. package/dist/scanners/storage/index.d.ts.map +1 -0
  93. package/dist/scanners/storage/index.js +22 -0
  94. package/dist/scanners/storage/index.js.map +1 -0
  95. package/package.json +1 -1
  96. package/reports/{supasec-audityour-app-2026-01-28-17-09-24.html → supasec-audityour-app-2026-01-28-19-42-22.html} +51 -16
  97. package/reports/supasec-audityour-app-2026-01-28-19-49-18.html +1122 -0
  98. package/COMPLETION_REPORT.md +0 -324
  99. package/FIXES_SUMMARY.md +0 -224
  100. package/IMPLEMENTATION_NOTES.md +0 -305
  101. package/QUICK_REFERENCE.md +0 -185
  102. package/REPORTING.md +0 -217
  103. package/STATUS.md +0 -269
  104. package/reports/supasec---------app-2026-01-28-16-58-47.html +0 -804
  105. package/reports/supasec---------app-2026-01-28-17-06-43.html +0 -722
  106. package/reports/supasec---------app-2026-01-28-17-07-23.html +0 -722
  107. package/reports/supasec---------app-2026-01-28-17-08-00.html +0 -722
  108. package/reports/supasec---------app-2026-01-28-17-08-20.html +0 -722
  109. package/reports/supasec---------app-2026-01-28-17-08-41.html +0 -722
  110. package/reports/supasec-au---your-app-2026-01-28-17-14-57.html +0 -715
  111. package/reports/supasec-au---your-app-2026-01-28-17-19-03.html +0 -715
  112. package/reports/supasec-ex-mple-com-2026-01-28-17-14-52.json +0 -229
  113. package/reports/supasec-ex-mple-com-2026-01-28-17-15-39.html +0 -715
  114. package/reports/supasec-ex-mple-com-2026-01-28-17-17-22.html +0 -715
  115. package/reports/supasec-example-com-2026-01-28-17-15-06.html +0 -715
  116. package/reports/supasec-my--------------name-com-2026-01-28-17-15-02.html +0 -715
  117. package/reports/supasec-st-ging-com-2026-01-28-17-16-17.html +0 -715
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  /**
3
3
  * HTML Reporter
4
- * Generates detailed HTML reports matching the Supascan.io style
4
+ * Generates enterprise-grade professional HTML reports
5
5
  */
6
6
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
7
  if (k2 === undefined) k2 = k;
@@ -41,10 +41,10 @@ exports.generateHTMLReport = generateHTMLReport;
41
41
  exports.saveHTMLReport = saveHTMLReport;
42
42
  const finding_js_1 = require("../models/finding.js");
43
43
  /**
44
- * Generate HTML report from scan result
44
+ * Generate enterprise-grade HTML report from scan result
45
45
  */
46
46
  function generateHTMLReport(result, options = {}) {
47
- const { title = 'SupaSec Security Audit Report', includeDetails = true } = options;
47
+ const { title = 'SupaSec Security Audit Report', includeDetails = true, theme = 'light' } = options;
48
48
  const counts = (0, finding_js_1.countFindingsBySeverity)(result.findings);
49
49
  const sortedFindings = (0, finding_js_1.sortFindingsBySeverity)(result.findings);
50
50
  // Group findings by severity
@@ -59,6 +59,8 @@ function generateHTMLReport(result, options = {}) {
59
59
  findingsBySeverity[finding.severity].push(finding);
60
60
  }
61
61
  const scanDate = new Date(result.scan_metadata.scan_date).toLocaleString();
62
+ const grade = result.grading.overall_grade;
63
+ const score = result.grading.overall_score;
62
64
  return `<!DOCTYPE html>
63
65
  <html lang="en">
64
66
  <head>
@@ -66,6 +68,25 @@ function generateHTMLReport(result, options = {}) {
66
68
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
67
69
  <title>${escapeHtml(title)}</title>
68
70
  <style>
71
+ :root {
72
+ --color-bg: ${theme === 'dark' ? '#0f172a' : '#f8fafc'};
73
+ --color-card: ${theme === 'dark' ? '#1e293b' : '#ffffff'};
74
+ --color-text: ${theme === 'dark' ? '#f1f5f9' : '#1e293b'};
75
+ --color-text-secondary: ${theme === 'dark' ? '#94a3b8' : '#64748b'};
76
+ --color-border: ${theme === 'dark' ? '#334155' : '#e2e8f0'};
77
+ --color-primary: #3b82f6;
78
+ --color-primary-light: #eff6ff;
79
+ --color-success: #22c55e;
80
+ --color-warning: #f59e0b;
81
+ --color-danger: #ef4444;
82
+ --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
83
+ --shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
84
+ --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
85
+ --radius-sm: 6px;
86
+ --radius: 12px;
87
+ --radius-lg: 16px;
88
+ }
89
+
69
90
  * {
70
91
  margin: 0;
71
92
  padding: 0;
@@ -73,140 +94,214 @@ function generateHTMLReport(result, options = {}) {
73
94
  }
74
95
 
75
96
  body {
76
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
77
- background: #f8fafc;
97
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
98
+ background: var(--color-bg);
78
99
  min-height: 100vh;
79
- color: #334155;
100
+ color: var(--color-text);
80
101
  line-height: 1.6;
81
102
  }
82
103
 
83
104
  .container {
84
- max-width: 1000px;
105
+ max-width: 1200px;
85
106
  margin: 0 auto;
86
- padding: 20px;
107
+ padding: 24px;
87
108
  }
88
109
 
89
110
  /* Header */
90
111
  .header {
91
- background: white;
92
- padding: 16px 24px;
93
- border-bottom: 1px solid #e2e8f0;
112
+ background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
113
+ padding: 32px;
114
+ border-radius: var(--radius-lg);
115
+ margin-bottom: 24px;
116
+ box-shadow: var(--shadow-lg);
117
+ color: white;
118
+ }
119
+
120
+ .header-top {
121
+ display: flex;
122
+ align-items: center;
123
+ justify-content: space-between;
124
+ margin-bottom: 24px;
125
+ }
126
+
127
+ .brand {
94
128
  display: flex;
95
129
  align-items: center;
96
130
  gap: 12px;
97
- margin: -20px -20px 20px -20px;
98
131
  }
99
132
 
100
133
  .logo {
101
- width: 32px;
102
- height: 32px;
103
- background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
104
- border-radius: 8px;
134
+ width: 48px;
135
+ height: 48px;
136
+ background: rgba(255,255,255,0.2);
137
+ border-radius: var(--radius);
105
138
  display: flex;
106
139
  align-items: center;
107
140
  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;
141
+ font-size: 24px;
142
+ font-weight: 700;
143
+ backdrop-filter: blur(10px);
116
144
  }
117
145
 
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;
146
+ .brand-text h1 {
147
+ font-size: 24px;
148
+ font-weight: 700;
149
+ margin: 0;
128
150
  }
129
151
 
130
- .info-banner .icon {
131
- color: #3b82f6;
132
- font-size: 18px;
133
- margin-top: 2px;
152
+ .brand-text p {
153
+ font-size: 13px;
154
+ opacity: 0.9;
155
+ margin: 0;
134
156
  }
135
157
 
136
- .info-banner h2 {
137
- font-size: 16px;
138
- color: #1e40af;
139
- margin-bottom: 4px;
140
- font-weight: 600;
158
+ .report-meta {
159
+ text-align: right;
141
160
  }
142
161
 
143
- .info-banner p {
144
- font-size: 14px;
145
- color: #3b82f6;
162
+ .report-id {
163
+ font-size: 12px;
164
+ opacity: 0.8;
165
+ font-family: monospace;
146
166
  }
147
167
 
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;
168
+ .report-date {
169
+ font-size: 13px;
170
+ opacity: 0.9;
155
171
  }
156
172
 
157
- .success-header {
173
+ /* Grade Section */
174
+ .grade-section {
158
175
  display: flex;
159
176
  align-items: center;
160
- gap: 12px;
161
- margin-bottom: 16px;
177
+ gap: 32px;
178
+ padding: 24px;
179
+ background: rgba(255,255,255,0.1);
180
+ border-radius: var(--radius);
181
+ backdrop-filter: blur(10px);
162
182
  }
163
183
 
164
- .success-icon {
165
- width: 40px;
166
- height: 40px;
167
- background: #dcfce7;
184
+ .grade-circle {
185
+ width: 120px;
186
+ height: 120px;
168
187
  border-radius: 50%;
169
188
  display: flex;
189
+ flex-direction: column;
170
190
  align-items: center;
171
191
  justify-content: center;
172
- color: #16a34a;
173
- font-size: 20px;
192
+ background: ${getGradeColor(grade)};
193
+ box-shadow: 0 8px 32px rgba(0,0,0,0.2);
194
+ position: relative;
195
+ }
196
+
197
+ .grade-circle::before {
198
+ content: '';
199
+ position: absolute;
200
+ inset: 4px;
201
+ border-radius: 50%;
202
+ border: 3px solid rgba(255,255,255,0.3);
174
203
  }
175
204
 
176
- .success-title {
177
- font-size: 18px;
205
+ .grade-letter {
206
+ font-size: 56px;
207
+ font-weight: 800;
208
+ line-height: 1;
209
+ }
210
+
211
+ .grade-score {
212
+ font-size: 14px;
178
213
  font-weight: 600;
179
- color: #1e293b;
214
+ opacity: 0.9;
180
215
  }
181
216
 
182
- .success-title a {
183
- color: #3b82f6;
184
- text-decoration: none;
217
+ .grade-info {
218
+ flex: 1;
219
+ }
220
+
221
+ .grade-info h2 {
222
+ font-size: 20px;
223
+ font-weight: 600;
224
+ margin-bottom: 8px;
185
225
  }
186
226
 
187
- .success-title a:hover {
188
- text-decoration: underline;
227
+ .grade-info p {
228
+ font-size: 14px;
229
+ opacity: 0.9;
230
+ margin-bottom: 16px;
189
231
  }
190
232
 
191
- /* Scan Info Grid */
192
- .scan-info-grid {
193
- display: grid;
194
- grid-template-columns: repeat(2, 1fr);
233
+ .security-meter {
234
+ height: 8px;
235
+ background: rgba(255,255,255,0.2);
236
+ border-radius: 4px;
237
+ overflow: hidden;
238
+ }
239
+
240
+ .security-meter-fill {
241
+ height: 100%;
242
+ background: white;
243
+ border-radius: 4px;
244
+ transition: width 1s ease;
245
+ width: ${score}%;
246
+ }
247
+
248
+ .security-meter-labels {
249
+ display: flex;
250
+ justify-content: space-between;
251
+ margin-top: 4px;
252
+ font-size: 11px;
253
+ opacity: 0.8;
254
+ }
255
+
256
+ /* Target Info Card */
257
+ .target-card {
258
+ background: var(--color-card);
259
+ border: 1px solid var(--color-border);
260
+ border-radius: var(--radius);
261
+ padding: 20px;
262
+ margin-bottom: 24px;
263
+ box-shadow: var(--shadow-sm);
264
+ }
265
+
266
+ .target-card h3 {
267
+ font-size: 12px;
268
+ font-weight: 600;
269
+ text-transform: uppercase;
270
+ letter-spacing: 0.5px;
271
+ color: var(--color-text-secondary);
272
+ margin-bottom: 8px;
273
+ }
274
+
275
+ .target-url {
276
+ font-size: 16px;
277
+ font-weight: 500;
278
+ color: var(--color-primary);
279
+ word-break: break-all;
280
+ }
281
+
282
+ .target-meta {
283
+ display: flex;
195
284
  gap: 24px;
285
+ margin-top: 16px;
286
+ padding-top: 16px;
287
+ border-top: 1px solid var(--color-border);
196
288
  }
197
289
 
198
- .info-group h4 {
290
+ .target-meta-item {
291
+ display: flex;
292
+ flex-direction: column;
293
+ gap: 4px;
294
+ }
295
+
296
+ .target-meta-item span:first-child {
199
297
  font-size: 11px;
298
+ color: var(--color-text-secondary);
200
299
  text-transform: uppercase;
201
- color: #64748b;
202
- font-weight: 600;
203
300
  letter-spacing: 0.5px;
204
- margin-bottom: 6px;
205
301
  }
206
302
 
207
- .info-group p {
208
- font-size: 14px;
209
- color: #334155;
303
+ .target-meta-item span:last-child {
304
+ font-size: 13px;
210
305
  font-weight: 500;
211
306
  }
212
307
 
@@ -219,92 +314,225 @@ function generateHTMLReport(result, options = {}) {
219
314
  }
220
315
 
221
316
  .stat-card {
222
- background: white;
223
- border: 1px solid #e2e8f0;
224
- border-radius: 12px;
317
+ background: var(--color-card);
318
+ border: 1px solid var(--color-border);
319
+ border-radius: var(--radius);
225
320
  padding: 20px;
226
321
  text-align: center;
322
+ box-shadow: var(--shadow-sm);
323
+ transition: transform 0.2s, box-shadow 0.2s;
324
+ }
325
+
326
+ .stat-card:hover {
327
+ transform: translateY(-2px);
328
+ box-shadow: var(--shadow);
329
+ }
330
+
331
+ .stat-icon {
332
+ width: 40px;
333
+ height: 40px;
334
+ border-radius: var(--radius-sm);
335
+ display: flex;
336
+ align-items: center;
337
+ justify-content: center;
338
+ margin: 0 auto 12px;
339
+ font-size: 20px;
340
+ }
341
+
342
+ .stat-icon.critical { background: #fef2f2; }
343
+ .stat-icon.high { background: #fffbeb; }
344
+ .stat-icon.medium { background: #fefce8; }
345
+ .stat-icon.low { background: #eff6ff; }
346
+
347
+ .stat-value {
348
+ font-size: 36px;
349
+ font-weight: 700;
350
+ line-height: 1;
351
+ margin-bottom: 4px;
227
352
  }
228
353
 
354
+ .stat-value.critical { color: #dc2626; }
355
+ .stat-value.high { color: #d97706; }
356
+ .stat-value.medium { color: #a16207; }
357
+ .stat-value.low { color: #2563eb; }
358
+
229
359
  .stat-label {
230
- font-size: 11px;
231
- text-transform: uppercase;
232
- color: #64748b;
360
+ font-size: 12px;
233
361
  font-weight: 600;
362
+ text-transform: uppercase;
234
363
  letter-spacing: 0.5px;
235
- margin-bottom: 8px;
364
+ color: var(--color-text-secondary);
236
365
  }
237
366
 
238
- .stat-value {
239
- font-size: 32px;
367
+ /* Category Scores */
368
+ .category-section {
369
+ background: var(--color-card);
370
+ border: 1px solid var(--color-border);
371
+ border-radius: var(--radius);
372
+ padding: 24px;
373
+ margin-bottom: 24px;
374
+ box-shadow: var(--shadow-sm);
375
+ }
376
+
377
+ .category-section h2 {
378
+ font-size: 16px;
379
+ font-weight: 600;
380
+ margin-bottom: 20px;
381
+ display: flex;
382
+ align-items: center;
383
+ gap: 8px;
384
+ }
385
+
386
+ .category-grid {
387
+ display: grid;
388
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
389
+ gap: 16px;
390
+ }
391
+
392
+ .category-item {
393
+ display: flex;
394
+ align-items: center;
395
+ gap: 12px;
396
+ padding: 16px;
397
+ background: ${theme === 'dark' ? '#334155' : '#f8fafc'};
398
+ border-radius: var(--radius-sm);
399
+ }
400
+
401
+ .category-grade {
402
+ width: 40px;
403
+ height: 40px;
404
+ border-radius: 50%;
405
+ display: flex;
406
+ align-items: center;
407
+ justify-content: center;
240
408
  font-weight: 700;
241
- color: #1e293b;
409
+ font-size: 14px;
410
+ flex-shrink: 0;
411
+ }
412
+
413
+ .category-info {
414
+ flex: 1;
415
+ }
416
+
417
+ .category-name {
418
+ font-size: 14px;
419
+ font-weight: 600;
420
+ margin-bottom: 4px;
421
+ text-transform: capitalize;
422
+ }
423
+
424
+ .category-bar {
425
+ height: 6px;
426
+ background: ${theme === 'dark' ? '#475569' : '#e2e8f0'};
427
+ border-radius: 3px;
428
+ overflow: hidden;
429
+ }
430
+
431
+ .category-bar-fill {
432
+ height: 100%;
433
+ border-radius: 3px;
434
+ transition: width 1s ease;
435
+ }
436
+
437
+ .category-score {
438
+ font-size: 12px;
439
+ font-weight: 600;
440
+ color: var(--color-text-secondary);
441
+ margin-left: 8px;
242
442
  }
243
443
 
244
- /* Key Findings Section */
245
- .section {
444
+ /* Findings Section */
445
+ .findings-section {
246
446
  margin-bottom: 24px;
247
447
  }
248
448
 
249
449
  .section-header {
250
- margin-bottom: 16px;
450
+ display: flex;
451
+ align-items: center;
452
+ justify-content: space-between;
453
+ margin-bottom: 20px;
251
454
  }
252
455
 
253
456
  .section-header h2 {
254
457
  font-size: 20px;
255
458
  font-weight: 600;
256
- color: #1e293b;
257
- margin-bottom: 4px;
258
459
  }
259
460
 
260
461
  .section-header p {
261
462
  font-size: 14px;
262
- color: #64748b;
463
+ color: var(--color-text-secondary);
263
464
  }
264
465
 
265
- /* Accordion Cards */
266
- .accordion-card {
267
- background: white;
268
- border: 1px solid #e2e8f0;
269
- border-radius: 12px;
270
- margin-bottom: 12px;
466
+ .finding-card {
467
+ background: var(--color-card);
468
+ border: 1px solid var(--color-border);
469
+ border-radius: var(--radius);
470
+ margin-bottom: 16px;
271
471
  overflow: hidden;
472
+ box-shadow: var(--shadow-sm);
272
473
  }
273
474
 
274
- .accordion-header {
275
- padding: 16px 20px;
475
+ .finding-header {
476
+ padding: 20px;
276
477
  cursor: pointer;
277
478
  display: flex;
278
- align-items: center;
279
- justify-content: space-between;
280
- gap: 12px;
479
+ align-items: flex-start;
480
+ gap: 16px;
281
481
  transition: background 0.2s;
282
482
  }
283
483
 
284
- .accordion-header:hover {
285
- background: #f8fafc;
484
+ .finding-header:hover {
485
+ background: ${theme === 'dark' ? '#334155' : '#f8fafc'};
286
486
  }
287
487
 
288
- .accordion-title {
488
+ .finding-severity {
489
+ width: 48px;
490
+ height: 48px;
491
+ border-radius: var(--radius-sm);
289
492
  display: flex;
290
493
  align-items: center;
291
- gap: 12px;
494
+ justify-content: center;
495
+ font-size: 24px;
496
+ flex-shrink: 0;
497
+ }
498
+
499
+ .finding-severity.critical { background: #fef2f2; }
500
+ .finding-severity.high { background: #fffbeb; }
501
+ .finding-severity.medium { background: #fefce8; }
502
+ .finding-severity.low { background: #eff6ff; }
503
+
504
+ .finding-title-section {
292
505
  flex: 1;
293
506
  }
294
507
 
295
- .accordion-title h3 {
296
- font-size: 15px;
508
+ .finding-title {
509
+ font-size: 16px;
297
510
  font-weight: 600;
298
- color: #1e293b;
511
+ margin-bottom: 4px;
512
+ display: flex;
513
+ align-items: center;
514
+ gap: 8px;
299
515
  }
300
516
 
301
- .accordion-header p {
302
- font-size: 13px;
303
- color: #64748b;
304
- margin-top: 2px;
517
+ .finding-id {
518
+ font-size: 12px;
519
+ color: var(--color-text-secondary);
520
+ font-family: monospace;
521
+ }
522
+
523
+ .finding-preview {
524
+ font-size: 14px;
525
+ color: var(--color-text-secondary);
526
+ line-height: 1.5;
305
527
  }
306
528
 
307
- .badge {
529
+ .finding-meta {
530
+ display: flex;
531
+ gap: 12px;
532
+ margin-top: 8px;
533
+ }
534
+
535
+ .finding-badge {
308
536
  display: inline-flex;
309
537
  align-items: center;
310
538
  gap: 4px;
@@ -316,304 +544,357 @@ function generateHTMLReport(result, options = {}) {
316
544
  letter-spacing: 0.5px;
317
545
  }
318
546
 
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
- }
547
+ .finding-badge.critical { background: #fef2f2; color: #dc2626; }
548
+ .finding-badge.high { background: #fffbeb; color: #d97706; }
549
+ .finding-badge.medium { background: #fefce8; color: #a16207; }
550
+ .finding-badge.low { background: #eff6ff; color: #2563eb; }
348
551
 
349
- .badge-concern {
350
- background: #fef3c7;
351
- color: #d97706;
552
+ .finding-category {
553
+ background: ${theme === 'dark' ? '#475569' : '#e2e8f0'};
554
+ color: ${theme === 'dark' ? '#e2e8f0' : '#475569'};
352
555
  }
353
556
 
354
- .accordion-icon {
355
- color: #94a3b8;
356
- font-size: 12px;
557
+ .finding-toggle {
558
+ width: 32px;
559
+ height: 32px;
560
+ border-radius: 50%;
561
+ display: flex;
562
+ align-items: center;
563
+ justify-content: center;
564
+ background: ${theme === 'dark' ? '#475569' : '#e2e8f0'};
565
+ color: ${theme === 'dark' ? '#e2e8f0' : '#64748b'};
357
566
  transition: transform 0.2s;
358
567
  }
359
568
 
360
- .accordion-content {
569
+ .finding-content {
361
570
  display: none;
362
- padding: 0 20px 20px 20px;
363
- border-top: 1px solid #f1f5f9;
571
+ padding: 0 20px 20px 84px;
572
+ border-top: 1px solid var(--color-border);
364
573
  }
365
574
 
366
- .accordion-content.active {
575
+ .finding-content.active {
367
576
  display: block;
368
577
  }
369
578
 
370
- .content-section {
371
- margin-bottom: 20px;
579
+ .finding-content-section {
580
+ padding: 20px 0;
581
+ border-bottom: 1px solid var(--color-border);
372
582
  }
373
583
 
374
- .content-section:last-child {
375
- margin-bottom: 0;
584
+ .finding-content-section:last-child {
585
+ border-bottom: none;
376
586
  }
377
587
 
378
- .content-section h4 {
379
- font-size: 14px;
588
+ .finding-content-section h4 {
589
+ font-size: 12px;
380
590
  font-weight: 600;
381
- color: #1e293b;
382
- margin-bottom: 8px;
591
+ text-transform: uppercase;
592
+ letter-spacing: 0.5px;
593
+ color: var(--color-text-secondary);
594
+ margin-bottom: 12px;
383
595
  }
384
596
 
385
- .content-section p {
597
+ .finding-content-section p {
386
598
  font-size: 14px;
387
- color: #475569;
388
- line-height: 1.6;
599
+ line-height: 1.7;
600
+ color: var(--color-text);
389
601
  }
390
602
 
391
- .content-section ul {
603
+ .finding-content-section ul {
392
604
  list-style: none;
393
605
  padding: 0;
394
606
  }
395
607
 
396
- .content-section li {
608
+ .finding-content-section li {
397
609
  font-size: 14px;
398
- color: #475569;
399
- padding: 4px 0;
400
- padding-left: 16px;
610
+ padding: 6px 0;
611
+ padding-left: 20px;
401
612
  position: relative;
402
613
  }
403
614
 
404
- .content-section li::before {
405
- content: "";
615
+ .finding-content-section li::before {
616
+ content: "";
406
617
  position: absolute;
407
618
  left: 0;
408
- color: #94a3b8;
619
+ color: var(--color-primary);
409
620
  }
410
621
 
411
- .affected-assets {
412
- background: #f8fafc;
413
- border-radius: 8px;
414
- padding: 12px 16px;
622
+ /* Code Blocks */
623
+ .code-block {
624
+ background: #1e293b;
625
+ border-radius: var(--radius-sm);
626
+ padding: 16px;
415
627
  margin-top: 12px;
628
+ overflow-x: auto;
416
629
  }
417
630
 
418
- .affected-assets li {
419
- font-family: 'Monaco', 'Consolas', monospace;
631
+ .code-block pre {
632
+ margin: 0;
633
+ font-family: 'Monaco', 'Consolas', 'Courier New', monospace;
420
634
  font-size: 13px;
421
- color: #475569;
635
+ line-height: 1.6;
636
+ color: #e2e8f0;
422
637
  }
423
638
 
424
- .tech-details-btn {
425
- display: inline-flex;
426
- align-items: center;
427
- gap: 8px;
639
+ .code-block code {
640
+ font-family: inherit;
641
+ }
642
+
643
+ /* Technical Details */
644
+ .tech-details {
645
+ background: ${theme === 'dark' ? '#0f172a' : '#f8fafc'};
646
+ border: 1px solid var(--color-border);
647
+ border-radius: var(--radius-sm);
648
+ padding: 16px;
428
649
  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
650
  }
438
651
 
439
- .tech-details-btn:hover {
440
- background: #f8fafc;
441
- border-color: #cbd5e1;
652
+ .tech-details h5 {
653
+ font-size: 11px;
654
+ font-weight: 600;
655
+ text-transform: uppercase;
656
+ letter-spacing: 0.5px;
657
+ color: var(--color-text-secondary);
658
+ margin-bottom: 12px;
442
659
  }
443
660
 
444
- /* Endpoints Section */
445
- .endpoints-section {
446
- background: white;
447
- border: 1px solid #e2e8f0;
448
- border-radius: 12px;
449
- overflow: hidden;
661
+ .tech-row {
662
+ display: flex;
663
+ margin-bottom: 12px;
450
664
  }
451
665
 
452
- .endpoints-header {
453
- padding: 20px;
454
- border-bottom: 1px solid #f1f5f9;
666
+ .tech-row:last-child {
667
+ margin-bottom: 0;
455
668
  }
456
669
 
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;
670
+ .tech-label {
671
+ width: 120px;
672
+ font-size: 12px;
673
+ color: var(--color-text-secondary);
674
+ flex-shrink: 0;
465
675
  }
466
676
 
467
- .endpoints-header p {
677
+ .tech-value {
678
+ flex: 1;
468
679
  font-size: 13px;
469
- color: #64748b;
680
+ font-family: 'Monaco', 'Consolas', monospace;
681
+ color: var(--color-text);
682
+ word-break: break-all;
470
683
  }
471
684
 
472
- .endpoints-table {
473
- width: 100%;
685
+ /* Passed Checks */
686
+ .passed-section {
687
+ background: var(--color-card);
688
+ border: 1px solid var(--color-border);
689
+ border-radius: var(--radius);
690
+ padding: 24px;
691
+ margin-bottom: 24px;
692
+ box-shadow: var(--shadow-sm);
474
693
  }
475
694
 
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;
695
+ .passed-section h2 {
696
+ font-size: 16px;
484
697
  font-weight: 600;
485
- color: #64748b;
486
- text-transform: uppercase;
487
- letter-spacing: 0.5px;
698
+ margin-bottom: 16px;
699
+ display: flex;
700
+ align-items: center;
701
+ gap: 8px;
702
+ color: var(--color-success);
488
703
  }
489
704
 
490
- .table-row {
705
+ .passed-grid {
491
706
  display: grid;
492
- grid-template-columns: 2fr 1fr 1fr 1fr 1fr;
707
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
493
708
  gap: 12px;
494
- padding: 14px 20px;
495
- border-bottom: 1px solid #f1f5f9;
496
- align-items: center;
497
- font-size: 13px;
498
709
  }
499
710
 
500
- .table-row:last-child {
501
- border-bottom: none;
502
- }
503
-
504
- .table-row:hover {
505
- background: #f8fafc;
711
+ .passed-item {
712
+ display: flex;
713
+ align-items: flex-start;
714
+ gap: 12px;
715
+ padding: 12px;
716
+ background: #f0fdf4;
717
+ border: 1px solid #bbf7d0;
718
+ border-radius: var(--radius-sm);
506
719
  }
507
720
 
508
- .endpoint-path {
721
+ .passed-icon {
722
+ width: 20px;
723
+ height: 20px;
724
+ border-radius: 50%;
725
+ background: #22c55e;
509
726
  display: flex;
510
727
  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;
728
+ justify-content: center;
729
+ color: white;
730
+ font-size: 12px;
731
+ flex-shrink: 0;
519
732
  }
520
733
 
521
- .status-badge {
522
- display: inline-flex;
523
- padding: 4px 10px;
524
- border-radius: 20px;
525
- font-size: 11px;
734
+ .passed-content h4 {
735
+ font-size: 13px;
526
736
  font-weight: 600;
737
+ margin-bottom: 2px;
527
738
  }
528
739
 
529
- .status-at-risk {
530
- background: #fee2e2;
531
- color: #dc2626;
740
+ .passed-content p {
741
+ font-size: 12px;
742
+ color: var(--color-text-secondary);
532
743
  }
533
744
 
534
- .status-review {
535
- background: #fef3c7;
536
- color: #d97706;
745
+ /* Quick Actions */
746
+ .actions-section {
747
+ background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
748
+ border-radius: var(--radius);
749
+ padding: 24px;
750
+ margin-bottom: 24px;
751
+ color: white;
537
752
  }
538
753
 
539
- .status-secure {
540
- background: #dcfce7;
541
- color: #16a34a;
754
+ .actions-section h2 {
755
+ font-size: 16px;
756
+ font-weight: 600;
757
+ margin-bottom: 16px;
542
758
  }
543
759
 
544
- .icon-warning {
545
- color: #f59e0b;
760
+ .actions-grid {
761
+ display: grid;
762
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
763
+ gap: 12px;
546
764
  }
547
765
 
548
- .icon-check {
549
- color: #22c55e;
766
+ .action-item {
767
+ background: rgba(255,255,255,0.1);
768
+ border: 1px solid rgba(255,255,255,0.1);
769
+ border-radius: var(--radius-sm);
770
+ padding: 16px;
550
771
  }
551
772
 
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;
773
+ .action-item h4 {
774
+ font-size: 13px;
559
775
  font-weight: 600;
776
+ margin-bottom: 8px;
777
+ opacity: 0.9;
560
778
  }
561
779
 
562
- .sensitive-none {
563
- color: #64748b;
780
+ .action-item code {
781
+ display: block;
782
+ background: rgba(0,0,0,0.3);
783
+ padding: 8px 12px;
784
+ border-radius: 4px;
785
+ font-family: 'Monaco', 'Consolas', monospace;
786
+ font-size: 12px;
787
+ color: #60a5fa;
564
788
  }
565
789
 
566
790
  /* Footer */
567
791
  .footer {
568
792
  text-align: center;
569
- padding: 40px 20px;
793
+ padding: 40px 24px;
794
+ border-top: 1px solid var(--color-border);
795
+ margin-top: 40px;
796
+ }
797
+
798
+ .footer p {
570
799
  font-size: 12px;
571
- color: #94a3b8;
572
- line-height: 1.8;
800
+ color: var(--color-text-secondary);
801
+ margin-bottom: 8px;
802
+ }
803
+
804
+ /* Responsive */
805
+ @media (max-width: 768px) {
806
+ .container {
807
+ padding: 16px;
808
+ }
809
+
810
+ .header {
811
+ padding: 20px;
812
+ }
813
+
814
+ .grade-section {
815
+ flex-direction: column;
816
+ text-align: center;
817
+ }
818
+
819
+ .stats-grid {
820
+ grid-template-columns: repeat(2, 1fr);
821
+ }
822
+
823
+ .finding-content {
824
+ padding-left: 20px;
825
+ }
826
+
827
+ .target-meta {
828
+ flex-direction: column;
829
+ gap: 12px;
830
+ }
573
831
  }
574
832
 
575
833
  @media print {
576
834
  body { background: white; }
577
- .accordion-content { display: block !important; }
835
+ .finding-content { display: block !important; }
836
+ .header { background: #3b82f6 !important; -webkit-print-color-adjust: exact; }
578
837
  }
579
838
  </style>
580
839
  </head>
581
840
  <body>
582
841
  <div class="container">
583
- <!-- Header -->
842
+ <!-- Header with Grade -->
584
843
  <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>
844
+ <div class="header-top">
845
+ <div class="brand">
846
+ <div class="logo">S</div>
847
+ <div class="brand-text">
848
+ <h1>SupaSec Security Audit</h1>
849
+ <p>Enterprise-grade security assessment</p>
850
+ </div>
851
+ </div>
852
+ <div class="report-meta">
853
+ <div class="report-id">ID: ${escapeHtml(result.scan_metadata.scan_id)}</div>
854
+ <div class="report-date">${scanDate}</div>
598
855
  </div>
599
856
  </div>
600
857
 
601
- <div class="scan-info-grid">
602
- <div class="info-group">
603
- <h4>Target</h4>
604
- <p>${escapeHtml(result.scan_metadata.target_url)}</p>
858
+ <div class="grade-section">
859
+ <div class="grade-circle">
860
+ <div class="grade-letter">${grade}</div>
861
+ <div class="grade-score">${score}/100</div>
605
862
  </div>
606
- <div class="info-group">
607
- <h4>Scan Method</h4>
608
- <p>${result.scan_metadata.scanner_mode === 'url' ? 'URL Scan' : 'Project Scan'}</p>
863
+ <div class="grade-info">
864
+ <h2>Security Grade ${grade}</h2>
865
+ <p>${getGradeMessage(grade)}</p>
866
+ <div class="security-meter">
867
+ <div class="security-meter-fill"></div>
868
+ </div>
869
+ <div class="security-meter-labels">
870
+ <span>0</span>
871
+ <span>Security Score: ${score}</span>
872
+ <span>100</span>
873
+ </div>
609
874
  </div>
610
- <div class="info-group">
611
- <h4>Duration</h4>
612
- <p>${result.scan_metadata.scan_duration_seconds.toFixed(2)} seconds</p>
875
+ </div>
876
+ </div>
877
+
878
+ <!-- Target Info -->
879
+ <div class="target-card">
880
+ <h3>Target</h3>
881
+ <div class="target-url">${escapeHtml(result.scan_metadata.target_url)}</div>
882
+ <div class="target-meta">
883
+ <div class="target-meta-item">
884
+ <span>Scan Mode</span>
885
+ <span>${result.scan_metadata.scanner_mode === 'url' ? 'URL Analysis' : 'Project Scan'}</span>
613
886
  </div>
614
- <div class="info-group">
615
- <h4>Scan Date</h4>
616
- <p>${scanDate}</p>
887
+ <div class="target-meta-item">
888
+ <span>Duration</span>
889
+ <span>${result.scan_metadata.scan_duration_seconds.toFixed(2)}s</span>
890
+ </div>
891
+ <div class="target-meta-item">
892
+ <span>Findings</span>
893
+ <span>${result.summary.total_issues} issues detected</span>
894
+ </div>
895
+ <div class="target-meta-item">
896
+ <span>Passed Checks</span>
897
+ <span>${result.passed_checks.length} checks passed</span>
617
898
  </div>
618
899
  </div>
619
900
  </div>
@@ -621,43 +902,88 @@ function generateHTMLReport(result, options = {}) {
621
902
  <!-- Stats Grid -->
622
903
  <div class="stats-grid">
623
904
  <div class="stat-card">
905
+ <div class="stat-icon critical">🚨</div>
906
+ <div class="stat-value critical">${counts.CRITICAL}</div>
624
907
  <div class="stat-label">Critical</div>
625
- <div class="stat-value" style="color: #dc2626;">${counts.CRITICAL}</div>
626
908
  </div>
627
909
  <div class="stat-card">
910
+ <div class="stat-icon high">⚠️</div>
911
+ <div class="stat-value high">${counts.HIGH}</div>
628
912
  <div class="stat-label">High</div>
629
- <div class="stat-value" style="color: #d97706;">${counts.HIGH}</div>
630
913
  </div>
631
914
  <div class="stat-card">
915
+ <div class="stat-icon medium">⚡</div>
916
+ <div class="stat-value medium">${counts.MEDIUM}</div>
632
917
  <div class="stat-label">Medium</div>
633
- <div class="stat-value" style="color: #a16207;">${counts.MEDIUM}</div>
634
918
  </div>
635
919
  <div class="stat-card">
920
+ <div class="stat-icon low">ℹ️</div>
921
+ <div class="stat-value low">${counts.LOW}</div>
636
922
  <div class="stat-label">Low</div>
637
- <div class="stat-value" style="color: #2563eb;">${counts.LOW}</div>
638
923
  </div>
639
924
  </div>
640
925
 
641
- ${includeDetails ? generateKeyFindings(findingsBySeverity) : ''}
926
+ <!-- Category Scores -->
927
+ <div class="category-section">
928
+ <h2>📊 Category Breakdown</h2>
929
+ <div class="category-grid">
930
+ ${generateCategoryScores(result.grading.category_scores)}
931
+ </div>
932
+ </div>
933
+
934
+ <!-- Findings -->
935
+ ${includeDetails ? generateFindingsSection(findingsBySeverity) : ''}
642
936
 
643
- ${generateEndpointsSection(result)}
937
+ <!-- Passed Checks -->
938
+ ${result.passed_checks.length > 0 ? generatePassedChecksSection(result.passed_checks) : ''}
939
+
940
+ <!-- Quick Actions -->
941
+ <div class="actions-section">
942
+ <h2>🛠️ Quick Actions</h2>
943
+ <div class="actions-grid">
944
+ <div class="action-item">
945
+ <h4>Fix Critical Issues</h4>
946
+ <code>supasec fix --interactive</code>
947
+ </div>
948
+ <div class="action-item">
949
+ <h4>Export as JSON</h4>
950
+ <code>supasec scan --format json</code>
951
+ </div>
952
+ <div class="action-item">
953
+ <h4>Save Snapshot</h4>
954
+ <code>supasec snapshot save</code>
955
+ </div>
956
+ <div class="action-item">
957
+ <h4>Deep Scan</h4>
958
+ <code>supasec scan --deep</code>
959
+ </div>
960
+ </div>
961
+ </div>
644
962
 
645
963
  <!-- Footer -->
646
964
  <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>
965
+ <p>Generated by SupaSec v1.0.4 Report ID: ${escapeHtml(result.scan_metadata.scan_id)}</p>
966
+ <p>SupaSec is an independent security auditing tool and is not affiliated with Supabase Inc.</p>
650
967
  </div>
651
968
  </div>
652
969
 
653
970
  <script>
654
- // Accordion functionality
655
- document.querySelectorAll('.accordion-header').forEach(header => {
971
+ // Toggle finding details
972
+ document.querySelectorAll('.finding-header').forEach(header => {
656
973
  header.addEventListener('click', () => {
657
974
  const content = header.nextElementSibling;
658
- const icon = header.querySelector('.accordion-icon');
975
+ const toggle = header.querySelector('.finding-toggle');
659
976
  content.classList.toggle('active');
660
- icon.style.transform = content.classList.contains('active') ? 'rotate(180deg)' : 'rotate(0deg)';
977
+ toggle.style.transform = content.classList.contains('active') ? 'rotate(180deg)' : 'rotate(0deg)';
978
+ });
979
+ });
980
+
981
+ // Animate progress bars on load
982
+ window.addEventListener('load', () => {
983
+ document.querySelectorAll('.category-bar-fill').forEach(bar => {
984
+ const width = bar.style.width;
985
+ bar.style.width = '0';
986
+ setTimeout(() => bar.style.width = width, 100);
661
987
  });
662
988
  });
663
989
  </script>
@@ -665,262 +991,242 @@ function generateHTMLReport(result, options = {}) {
665
991
  </html>`;
666
992
  }
667
993
  /**
668
- * Generate Key Findings section with accordions
994
+ * Generate category scores HTML
995
+ */
996
+ function generateCategoryScores(categoryScores) {
997
+ const categoryIcons = {
998
+ secrets: '🔑',
999
+ rls: '🛡️',
1000
+ auth: '🔐',
1001
+ storage: '📦',
1002
+ api: '🔌',
1003
+ functions: '⚙️',
1004
+ database: '🗄️',
1005
+ transport: '🌐',
1006
+ pii: '👤'
1007
+ };
1008
+ return Object.entries(categoryScores)
1009
+ .map(([category, data]) => {
1010
+ const icon = categoryIcons[category] || '📋';
1011
+ const gradeColor = getGradeColor(data.grade);
1012
+ return `
1013
+ <div class="category-item">
1014
+ <div class="category-grade" style="background: ${gradeColor}; color: white;">${data.grade}</div>
1015
+ <div class="category-info">
1016
+ <div class="category-name">${icon} ${category}</div>
1017
+ <div style="display: flex; align-items: center;">
1018
+ <div class="category-bar" style="flex: 1;">
1019
+ <div class="category-bar-fill" style="width: ${data.score}%; background: ${gradeColor};"></div>
1020
+ </div>
1021
+ <span class="category-score">${data.score}/100</span>
1022
+ </div>
1023
+ </div>
1024
+ </div>
1025
+ `;
1026
+ })
1027
+ .join('');
1028
+ }
1029
+ /**
1030
+ * Generate findings section HTML
669
1031
  */
670
- function generateKeyFindings(findingsBySeverity) {
1032
+ function generateFindingsSection(findingsBySeverity) {
671
1033
  const severities = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'];
672
1034
  const allFindings = [];
673
1035
  for (const severity of severities) {
674
1036
  allFindings.push(...findingsBySeverity[severity]);
675
1037
  }
676
1038
  if (allFindings.length === 0) {
677
- return '';
1039
+ return `
1040
+ <div class="findings-section">
1041
+ <div class="section-header">
1042
+ <div>
1043
+ <h2>🔍 Security Findings</h2>
1044
+ <p>No security issues were detected in this scan.</p>
1045
+ </div>
1046
+ </div>
1047
+ <div class="passed-section" style="text-align: center; padding: 60px;">
1048
+ <div style="font-size: 64px; margin-bottom: 16px;">🎉</div>
1049
+ <h3 style="font-size: 20px; margin-bottom: 8px;">Excellent Security Posture!</h3>
1050
+ <p style="color: var(--color-text-secondary);">No vulnerabilities or security issues were found.</p>
1051
+ </div>
1052
+ </div>`;
678
1053
  }
679
1054
  let html = `
680
- <!-- Key Findings -->
681
- <div class="section" id="findings">
1055
+ <div class="findings-section">
682
1056
  <div class="section-header">
683
- <h2>Key Findings</h2>
684
- <p>High-level security assessment summary for your application</p>
685
- </div>
686
- `;
1057
+ <div>
1058
+ <h2>🔍 Security Findings</h2>
1059
+ <p>${allFindings.length} security issues identified across ${Object.keys(findingsBySeverity).filter(k => findingsBySeverity[k].length > 0).length} severity levels</p>
1060
+ </div>
1061
+ </div>`;
687
1062
  for (const finding of allFindings) {
688
- html += generateFindingAccordion(finding);
1063
+ html += generateFindingCard(finding);
689
1064
  }
690
1065
  html += '</div>';
691
1066
  return html;
692
1067
  }
693
1068
  /**
694
- * Generate a single finding accordion
1069
+ * Generate a single finding card
695
1070
  */
696
- function generateFindingAccordion(finding) {
697
- const badgeClass = getBadgeClass(finding.severity);
698
- const badgeText = getBadgeText(finding.severity);
1071
+ function generateFindingCard(finding) {
1072
+ const severityIcons = {
1073
+ CRITICAL: '🚨',
1074
+ HIGH: '⚠️',
1075
+ MEDIUM: '⚡',
1076
+ LOW: 'ℹ️',
1077
+ INFO: '📌'
1078
+ };
1079
+ const locationText = finding.location?.table
1080
+ ? `Table: ${finding.location.table}`
1081
+ : finding.location?.file
1082
+ ? `${finding.location.file}${finding.location.line ? ':' + finding.location.line : ''}`
1083
+ : finding.location?.url
1084
+ ? finding.location.url
1085
+ : '';
699
1086
  let contentHtml = '';
700
- // What we found
1087
+ // Description
701
1088
  contentHtml += `
702
- <div class="content-section">
703
- <h4>What we found</h4>
704
- <p>${escapeHtml(finding.description)}</p>
705
- </div>`;
1089
+ <div class="finding-content-section">
1090
+ <h4>Description</h4>
1091
+ <p>${escapeHtml(finding.description)}</p>
1092
+ </div>`;
706
1093
  // Impact
707
1094
  if (finding.impact) {
708
1095
  contentHtml += `
709
- <div class="content-section">
710
- <h4>Impact</h4>
711
- <p>${escapeHtml(finding.impact.description)}</p>
712
- </div>`;
1096
+ <div class="finding-content-section">
1097
+ <h4>Impact</h4>
1098
+ <p>${escapeHtml(finding.impact.description)}</p>
1099
+ ${finding.impact.compliance_violations ? `
1100
+ <div style="margin-top: 12px;">
1101
+ <span style="font-size: 12px; color: var(--color-text-secondary);">Compliance Violations: </span>
1102
+ ${finding.impact.compliance_violations.map(v => `<span style="font-size: 11px; background: #fef3c7; color: #92400e; padding: 2px 8px; border-radius: 4px; margin-right: 4px;">${escapeHtml(v)}</span>`).join('')}
1103
+ </div>` : ''}
1104
+ </div>`;
713
1105
  }
714
1106
  // Remediation
715
1107
  if (finding.remediation) {
716
1108
  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>`;
1109
+ <div class="finding-content-section">
1110
+ <h4>Remediation</h4>
1111
+ <p>${escapeHtml(finding.remediation.summary)}</p>
1112
+ ${finding.remediation.steps ? `
1113
+ <ul style="margin-top: 12px;">
1114
+ ${finding.remediation.steps.map(step => `<li>${escapeHtml(step.action)}</li>`).join('')}
1115
+ </ul>` : ''}
1116
+ ${finding.remediation.sql ? `
1117
+ <div class="code-block">
1118
+ <pre><code>${escapeHtml(finding.remediation.sql)}</code></pre>
1119
+ </div>` : ''}
1120
+ </div>`;
727
1121
  }
728
- // Affected assets
729
- if (finding.location?.table || finding.location?.file) {
1122
+ // Technical Details
1123
+ if (finding.evidence || finding.location) {
730
1124
  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>`;
1125
+ <div class="finding-content-section">
1126
+ <h4>Technical Details</h4>
1127
+ <div class="tech-details">`;
1128
+ if (finding.evidence?.sample_data?.masked) {
1129
+ contentHtml += `
1130
+ <div class="tech-row">
1131
+ <div class="tech-label">Exposed Key</div>
1132
+ <div class="tech-value">${escapeHtml(finding.evidence.sample_data.masked)}</div>
1133
+ </div>`;
1134
+ }
1135
+ if (finding.evidence?.matched_pattern) {
1136
+ contentHtml += `
1137
+ <div class="tech-row">
1138
+ <div class="tech-label">Key Type</div>
1139
+ <div class="tech-value">${escapeHtml(finding.evidence.matched_pattern)}</div>
1140
+ </div>`;
1141
+ }
1142
+ if (locationText) {
1143
+ contentHtml += `
1144
+ <div class="tech-row">
1145
+ <div class="tech-label">Location</div>
1146
+ <div class="tech-value">${escapeHtml(locationText)}</div>
1147
+ </div>`;
1148
+ }
1149
+ if (finding.evidence?.code_snippet) {
1150
+ contentHtml += `
1151
+ <div class="tech-row" style="flex-direction: column;">
1152
+ <div class="tech-label" style="margin-bottom: 8px;">Code Snippet</div>
1153
+ <div class="code-block" style="margin: 0;">
1154
+ <pre><code>${escapeHtml(finding.evidence.code_snippet)}</code></pre>
1155
+ </div>
1156
+ </div>`;
739
1157
  }
740
- contentHtml += `</ul></div>`;
1158
+ contentHtml += `
1159
+ </div>
1160
+ </div>`;
741
1161
  }
742
- // Technical details section (initially hidden, toggled by button)
743
- contentHtml += generateTechnicalDetails(finding);
744
1162
  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>
1163
+ <div class="finding-card">
1164
+ <div class="finding-header">
1165
+ <div class="finding-severity ${finding.severity.toLowerCase()}">${severityIcons[finding.severity]}</div>
1166
+ <div class="finding-title-section">
1167
+ <div class="finding-title">
1168
+ ${escapeHtml(finding.title)}
1169
+ <span class="finding-id">${finding.finding_id}</span>
1170
+ </div>
1171
+ <div class="finding-preview">${escapeHtml(finding.description.substring(0, 120))}${finding.description.length > 120 ? '...' : ''}</div>
1172
+ <div class="finding-meta">
1173
+ <span class="finding-badge ${finding.severity.toLowerCase()}">${finding.severity}</span>
1174
+ <span class="finding-badge finding-category">${finding.category}</span>
1175
+ ${locationText ? `<span class="finding-badge" style="background: #f3f4f6; color: #4b5563;">📍 ${escapeHtml(locationText.substring(0, 30))}${locationText.length > 30 ? '...' : ''}</span>` : ''}
1176
+ </div>
751
1177
  </div>
752
- <p>${escapeHtml(finding.description.substring(0, 100))}${finding.description.length > 100 ? '...' : ''}</p>
1178
+ <div class="finding-toggle">▼</div>
753
1179
  </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;
1180
+ <div class="finding-content">
1181
+ ${contentHtml}
1182
+ </div>
1183
+ </div>`;
829
1184
  }
830
1185
  /**
831
- * Get badge class based on severity
1186
+ * Generate passed checks section
832
1187
  */
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
- }
1188
+ function generatePassedChecksSection(passedChecks) {
1189
+ return `
1190
+ <div class="passed-section">
1191
+ <h2>✅ Passed Security Checks (${passedChecks.length})</h2>
1192
+ <div class="passed-grid">
1193
+ ${passedChecks.map(check => `
1194
+ <div class="passed-item">
1195
+ <div class="passed-icon">✓</div>
1196
+ <div class="passed-content">
1197
+ <h4>${escapeHtml(check.title)}</h4>
1198
+ <p>${escapeHtml(check.description)}</p>
1199
+ </div>
1200
+ </div>
1201
+ `).join('')}
1202
+ </div>
1203
+ </div>`;
841
1204
  }
842
1205
  /**
843
- * Get badge text based on severity
1206
+ * Get color for grade
844
1207
  */
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
- }
1208
+ function getGradeColor(grade) {
1209
+ const colors = {
1210
+ 'A': '#22c55e',
1211
+ 'B': '#84cc16',
1212
+ 'C': '#eab308',
1213
+ 'D': '#f97316',
1214
+ 'F': '#ef4444'
1215
+ };
1216
+ return colors[grade] || '#6b7280';
853
1217
  }
854
1218
  /**
855
- * Generate Endpoints section
856
- * Shows real endpoints from scan results or a message if none detected
1219
+ * Get message for grade
857
1220
  */
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>`;
1221
+ function getGradeMessage(grade) {
1222
+ const messages = {
1223
+ 'A': 'Excellent security posture! Your application follows security best practices.',
1224
+ 'B': 'Good security with minor improvements possible. Review medium and low priority issues.',
1225
+ 'C': 'Average security posture. Some issues need attention to improve your security grade.',
1226
+ 'D': 'Below average security. Multiple serious issues found that should be addressed.',
1227
+ 'F': 'Critical vulnerabilities detected! Immediate action required to secure your application.'
1228
+ };
1229
+ return messages[grade] || 'Review findings for details.';
924
1230
  }
925
1231
  /**
926
1232
  * Escape HTML special characters