foreman_cve_scanner 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +101 -3
  3. data/app/controllers/api/v2/cve_scans_controller.rb +170 -1
  4. data/app/models/foreman_cve_scanner/cve_scan.rb +15 -2
  5. data/app/models/host_status/cve_status.rb +3 -1
  6. data/app/services/concerns/foreman_cve_scanner/profiles_uploader.rb +25 -0
  7. data/app/services/foreman_cve_scanner/cve_report_scanner.rb +3 -6
  8. data/app/services/foreman_cve_scanner/scan_cleanup.rb +34 -0
  9. data/app/services/foreman_cve_scanner/scan_comparison.rb +145 -0
  10. data/app/services/foreman_cve_scanner/scan_importer.rb +17 -15
  11. data/app/views/api/v2/cve_scans/base.json.rabl +1 -1
  12. data/app/views/api/v2/cve_scans/main.json.rabl +1 -1
  13. data/app/views/foreman_cve_scanner/job_templates/install_cve_scanners.erb +3 -3
  14. data/app/views/foreman_cve_scanner/job_templates/run_cve_scanner.erb +5 -3
  15. data/config/routes.rb +6 -1
  16. data/db/migrate/20260514080000_add_source_and_scanned_at_to_foreman_cve_scanner_cve_scans.rb +26 -0
  17. data/lib/foreman_cve_scanner/engine.rb +68 -17
  18. data/lib/foreman_cve_scanner/template_helpers.rb +28 -0
  19. data/lib/foreman_cve_scanner/version.rb +1 -1
  20. data/lib/tasks/foreman_cve_scanner_tasks.rake +11 -0
  21. data/package.json +1 -1
  22. data/test/controllers/api/v2/cve_scans_controller_test.rb +260 -5
  23. data/test/lib/foreman_cve_scanner/profiles_uploader_test.rb +84 -0
  24. data/test/lib/foreman_cve_scanner/template_helpers_test.rb +29 -0
  25. data/test/models/foreman_cve_scanner/cve_scan_test.rb +120 -0
  26. data/test/models/host_status/cve_status_test.rb +12 -3
  27. data/test/services/foreman_cve_scanner/scan_cleanup_test.rb +69 -0
  28. data/test/services/foreman_cve_scanner/scan_comparison_test.rb +84 -0
  29. data/test/services/foreman_cve_scanner/scan_importer_test.rb +68 -5
  30. data/webpack/components/CveCompareModal.js +298 -0
  31. data/webpack/components/CveDetailsCard.js +141 -121
  32. data/webpack/components/CveFindingsModal.js +131 -111
  33. data/webpack/components/CveScansReports.js +227 -0
  34. data/webpack/components/CveScansTab.js +122 -119
  35. data/webpack/components/CveTrendChart.js +264 -0
  36. data/webpack/components/__tests__/CveCompareModal.test.js +104 -0
  37. data/webpack/components/__tests__/CveDetailsCard.test.js +106 -20
  38. data/webpack/components/__tests__/CveFindingsModal.test.js +54 -2
  39. data/webpack/components/__tests__/CveScansTab.test.js +185 -5
  40. data/webpack/components/__tests__/CveTrendChart.test.js +122 -0
  41. data/webpack/components/__tests__/cve_helpers.test.js +18 -0
  42. data/webpack/components/cve_helpers.js +139 -0
  43. data/webpack/components/cve_scans.scss +464 -9
  44. data/webpack/components/useModalScan.js +26 -0
  45. metadata +24 -3
  46. data/webpack/components/CveHistoryTable.js +0 -58
  47. data/webpack/components/CveOverviewCard.js +0 -67
@@ -7,6 +7,47 @@ const SEVERITY_RANK = {
7
7
  MEDIUM: 2,
8
8
  LOW: 1,
9
9
  };
