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
@@ -0,0 +1,298 @@
1
+ /* eslint-disable import/no-unresolved */
2
+ /* eslint-disable camelcase */
3
+ import React, { useEffect, useMemo, useState } from 'react';
4
+ import PropTypes from 'prop-types';
5
+ import {
6
+ Modal,
7
+ Button,
8
+ Text,
9
+ TextVariants,
10
+ SearchInput,
11
+ } from '@patternfly/react-core';
12
+ import {
13
+ Table,
14
+ Tbody,
15
+ Tr,
16
+ Th,
17
+ Thead,
18
+ Td,
19
+ SortByDirection,
20
+ } from '@patternfly/react-table';
21
+ import { useAPI } from 'foremanReact/common/hooks/API/APIHooks';
22
+ import { foremanUrl } from 'foremanReact/common/helpers';
23
+ import { translate as __ } from 'foremanReact/common/I18n';
24
+ import { STATUS } from 'foremanReact/constants';
25
+ import {
26
+ comparisonDiffEntries,
27
+ comparisonColumns,
28
+ comparisonFilters,
29
+ comparisonMatchesSearch,
30
+ comparisonSorters,
31
+ comparisonStatusLabels,
32
+ formatDateTime,
33
+ formatScanOrigin,
34
+ formatScannedAt,
35
+ normalizeSearchInputValue,
36
+ } from './cve_helpers';
37
+ import './cve_scans.scss';
38
+
39
+ const CveCompareModal = ({ hostId, isOpen, onClose, scanIds }) => {
40
+ const [filter, setFilter] = useState('all');
41
+ const [search, setSearch] = useState('');
42
+ const [sortBy, setSortBy] = useState({
43
+ direction: SortByDirection.desc,
44
+ column: 'status',
45
+ });
46
+ const previousScanId = scanIds[0];
47
+ const currentScanId = scanIds[1];
48
+ const compareUrl =
49
+ isOpen && previousScanId && currentScanId
50
+ ? foremanUrl(
51
+ `/api/v2/hosts/${hostId}/cve_scans/compare?first_id=${previousScanId}&second_id=${currentScanId}`
52
+ )
53
+ : null;
54
+ const compareRequest = useAPI(isOpen ? 'get' : null, compareUrl, {
55
+ key: `CVE_COMPARE_${previousScanId}_${currentScanId}`,
56
+ });
57
+ useEffect(() => {
58
+ if (!isOpen) return;
59
+ setFilter('all');
60
+ setSearch('');
61
+ setSortBy({
62
+ direction: SortByDirection.desc,
63
+ column: 'status',
64
+ });
65
+ }, [currentScanId, isOpen, previousScanId]);
66
+
67
+ const comparison = compareRequest.response || {};
68
+ const previousScan = comparison.previous || {};
69
+ const currentScan = comparison.current || {};
70
+ const previousOrigin = formatScanOrigin(
71
+ previousScan.scanner,
72
+ previousScan.source
73
+ );
74
+ const currentOrigin = formatScanOrigin(
75
+ currentScan.scanner,
76
+ currentScan.source
77
+ );
78
+ const comparisonRows = useMemo(() => comparison.results || [], [
79
+ comparison.results,
80
+ ]);
81
+ const summary = comparison.summary || {};
82
+ const normalizedSearch = search.trim().toLowerCase();
83
+ const filteredRows = useMemo(() => {
84
+ let rows = comparisonRows;
85
+ if (filter !== 'all') {
86
+ rows = rows.filter(row => row.status === filter);
87
+ }
88
+ if (!normalizedSearch) return rows;
89
+ return rows.filter(row => comparisonMatchesSearch(row, normalizedSearch));
90
+ }, [comparisonRows, filter, normalizedSearch]);
91
+ const sortedRows = useMemo(() => {
92
+ const rows = [...filteredRows];
93
+ const sorter = comparisonSorters[sortBy.column] || comparisonSorters.status;
94
+ rows.sort((a, b) => {
95
+ const result = sorter(a, b);
96
+ return sortBy.direction === SortByDirection.asc ? result : -result;
97
+ });
98
+ return rows;
99
+ }, [filteredRows, sortBy]);
100
+ const status = compareRequest.status || STATUS.PENDING;
101
+ const subtitle =
102
+ previousScan.scanned_at && currentScan.scanned_at
103
+ ? `${formatScannedAt(previousScan.scanned_at)} -> ${formatScannedAt(
104
+ currentScan.scanned_at
105
+ )}`
106
+ : __('Compare CVE reports');
107
+
108
+ const onSort = column => {
109
+ const nextDirection =
110
+ sortBy.column === column && sortBy.direction === SortByDirection.asc
111
+ ? SortByDirection.desc
112
+ : SortByDirection.asc;
113
+ setSortBy({ column, direction: nextDirection });
114
+ };
115
+ const onSearchChange = (value, event) => {
116
+ setSearch(normalizeSearchInputValue(value, event));
117
+ };
118
+ const renderDiff = diff => {
119
+ const entries = comparisonDiffEntries(diff);
120
+ if (entries.length === 0) return '-';
121
+ return entries.map(entry => (
122
+ <div key={entry.field} className="cve-compare-diff-entry">
123
+ <span className="cve-compare-diff-label">{entry.label}:</span>{' '}
124
+ <span className="cve-compare-diff-values">
125
+ <span className="cve-compare-diff-old">{entry.oldValue}</span>{' '}
126
+ <span className="cve-compare-diff-arrow">-&gt;</span>{' '}
127
+ <span className="cve-compare-diff-new">{entry.newValue}</span>
128
+ </span>
129
+ </div>
130
+ ));
131
+ };
132
+
133
+ return (
134
+ <Modal
135
+ title={__('Compare CVE reports')}
136
+ description={subtitle}
137
+ isOpen={isOpen}
138
+ onClose={onClose}
139
+ width="80%"
140
+ maxWidth="80%"
141
+ ouiaId="cve-compare-modal"
142
+ actions={[
143
+ <Button
144
+ key="close"
145
+ variant="primary"
146
+ onClick={onClose}
147
+ ouiaId="cve-compare-close"
148
+ >
149
+ {__('Close')}
150
+ </Button>,
151
+ ]}
152
+ >
153
+ {status === STATUS.PENDING ? (
154
+ <Text component={TextVariants.small} ouiaId="cve-compare-loading">
155
+ {__('Loading...')}
156
+ </Text>
157
+ ) : (
158
+ <div className="cve-modal-body cve-compare-body">
159
+ <div className="cve-compare-summary">
160
+ <div className="cve-compare-meta">
161
+ <Text
162
+ component={TextVariants.small}
163
+ ouiaId="cve-compare-previous"
164
+ >
165
+ {__('Previous')}: {previousOrigin} /{' '}
166
+ {formatScannedAt(previousScan.scanned_at)}
167
+ </Text>
168
+ <Text component={TextVariants.small} ouiaId="cve-compare-current">
169
+ {__('Current')}: {currentOrigin} /{' '}
170
+ {formatScannedAt(currentScan.scanned_at)}
171
+ </Text>
172
+ </div>
173
+ <div className="cve-compare-cards">
174
+ {comparisonFilters
175
+ .filter(item => item.key !== 'all')
176
+ .map(item => (
177
+ <button
178
+ key={item.key}
179
+ type="button"
180
+ className={
181
+ filter === item.key
182
+ ? 'cve-compare-card is-active'
183
+ : 'cve-compare-card'
184
+ }
185
+ onClick={() => setFilter(item.key)}
186
+ >
187
+ <span className="cve-compare-card-label">{item.label}</span>
188
+ <span className="cve-compare-card-value">
189
+ {summary[item.key]}
190
+ </span>
191
+ </button>
192
+ ))}
193
+ </div>
194
+ </div>
195
+ <div className="cve-modal-filters">
196
+ <SearchInput
197
+ id="cve-compare-search"
198
+ value={search}
199
+ onChange={onSearchChange}
200
+ onClear={() => setSearch('')}
201
+ onSearch={onSearchChange}
202
+ placeholder={__('Search comparison by CVE, package, diff...')}
203
+ aria-label={__('Search CVE comparison')}
204
+ className="cve-modal-search"
205
+ />
206
+ <div className="cve-modal-quick-filters">
207
+ <div className="cve-filter-buttons">
208
+ {comparisonFilters.map(item => (
209
+ <button
210
+ key={item.key}
211
+ type="button"
212
+ aria-pressed={filter === item.key}
213
+ className={
214
+ filter === item.key
215
+ ? 'cve-filter-button is-active'
216
+ : 'cve-filter-button'
217
+ }
218
+ onClick={() => setFilter(item.key)}
219
+ >
220
+ {item.label}
221
+ </button>
222
+ ))}
223
+ </div>
224
+ </div>
225
+ </div>
226
+ <div className="cve-modal-results">
227
+ {sortedRows.length === 0 ? (
228
+ <Text component={TextVariants.small} ouiaId="cve-compare-empty">
229
+ {__('No CVE differences match the selected filters')}
230
+ </Text>
231
+ ) : (
232
+ <Table
233
+ variant="compact"
234
+ aria-label="CVE comparison table"
235
+ ouiaId="cve-compare-table"
236
+ >
237
+ <Thead>
238
+ <Tr ouiaId="cve-compare-header">
239
+ {comparisonColumns.map(column => (
240
+ <Th
241
+ key={column.key}
242
+ className="cve-sortable"
243
+ onClick={() => onSort(column.key)}
244
+ >
245
+ {column.label}
246
+ </Th>
247
+ ))}
248
+ </Tr>
249
+ </Thead>
250
+ <Tbody>
251
+ {sortedRows.map((row, index) => (
252
+ <Tr key={row.key} ouiaId={`cve-compare-row-${index}`}>
253
+ <Td dataLabel={__('Status')}>
254
+ {comparisonStatusLabels[row.status]}
255
+ </Td>
256
+ <Td dataLabel={__('CVE')}>
257
+ {row.url ? (
258
+ <a href={row.url} target="_blank" rel="noreferrer">
259
+ {row.id}
260
+ </a>
261
+ ) : (
262
+ row.id
263
+ )}
264
+ </Td>
265
+ <Td dataLabel={__('Package')}>{row.name}</Td>
266
+ <Td dataLabel={__('Severity')}>{row.severity}</Td>
267
+ <Td dataLabel={__('Version')}>{row.version}</Td>
268
+ <Td dataLabel={__('Fixed')}>{row.fixed}</Td>
269
+ <Td dataLabel={__('Scan status')}>{row.scan_status}</Td>
270
+ <Td dataLabel={__('Diff')}>{renderDiff(row.diff)}</Td>
271
+ <Td dataLabel={__('Published')}>
272
+ {formatDateTime(row.published)}
273
+ </Td>
274
+ <Td dataLabel={__('Title')} title={row.title}>
275
+ <span className="cve-truncate">{row.title}</span>
276
+ </Td>
277
+ </Tr>
278
+ ))}
279
+ </Tbody>
280
+ </Table>
281
+ )}
282
+ </div>
283
+ </div>
284
+ )}
285
+ </Modal>
286
+ );
287
+ };
288
+
289
+ CveCompareModal.propTypes = {
290
+ hostId: PropTypes.number.isRequired,
291
+ isOpen: PropTypes.bool.isRequired,
292
+ onClose: PropTypes.func.isRequired,
293
+ scanIds: PropTypes.arrayOf(PropTypes.number),
294
+ };
295
+ CveCompareModal.defaultProps = {
296
+ scanIds: [],
297
+ };
298
+ export default CveCompareModal;
@@ -1,5 +1,5 @@
1
1
  /* eslint-disable import/no-unresolved */
