foreman_cve_scanner 0.5.0 → 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 -5
- 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 +5 -8
- 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 +22 -9
- 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,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'test_plugin_helper'
|
|
4
|
+
|
|
5
|
+
module ForemanCveScanner
|
|
6
|
+
class ScanComparisonTest < ActiveSupport::TestCase
|
|
7
|
+
def setup
|
|
8
|
+
@host = FactoryBot.create(:host)
|
|
9
|
+
@first_scan = create_scan(
|
|
10
|
+
scanned_at: 2.hours.ago,
|
|
11
|
+
findings: first_findings
|
|
12
|
+
)
|
|
13
|
+
@second_scan = create_scan(
|
|
14
|
+
scanned_at: 1.hour.ago,
|
|
15
|
+
findings: second_findings
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
test 'compare builds summary and result rows' do
|
|
20
|
+
comparison = ScanComparison.compare(@first_scan, @second_scan)
|
|
21
|
+
statuses = comparison[:results].to_h { |row| [row[:id], row] }
|
|
22
|
+
|
|
23
|
+
assert_equal 1, comparison[:summary]['updated']
|
|
24
|
+
assert_equal 1, comparison[:summary]['resolved']
|
|
25
|
+
assert_equal 1, comparison[:summary]['new']
|
|
26
|
+
assert_equal 'updated', statuses['CVE-1'][:status]
|
|
27
|
+
assert_equal(
|
|
28
|
+
{ old: 'HIGH', new: 'CRITICAL' },
|
|
29
|
+
statuses['CVE-1'][:diff]['severity']
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
test 'compare includes previous and current scan metadata' do
|
|
34
|
+
comparison = ScanComparison.compare(@first_scan, @second_scan)
|
|
35
|
+
|
|
36
|
+
assert_equal @first_scan.id, comparison[:previous][:id]
|
|
37
|
+
assert_equal @second_scan.id, comparison[:current][:id]
|
|
38
|
+
assert_equal @first_scan.scanner, comparison[:previous][:scanner]
|
|
39
|
+
assert_equal @second_scan.scanner, comparison[:current][:scanner]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def finding(id, name, severity, version)
|
|
45
|
+
{
|
|
46
|
+
'id' => id,
|
|
47
|
+
'name' => name,
|
|
48
|
+
'severity' => severity,
|
|
49
|
+
'version' => version,
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def first_findings
|
|
54
|
+
[
|
|
55
|
+
finding('CVE-1', 'openssl', 'HIGH', '1.0'),
|
|
56
|
+
finding('CVE-2', 'curl', 'LOW', '1.0'),
|
|
57
|
+
]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def second_findings
|
|
61
|
+
[
|
|
62
|
+
finding('CVE-1', 'openssl', 'CRITICAL', '1.0'),
|
|
63
|
+
finding('CVE-3', 'glibc', 'HIGH', '1.0'),
|
|
64
|
+
]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def create_scan(scanned_at:, findings:)
|
|
68
|
+
ForemanCveScanner::CveScan.create!(
|
|
69
|
+
host: @host,
|
|
70
|
+
scanner: 'trivy',
|
|
71
|
+
source: 'rex',
|
|
72
|
+
scanned_at: scanned_at,
|
|
73
|
+
raw: { 'dummy' => true },
|
|
74
|
+
summary: { 'worst' => 'high' },
|
|
75
|
+
findings: findings,
|
|
76
|
+
total: findings.size,
|
|
77
|
+
critical: findings.count { |finding| finding['severity'] == 'CRITICAL' },
|
|
78
|
+
high: findings.count { |finding| finding['severity'] == 'HIGH' },
|
|
79
|
+
medium: findings.count { |finding| finding['severity'] == 'MEDIUM' },
|
|
80
|
+
low: findings.count { |finding| finding['severity'] == 'LOW' }
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -6,6 +6,11 @@ module ForemanCveScanner
|
|
|
6
6
|
class ScanImporterTest < ActiveSupport::TestCase
|
|
7
7
|
def setup
|
|
8
8
|
@host = FactoryBot.create(:host)
|
|
9
|
+
@previous_retention = Setting[:cve_scan_delete_after_days]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def teardown
|
|
13
|
+
Setting[:cve_scan_delete_after_days] = @previous_retention
|
|
9
14
|
end
|
|
10
15
|
|
|
11
16
|
test 'import_for_host! persists scan from trivy output' do
|
|
@@ -19,6 +24,7 @@ module ForemanCveScanner
|
|
|
19
24
|
assert_not_nil scan
|
|
20
25
|
assert_equal @host.id, scan.host_id
|
|
21
26
|
assert_equal 'trivy', scan.scanner
|
|
27
|
+
assert_equal 'rex', scan.source
|
|
22
28
|
end
|
|
23
29
|
|
|
24
30
|
test 'import_for_host! sets totals and findings for trivy output' do
|
|
@@ -29,6 +35,7 @@ module ForemanCveScanner
|
|
|
29
35
|
|
|
30
36
|
assert_operator scan.total, :>, 0
|
|
31
37
|
assert_equal scan.total, scan.findings.count
|
|
38
|
+
assert_not_nil scan.scanned_at
|
|
32
39
|
end
|
|
33
40
|
|
|
34
41
|
test 'import_for_host! persists scan from proxy output' do
|
|
@@ -51,20 +58,76 @@ module ForemanCveScanner
|
|
|
51
58
|
assert_operator scan.total, :>, 0
|
|
52
59
|
end
|
|
53
60
|
|
|
54
|
-
test 'import_for_host!
|
|
61
|
+
test 'import_for_host! raises when no json markers' do
|
|
55
62
|
importer = ForemanCveScanner::ScanImporter.new('no markers here')
|
|
56
63
|
|
|
64
|
+
assert_raises(::Foreman::Exception) do
|
|
65
|
+
importer.import_for_host!(@host)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
test 'import_for_host! raises when json is invalid' do
|
|
70
|
+
importer = ForemanCveScanner::ScanImporter.new("===START\n{bad\n===END")
|
|
71
|
+
|
|
72
|
+
assert_raises(::Foreman::Exception) do
|
|
73
|
+
importer.import_for_host!(@host)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
test 'import_for_host! raises when marker block is empty' do
|
|
78
|
+
importer = ForemanCveScanner::ScanImporter.new("===START\n===END")
|
|
79
|
+
|
|
80
|
+
assert_raises(::Foreman::Exception) do
|
|
81
|
+
importer.import_for_host!(@host)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
test 'import_for_host! cleans up old scans for the host using retention setting' do
|
|
86
|
+
Setting[:cve_scan_delete_after_days] = 1
|
|
87
|
+
old_scan = ForemanCveScanner::CveScan.create!(
|
|
88
|
+
host: @host,
|
|
89
|
+
scanner: 'trivy',
|
|
90
|
+
source: 'rex',
|
|
91
|
+
scanned_at: 3.days.ago,
|
|
92
|
+
raw: { 'dummy' => true },
|
|
93
|
+
summary: { 'worst' => 'low' },
|
|
94
|
+
findings: [{ 'id' => 'CVE-0000-0000' }],
|
|
95
|
+
total: 1,
|
|
96
|
+
critical: 0,
|
|
97
|
+
high: 0,
|
|
98
|
+
medium: 0,
|
|
99
|
+
low: 1
|
|
100
|
+
)
|
|
101
|
+
output = wrap_output(load_fixture('trivy.json'))
|
|
102
|
+
importer = ForemanCveScanner::ScanImporter.new(output)
|
|
103
|
+
|
|
57
104
|
scan = importer.import_for_host!(@host)
|
|
58
105
|
|
|
59
|
-
|
|
106
|
+
assert_not_nil scan
|
|
107
|
+
assert_not ForemanCveScanner::CveScan.exists?(old_scan.id)
|
|
108
|
+
assert ForemanCveScanner::CveScan.exists?(scan.id)
|
|
60
109
|
end
|
|
61
110
|
|
|
62
|
-
test 'import_for_host!
|
|
63
|
-
|
|
111
|
+
test 'import_for_host! persists scan with zero findings' do
|
|
112
|
+
output = wrap_output(
|
|
113
|
+
{
|
|
114
|
+
'Results' => [
|
|
115
|
+
{
|
|
116
|
+
'Target' => '/',
|
|
117
|
+
'Class' => 'os-pkgs',
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
}.to_json
|
|
121
|
+
)
|
|
122
|
+
importer = ForemanCveScanner::ScanImporter.new(output)
|
|
64
123
|
|
|
65
124
|
scan = importer.import_for_host!(@host)
|
|
66
125
|
|
|
67
|
-
|
|
126
|
+
assert_not_nil scan
|
|
127
|
+
assert_equal 'trivy', scan.scanner
|
|
128
|
+
assert_empty scan.findings
|
|
129
|
+
assert_equal 0, scan.total
|
|
130
|
+
assert_equal 'none', scan.summary['worst']
|
|
68
131
|
end
|
|
69
132
|
|
|
70
133
|
private
|
|
@@ -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;
|