10
+ const COMPARE_STATUS_RANK = {
11
+ new: 4,
12
+ resolved: 3,
13
+ updated: 2,
14
+ unchanged: 1,
15
+ };
16
+ const COMPARISON_DIFF_LABELS = {
17
+ severity: __('Severity'),
18
+ version: __('Version'),
19
+ fixed: __('Fixed'),
20
+ scan_status: __('Scan status'),
21
+ title: __('Title'),
22
+ published: __('Published'),
23
+ url: __('URL'),
24
+ };
25
+
26
+ export const comparisonFilters = [
27
+ { key: 'all', label: __('All') },
28
+ { key: 'new', label: __('New') },
29
+ { key: 'resolved', label: __('Resolved') },
30
+ { key: 'updated', label: __('Updated') },
31
+ { key: 'unchanged', label: __('Unchanged') },
32
+ ];
33
+ export const comparisonColumns = [
34
+ { key: 'status', label: __('Status') },
35
+ { key: 'id', label: __('CVE') },
36
+ { key: 'name', label: __('Package') },
37
+ { key: 'severity', label: __('Severity') },
38
+ { key: 'version', label: __('Version') },
39
+ { key: 'fixed', label: __('Fixed') },
40
+ { key: 'scan_status', label: __('Scan status') },
41
+ { key: 'diff', label: __('Diff') },
42
+ { key: 'published', label: __('Published') },
43
+ { key: 'title', label: __('Title') },
44
+ ];
45
+ export const comparisonStatusLabels = {
46
+ new: __('New'),
47
+ resolved: __('Resolved'),
48
+ updated: __('Updated'),
49
+ unchanged: __('Unchanged'),
50
+ };
10
51
 
11
52
  export const severityRank = sev =>
12
53
  SEVERITY_RANK[(sev || '').toUpperCase()] || 0;
@@ -31,5 +72,103 @@ export const formatDateTime = value => {
31
72
  export const compareStrings = (a, b) =>
32
73
  (a || '').toString().localeCompare((b || '').toString());
33
74
 
75
+ export const normalizeSearchInputValue = (value, event) => {
76
+ if (typeof value === 'string') return value;
77
+ if (typeof event === 'string') return event;
78
+ return value?.target?.value || event?.target?.value || '';
79
+ };
80
+
81
+ export const findingMatchesSearch = (finding, query) =>
82
+ [
83
+ finding.id,
84
+ finding.name,
85
+ finding.version,
86
+ finding.fixed,
87
+ finding.status,
88
+ finding.title,
89
+ finding.url,
90
+ finding.severity,
91
+ ].some(value =>
92
+ (value || '')
93
+ .toString()
94
+ .toLowerCase()
95
+ .includes(query)
96
+ );
97
+
98
+ export const findingSorters = {
99
+ published: (a, b) => new Date(a.published || 0) - new Date(b.published || 0),
100
+ name: (a, b) => compareStrings(a.name, b.name),
101
+ version: (a, b) => compareStrings(a.version, b.version),
102
+ fixed: (a, b) => compareStrings(a.fixed, b.fixed),
103
+ id: (a, b) => compareStrings(a.id, b.id),
104
+ status: (a, b) => compareStrings(a.status, b.status),
105
+ title: (a, b) => compareStrings(a.title, b.title),
106
+ severity: (a, b) => severityRank(a.severity) - severityRank(b.severity),
107
+ };
108
+
109
+ export const comparisonStatusRank = status => COMPARE_STATUS_RANK[status] || 0;
110
+
111
+ export const comparisonDiffEntries = diff =>
112
+ Object.entries(diff || {}).map(([field, values]) => ({
113
+ field,
114
+ label: COMPARISON_DIFF_LABELS[field] || field,
115
+ oldValue: values.old || '-',
116
+ newValue: values.new || '-',
117
+ }));
118
+
119
+ export const formatComparisonDiff = diff =>
120
+ comparisonDiffEntries(diff).map(
121
+ entry => `${entry.label}: ${entry.oldValue} -> ${entry.newValue}`
122
+ );
123
+
124
+ export const comparisonMatchesSearch = (row, query) =>
125
+ [
126
+ row.id,
127
+ row.name,
128
+ row.title,
129
+ row.published,
130
+ row.severity,
131
+ row.version,
132
+ row.fixed,
133
+ row.scan_status,
134
+ row.status,
135
+ ...formatComparisonDiff(row.diff),
136
+ ].some(value =>
137
+ (value || '')
138
+ .toString()
139
+ .toLowerCase()
140
+ .includes(query)
141
+ );
142
+
143
+ export const comparisonSorters = {
144
+ status: (a, b) =>
145
+ comparisonStatusRank(a.status) - comparisonStatusRank(b.status),
146
+ id: (a, b) => compareStrings(a.id, b.id),
147
+ name: (a, b) => compareStrings(a.name, b.name),
148
+ published: (a, b) => new Date(a.published || 0) - new Date(b.published || 0),
149
+ severity: (a, b) => severityRank(a.severity) - severityRank(b.severity),
150
+ version: (a, b) => compareStrings(a.version, b.version),
151
+ fixed: (a, b) => compareStrings(a.fixed, b.fixed),
152
+ scan_status: (a, b) => compareStrings(a.scan_status, b.scan_status),
153
+ diff: (a, b) =>
154
+ compareStrings(
155
+ formatComparisonDiff(a.diff).join(' '),
156
+ formatComparisonDiff(b.diff).join(' ')
157
+ ),
158
+ title: (a, b) => compareStrings(a.title, b.title),
159
+ };
160
+
34
161
  export const noReportsTitle = () => __('No CVE reports for this host');
35
162
  export const noReportsBody = () => __('Run a CVE scan to see results here.');
163
+ export const noFindingsTitle = () => __('No CVEs found');
164
+ export const noFindingsBody = () =>
165
+ __('The latest CVE scan found no vulnerabilities for this host.');
166
+ export const visibleScanSource = source =>
167
+ (source || '').toLowerCase() === 'rex' ? '' : source || '';
168
+ export const formatScanOrigin = (scanner, source, fallback = __('Unknown')) => {
169
+ const scannerLabel = scanner || fallback;
170
+ const visibleSource = visibleScanSource(source);
171
+ return visibleSource ? `${scannerLabel} / ${visibleSource}` : scannerLabel;
172
+ };
173
+ export const formatScannedAt = value =>
174
+ formatDateTime(value) || __('Unknown time');
@@ -17,7 +17,10 @@
17
17
  display: inline-flex;
18
18
  }
