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.
- checksums.yaml +4 -4
- data/README.md +101 -3
- data/app/controllers/api/v2/cve_scans_controller.rb +170 -1
- data/app/models/foreman_cve_scanner/cve_scan.rb +15 -2
- data/app/models/host_status/cve_status.rb +3 -1
- data/app/services/concerns/foreman_cve_scanner/profiles_uploader.rb +25 -0
- data/app/services/foreman_cve_scanner/cve_report_scanner.rb +3 -6
- data/app/services/foreman_cve_scanner/scan_cleanup.rb +34 -0
- data/app/services/foreman_cve_scanner/scan_comparison.rb +145 -0
- data/app/services/foreman_cve_scanner/scan_importer.rb +17 -15
- data/app/views/api/v2/cve_scans/base.json.rabl +1 -1
- data/app/views/api/v2/cve_scans/main.json.rabl +1 -1
- data/app/views/foreman_cve_scanner/job_templates/install_cve_scanners.erb +3 -3
- data/app/views/foreman_cve_scanner/job_templates/run_cve_scanner.erb +5 -3
- data/config/routes.rb +6 -1
- data/db/migrate/20260514080000_add_source_and_scanned_at_to_foreman_cve_scanner_cve_scans.rb +26 -0
- data/lib/foreman_cve_scanner/engine.rb +68 -17
- data/lib/foreman_cve_scanner/template_helpers.rb +28 -0
- data/lib/foreman_cve_scanner/version.rb +1 -1
- data/lib/tasks/foreman_cve_scanner_tasks.rake +11 -0
- data/package.json +1 -1
- data/test/controllers/api/v2/cve_scans_controller_test.rb +260 -5
- data/test/lib/foreman_cve_scanner/profiles_uploader_test.rb +84 -0
- data/test/lib/foreman_cve_scanner/template_helpers_test.rb +29 -0
- data/test/models/foreman_cve_scanner/cve_scan_test.rb +120 -0
- data/test/models/host_status/cve_status_test.rb +12 -3
- data/test/services/foreman_cve_scanner/scan_cleanup_test.rb +69 -0
- data/test/services/foreman_cve_scanner/scan_comparison_test.rb +84 -0
- data/test/services/foreman_cve_scanner/scan_importer_test.rb +68 -5
- data/webpack/components/CveCompareModal.js +298 -0
- data/webpack/components/CveDetailsCard.js +141 -121
- data/webpack/components/CveFindingsModal.js +131 -111
- data/webpack/components/CveScansReports.js +227 -0
- data/webpack/components/CveScansTab.js +122 -119
- data/webpack/components/CveTrendChart.js +264 -0
- data/webpack/components/__tests__/CveCompareModal.test.js +104 -0
- data/webpack/components/__tests__/CveDetailsCard.test.js +106 -20
- data/webpack/components/__tests__/CveFindingsModal.test.js +54 -2
- data/webpack/components/__tests__/CveScansTab.test.js +185 -5
- data/webpack/components/__tests__/CveTrendChart.test.js +122 -0
- data/webpack/components/__tests__/cve_helpers.test.js +18 -0
- data/webpack/components/cve_helpers.js +139 -0
- data/webpack/components/cve_scans.scss +464 -9
- data/webpack/components/useModalScan.js +26 -0
- metadata +24 -3
- data/webpack/components/CveHistoryTable.js +0 -58
- 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">-></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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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={
|
|
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
|
-
<
|
|
90
|
-
<
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
<
|
|
159
|
-
{__('
|
|
160
|
-
<
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
{
|
|
166
|
-
<
|
|
167
|
-
{
|
|
168
|
-
|
|
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
|
-
{
|
|
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 >
|
|
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={
|
|
244
|
-
onClose={
|
|
263
|
+
isOpen={isOpen}
|
|
264
|
+
onClose={closeModal}
|
|
245
265
|
hostId={hostId}
|
|
246
|
-
scanId={
|
|
247
|
-
initialFilter={
|
|
266
|
+
scanId={scanId}
|
|
267
|
+
initialFilter={filter}
|
|
248
268
|
/>
|
|
249
269
|
</CardTemplate>
|
|
250
270
|
);
|