solidstats 0.0.4 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +78 -5
- data/README.md +54 -5
- data/app/controllers/solidstats/dashboard_controller.rb +60 -121
- data/app/services/solidstats/audit_service.rb +56 -0
- data/app/services/solidstats/data_collector_service.rb +83 -0
- data/app/services/solidstats/log_size_monitor_service.rb +94 -0
- data/app/services/solidstats/todo_service.rb +114 -0
- data/app/views/solidstats/dashboard/_log_monitor.html.erb +759 -0
- data/app/views/solidstats/dashboard/_todos.html.erb +6 -27
- data/app/views/solidstats/dashboard/audit/_additional_styles.css +22 -0
- data/app/views/solidstats/dashboard/audit/_audit_details.html.erb +47 -58
- data/app/views/solidstats/dashboard/audit/_audit_summary.html.erb +1 -1
- data/app/views/solidstats/dashboard/audit/_security_audit.html.erb +0 -23
- data/app/views/solidstats/dashboard/audit/_vulnerabilities_table.html.erb +1092 -38
- data/app/views/solidstats/dashboard/audit/_vulnerability_details.html.erb +4 -3
- data/app/views/solidstats/dashboard/index.html.erb +1166 -162
- data/config/routes.rb +5 -0
- data/lib/solidstats/version.rb +1 -1
- data/lib/tasks/solidstats_release.rake +69 -0
- metadata +12 -5
- data/app/views/solidstats/dashboard/audit/_audit_filters.html.erb +0 -5
@@ -1,66 +1,1120 @@
|
|
1
|
-
|
1
|
+
<% results = @audit_output.dig("results") || [] %>
|
2
|
+
<div class="vulnerabilities-controls">
|
3
|
+
<div class="vulnerability-filters">
|
4
|
+
<div class="filter-group">
|
5
|
+
<label>Severity:</label>
|
6
|
+
<button class="filter-btn active" data-filter="all">All</button>
|
7
|
+
<button class="filter-btn critical-filter" data-filter="critical">Critical</button>
|
8
|
+
<button class="filter-btn high-filter" data-filter="high">High</button>
|
9
|
+
<button class="filter-btn" data-filter="medium">Medium</button>
|
10
|
+
<button class="filter-btn" data-filter="low">Low</button>
|
11
|
+
<button class="filter-btn" data-filter="unknown">Unknown</button>
|
12
|
+
</div>
|
13
|
+
<div class="search-container">
|
14
|
+
<input type="text" id="vulnerability-search" placeholder="Search vulnerabilities..." class="search-input">
|
15
|
+
<div class="search-icon">🔍</div>
|
16
|
+
</div>
|
17
|
+
</div>
|
18
|
+
<div class="vulnerability-actions">
|
19
|
+
<button class="action-button bulk-action-button" disabled>
|
20
|
+
<span class="action-icon">✓</span> Bulk Update
|
21
|
+
</button>
|
22
|
+
<button class="action-button export-csv-button" disabled>
|
23
|
+
<span class="action-icon">↓</span> Export CSV
|
24
|
+
</button>
|
25
|
+
</div>
|
26
|
+
</div>
|
27
|
+
|
28
|
+
<div class="vulnerability-summary">
|
29
|
+
<div class="donut-chart">
|
30
|
+
<div class="chart-container" id="severity-donut-chart">
|
31
|
+
<!-- CSS-based donut chart -->
|
32
|
+
<%
|
33
|
+
# Calculate counts for each severity
|
34
|
+
critical_count = results.count { |r| r.dig("advisory", "criticality").to_s.downcase == "critical" }
|
35
|
+
high_count = results.count { |r| r.dig("advisory", "criticality").to_s.downcase == "high" }
|
36
|
+
medium_count = results.count { |r| r.dig("advisory", "criticality").to_s.downcase == "medium" }
|
37
|
+
low_count = results.count { |r| r.dig("advisory", "criticality").to_s.downcase == "low" }
|
38
|
+
unknown_count = results.count { |r| ["", nil].include?(r.dig("advisory", "criticality")) ||
|
39
|
+
r.dig("advisory", "criticality").to_s.downcase == "unknown" }
|
40
|
+
|
41
|
+
# Calculate total for percentages
|
42
|
+
total = critical_count + high_count + medium_count + low_count + unknown_count
|
43
|
+
|
44
|
+
# Calculate percentages (avoid division by zero)
|
45
|
+
critical_pct = total > 0 ? critical_count.to_f / total : 0
|
46
|
+
high_pct = total > 0 ? high_count.to_f / total : 0
|
47
|
+
medium_pct = total > 0 ? medium_count.to_f / total : 0
|
48
|
+
low_pct = total > 0 ? low_count.to_f / total : 0
|
49
|
+
unknown_pct = total > 0 ? unknown_count.to_f / total : 0
|
50
|
+
%>
|
51
|
+
<div class="donut"
|
52
|
+
style="--critical: <%= critical_pct %>;
|
53
|
+
--high: <%= high_pct %>;
|
54
|
+
--medium: <%= medium_pct %>;
|
55
|
+
--low: <%= low_pct %>;
|
56
|
+
--unknown: <%= unknown_pct %>;"
|
57
|
+
data-count="<%= total %>"
|
58
|
+
data-critical="<%= critical_count %>"
|
59
|
+
data-high="<%= high_count %>"
|
60
|
+
data-medium="<%= medium_count %>"
|
61
|
+
data-low="<%= low_count %>"
|
62
|
+
data-unknown="<%= unknown_count %>">
|
63
|
+
</div>
|
64
|
+
</div>
|
65
|
+
<div class="chart-legend">
|
66
|
+
<div class="legend-item">
|
67
|
+
<span class="legend-color critical"></span>
|
68
|
+
<span class="legend-label">Critical</span>
|
69
|
+
</div>
|
70
|
+
<div class="legend-item">
|
71
|
+
<span class="legend-color high"></span>
|
72
|
+
<span class="legend-label">High</span>
|
73
|
+
</div>
|
74
|
+
<div class="legend-item">
|
75
|
+
<span class="legend-color medium"></span>
|
76
|
+
<span class="legend-label">Medium</span>
|
77
|
+
</div>
|
78
|
+
<div class="legend-item">
|
79
|
+
<span class="legend-color low"></span>
|
80
|
+
<span class="legend-label">Low</span>
|
81
|
+
</div>
|
82
|
+
<div class="legend-item">
|
83
|
+
<span class="legend-color unknown"></span>
|
84
|
+
<span class="legend-label">Unknown</span>
|
85
|
+
</div>
|
86
|
+
</div>
|
87
|
+
</div>
|
88
|
+
|
89
|
+
<div class="vulnerability-stats">
|
90
|
+
<div class="stat-item">
|
91
|
+
<div class="stat-value"><%= results.count { |r| %w[critical high].include?(r.dig("advisory", "criticality").to_s.downcase) } %></div>
|
92
|
+
<div class="stat-label">Critical/High</div>
|
93
|
+
</div>
|
94
|
+
<div class="stat-item">
|
95
|
+
<div class="stat-value"><%= results.count { |r| r.dig("advisory", "criticality").to_s.downcase == "medium" } %></div>
|
96
|
+
<div class="stat-label">Medium</div>
|
97
|
+
</div>
|
98
|
+
<div class="stat-item">
|
99
|
+
<div class="stat-value"><%= results.count { |r| r.dig("advisory", "criticality").to_s.downcase == "low" } %></div>
|
100
|
+
<div class="stat-label">Low</div>
|
101
|
+
</div>
|
102
|
+
</div>
|
103
|
+
</div>
|
104
|
+
|
2
105
|
<div class="vulnerabilities-table">
|
3
106
|
<table class="table">
|
4
107
|
<thead>
|
5
108
|
<tr>
|
6
|
-
<th>
|
7
|
-
<th>
|
8
|
-
<th>
|
9
|
-
<th>CVE</th>
|
10
|
-
<th>
|
11
|
-
<th>
|
109
|
+
<th class="severity-column">Severity</th>
|
110
|
+
<th class="gem-column">Gem</th>
|
111
|
+
<th class="version-column">Version</th>
|
112
|
+
<th class="cve-column">CVE</th>
|
113
|
+
<th class="description-column">Description</th>
|
114
|
+
<th class="remediation-column">Remediation</th>
|
12
115
|
</tr>
|
13
116
|
</thead>
|
14
117
|
<tbody>
|
15
|
-
<%
|
118
|
+
<% # We already defined results at the top of the file %>
|
16
119
|
<% results.each_with_index do |result, index| %>
|
17
120
|
<%
|
18
121
|
gem = result.dig("gem") || {}
|
19
122
|
advisory = result.dig("advisory") || {}
|
20
|
-
criticality = advisory["criticality"] || "
|
21
|
-
|
123
|
+
criticality = advisory["criticality"] || "unknown"
|
124
|
+
criticality = criticality.to_s.downcase # Ensure consistent lowercase
|
125
|
+
is_high = %w[high critical].include?(criticality)
|
126
|
+
|
127
|
+
# Extract CVE or GHSA ID
|
128
|
+
vuln_id = advisory["cve"].present? ? "CVE-#{advisory["cve"]}" :
|
129
|
+
(advisory["ghsa"].present? ? "GHSA-#{advisory["ghsa"]}" : "N/A")
|
130
|
+
|
131
|
+
# Format the link
|
132
|
+
vuln_link = advisory["cve"].present? ? "https://nvd.nist.gov/vuln/detail/CVE-#{advisory["cve"]}" :
|
133
|
+
(advisory["ghsa"].present? ? "https://github.com/advisories/#{advisory["ghsa"]}" : "#")
|
134
|
+
|
135
|
+
# Extract published date and format it
|
136
|
+
published_date = advisory["date"] || "Unknown"
|
137
|
+
formatted_date = published_date != "Unknown" ? Date.parse(published_date).strftime("%b %d, %Y") : "Unknown"
|
22
138
|
%>
|
23
|
-
<tr class="vulnerability-row <%=
|
24
|
-
<td
|
25
|
-
|
26
|
-
<td>
|
27
|
-
<span class="severity <%= criticality.to_s.downcase %>">
|
139
|
+
<tr class="vulnerability-row <%= criticality.to_s.downcase %>" data-severity="<%= criticality.to_s.downcase %>" data-gem="<%= gem["name"] %>" data-index="<%= index %>">
|
140
|
+
<td class="severity-column">
|
141
|
+
<div class="severity-badge <%= criticality.to_s.downcase %>">
|
28
142
|
<%= criticality %>
|
29
|
-
</
|
143
|
+
</div>
|
30
144
|
</td>
|
31
|
-
<td>
|
32
|
-
|
33
|
-
|
34
|
-
|
145
|
+
<td class="gem-column">
|
146
|
+
<div class="gem-name"><%= gem["name"] %></div>
|
147
|
+
<% if gem["source_url"].present? %>
|
148
|
+
<a href="<%= gem["source_url"] %>" target="_blank" class="gem-source-link">
|
149
|
+
<small>Repository <span class="external-link-icon">↗</span></small>
|
35
150
|
</a>
|
36
|
-
<%
|
37
|
-
|
38
|
-
|
151
|
+
<% end %>
|
152
|
+
</td>
|
153
|
+
<td class="version-column">
|
154
|
+
<div class="version-info">
|
155
|
+
<span class="current-version"><%= gem["version"] %></span>
|
156
|
+
</div>
|
157
|
+
<div class="vulnerability-meta">
|
158
|
+
<small>Published: <%= formatted_date %></small>
|
159
|
+
</div>
|
160
|
+
</td>
|
161
|
+
<td class="cve-column">
|
162
|
+
<% if vuln_id != "N/A" %>
|
163
|
+
<a href="<%= vuln_link %>" target="_blank" class="vuln-link">
|
164
|
+
<%= vuln_id %>
|
165
|
+
<span class="external-link-icon">↗</span>
|
39
166
|
</a>
|
40
167
|
<% else %>
|
41
168
|
N/A
|
42
169
|
<% end %>
|
43
170
|
</td>
|
44
|
-
<td>
|
45
|
-
|
171
|
+
<td class="description-column">
|
172
|
+
<div class="vulnerability-title"><%= advisory["title"] %></div>
|
46
173
|
<% if advisory["description"].present? %>
|
47
|
-
<a href="#vulnerability-<%= index %>" class="
|
174
|
+
<a href="#vulnerability-<%= index %>" class="details-link-btn scroll-to-details">
|
175
|
+
<span class="details-icon">⤵</span> More details
|
176
|
+
</a>
|
48
177
|
<% end %>
|
49
178
|
</td>
|
50
|
-
<td>
|
51
|
-
|
52
|
-
|
53
|
-
<
|
54
|
-
<
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
179
|
+
<td class="remediation-column">
|
180
|
+
<% if advisory["patched_versions"].present? %>
|
181
|
+
<div class="remediation-action">
|
182
|
+
<div class="patched-versions-pill">
|
183
|
+
<div class="versions-container">
|
184
|
+
<% advisory["patched_versions"].each_with_index do |version_text, index| %>
|
185
|
+
<% version_display = version_text.include?(">=") ? version_text : ">= #{version_text}" %>
|
186
|
+
<span class="solution-version<%= index == advisory["patched_versions"].size - 1 ? ' last-version' : '' %>"><%= version_display %></span>
|
187
|
+
<% end %>
|
188
|
+
</div>
|
189
|
+
<button class="copy-btn" data-solution="<%= "#{gem["name"]}, #{advisory["patched_versions"].join(", ")}" %>" title="Copy gem name and patched versions">
|
190
|
+
<span class="copy-icon">📋</span>
|
191
|
+
</button>
|
192
|
+
</div>
|
193
|
+
</div>
|
194
|
+
<% else %>
|
195
|
+
<span class="no-patch">No patch available</span>
|
196
|
+
<% end %>
|
61
197
|
</td>
|
62
198
|
</tr>
|
63
199
|
<% end %>
|
64
200
|
</tbody>
|
65
201
|
</table>
|
66
|
-
</div>
|
202
|
+
</div>
|
203
|
+
|
204
|
+
<style>
|
205
|
+
.vulnerabilities-controls {
|
206
|
+
display: flex;
|
207
|
+
justify-content: space-between;
|
208
|
+
align-items: center;
|
209
|
+
margin-bottom: 1.5rem;
|
210
|
+
flex-wrap: wrap;
|
211
|
+
gap: 1rem;
|
212
|
+
}
|
213
|
+
|
214
|
+
.vulnerability-filters {
|
215
|
+
display: flex;
|
216
|
+
align-items: center;
|
217
|
+
gap: 1.5rem;
|
218
|
+
flex-wrap: wrap;
|
219
|
+
}
|
220
|
+
|
221
|
+
.filter-group {
|
222
|
+
display: flex;
|
223
|
+
align-items: center;
|
224
|
+
gap: 0.5rem;
|
225
|
+
}
|
226
|
+
|
227
|
+
.filter-group label {
|
228
|
+
font-size: 0.9rem;
|
229
|
+
color: #555;
|
230
|
+
}
|
231
|
+
|
232
|
+
.filter-btn {
|
233
|
+
background: #f8f9fa;
|
234
|
+
border: 1px solid #dee2e6;
|
235
|
+
padding: 0.35rem 0.75rem;
|
236
|
+
border-radius: 4px;
|
237
|
+
font-size: 0.85rem;
|
238
|
+
cursor: pointer;
|
239
|
+
transition: all 0.2s;
|
240
|
+
}
|
241
|
+
|
242
|
+
.filter-btn:hover {
|
243
|
+
background-color: #e9ecef;
|
244
|
+
}
|
245
|
+
|
246
|
+
.filter-btn.active {
|
247
|
+
background-color: #e9f5ff;
|
248
|
+
color: #0366d6;
|
249
|
+
border-color: #0366d6;
|
250
|
+
}
|
251
|
+
|
252
|
+
.critical-filter {
|
253
|
+
border-left: 3px solid #dc3545;
|
254
|
+
}
|
255
|
+
|
256
|
+
.high-filter {
|
257
|
+
border-left: 3px solid #fd7e14;
|
258
|
+
}
|
259
|
+
|
260
|
+
.search-container {
|
261
|
+
position: relative;
|
262
|
+
}
|
263
|
+
|
264
|
+
.search-input {
|
265
|
+
padding: 0.5rem 0.75rem 0.5rem 2rem;
|
266
|
+
border: 1px solid #dee2e6;
|
267
|
+
border-radius: 4px;
|
268
|
+
font-size: 0.9rem;
|
269
|
+
width: 250px;
|
270
|
+
}
|
271
|
+
|
272
|
+
.search-icon {
|
273
|
+
position: absolute;
|
274
|
+
left: 0.75rem;
|
275
|
+
top: 50%;
|
276
|
+
transform: translateY(-50%);
|
277
|
+
color: #6c757d;
|
278
|
+
font-size: 0.9rem;
|
279
|
+
}
|
280
|
+
|
281
|
+
.vulnerability-summary {
|
282
|
+
display: flex;
|
283
|
+
gap: 1.5rem;
|
284
|
+
margin-bottom: 1.5rem;
|
285
|
+
background-color: #f9fafb;
|
286
|
+
border-radius: 8px;
|
287
|
+
padding: 1.5rem;
|
288
|
+
align-items: center;
|
289
|
+
}
|
290
|
+
|
291
|
+
.donut-chart {
|
292
|
+
display: flex;
|
293
|
+
align-items: center;
|
294
|
+
gap: 1.5rem;
|
295
|
+
}
|
296
|
+
|
297
|
+
.chart-container {
|
298
|
+
width: 100px;
|
299
|
+
height: 100px;
|
300
|
+
position: relative;
|
301
|
+
}
|
302
|
+
|
303
|
+
.donut {
|
304
|
+
width: 100%;
|
305
|
+
height: 100%;
|
306
|
+
border-radius: 50%;
|
307
|
+
background: conic-gradient(
|
308
|
+
#dc3545 0% calc(var(--critical) * 100%),
|
309
|
+
#fd7e14 calc(var(--critical) * 100%) calc((var(--critical) + var(--high)) * 100%),
|
310
|
+
#ffc107 calc((var(--critical) + var(--high)) * 100%) calc((var(--critical) + var(--high) + var(--medium)) * 100%),
|
311
|
+
#28a745 calc((var(--critical) + var(--high) + var(--medium)) * 100%) calc((var(--critical) + var(--high) + var(--medium) + var(--low)) * 100%),
|
312
|
+
#adb5bd calc((var(--critical) + var(--high) + var(--medium) + var(--low)) * 100%) 100%
|
313
|
+
);
|
314
|
+
display: flex;
|
315
|
+
align-items: center;
|
316
|
+
justify-content: center;
|
317
|
+
position: relative;
|
318
|
+
}
|
319
|
+
|
320
|
+
.donut::after {
|
321
|
+
content: attr(data-count);
|
322
|
+
width: 60px;
|
323
|
+
height: 60px;
|
324
|
+
background: white;
|
325
|
+
border-radius: 50%;
|
326
|
+
position: absolute;
|
327
|
+
display: flex;
|
328
|
+
align-items: center;
|
329
|
+
justify-content: center;
|
330
|
+
font-weight: bold;
|
331
|
+
font-size: 16px;
|
332
|
+
color: #333;
|
333
|
+
}
|
334
|
+
|
335
|
+
.chart-legend {
|
336
|
+
display: flex;
|
337
|
+
flex-direction: column;
|
338
|
+
gap: 0.5rem;
|
339
|
+
}
|
340
|
+
|
341
|
+
.legend-item {
|
342
|
+
display: flex;
|
343
|
+
align-items: center;
|
344
|
+
gap: 0.5rem;
|
345
|
+
font-size: 0.85rem;
|
346
|
+
}
|
347
|
+
|
348
|
+
.legend-color {
|
349
|
+
width: 12px;
|
350
|
+
height: 12px;
|
351
|
+
border-radius: 2px;
|
352
|
+
}
|
353
|
+
|
354
|
+
.legend-color.critical {
|
355
|
+
background-color: #dc3545;
|
356
|
+
}
|
357
|
+
|
358
|
+
.legend-color.high {
|
359
|
+
background-color: #fd7e14;
|
360
|
+
}
|
361
|
+
|
362
|
+
.legend-color.medium {
|
363
|
+
background-color: #ffc107;
|
364
|
+
}
|
365
|
+
|
366
|
+
.legend-color.low {
|
367
|
+
background-color: #28a745;
|
368
|
+
}
|
369
|
+
|
370
|
+
.legend-color.unknown {
|
371
|
+
background-color: #adb5bd;
|
372
|
+
}
|
373
|
+
|
374
|
+
.vulnerability-stats {
|
375
|
+
display: flex;
|
376
|
+
gap: 1.5rem;
|
377
|
+
margin-left: auto;
|
378
|
+
}
|
379
|
+
|
380
|
+
.stat-item {
|
381
|
+
text-align: center;
|
382
|
+
min-width: 80px;
|
383
|
+
}
|
384
|
+
|
385
|
+
.stat-value {
|
386
|
+
font-size: 2rem;
|
387
|
+
font-weight: 700;
|
388
|
+
line-height: 1;
|
389
|
+
}
|
390
|
+
|
391
|
+
.stat-label {
|
392
|
+
font-size: 0.85rem;
|
393
|
+
color: #6c757d;
|
394
|
+
margin-top: 0.5rem;
|
395
|
+
}
|
396
|
+
|
397
|
+
.vulnerabilities-table {
|
398
|
+
margin-bottom: 1.5rem;
|
399
|
+
width: 100%;
|
400
|
+
}
|
401
|
+
|
402
|
+
.table {
|
403
|
+
width: 100%;
|
404
|
+
border-collapse: separate;
|
405
|
+
border-spacing: 0;
|
406
|
+
font-size: 0.9rem;
|
407
|
+
table-layout: fixed;
|
408
|
+
}
|
409
|
+
|
410
|
+
.table th {
|
411
|
+
background-color: #f8f9fa;
|
412
|
+
padding: 0.75rem;
|
413
|
+
text-align: left;
|
414
|
+
font-weight: 600;
|
415
|
+
border-bottom: 2px solid #dee2e6;
|
416
|
+
}
|
417
|
+
|
418
|
+
.table td {
|
419
|
+
padding: 0.65rem 0.75rem;
|
420
|
+
border-bottom: 1px solid #dee2e6;
|
421
|
+
vertical-align: middle;
|
422
|
+
word-break: break-word;
|
423
|
+
overflow-wrap: break-word;
|
424
|
+
}
|
425
|
+
|
426
|
+
.severity-column {
|
427
|
+
width: 8%;
|
428
|
+
min-width: 90px;
|
429
|
+
}
|
430
|
+
|
431
|
+
.gem-column {
|
432
|
+
width: 15%;
|
433
|
+
min-width: 120px;
|
434
|
+
}
|
435
|
+
|
436
|
+
.version-column {
|
437
|
+
width: 10%;
|
438
|
+
min-width: 100px;
|
439
|
+
}
|
440
|
+
|
441
|
+
.cve-column {
|
442
|
+
width: 12%;
|
443
|
+
min-width: 110px;
|
444
|
+
}
|
445
|
+
|
446
|
+
.description-column {
|
447
|
+
width: 30%;
|
448
|
+
min-width: 180px;
|
449
|
+
}
|
450
|
+
|
451
|
+
.remediation-column {
|
452
|
+
width: 20%;
|
453
|
+
min-width: 160px;
|
454
|
+
}
|
455
|
+
|
456
|
+
.vulnerability-row {
|
457
|
+
transition: background-color 0.2s;
|
458
|
+
}
|
459
|
+
|
460
|
+
.vulnerability-row:hover {
|
461
|
+
background-color: #f8f9fa;
|
462
|
+
}
|
463
|
+
|
464
|
+
.vulnerability-row.critical {
|
465
|
+
border-left: 4px solid #dc3545;
|
466
|
+
}
|
467
|
+
|
468
|
+
.vulnerability-row.high {
|
469
|
+
border-left: 4px solid #fd7e14;
|
470
|
+
}
|
471
|
+
|
472
|
+
.vulnerability-row.medium {
|
473
|
+
border-left: 4px solid #ffc107;
|
474
|
+
}
|
475
|
+
|
476
|
+
.vulnerability-row.low {
|
477
|
+
border-left: 4px solid #28a745;
|
478
|
+
}
|
479
|
+
|
480
|
+
.vulnerability-row.unknown {
|
481
|
+
border-left: 4px solid #adb5bd;
|
482
|
+
}
|
483
|
+
|
484
|
+
.severity-badge {
|
485
|
+
display: inline-block;
|
486
|
+
padding: 0.3rem 0.6rem;
|
487
|
+
border-radius: 50px;
|
488
|
+
font-weight: 600;
|
489
|
+
font-size: 0.8rem;
|
490
|
+
text-align: center;
|
491
|
+
white-space: nowrap;
|
492
|
+
text-transform: uppercase;
|
493
|
+
letter-spacing: 0.03em;
|
494
|
+
min-width: 70px;
|
495
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
496
|
+
}
|
497
|
+
|
498
|
+
.severity-badge.critical {
|
499
|
+
background-color: #dc3545;
|
500
|
+
color: white;
|
501
|
+
}
|
502
|
+
|
503
|
+
.severity-badge.high {
|
504
|
+
background-color: #fd7e14;
|
505
|
+
color: white;
|
506
|
+
}
|
507
|
+
|
508
|
+
.severity-badge.medium {
|
509
|
+
background-color: #ffc107;
|
510
|
+
color: #212529;
|
511
|
+
}
|
512
|
+
|
513
|
+
.severity-badge.low {
|
514
|
+
background-color: #28a745;
|
515
|
+
color: white;
|
516
|
+
}
|
517
|
+
|
518
|
+
.severity-badge.unknown {
|
519
|
+
background-color: #adb5bd;
|
520
|
+
color: white;
|
521
|
+
}
|
522
|
+
|
523
|
+
.details-icon {
|
524
|
+
font-size: 0.9rem;
|
525
|
+
font-weight: bold;
|
526
|
+
}
|
527
|
+
|
528
|
+
.gem-name {
|
529
|
+
font-weight: 500;
|
530
|
+
overflow-wrap: break-word;
|
531
|
+
word-wrap: break-word;
|
532
|
+
word-break: break-word;
|
533
|
+
hyphens: auto;
|
534
|
+
}
|
535
|
+
|
536
|
+
.version-info {
|
537
|
+
display: flex;
|
538
|
+
align-items: center;
|
539
|
+
gap: 0.35rem;
|
540
|
+
flex-wrap: wrap;
|
541
|
+
}
|
542
|
+
|
543
|
+
.current-version {
|
544
|
+
font-family: monospace;
|
545
|
+
font-size: 0.85rem;
|
546
|
+
word-break: break-all;
|
547
|
+
}
|
548
|
+
|
549
|
+
.solution-version {
|
550
|
+
font-family: monospace;
|
551
|
+
font-size: 0.85rem;
|
552
|
+
color: #28a745;
|
553
|
+
word-break: break-all;
|
554
|
+
font-weight: 500;
|
555
|
+
padding: 0.1rem 0.25rem;
|
556
|
+
display: inline-block;
|
557
|
+
background-color: rgba(40, 167, 69, 0.08);
|
558
|
+
border-radius: 3px;
|
559
|
+
margin-right: 0.25rem;
|
560
|
+
}
|
561
|
+
|
562
|
+
.solution-version.last-version {
|
563
|
+
margin-bottom: 0;
|
564
|
+
}
|
565
|
+
|
566
|
+
.solution-version.single-version {
|
567
|
+
margin-right: 0;
|
568
|
+
}
|
569
|
+
|
570
|
+
.vuln-link {
|
571
|
+
color: #0366d6;
|
572
|
+
text-decoration: none;
|
573
|
+
display: flex;
|
574
|
+
align-items: center;
|
575
|
+
gap: 0.35rem;
|
576
|
+
word-break: break-all;
|
577
|
+
}
|
578
|
+
|
579
|
+
.external-link-icon {
|
580
|
+
font-size: 0.75rem;
|
581
|
+
opacity: 0.75;
|
582
|
+
}
|
583
|
+
|
584
|
+
.vulnerability-title {
|
585
|
+
font-weight: 500;
|
586
|
+
margin-bottom: 0.5rem;
|
587
|
+
overflow-wrap: break-word;
|
588
|
+
word-wrap: break-word;
|
589
|
+
hyphens: auto;
|
590
|
+
}
|
591
|
+
|
592
|
+
.details-toggle-btn {
|
593
|
+
background: none;
|
594
|
+
border: none;
|
595
|
+
color: #0366d6;
|
596
|
+
font-size: 0.85rem;
|
597
|
+
padding: 0;
|
598
|
+
cursor: pointer;
|
599
|
+
display: flex;
|
600
|
+
align-items: center;
|
601
|
+
gap: 0.25rem;
|
602
|
+
}
|
603
|
+
|
604
|
+
.toggle-icon {
|
605
|
+
transition: transform 0.2s;
|
606
|
+
}
|
607
|
+
|
608
|
+
.vulnerability-details-container {
|
609
|
+
position: relative;
|
610
|
+
z-index: 5;
|
611
|
+
}
|
612
|
+
|
613
|
+
.vulnerability-details {
|
614
|
+
max-height: 0;
|
615
|
+
overflow: hidden;
|
616
|
+
transition: max-height 0.3s ease, opacity 0.3s ease;
|
617
|
+
margin-top: 0.75rem;
|
618
|
+
font-size: 0.85rem;
|
619
|
+
opacity: 0;
|
620
|
+
}
|
621
|
+
|
622
|
+
.vulnerability-details.expanded {
|
623
|
+
max-height: 300px;
|
624
|
+
overflow-y: auto;
|
625
|
+
width: 100%;
|
626
|
+
opacity: 1;
|
627
|
+
position: relative;
|
628
|
+
z-index: 10;
|
629
|
+
}
|
630
|
+
|
631
|
+
.details-content {
|
632
|
+
background-color: #f8f9fa;
|
633
|
+
padding: 0.75rem;
|
634
|
+
border-radius: 4px;
|
635
|
+
white-space: normal;
|
636
|
+
color: #495057;
|
637
|
+
overflow-wrap: break-word;
|
638
|
+
word-wrap: break-word;
|
639
|
+
border: 1px solid #e9ecef;
|
640
|
+
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
|
641
|
+
}
|
642
|
+
|
643
|
+
.remediation-action {
|
644
|
+
display: flex;
|
645
|
+
align-items: center;
|
646
|
+
gap: 0.5rem;
|
647
|
+
flex-wrap: wrap;
|
648
|
+
}
|
649
|
+
|
650
|
+
.patched-versions-pill {
|
651
|
+
display: flex;
|
652
|
+
align-items: center;
|
653
|
+
background-color: rgba(40, 167, 69, 0.08);
|
654
|
+
border-radius: 4px;
|
655
|
+
padding: 0.35rem 0.5rem;
|
656
|
+
max-width: 100%;
|
657
|
+
overflow-wrap: break-word;
|
658
|
+
border-left: 3px solid #28a745;
|
659
|
+
}
|
660
|
+
|
661
|
+
.versions-container {
|
662
|
+
display: flex;
|
663
|
+
flex-direction: column;
|
664
|
+
gap: 0.25rem;
|
665
|
+
flex-grow: 1;
|
666
|
+
}
|
667
|
+
|
668
|
+
.more-versions {
|
669
|
+
font-size: 0.75rem;
|
670
|
+
background-color: rgba(40, 167, 69, 0.2);
|
671
|
+
color: #28a745;
|
672
|
+
font-weight: 500;
|
673
|
+
border-radius: 3px;
|
674
|
+
padding: 0.15rem 0.35rem;
|
675
|
+
cursor: help;
|
676
|
+
margin-left: 0.25rem;
|
677
|
+
display: inline-block;
|
678
|
+
min-width: 1.5rem;
|
679
|
+
text-align: center;
|
680
|
+
}
|
681
|
+
|
682
|
+
.copy-btn {
|
683
|
+
background-color: transparent;
|
684
|
+
color: #495057;
|
685
|
+
border: none;
|
686
|
+
padding: 0.25rem;
|
687
|
+
margin-left: 0.25rem;
|
688
|
+
border-radius: 4px;
|
689
|
+
font-size: 0.85rem;
|
690
|
+
cursor: pointer;
|
691
|
+
transition: all 0.2s;
|
692
|
+
}
|
693
|
+
|
694
|
+
.copy-btn:hover {
|
695
|
+
background-color: #e9ecef;
|
696
|
+
}
|
697
|
+
|
698
|
+
.action-button:disabled {
|
699
|
+
opacity: 0.6;
|
700
|
+
cursor: not-allowed;
|
701
|
+
position: relative;
|
702
|
+
}
|
703
|
+
|
704
|
+
.action-button:disabled:hover::after {
|
705
|
+
content: "Currently unavailable";
|
706
|
+
position: absolute;
|
707
|
+
up: -150px; /* Position tooltip to the right of the button */
|
708
|
+
top: 50%;
|
709
|
+
transform: translateY(-50%);
|
710
|
+
background-color: #333;
|
711
|
+
color: white;
|
712
|
+
padding: 4px 8px;
|
713
|
+
border-radius: 4px;
|
714
|
+
font-size: 12px;
|
715
|
+
white-space: nowrap;
|
716
|
+
z-index: 100;
|
717
|
+
}
|
718
|
+
|
719
|
+
.no-patch {
|
720
|
+
color: #6c757d;
|
721
|
+
font-style: italic;
|
722
|
+
font-size: 0.85rem;
|
723
|
+
}
|
724
|
+
|
725
|
+
.vulnerabilities-pagination {
|
726
|
+
display: flex;
|
727
|
+
justify-content: space-between;
|
728
|
+
align-items: center;
|
729
|
+
margin-top: 1.5rem;
|
730
|
+
padding-top: 1rem;
|
731
|
+
border-top: 1px solid #dee2e6;
|
732
|
+
}
|
733
|
+
|
734
|
+
.pagination-text {
|
735
|
+
color: #6c757d;
|
736
|
+
font-size: 0.85rem;
|
737
|
+
}
|
738
|
+
|
739
|
+
.pagination-controls {
|
740
|
+
display: flex;
|
741
|
+
align-items: center;
|
742
|
+
gap: 1rem;
|
743
|
+
}
|
744
|
+
|
745
|
+
.pagination-pages {
|
746
|
+
font-size: 0.9rem;
|
747
|
+
}
|
748
|
+
|
749
|
+
.pagination-btn {
|
750
|
+
background-color: #f8f9fa;
|
751
|
+
border: 1px solid #dee2e6;
|
752
|
+
padding: 0.35rem 0.75rem;
|
753
|
+
border-radius: 4px;
|
754
|
+
font-size: 0.85rem;
|
755
|
+
cursor: pointer;
|
756
|
+
transition: all 0.2s;
|
757
|
+
}
|
758
|
+
|
759
|
+
.pagination-btn:hover:not(:disabled) {
|
760
|
+
background-color: #e9ecef;
|
761
|
+
}
|
762
|
+
|
763
|
+
.pagination-btn:disabled {
|
764
|
+
opacity: 0.5;
|
765
|
+
cursor: not-allowed;
|
766
|
+
}
|
767
|
+
|
768
|
+
.details-link-btn {
|
769
|
+
color: #0366d6;
|
770
|
+
font-size: 0.85rem;
|
771
|
+
text-decoration: none;
|
772
|
+
display: inline-flex;
|
773
|
+
align-items: center;
|
774
|
+
gap: 0.4rem;
|
775
|
+
padding: 0.2rem 0.5rem;
|
776
|
+
border-radius: 4px;
|
777
|
+
transition: background-color 0.2s;
|
778
|
+
}
|
779
|
+
|
780
|
+
.details-link-btn:hover {
|
781
|
+
background-color: rgba(3, 102, 214, 0.1);
|
782
|
+
text-decoration: none;
|
783
|
+
}
|
784
|
+
|
785
|
+
.scroll-to-details {
|
786
|
+
cursor: pointer;
|
787
|
+
}
|
788
|
+
|
789
|
+
/* Responsive styles for smaller screens */
|
790
|
+
@media (max-width: 992px) {
|
791
|
+
.vulnerability-summary {
|
792
|
+
flex-direction: column;
|
793
|
+
align-items: flex-start;
|
794
|
+
}
|
795
|
+
|
796
|
+
.vulnerability-stats {
|
797
|
+
margin-left: 0;
|
798
|
+
margin-top: 1rem;
|
799
|
+
width: 100%;
|
800
|
+
justify-content: space-around;
|
801
|
+
}
|
802
|
+
}
|
803
|
+
|
804
|
+
@media (max-width: 768px) {
|
805
|
+
.severity-column {
|
806
|
+
width: 12%;
|
807
|
+
text-align: center;
|
808
|
+
}
|
809
|
+
|
810
|
+
.severity-badge {
|
811
|
+
min-width: 60px;
|
812
|
+
padding: 0.25rem 0.4rem;
|
813
|
+
font-size: 0.75rem;
|
814
|
+
}
|
815
|
+
|
816
|
+
.gem-column {
|
817
|
+
width: 18%;
|
818
|
+
}
|
819
|
+
|
820
|
+
.version-column {
|
821
|
+
width: 12%;
|
822
|
+
}
|
823
|
+
|
824
|
+
.cve-column {
|
825
|
+
width: 15%;
|
826
|
+
}
|
827
|
+
|
828
|
+
.description-column {
|
829
|
+
width: 25%;
|
830
|
+
}
|
831
|
+
|
832
|
+
.remediation-column {
|
833
|
+
width: 15%;
|
834
|
+
}
|
835
|
+
|
836
|
+
.vulnerability-filters {
|
837
|
+
flex-direction: column;
|
838
|
+
align-items: flex-start;
|
839
|
+
gap: 0.75rem;
|
840
|
+
}
|
841
|
+
|
842
|
+
.search-input {
|
843
|
+
width: 100%;
|
844
|
+
}
|
845
|
+
}
|
846
|
+
|
847
|
+
/* Extra small devices */
|
848
|
+
@media (max-width: 576px) {
|
849
|
+
.table {
|
850
|
+
table-layout: auto;
|
851
|
+
}
|
852
|
+
|
853
|
+
.severity-column {
|
854
|
+
width: 15%;
|
855
|
+
}
|
856
|
+
|
857
|
+
.vulnerability-stats {
|
858
|
+
justify-content: space-between;
|
859
|
+
width: 100%;
|
860
|
+
}
|
861
|
+
|
862
|
+
.stat-item {
|
863
|
+
min-width: auto;
|
864
|
+
}
|
865
|
+
|
866
|
+
/* Ensure text doesn't overflow in vulnerability details */
|
867
|
+
.vulnerability-details .details-content {
|
868
|
+
max-width: 100%;
|
869
|
+
white-space: normal;
|
870
|
+
}
|
871
|
+
|
872
|
+
/* Make the action buttons more touchable on mobile */
|
873
|
+
.update-btn, .copy-btn {
|
874
|
+
padding: 0.5rem;
|
875
|
+
min-height: 44px; /* Better touch target */
|
876
|
+
}
|
877
|
+
|
878
|
+
/* Enhance remediation column display on small screens */
|
879
|
+
.remediation-column .patched-versions-pill {
|
880
|
+
flex-direction: column;
|
881
|
+
align-items: flex-start;
|
882
|
+
}
|
883
|
+
|
884
|
+
.remediation-column .versions-container {
|
885
|
+
width: 100%;
|
886
|
+
margin-bottom: 0.25rem;
|
887
|
+
}
|
888
|
+
|
889
|
+
.copy-btn {
|
890
|
+
align-self: flex-end;
|
891
|
+
}
|
892
|
+
}
|
893
|
+
</style>
|
894
|
+
|
895
|
+
<script>
|
896
|
+
document.addEventListener('DOMContentLoaded', function() {
|
897
|
+
// Filter buttons functionality
|
898
|
+
document.querySelectorAll('.filter-btn').forEach(function(button) {
|
899
|
+
button.addEventListener('click', function() {
|
900
|
+
// Update active state
|
901
|
+
document.querySelectorAll('.filter-btn').forEach(btn => btn.classList.remove('active'));
|
902
|
+
this.classList.add('active');
|
903
|
+
|
904
|
+
const filter = this.getAttribute('data-filter');
|
905
|
+
const rows = document.querySelectorAll('.vulnerability-row');
|
906
|
+
|
907
|
+
// Counts for updating donut chart
|
908
|
+
let visibleCritical = 0;
|
909
|
+
let visibleHigh = 0;
|
910
|
+
let visibleMedium = 0;
|
911
|
+
let visibleLow = 0;
|
912
|
+
let visibleUnknown = 0;
|
913
|
+
let visibleTotal = 0;
|
914
|
+
|
915
|
+
rows.forEach(function(row) {
|
916
|
+
const severity = row.getAttribute('data-severity');
|
917
|
+
|
918
|
+
if (filter === 'all') {
|
919
|
+
row.style.display = '';
|
920
|
+
|
921
|
+
// Count by severity for all visible rows
|
922
|
+
if (severity === 'critical') visibleCritical++;
|
923
|
+
else if (severity === 'high') visibleHigh++;
|
924
|
+
else if (severity === 'medium') visibleMedium++;
|
925
|
+
else if (severity === 'low') visibleLow++;
|
926
|
+
else visibleUnknown++;
|
927
|
+
|
928
|
+
visibleTotal++;
|
929
|
+
|
930
|
+
} else {
|
931
|
+
if (severity === filter) {
|
932
|
+
row.style.display = '';
|
933
|
+
|
934
|
+
// For filtered view, only count matching severity
|
935
|
+
if (severity === 'critical') visibleCritical++;
|
936
|
+
else if (severity === 'high') visibleHigh++;
|
937
|
+
else if (severity === 'medium') visibleMedium++;
|
938
|
+
else if (severity === 'low') visibleLow++;
|
939
|
+
else visibleUnknown++;
|
940
|
+
|
941
|
+
visibleTotal++;
|
942
|
+
} else {
|
943
|
+
row.style.display = 'none';
|
944
|
+
}
|
945
|
+
}
|
946
|
+
});
|
947
|
+
|
948
|
+
// Update donut chart percentages based on visible rows
|
949
|
+
const donut = document.querySelector('.donut');
|
950
|
+
if (donut) {
|
951
|
+
// Calculate percentages
|
952
|
+
const criticalPct = visibleTotal > 0 ? visibleCritical / visibleTotal : 0;
|
953
|
+
const highPct = visibleTotal > 0 ? visibleHigh / visibleTotal : 0;
|
954
|
+
const mediumPct = visibleTotal > 0 ? visibleMedium / visibleTotal : 0;
|
955
|
+
const lowPct = visibleTotal > 0 ? visibleLow / visibleTotal : 0;
|
956
|
+
const unknownPct = visibleTotal > 0 ? visibleUnknown / visibleTotal : 0;
|
957
|
+
|
958
|
+
// Update CSS variables
|
959
|
+
donut.style.setProperty('--critical', criticalPct);
|
960
|
+
donut.style.setProperty('--high', highPct);
|
961
|
+
donut.style.setProperty('--medium', mediumPct);
|
962
|
+
donut.style.setProperty('--low', lowPct);
|
963
|
+
donut.style.setProperty('--unknown', unknownPct);
|
964
|
+
|
965
|
+
// Update data attributes
|
966
|
+
donut.setAttribute('data-count', visibleTotal);
|
967
|
+
donut.setAttribute('data-critical', visibleCritical);
|
968
|
+
donut.setAttribute('data-high', visibleHigh);
|
969
|
+
donut.setAttribute('data-medium', visibleMedium);
|
970
|
+
donut.setAttribute('data-low', visibleLow);
|
971
|
+
donut.setAttribute('data-unknown', visibleUnknown);
|
972
|
+
}
|
973
|
+
|
974
|
+
// Update stats counters
|
975
|
+
const critHighCounter = document.querySelector('.vulnerability-stats .stat-value:nth-child(1)');
|
976
|
+
const mediumCounter = document.querySelector('.vulnerability-stats .stat-item:nth-child(2) .stat-value');
|
977
|
+
const lowCounter = document.querySelector('.vulnerability-stats .stat-item:nth-child(3) .stat-value');
|
978
|
+
|
979
|
+
if (critHighCounter) {
|
980
|
+
critHighCounter.textContent = visibleCritical + visibleHigh;
|
981
|
+
}
|
982
|
+
if (mediumCounter) {
|
983
|
+
mediumCounter.textContent = visibleMedium;
|
984
|
+
}
|
985
|
+
if (lowCounter) {
|
986
|
+
lowCounter.textContent = visibleLow;
|
987
|
+
}
|
988
|
+
});
|
989
|
+
});
|
990
|
+
|
991
|
+
// Search functionality
|
992
|
+
const searchInput = document.getElementById('vulnerability-search');
|
993
|
+
if (searchInput) {
|
994
|
+
searchInput.addEventListener('input', function() {
|
995
|
+
const searchTerm = this.value.toLowerCase();
|
996
|
+
const rows = document.querySelectorAll('.vulnerability-row');
|
997
|
+
|
998
|
+
rows.forEach(function(row) {
|
999
|
+
const text = row.textContent.toLowerCase();
|
1000
|
+
const gemName = row.getAttribute('data-gem').toLowerCase();
|
1001
|
+
|
1002
|
+
if (text.includes(searchTerm) || gemName.includes(searchTerm)) {
|
1003
|
+
row.style.display = '';
|
1004
|
+
} else {
|
1005
|
+
row.style.display = 'none';
|
1006
|
+
}
|
1007
|
+
});
|
1008
|
+
});
|
1009
|
+
}
|
1010
|
+
|
1011
|
+
// Scroll to vulnerability details when clicking "More details"
|
1012
|
+
document.querySelectorAll('.scroll-to-details').forEach(function(link) {
|
1013
|
+
link.addEventListener('click', function(e) {
|
1014
|
+
e.preventDefault();
|
1015
|
+
const targetId = this.getAttribute('href');
|
1016
|
+
const detailsSection = document.querySelector('.vulnerabilities-details-section');
|
1017
|
+
|
1018
|
+
// Ensure the details section is visible
|
1019
|
+
if (detailsSection.classList.contains('hidden')) {
|
1020
|
+
detailsSection.classList.remove('hidden');
|
1021
|
+
const toggleBtn = document.querySelector('.toggle-details-btn');
|
1022
|
+
if (toggleBtn) {
|
1023
|
+
toggleBtn.textContent = 'Hide Details';
|
1024
|
+
toggleBtn.setAttribute('aria-expanded', 'true');
|
1025
|
+
}
|
1026
|
+
}
|
1027
|
+
|
1028
|
+
// Scroll to the specific vulnerability detail
|
1029
|
+
setTimeout(() => {
|
1030
|
+
const targetElement = document.querySelector(targetId);
|
1031
|
+
if (targetElement) {
|
1032
|
+
const headerHeight = 60; // Approximate height of sticky header
|
1033
|
+
const targetPosition = targetElement.getBoundingClientRect().top + window.pageYOffset - headerHeight - 20;
|
1034
|
+
window.scrollTo({
|
1035
|
+
top: targetPosition,
|
1036
|
+
behavior: 'smooth'
|
1037
|
+
});
|
1038
|
+
|
1039
|
+
// Highlight the target element briefly
|
1040
|
+
targetElement.classList.add('highlight-detail');
|
1041
|
+
setTimeout(() => {
|
1042
|
+
targetElement.classList.remove('highlight-detail');
|
1043
|
+
}, 1500);
|
1044
|
+
}
|
1045
|
+
}, 100);
|
1046
|
+
});
|
1047
|
+
});
|
1048
|
+
|
1049
|
+
// Tooltip functionality for patched versions
|
1050
|
+
document.querySelectorAll('.more-versions').forEach(function(element) {
|
1051
|
+
element.addEventListener('mouseenter', function() {
|
1052
|
+
// Could implement custom tooltip here if needed
|
1053
|
+
});
|
1054
|
+
});
|
1055
|
+
|
1056
|
+
// Copy button functionality
|
1057
|
+
document.querySelectorAll('.copy-btn').forEach(function(button) {
|
1058
|
+
button.addEventListener('click', function() {
|
1059
|
+
const solution = this.getAttribute('data-solution');
|
1060
|
+
|
1061
|
+
// Create a temporary input element
|
1062
|
+
const tempInput = document.createElement('input');
|
1063
|
+
tempInput.value = solution;
|
1064
|
+
document.body.appendChild(tempInput);
|
1065
|
+
tempInput.select();
|
1066
|
+
document.execCommand('copy');
|
1067
|
+
document.body.removeChild(tempInput);
|
1068
|
+
|
1069
|
+
// Show feedback
|
1070
|
+
const originalIcon = this.innerHTML;
|
1071
|
+
this.innerHTML = '<span>✓</span>';
|
1072
|
+
setTimeout(() => {
|
1073
|
+
this.innerHTML = originalIcon;
|
1074
|
+
|
1075
|
+
// Show toast notification
|
1076
|
+
if (typeof showNotification === 'function') {
|
1077
|
+
showNotification('Copied to clipboard!', 'success');
|
1078
|
+
}
|
1079
|
+
}, 1500);
|
1080
|
+
});
|
1081
|
+
});
|
1082
|
+
|
1083
|
+
// No select-all checkbox or update buttons anymore
|
1084
|
+
|
1085
|
+
// Ensure table fits within viewport
|
1086
|
+
function adjustTableResponsiveness() {
|
1087
|
+
const table = document.querySelector('.vulnerabilities-table table');
|
1088
|
+
const viewportWidth = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);
|
1089
|
+
|
1090
|
+
// If viewport is very small, change display approach
|
1091
|
+
if (viewportWidth < 576) {
|
1092
|
+
document.querySelectorAll('.vulnerability-row').forEach(row => {
|
1093
|
+
// Add more bottom padding for mobile view
|
1094
|
+
row.style.paddingBottom = '10px';
|
1095
|
+
});
|
1096
|
+
|
1097
|
+
// Ensure description column takes necessary space
|
1098
|
+
const descriptionCells = document.querySelectorAll('.description-column');
|
1099
|
+
descriptionCells.forEach(cell => {
|
1100
|
+
cell.style.maxWidth = '100%';
|
1101
|
+
cell.style.width = 'auto';
|
1102
|
+
});
|
1103
|
+
|
1104
|
+
// Make severity column more compact
|
1105
|
+
const severityCells = document.querySelectorAll('.severity-column');
|
1106
|
+
severityCells.forEach(cell => {
|
1107
|
+
const badge = cell.querySelector('.severity-badge');
|
1108
|
+
if (badge) {
|
1109
|
+
badge.style.padding = '0.2rem 0.4rem';
|
1110
|
+
badge.style.minWidth = 'auto';
|
1111
|
+
}
|
1112
|
+
});
|
1113
|
+
}
|
1114
|
+
}
|
1115
|
+
|
1116
|
+
// Run on page load and on resize
|
1117
|
+
adjustTableResponsiveness();
|
1118
|
+
window.addEventListener('resize', adjustTableResponsiveness);
|
1119
|
+
});
|
1120
|
+
</script>
|