19
19
 
20
- .cve-severity--critical,
20
+ .cve-severity--critical {
21
+ color: var(--pf-v5-global--palette--red-200);
22
+ }
23
+
21
24
  .cve-severity--high {
22
25
  color: var(--pf-v5-global--danger-color--100);
23
26
  }
@@ -43,6 +46,30 @@
43
46
  margin: 0;
44
47
  }
45
48
 
49
+ .cve-overview {
50
+ background: var(--pf-v5-global--BackgroundColor--200);
51
+ border: 1px solid var(--pf-v5-global--BorderColor--100);
52
+ border-radius: 8px;
53
+ margin-bottom: 1rem;
54
+ padding: 1rem;
55
+ }
56
+
57
+ .cve-overview-meta {
58
+ row-gap: 0.75rem;
59
+ }
60
+
61
+ .cve-overview-total {
62
+ align-items: center;
63
+ display: inline-flex;
64
+ flex-wrap: wrap;
65
+ gap: 0.5rem;
66
+ }
67
+
68
+ .cve-overview-source {
69
+ color: var(--pf-v5-global--Color--200);
70
+ font-weight: 500;
71
+ }
72
+
46
73
  button.cve-summary {
47
74
  background: none;
48
75
  border: 0;
@@ -52,8 +79,18 @@ button.cve-summary {
52
79
 
53
80
  .cve-count {
54
81
  align-items: center;
82
+ display: flex;
83
+ gap: 0.5rem;
84
+ justify-content: space-between;
85
+ width: 100%;
86
+ }
87
+
88
+ .cve-count-label {
89
+ align-items: center;
90
+ color: var(--pf-v5-global--Color--100);
55
91
  display: inline-flex;
56
- gap: 0.35rem;
92
+ gap: 0.4rem;
93
+ text-transform: capitalize;
57
94
  }
58
95
 
59
96
  .cve-bubble {
@@ -66,6 +103,26 @@ button.cve-summary {
66
103
  padding: 0 0.5rem;
67
104
  }
68
105
 
106
+ .cve-bubble--critical {
107
+ background: var(--pf-v5-global--palette--red-200);
108
+ color: var(--pf-v5-global--BackgroundColor--100);
109
+ }
110
+
111
+ .cve-bubble--high {
112
+ background: var(--pf-v5-global--danger-color--100);
113
+ color: var(--pf-v5-global--BackgroundColor--100);
114
+ }
115
+
116
+ .cve-bubble--medium {
117
+ background: var(--pf-v5-global--warning-color--100);
118
+ color: var(--pf-v5-global--BackgroundColor--100);
119
+ }
120
+
121
+ .cve-bubble--low {
122
+ background: var(--pf-v5-global--palette--blue-400);
123
+ color: var(--pf-v5-global--BackgroundColor--100);
124
+ }
125
+
69
126
  .cve-total-bubble {
70
127
  align-items: center;
71
128
  border-radius: 1em;
@@ -96,15 +153,46 @@ button.cve-summary {
96
153
 
97
154
  .cve-counts {
98
155
  display: grid;
99
- gap: 0.35rem 1rem;
100
- grid-template-columns: auto auto;
101
- margin-top: 0.25rem;
156
+ gap: 0.75rem;
157
+ grid-template-columns: repeat(2, minmax(0, 1fr));
158
+ margin-top: 1rem;
102
159
  }
103
160
 
104
161
  .cve-counts--compact {
105
- column-gap: 0.5rem;
106
- row-gap: 0.25rem;
107
- margin-top: 0.15rem;
162
+ column-gap: 0.75rem;
163
+ row-gap: 0.75rem;
164
+ margin-top: 1rem;
165
+ }
166
+
167
+ .cve-count-card {
168
+ background: var(--pf-v5-global--BackgroundColor--100);
169
+ border: 1px solid var(--pf-v5-global--BorderColor--100);
170
+ border-radius: 8px;
171
+ border-left-width: 4px;
172
+ padding: 0.65rem 0.8rem;
173
+ text-align: left;
174
+ width: 100%;
175
+ }
176
+
177
+ .cve-count-card:hover {
178
+ border-color: var(--pf-v5-global--primary-color--100);
179
+ box-shadow: var(--pf-v5-global--BoxShadow--sm);
180
+ }
181
+
182
+ .cve-count-card--critical {
183
+ border-left-color: var(--pf-v5-global--palette--red-200);
184
+ }
185
+
186
+ .cve-count-card--high {
187
+ border-left-color: var(--pf-v5-global--danger-color--100);
188
+ }
189
+
190
+ .cve-count-card--medium {
191
+ border-left-color: var(--pf-v5-global--warning-color--100);
192
+ }
193
+
194
+ .cve-count-card--low {
195
+ border-left-color: var(--pf-v5-global--palette--blue-400);
108
196
  }
109
197
 
110
198
  .cve-filter-buttons {
@@ -135,16 +223,383 @@ button.cve-summary {
135
223
  padding: 0.25rem 0.75rem;
136
224
  }
137
225
 
226
+ .cve-scans-tab {
227
+ padding: 0 1rem 1rem;
228
+ }
229
+
230
+ .cve-scans-subtabs {
231
+ margin-top: 0.5rem;
232
+ }
233
+
234
+ .cve-scans-section {
235
+ background: var(--pf-v5-global--BackgroundColor--100);
236
+ border: 1px solid var(--pf-v5-global--BorderColor--100);
237
+ border-radius: 8px;
238
+ margin-top: 1rem;
239
+ padding: 1rem;
240
+ }
241
+
242
+ .cve-scans-section-header {
243
+ align-items: center;
244
+ display: flex;
245
+ flex-wrap: wrap;
246
+ gap: 1rem;
247
+ justify-content: space-between;
248
+ margin-bottom: 1rem;
249
+ }
250
+
251
+ .cve-scans-section-title {
252
+ min-width: 0;
253
+ }
254
+
255
+ .cve-trend {
256
+ margin-bottom: 0;
257
+ }
258
+
259
+ .cve-trend-summary {
260
+ display: grid;
261
+ gap: 0.75rem;
262
+ grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
263
+ margin-bottom: 1rem;
264
+ }
265
+
266
+ .cve-trend-card {
267
+ background: var(--pf-v5-global--BackgroundColor--200);
268
+ border: 1px solid var(--pf-v5-global--BorderColor--100);
269
+ border-radius: 8px;
270
+ display: flex;
271
+ flex-direction: column;
272
+ gap: 0.35rem;
273
+ padding: 0.85rem 1rem;
274
+ }
275
+
276
+ .cve-trend-card-label {
277
+ color: var(--pf-v5-global--Color--200);
278
+ font-size: 0.85rem;
279
+ }
280
+
281
+ .cve-trend-card-label--help {
282
+ border-bottom: 1px dotted var(--pf-v5-global--BorderColor--100);
283
+ cursor: help;
284
+ }
285
+
286
+ .cve-trend-card-value {
287
+ font-size: 1.4rem;
288
+ font-weight: 700;
289
+ }
290
+
291
+ .cve-trend-card-value.is-up {
292
+ color: var(--pf-v5-global--danger-color--100);
293
+ }
294
+
295
+ .cve-trend-card-value.is-down {
296
+ color: var(--pf-v5-global--success-color--100);
297
+ }
298
+
299
+ .cve-trend-card-value.is-flat {
300
+ color: var(--pf-v5-global--Color--100);
301
+ }
302
+
303
+ .cve-trend-card-value--small {
304
+ font-size: 1rem;
305
+ }
306
+
307
+ .cve-trend-legend {
308
+ display: flex;
309
+ flex-wrap: wrap;
310
+ gap: 0.75rem 1rem;
311
+ margin-bottom: 0.75rem;
312
+ }
313
+
314
+ .cve-trend-controls {
315
+ display: flex;
316
+ flex-wrap: wrap;
317
+ justify-content: flex-end;
318
+ margin-bottom: 0.75rem;
319
+ }
320
+
321
+ .cve-compare-diff-entry {
322
+ display: grid;
323
+ gap: 0.15rem;
324
+ }
325
+
326
+ .cve-compare-diff-entry + .cve-compare-diff-entry {
327
+ margin-top: 0.35rem;
328
+ }
329
+
330
+ .cve-compare-diff-label {
331
+ color: var(--pf-v5-global--Color--200);
332
+ font-size: 0.85rem;
333
+ font-weight: 600;
334
+ }
335
+
336
+ .cve-compare-diff-values {
337
+ align-items: center;
338
+ display: inline-flex;
339
+ flex-wrap: wrap;
340
+ gap: 0.35rem;
341
+ }
342
+
343
+ .cve-compare-diff-old,
344
+ .cve-compare-diff-new {
345
+ background: var(--pf-v5-global--BackgroundColor--200);
346
+ border-radius: 4px;
347
+ display: inline-flex;
348
+ line-height: 1.3;
349
+ padding: 0.1rem 0.35rem;
350
+ }
351
+
352
+ .cve-compare-diff-new {
353
+ font-weight: 600;
354
+ }
355
+
356
+ .cve-compare-diff-arrow {
357
+ color: var(--pf-v5-global--Color--200);
358
+ font-size: 0.85rem;
359
+ }
360
+
361
+ .cve-trend-actions {
362
+ align-items: center;
363
+ display: flex;
364
+ flex-wrap: wrap;
365
+ gap: 0.75rem;
366
+ }
367
+
368
+ .cve-trend-compare-state {
369
+ color: var(--pf-v5-global--Color--200);
370
+ font-size: 0.9rem;
371
+ font-weight: 500;
372
+ }
373
+
374
+ .cve-trend-legend-item {
375
+ align-items: center;
376
+ color: var(--pf-v5-global--Color--200);
377
+ display: inline-flex;
378
+ font-size: 0.85rem;
379
+ gap: 0.4rem;
380
+ }
381
+
382
+ .cve-trend-legend-swatch {
383
+ border-radius: 999px;
384
+ display: inline-block;
385
+ height: 0.7rem;
386
+ width: 0.7rem;
387
+ }
388
+
389
+ .cve-trend-bars {
390
+ align-items: end;
391
+ background: var(--pf-v5-global--BackgroundColor--200);
392
+ border: 1px solid var(--pf-v5-global--BorderColor--100);
393
+ border-radius: 8px;
394
+ display: grid;
395
+ gap: 0.75rem;
396
+ grid-template-columns: repeat(auto-fit, minmax(3.5rem, 1fr));
397
+ padding: 1rem;
398
+ }
399
+
400
+ .cve-trend-bar-item {
401
+ align-items: center;
402
+ display: flex;
403
+ flex-direction: column;
404
+ gap: 0.35rem;
405
+ }
406
+
407
+ .cve-trend-bar-button {
408
+ align-items: center;
409
+ background: none;
410
+ border: 0;
411
+ cursor: pointer;
412
+ display: flex;
413
+ flex-direction: column;
414
+ gap: 0.25rem;
415
+ padding: 0;
416
+ width: 100%;
417
+ }
418
+
419
+ .cve-trend-bar-button:hover .cve-trend-bar-frame,
420
+ .cve-trend-bar-button:focus .cve-trend-bar-frame,
421
+ .cve-trend-bar-button:focus-visible .cve-trend-bar-frame {
422
+ border-color: var(--pf-v5-global--primary-color--100);
423
+ box-shadow: 0 0 0 1px var(--pf-v5-global--primary-color--100);
424
+ }
425
+
426
+ .cve-trend-bar-button.is-selected .cve-trend-bar-frame {
427
+ border-color: var(--pf-v5-global--primary-color--100);
428
+ box-shadow: 0 0 0 2px var(--pf-v5-global--primary-color--100);
429
+ }
430
+
431
+ .cve-trend-bar-button.is-selected .cve-trend-bar-total {
432
+ color: var(--pf-v5-global--primary-color--100);
433
+ }
434
+
435
+ .cve-trend-bar-frame {
436
+ align-items: end;
437
+ background: var(--pf-v5-global--BackgroundColor--100);
438
+ border: 1px solid var(--pf-v5-global--BorderColor--100);
439
+ border-radius: 6px;
440
+ display: flex;
441
+ flex-direction: column-reverse;
442
+ height: 11rem;
443
+ justify-content: flex-start;
444
+ overflow: hidden;
445
+ width: 100%;
446
+ }
447
+
448
+ .cve-trend-bar-frame--clean {
449
+ background: #edf8ef;
450
+ }
451
+
452
+ .cve-trend-segment {
453
+ display: block;
454
+ min-height: 0;
455
+ width: 100%;
456
+ }
457
+
458
+ .cve-trend-segment--critical {
459
+ background: var(--pf-v5-global--palette--red-200);
460
+ }
461
+
462
+ .cve-trend-segment--high {
463
+ background: var(--pf-v5-global--danger-color--100);
464
+ }
465
+
466
+ .cve-trend-segment--medium {
467
+ background: var(--pf-v5-global--warning-color--100);
468
+ }
469
+
470
+ .cve-trend-segment--low {
471
+ background: var(--pf-v5-global--palette--blue-400);
472
+ }
473
+
474
+ .cve-trend-bar-total {
475
+ font-size: 0.9rem;
476
+ font-weight: 700;
477
+ }
478
+
479
+ .cve-trend-bar-label {
480
+ color: var(--pf-v5-global--Color--200);
481
+ font-size: 0.75rem;
482
+ }
483
+
484
+ .cve-trend-tooltip {
485
+ text-align: left;
486
+ }
487
+
488
+ .cve-trend-tooltip-line + .cve-trend-tooltip-line {
489
+ margin-top: 0.15rem;
490
+ }
491
+
492
+
493
+ .cve-scans-toolbar {
494
+ align-items: center;
495
+ display: flex;
496
+ flex-wrap: wrap;
497
+ gap: 1rem;
498
+ justify-content: space-between;
499
+ margin-bottom: 1rem;
500
+ }
501
+
502
+ .cve-scans-actions {
503
+ align-items: center;
504
+ display: flex;
505
+ flex-wrap: wrap;
506
+ gap: 0.75rem;
507
+ }
508
+
509
+ .cve-scans-selection {
510
+ color: var(--pf-v5-global--Color--200);
511
+ font-size: 0.9rem;
512
+ font-weight: 500;
513
+ }
514
+
138
515
  .cve-modal-body {
516
+ display: flex;
517
+ flex-direction: column;
518
+ height: min(60vh, 34rem);
139
519
  position: relative;
140
- min-height: 420px;
520
+ min-height: 26rem;
141
521
  }
142
522
 
143
523
  .cve-modal-filters {
524
+ flex: 0 0 auto;
525
+ align-items: flex-end;
526
+ display: flex;
527
+ flex-wrap: wrap;
528
+ gap: 1rem;
529
+ justify-content: space-between;
530
+ margin-bottom: 1rem;
144
531
  position: relative;
145
532
  z-index: 1;
146
533
  }
147
534
 
535
+ .cve-modal-search {
536
+ flex: 1 1 18rem;
537
+ margin-bottom: 0;
538
+ }
539
+
540
+ .cve-modal-quick-filters {
541
+ flex: 0 1 auto;
542
+ margin-bottom: 0;
543
+ }
544
+
545
+ .cve-modal-quick-filters .cve-filter-buttons {
546
+ justify-content: flex-end;
547
+ }
548
+
549
+ .cve-modal-results {
550
+ flex: 1 1 auto;
551
+ min-height: 0;
552
+ overflow: auto;
553
+ }
554
+
555
+ .cve-compare-body {
556
+ height: min(68vh, 42rem);
557
+ }
558
+
559
+ .cve-compare-summary {
560
+ flex: 0 0 auto;
561
+ margin-bottom: 1rem;
562
+ }
563
+
564
+ .cve-compare-meta {
565
+ display: grid;
566
+ gap: 0.35rem;
567
+ margin-bottom: 0.75rem;
568
+ }
569
+
570
+ .cve-compare-cards {
571
+ display: grid;
572
+ gap: 0.75rem;
573
+ grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
574
+ }
575
+
576
+ .cve-compare-card {
577
+ background: var(--pf-v5-global--BackgroundColor--200);
578
+ border: 1px solid var(--pf-v5-global--BorderColor--100);
579
+ border-radius: 8px;
580
+ cursor: pointer;
581
+ display: flex;
582
+ flex-direction: column;
583
+ gap: 0.35rem;
584
+ padding: 0.75rem;
585
+ text-align: left;
586
+ }
587
+
588
+ .cve-compare-card.is-active {
589
+ border-color: var(--pf-v5-global--primary-color--100);
590
+ box-shadow: 0 0 0 2px var(--pf-v5-global--primary-color--100) inset;
591
+ }
592
+
593
+ .cve-compare-card-label {
594
+ color: var(--pf-v5-global--Color--200);
595
+ font-size: 0.85rem;
596
+ }
597
+
598
+ .cve-compare-card-value {
599
+ font-size: 1.4rem;
600
+ font-weight: 700;
601
+ }
602
+
148
603
  .cve-more {
149
604
  margin-top: 0.5rem;
150
605
  }
@@ -0,0 +1,26 @@
1
+ /* eslint-disable import/no-unresolved */
2
+ import { useState } from 'react';
3
+
4
+ const useModalScan = () => {
5
+ const [isOpen, setIsOpen] = useState(false);
6
+ const [scanId, setScanId] = useState(null);
7
+ const [filter, setFilter] = useState('all');
8
+
9
+ const openModal = (nextScanId, nextFilter) => {
10
+ setScanId(nextScanId);
11
+ setFilter(nextFilter || 'all');
12
+ setIsOpen(true);
13
+ };
14
+
15
+ const closeModal = () => setIsOpen(false);
16
+
17
+ return {
18
+ isOpen,
19
+ scanId,
20
+ filter,
21
+ openModal,
22
+ closeModal,
23
+ };
24
+ };
25
+
26
+ export default useModalScan;