2
- import React, { useState } from 'react';
2
+ import React from 'react';
3
3
  import PropTypes from 'prop-types';
4
4
  import { Table, Tbody, Tr, Th, Thead, Td } from '@patternfly/react-table';
5
5
  import {
@@ -16,7 +16,7 @@ import {
16
16
  EmptyStateBody,
17
17
  Title,
18
18
  } from '@patternfly/react-core';
19
- import { SearchIcon } from '@patternfly/react-icons';
19
+ import { CheckCircleIcon, SearchIcon } from '@patternfly/react-icons';
20
20
  import { useAPI } from 'foremanReact/common/hooks/API/APIHooks';
21
21
  import { foremanUrl } from 'foremanReact/common/helpers';
22
22
  import { translate as __ } from 'foremanReact/common/I18n';
@@ -26,56 +26,54 @@ import { STATUS } from 'foremanReact/constants';
26
26
  import RelativeDateTime from 'foremanReact/components/common/dates/RelativeDateTime';
27
27
  import SeverityIcon from './SeverityIcon';
28
28
  import CveFindingsModal from './CveFindingsModal';
29
- import CveHistoryTable from './CveHistoryTable';
29
+ import useModalScan from './useModalScan';
30
30
  import {
31
+ noFindingsBody,
32
+ noFindingsTitle,
33
+ formatScanOrigin,
31
34
  noReportsBody,
32
35
  noReportsTitle,
33
36
  riskLevelFromWorst,
37
+ severityRank,
34
38
  } from './cve_helpers';
35
39
  import './cve_scans.scss';
36
40
 
37
41
  const CveDetailsCard = ({ hostDetails }) => {
38
42
  const hostId = hostDetails?.id;
39
- const historyUrl = hostId
40
- ? foremanUrl(`/api/v2/hosts/${hostId}/cve_scans?per_page=3`)
41
- : null;
42
43
  const latestUrl = hostId
43
44
  ? foremanUrl(`/api/v2/hosts/${hostId}/cve_scans/latest`)
44
45
  : null;
45
- const { response: historyResponse, status } = useAPI('get', historyUrl, {
46
- key: `CVE_DETAILS_${hostId}`,
47
- });
48
46
  const { response: latestResponse, status: latestStatus } = useAPI(
49
47
  'get',
50
48
  latestUrl,
51
49
  { key: `CVE_DETAILS_LATEST_${hostId}` }
52
50
  );
53
- const [isModalOpen, setIsModalOpen] = useState(false);
54
- const [modalScanId, setModalScanId] = useState(null);
55
- const [modalFilter, setModalFilter] = useState('all');
51
+ const { isOpen, scanId, filter, openModal, closeModal } = useModalScan();
56
52
  if (!hostId) return null;
57
- const scans = historyResponse?.results || [];
58
- const latest = latestResponse?.id ? latestResponse : scans[0];
59
- const historyScans =
60
- latest && Array.isArray(scans)
61
- ? scans.filter(scan => scan.id !== latest.id)
62
- : scans;
53
+ const latest = latestResponse?.id ? latestResponse : null;
63
54
  const findings = latest?.findings || [];
64
55
  const sortedFindings = [...findings].sort((a, b) => {
56
+ const severityDiff = severityRank(b.severity) - severityRank(a.severity);
57
+ if (severityDiff !== 0) return severityDiff;
65
58
  const aTime = new Date(a.published || 0).getTime();
66
59
  const bTime = new Date(b.published || 0).getTime();
67
60
  return bTime - aTime;
68
61
  });
62
+ // The host details preview should show ranked CVEs first and only fall back
63
+ // to unknown severities when a scan has no ranked findings at all.
64
+ const previewFindings = sortedFindings.filter(
65
+ finding => severityRank(finding.severity) > 0
66
+ );
67
+ const visibleFindings =
68
+ previewFindings.length > 0
69
+ ? previewFindings.slice(0, 5)
70
+ : sortedFindings.slice(0, 5);
69
71
  const worst = latest?.summary?.worst || 'none';
70
72
  const riskLevel = riskLevelFromWorst(worst);
71
- const openModal = (scanId, filter) => {
72
- setModalScanId(scanId);
73
- setModalFilter(filter || 'all');
74
- setIsModalOpen(true);
75
- };
73
+ const origin = formatScanOrigin(latest?.scanner, latest?.source);
76
74
  return (
77
75
  <CardTemplate header={__('CVE scan details')} expandable masonryLayout>
78
- <SkeletonLoader status={status || latestStatus || STATUS.PENDING}>
76
+ <SkeletonLoader status={latestStatus || STATUS.PENDING}>
79
77
  {!latest ? (
80
78
  <EmptyState>
81
79
  <EmptyStateIcon icon={SearchIcon} />
@@ -86,86 +84,122 @@ const CveDetailsCard = ({ hostDetails }) => {
86
84
  </EmptyState>
87
85
  ) : (
88
86
  <>
89
- <DescriptionList isCompact isHorizontal>
90
- <DescriptionListGroup>
91
- <DescriptionListTerm>{__('Report')}</DescriptionListTerm>
92
- <DescriptionListDescription>
93
- <RelativeDateTime
94
- date={latest.created_at}
95
- defaultValue={__('Unknown time')}
96
- />
97
- </DescriptionListDescription>
98
- </DescriptionListGroup>
99
- <DescriptionListGroup>
100
- <DescriptionListTerm>{__('Total')}</DescriptionListTerm>
101
- <DescriptionListDescription>
102
- <Button
103
- variant="link"
104
- className="cve-summary-link"
105
- onClick={() => openModal(latest.id, 'all')}
106
- ouiaId="cve-details-total-button"
107
- >
108
- <span
109
- className={`cve-total-bubble cve-total-bubble--${riskLevel}`}
110
- >
111
- {latest.total}
112
- </span>
113
- </Button>{' '}
114
- {__('CVEs by')} {latest.scanner}
115
- </DescriptionListDescription>
116
- </DescriptionListGroup>
117
- </DescriptionList>
118
- <div className="cve-counts cve-counts--compact">
119
- <Button
120
- variant="link"
121
- className="cve-summary-link"
122
- onClick={() => openModal(latest.id, 'critical')}
123
- ouiaId="cve-details-critical-button"
124
- >
125
- <span className="cve-count">
126
- {__('critical')}
127
- <span className="cve-bubble">{latest.critical}</span>
128
- </span>
129
- </Button>
130
- <Button
131
- variant="link"
132
- className="cve-summary-link"
133
- onClick={() => openModal(latest.id, 'medium')}
134
- ouiaId="cve-details-medium-button"
135
- >
136
- <span className="cve-count">
137
- {__('medium')}
138
- <span className="cve-bubble">{latest.medium}</span>
139
- </span>
140
- </Button>
141
- <Button
142
- variant="link"
143
- className="cve-summary-link"
144
- onClick={() => openModal(latest.id, 'high')}
145
- ouiaId="cve-details-high-button"
146
- >
147
- <span className="cve-count">
148
- {__('high')}
149
- <span className="cve-bubble">{latest.high}</span>
150
- </span>
151
- </Button>
152
- <Button
153
- variant="link"
154
- className="cve-summary-link"
155
- onClick={() => openModal(latest.id, 'low')}
156
- ouiaId="cve-details-low-button"
87
+ <div className="cve-overview">
88
+ <DescriptionList
89
+ isCompact
90
+ isHorizontal
91
+ className="cve-overview-meta"
157
92
  >
158
- <span className="cve-count">
159
- {__('low')}
160
- <span className="cve-bubble">{latest.low}</span>
161
- </span>
162
- </Button>
93
+ <DescriptionListGroup>
94
+ <DescriptionListTerm>{__('Report')}</DescriptionListTerm>
95
+ <DescriptionListDescription>
96
+ <RelativeDateTime
97
+ date={latest.scanned_at}
98
+ defaultValue={__('Unknown time')}
99
+ />
100
+ </DescriptionListDescription>
101
+ </DescriptionListGroup>
102
+ <DescriptionListGroup>
103
+ <DescriptionListTerm>{__('Total')}</DescriptionListTerm>
104
+ <DescriptionListDescription>
105
+ <span className="cve-overview-total">
106
+ <Button
107
+ variant="link"
108
+ className="cve-summary-link"
109
+ onClick={() => openModal(latest.id, 'all')}
110
+ ouiaId="cve-details-total-button"
111
+ >
112
+ <span
113
+ className={`cve-total-bubble cve-total-bubble--${riskLevel}`}
114
+ >
115
+ {latest.total}
116
+ </span>
117
+ </Button>
118
+ <span className="cve-overview-source">{origin}</span>
119
+ </span>
120
+ </DescriptionListDescription>
121
+ </DescriptionListGroup>
122
+ </DescriptionList>
123
+ <div className="cve-counts cve-counts--compact">
124
+ <Button
125
+ variant="plain"
126
+ className="cve-count-card cve-count-card--critical"
127
+ onClick={() => openModal(latest.id, 'critical')}
128
+ ouiaId="cve-details-critical-button"
129
+ >
130
+ <span className="cve-count">
131
+ <span className="cve-count-label">
132
+ <SeverityIcon severity="critical" />
133
+ <span>{__('critical')}</span>
134
+ </span>
135
+ <span className="cve-bubble cve-bubble--critical">
136
+ {latest.critical}
137
+ </span>
138
+ </span>
139
+ </Button>
140
+ <Button
141
+ variant="plain"
142
+ className="cve-count-card cve-count-card--medium"
143
+ onClick={() => openModal(latest.id, 'medium')}
144
+ ouiaId="cve-details-medium-button"
145
+ >
146
+ <span className="cve-count">
147
+ <span className="cve-count-label">
148
+ <SeverityIcon severity="medium" />
149
+ <span>{__('medium')}</span>
150
+ </span>
151
+ <span className="cve-bubble cve-bubble--medium">
152
+ {latest.medium}
153
+ </span>
154
+ </span>
155
+ </Button>
156
+ <Button
157
+ variant="plain"
158
+ className="cve-count-card cve-count-card--high"
159
+ onClick={() => openModal(latest.id, 'high')}
160
+ ouiaId="cve-details-high-button"
161
+ >
162
+ <span className="cve-count">
163
+ <span className="cve-count-label">
164
+ <SeverityIcon severity="high" />
165
+ <span>{__('high')}</span>
166
+ </span>
167
+ <span className="cve-bubble cve-bubble--high">
168
+ {latest.high}
169
+ </span>
170
+ </span>
171
+ </Button>
172
+ <Button
173
+ variant="plain"
174
+ className="cve-count-card cve-count-card--low"
175
+ onClick={() => openModal(latest.id, 'low')}
176
+ ouiaId="cve-details-low-button"
177
+ >
178
+ <span className="cve-count">
179
+ <span className="cve-count-label">
180
+ <SeverityIcon severity="low" />
181
+ <span>{__('low')}</span>
182
+ </span>
183
+ <span className="cve-bubble cve-bubble--low">
184
+ {latest.low}
185
+ </span>
186
+ </span>
187
+ </Button>
188
+ </div>
163
189
  </div>
164
190
 
165
- {sortedFindings.length === 0 ? (
166
- <Text component={TextVariants.small} ouiaId="cve-details-empty">
167
- {__('No vulnerabilities reported')}
168
- </Text>
191
+ {visibleFindings.length === 0 ? (
192
+ <EmptyState className="cve-empty-state cve-empty-state--success">
193
+ <EmptyStateIcon icon={CheckCircleIcon} />
194
+ <Title
195
+ headingLevel="h4"
196
+ size="md"
197
+ ouiaId="cve-details-clean-title"
198
+ >
199
+ {noFindingsTitle()}
200
+ </Title>
201
+ <EmptyStateBody>{noFindingsBody()}</EmptyStateBody>
202
+ </EmptyState>
169
203
  ) : (
170
204
  <>
171
205
  <TextContent className="cve-section-title">
@@ -189,7 +223,7 @@ const CveDetailsCard = ({ hostDetails }) => {
189
223
  </Tr>
190
224
  </Thead>
191
225
  <Tbody>
192
- {sortedFindings.slice(0, 5).map((finding, index) => (
226
+ {visibleFindings.map((finding, index) => (
193
227
  <Tr
194
228
  key={finding.id}
195
229
  ouiaId={`cve-details-finding-row-${index}`}
@@ -208,7 +242,7 @@ const CveDetailsCard = ({ hostDetails }) => {
208
242
  ))}
209
243
  </Tbody>
210
244
  </Table>
211
- {sortedFindings.length > 5 && (
245
+ {sortedFindings.length > visibleFindings.length && (
212
246
  <div className="cve-more">
213
247
  <Button
214
248
  variant="link"
@@ -222,29 +256,15 @@ const CveDetailsCard = ({ hostDetails }) => {
222
256
  )}
223
257
  </>
224
258
  )}
225
-
226
- {historyScans.length > 0 && (
227
- <>
228
- <TextContent className="cve-section-title">
229
- <Text
230
- component={TextVariants.h4}
231
- ouiaId="cve-details-recent-title"
232
- >
233
- {__('Recent scans')}
234
- </Text>
235
- </TextContent>
236
- <CveHistoryTable scans={historyScans} onOpen={openModal} />
237
- </>
238
- )}
239
259
  </>
240
260
  )}
241
261
  </SkeletonLoader>
242
262
  <CveFindingsModal
243
- isOpen={isModalOpen}
244
- onClose={() => setIsModalOpen(false)}
263
+ isOpen={isOpen}
264
+ onClose={closeModal}
245
265
  hostId={hostId}
246
- scanId={modalScanId}
247
- initialFilter={modalFilter}
266
+ scanId={scanId}
267
+ initialFilter={filter}
248
268
  />
249
269
  </CardTemplate>
250
270
  );