foreman_cve_scanner 0.6.0 → 0.7.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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +22 -4
  3. data/app/controllers/api/v2/cve_scans_controller.rb +23 -1
  4. data/app/models/concerns/foreman_cve_scanner/host_extensions.rb +122 -1
  5. data/app/models/host_status/cve_status.rb +4 -0
  6. data/app/services/foreman_cve_scanner/cve_report_scanner.rb +9 -7
  7. data/app/services/foreman_cve_scanner/katello_fix_availability.rb +208 -0
  8. data/app/services/foreman_cve_scanner/latest_scan_summaries.rb +51 -0
  9. data/app/services/foreman_cve_scanner/scan_importer.rb +2 -16
  10. data/app/services/foreman_cve_scanner/scan_post_processor.rb +29 -0
  11. data/app/views/api/v2/cve_scans/main.json.rabl +3 -1
  12. data/app/views/foreman_cve_scanner/job_templates/install_cve_scanners.erb +94 -57
  13. data/config/routes.rb +1 -0
  14. data/lib/foreman_cve_scanner/engine.rb +28 -19
  15. data/lib/foreman_cve_scanner/version.rb +1 -1
  16. data/test/controllers/api/v2/cve_scans_controller_test.rb +96 -0
  17. data/test/models/foreman_cve_scanner/host_extensions_test.rb +91 -0
  18. data/test/models/host_status/cve_status_test.rb +8 -0
  19. data/test/services/foreman_cve_scanner/cve_report_scanner_test.rb +86 -2
  20. data/test/services/foreman_cve_scanner/katello_fix_availability_test.rb +164 -0
  21. data/test/services/foreman_cve_scanner/latest_scan_summaries_test.rb +95 -0
  22. data/test/services/foreman_cve_scanner/scan_comparison_test.rb +28 -0
  23. data/test/services/foreman_cve_scanner/scan_importer_test.rb +55 -0
  24. data/test/services/foreman_cve_scanner/scan_post_processor_test.rb +59 -0
  25. data/webpack/components/CveCompareModal.js +9 -28
  26. data/webpack/components/CveDetailsCard.js +54 -65
  27. data/webpack/components/CveFindingsModal.js +15 -234
  28. data/webpack/components/CveFindingsView.js +307 -0
  29. data/webpack/components/CveKatelloFixLink.js +59 -0
  30. data/webpack/components/CveLink.js +28 -0
  31. data/webpack/components/CveScansReports.js +25 -62
  32. data/webpack/components/CveScansTab.js +42 -9
  33. data/webpack/components/CveSummaryCell.js +31 -19
  34. data/webpack/components/CveTrendChart.js +1 -16
  35. data/webpack/components/__tests__/CveDetailsCard.test.js +83 -14
  36. data/webpack/components/__tests__/CveFindingsModal.test.js +5 -2
  37. data/webpack/components/__tests__/CveFindingsView.test.js +153 -0
  38. data/webpack/components/__tests__/CveScansTab.test.js +141 -147
  39. data/webpack/components/__tests__/CveSummaryCell.test.js +68 -0
  40. data/webpack/components/__tests__/CveTrendChart.test.js +1 -3
  41. data/webpack/components/__tests__/cve_helpers.test.js +84 -0
  42. data/webpack/components/cve_helpers.js +114 -0
  43. data/webpack/components/cve_scans.scss +49 -0
  44. data/webpack/components/cve_summary_store.js +91 -0
  45. data/webpack/components/useSortBy.js +34 -0
  46. metadata +24 -4
@@ -1,64 +1,21 @@
1
1
  /* eslint-disable import/no-unresolved */
2
- /* eslint-disable camelcase */
3
- import React, { useEffect, useMemo, useState } from 'react';
2
+ import React from 'react';
4
3
  import PropTypes from 'prop-types';
5
- import {
6
- Modal,
7
- Button,
8
- Text,
9
- TextVariants,
10
- FormGroup,
11
- SearchInput,
12
- } from '@patternfly/react-core';
13
- import {
14
- Table,
15
- Tbody,
16
- Tr,
17
- Th,
18
- Thead,
19
- Td,
20
- SortByDirection,
21
- } from '@patternfly/react-table';
4
+ import { Modal, Button } from '@patternfly/react-core';
22
5
  import { useAPI } from 'foremanReact/common/hooks/API/APIHooks';
23
6
  import { foremanUrl } from 'foremanReact/common/helpers';
24
7
  import { translate as __ } from 'foremanReact/common/I18n';
25
- import { STATUS } from 'foremanReact/constants';
26
- import SeverityIcon from './SeverityIcon';
27
- import {
28
- findingMatchesSearch,
29
- formatDateTime,
30
- formatScanOrigin,
31
- formatScannedAt,
32
- findingSorters,
33
- normalizeSearchInputValue,
34
- } from './cve_helpers';
8
+ import CveFindingsView from './CveFindingsView';
35
9
  import './cve_scans.scss';
36
10
 
37
- const SEVERITY_OPTIONS = ['all', 'critical', 'high', 'medium', 'low'];
38
- const COLUMNS = [
39
- { key: 'severity', label: __('Severity') },
40
- { key: 'published', label: __('Published') },
41
- { key: 'name', label: __('Package') },
42
- { key: 'version', label: __('Affected version') },
43
- { key: 'fixed', label: __('Fixed version') },
44
- { key: 'status', label: __('Status') },
45
- { key: 'id', label: __('CVE') },
46
- { key: 'title', label: __('Title') },
47
- ];
48
-
49
11
  const CveFindingsModal = ({
50
12
  isOpen,
51
13
  onClose,
52
14
  hostId,
15
+ hostName,
53
16
  scanId,
54
17
  initialFilter,
55
18
  }) => {
56
- const [filter, setFilter] = useState(initialFilter || 'all');
57
- const [search, setSearch] = useState('');
58
- const [sortBy, setSortBy] = useState({
59
- direction: SortByDirection.desc,
60
- column: 'severity',
61
- });
62
19
  const normalizedScanId =
63
20
  scanId !== null && scanId !== undefined && String(scanId).trim() !== ''
64
21
  ? scanId
@@ -68,64 +25,12 @@ const CveFindingsModal = ({
68
25
  : foremanUrl(`/api/v2/hosts/${hostId}/cve_scans/latest`);
69
26
 
70
27
  const { response, status } = useAPI(isOpen ? 'get' : null, url, {
71
- key: `CVE_SCAN_${normalizedScanId || 'latest'}`,
28
+ key: `CVE_SCAN_${hostId}_${normalizedScanId || 'latest'}`,
72
29
  });
73
30
 
74
- useEffect(() => {
75
- if (!isOpen) return;
76
- setFilter(initialFilter || 'all');
77
- setSearch('');
78
- }, [initialFilter, isOpen, scanId]);
79
-
80
- const payload = response || {};
81
- const findings = Array.isArray(payload.findings) ? payload.findings : [];
82
- const origin = formatScanOrigin(payload.scanner, payload.source);
83
-
84
- const normalizedFilter = (filter || 'all').toLowerCase();
85
- const filteredFindings = useMemo(() => {
86
- if (normalizedFilter === 'all') return findings;
87
- return findings.filter(
88
- f => (f.severity || '').toLowerCase() === normalizedFilter
89
- );
90
- }, [normalizedFilter, findings]);
91
- const normalizedSearch = search.trim().toLowerCase();
92
- const visibleFindings = useMemo(() => {
93
- if (!normalizedSearch) return filteredFindings;
94
- return filteredFindings.filter(finding =>
95
- findingMatchesSearch(finding, normalizedSearch)
96
- );
97
- }, [filteredFindings, normalizedSearch]);
98
- const sortedFindings = useMemo(() => {
99
- const list = [...visibleFindings];
100
- const sortKey = sortBy.column || 'severity';
101
- const sorter = findingSorters[sortKey] || findingSorters.severity;
102
- list.sort((a, b) => {
103
- const result = sorter(a, b);
104
- return sortBy.direction === SortByDirection.asc ? result : -result;
105
- });
106
- return list;
107
- }, [visibleFindings, sortBy]);
108
-
109
- const onSort = column => {
110
- const nextDirection =
111
- sortBy.column === column && sortBy.direction === SortByDirection.asc
112
- ? SortByDirection.desc
113
- : SortByDirection.asc;
114
- setSortBy({ column, direction: nextDirection });
115
- };
116
- const onSearchChange = (value, event) => {
117
- setSearch(normalizeSearchInputValue(value, event));
118
- };
119
-
120
31
  return (
121
32
  <Modal
122
- title={
123
- payload?.scanned_at && typeof payload?.total !== 'undefined'
124
- ? `${__('Report from')} ${formatScannedAt(
125
- payload.scanned_at
126
- )} - ${origin} - ${__('Total')}: ${payload.total}`
127
- : __('CVE findings')
128
- }
33
+ title={__('CVE findings')}
129
34
  isOpen={isOpen}
130
35
  onClose={onClose}
131
36
  width="70%"
@@ -142,151 +47,27 @@ const CveFindingsModal = ({
142
47
  </Button>,
143
48
  ]}
144
49
  >
145
- {status === STATUS.PENDING ? (
146
- <Text component={TextVariants.small} ouiaId="cve-findings-loading">
147
- {__('Loading...')}
148
- </Text>
149
- ) : (
150
- <div className="cve-modal-body">
151
- <div className="cve-modal-filters">
152
- <FormGroup fieldId="cve-search" className="cve-modal-search">
153
- <SearchInput
154
- id="cve-search"
155
- value={search}
156
- onChange={onSearchChange}
157
- onClear={() => setSearch('')}
158
- onSearch={onSearchChange}
159
- placeholder={__('Search CVE, package, status, title...')}
160
- aria-label={__('Search CVE report')}
161
- />
162
- </FormGroup>
163
- <FormGroup fieldId="cve-filter" className="cve-modal-quick-filters">
164
- <div className="cve-filter-buttons">
165
- {SEVERITY_OPTIONS.map(opt => (
166
- <button
167
- key={opt}
168
- type="button"
169
- aria-pressed={filter === opt}
170
- className={
171
- filter === opt
172
- ? 'cve-filter-button is-active'
173
- : 'cve-filter-button'
174
- }
175
- onClick={() => setFilter(opt)}
176
- >
177
- {__(opt)}
178
- </button>
179
- ))}
180
- </div>
181
- </FormGroup>
182
- </div>
183
-
184
- <div className="cve-modal-results">
185
- {response?.error && (
186
- <Text component={TextVariants.small} ouiaId="cve-findings-error">
187
- {response.error.message}
188
- </Text>
189
- )}
190
- {sortedFindings.length === 0 && !response?.error ? (
191
- <Text component={TextVariants.small} ouiaId="cve-findings-empty">
192
- {__('No findings for selected filter')}
193
- </Text>
194
- ) : (
195
- <Table
196
- variant="compact"
197
- aria-label="CVE findings table"
198
- ouiaId="cve-findings-table"
199
- >
200
- <Thead>
201
- <Tr ouiaId="cve-findings-header">
202
- {COLUMNS.map(col => (
203
- <Th
204
- key={col.key}
205
- className={`cve-col-${col.key} cve-sortable`}
206
- aria-label={
207
- col.key === 'severity' ? __('Severity') : undefined
208
- }
209
- onClick={() => onSort(col.key)}
210
- >
211
- {col.key === 'severity' ? (
212
- <SeverityIcon severity="high" />
213
- ) : (
214
- col.label
215
- )}
216
- {sortBy.column === col.key && (
217
- <span className="cve-sort-indicator">
218
- {sortBy.direction === SortByDirection.asc
219
- ? ' ▲'
220
- : ' ▼'}
221
- </span>
222
- )}
223
- </Th>
224
- ))}
225
- </Tr>
226
- </Thead>
227
- <Tbody>
228
- {sortedFindings.map((finding, index) => (
229
- <Tr key={finding.id} ouiaId={`cve-findings-row-${index}`}>
230
- <Td dataLabel={__('Severity')}>
231
- <span
232
- className="cve-summary cve-summary--icon-only"
233
- title={finding.severity}
234
- aria-label={finding.severity}
235
- >
236
- <SeverityIcon
237
- severity={(finding.severity || '').toLowerCase()}
238
- />
239
- </span>
240
- </Td>
241
- <Td dataLabel={__('Published')}>
242
- {formatDateTime(finding.published)}
243
- </Td>
244
- <Td dataLabel={__('Package')}>{finding.name}</Td>
245
- <Td dataLabel={__('Affected version')}>
246
- {finding.version}
247
- </Td>
248
- <Td dataLabel={__('Fixed version')} title={finding.fixed}>
249
- <span className="cve-truncate">{finding.fixed}</span>
250
- </Td>
251
- <Td dataLabel={__('Status')}>
252
- {finding.status || __('open')}
253
- </Td>
254
- <Td dataLabel={__('CVE')}>
255
- {finding.url ? (
256
- <a
257
- href={finding.url}
258
- target="_blank"
259
- rel="noreferrer"
260
- >
261
- {finding.id}
262
- </a>
263
- ) : (
264
- finding.id
265
- )}
266
- </Td>
267
- <Td dataLabel={__('Title')} title={finding.title}>
268
- <span className="cve-truncate">{finding.title}</span>
269
- </Td>
270
- </Tr>
271
- ))}
272
- </Tbody>
273
- </Table>
274
- )}
275
- </div>
276
- </div>
277
- )}
50
+ <CveFindingsView
51
+ scan={response}
52
+ status={status}
53
+ initialFilter={initialFilter}
54
+ hostName={hostName}
55
+ />
278
56
  </Modal>
279
57
  );
280
58
  };
59
+
281
60
  CveFindingsModal.propTypes = {
282
61
  isOpen: PropTypes.bool.isRequired,
283
62
  onClose: PropTypes.func.isRequired,
284
63
  hostId: PropTypes.number.isRequired,
64
+ hostName: PropTypes.string,
285
65
  scanId: PropTypes.number,
286
66
  initialFilter: PropTypes.string,
287
67
  };
288
68
 
289
69
  CveFindingsModal.defaultProps = {
70
+ hostName: undefined,
290
71
  scanId: undefined,
291
72
  initialFilter: 'all',
292
73
  };
@@ -0,0 +1,307 @@
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
+ EmptyState,
7
+ EmptyStateBody,
8
+ EmptyStateIcon,
9
+ FormGroup,
10
+ SearchInput,
11
+ Text,
12
+ TextContent,
13
+ TextVariants,
14
+ Title,
15
+ } from '@patternfly/react-core';
16
+ import {
17
+ Table,
18
+ Tbody,
19
+ Tr,
20
+ Th,
21
+ Thead,
22
+ Td,
23
+ SortByDirection,
24
+ } from '@patternfly/react-table';
25
+ import { CheckCircleIcon, SearchIcon } from '@patternfly/react-icons';
26
+ import { translate as __ } from 'foremanReact/common/I18n';
27
+ import { STATUS } from 'foremanReact/constants';
28
+ import SeverityIcon from './SeverityIcon';
29
+ import CveLink from './CveLink';
30
+ import CveKatelloFixLink from './CveKatelloFixLink';
31
+ import useSortBy from './useSortBy';
32
+ import {
33
+ findingIdentity,
34
+ findingMatchesSearch,
35
+ findingSorters,
36
+ formatDateTime,
37
+ formatScanOrigin,
38
+ formatScannedAt,
39
+ katelloFixTitle,
40
+ noFindingsBody,
41
+ noFindingsTitle,
42
+ noReportsBody,
43
+ noReportsTitle,
44
+ normalizeSearchInputValue,
45
+ shouldShowKatelloFixColumn,
46
+ } from './cve_helpers';
47
+ import './cve_scans.scss';
48
+
49
+ const SEVERITY_OPTIONS = ['all', 'critical', 'high', 'medium', 'low'];
50
+ const COLUMNS = [
51
+ { key: 'severity', label: __('Severity') },
52
+ { key: 'published', label: __('Published') },
53
+ { key: 'name', label: __('Package') },
54
+ { key: 'version', label: __('Affected version') },
55
+ { key: 'fixed', label: __('Fixed version') },
56
+ { key: 'katello_fix', label: __('Fix availability') },
57
+ { key: 'status', label: __('Status') },
58
+ { key: 'id', label: __('CVE') },
59
+ { key: 'title', label: __('Title') },
60
+ ];
61
+
62
+ const CveFindingsView = ({ scan, status, initialFilter, hostName }) => {
63
+ const [filter, setFilter] = useState(initialFilter || 'all');
64
+ const [search, setSearch] = useState('');
65
+ const scanId = scan?.id;
66
+ const scanFindings = scan?.findings;
67
+ const findings = useMemo(
68
+ () => (Array.isArray(scanFindings) ? scanFindings : []),
69
+ [scanFindings]
70
+ );
71
+ const showKatelloFixColumn = shouldShowKatelloFixColumn(findings);
72
+ const columns = showKatelloFixColumn
73
+ ? COLUMNS
74
+ : COLUMNS.filter(column => column.key !== 'katello_fix');
75
+ const origin = formatScanOrigin(scan?.scanner, scan?.source);
76
+ const normalizedFilter = (filter || 'all').toLowerCase();
77
+ const filteredFindings =
78
+ normalizedFilter === 'all'
79
+ ? findings
80
+ : findings.filter(
81
+ finding => (finding.severity || '').toLowerCase() === normalizedFilter
82
+ );
83
+ const normalizedSearch = search.trim().toLowerCase();
84
+ const visibleFindings = useMemo(() => {
85
+ if (!normalizedSearch) return filteredFindings;
86
+ return filteredFindings.filter(finding =>
87
+ findingMatchesSearch(finding, normalizedSearch)
88
+ );
89
+ }, [filteredFindings, normalizedSearch]);
90
+ const { sortBy, onSort, sortedItems: sortedFindings } = useSortBy(
91
+ 'severity',
92
+ findingSorters,
93
+ visibleFindings
94
+ );
95
+
96
+ useEffect(() => {
97
+ setFilter(initialFilter || 'all');
98
+ setSearch('');
99
+ }, [initialFilter, scanId]);
100
+
101
+ if (status === STATUS.PENDING) {
102
+ return (
103
+ <Text component={TextVariants.small} ouiaId="cve-findings-loading">
104
+ {__('Loading...')}
105
+ </Text>
106
+ );
107
+ }
108
+
109
+ if (scan?.error) {
110
+ return (
111
+ <Text component={TextVariants.small} ouiaId="cve-findings-error">
112
+ {scan.error.message}
113
+ </Text>
114
+ );
115
+ }
116
+
117
+ if (!scan?.id) {
118
+ return (
119
+ <EmptyState>
120
+ <EmptyStateIcon icon={SearchIcon} />
121
+ <Title headingLevel="h4" size="md" ouiaId="cve-findings-empty-title">
122
+ {noReportsTitle()}
123
+ </Title>
124
+ <EmptyStateBody>{noReportsBody()}</EmptyStateBody>
125
+ </EmptyState>
126
+ );
127
+ }
128
+
129
+ const onSearchChange = (value, event) => {
130
+ setSearch(normalizeSearchInputValue(value, event));
131
+ };
132
+
133
+ return (
134
+ <div className="cve-modal-body">
135
+ <TextContent className="cve-findings-meta">
136
+ <Text component={TextVariants.small} ouiaId="cve-findings-summary">
137
+ {`${__('Report from')} ${formatScannedAt(
138
+ scan.scanned_at
139
+ )} - ${origin} - ${__('Total')}: ${scan.total}`}
140
+ </Text>
141
+ </TextContent>
142
+
143
+ {findings.length === 0 ? (
144
+ <EmptyState className="cve-empty-state cve-empty-state--success">
145
+ <EmptyStateIcon icon={CheckCircleIcon} />
146
+ <Title headingLevel="h4" size="md" ouiaId="cve-findings-clean-title">
147
+ {noFindingsTitle()}
148
+ </Title>
149
+ <EmptyStateBody>{noFindingsBody()}</EmptyStateBody>
150
+ </EmptyState>
151
+ ) : (
152
+ <>
153
+ <div className="cve-modal-filters">
154
+ <FormGroup fieldId="cve-search" className="cve-modal-search">
155
+ <SearchInput
156
+ id="cve-search"
157
+ value={search}
158
+ onChange={onSearchChange}
159
+ onClear={() => setSearch('')}
160
+ onSearch={onSearchChange}
161
+ placeholder={__('Search CVE, package, status, title...')}
162
+ aria-label={__('Search CVE report')}
163
+ />
164
+ </FormGroup>
165
+ <FormGroup fieldId="cve-filter" className="cve-modal-quick-filters">
166
+ <div className="cve-filter-buttons">
167
+ {SEVERITY_OPTIONS.map(opt => (
168
+ <button
169
+ key={opt}
170
+ type="button"
171
+ aria-pressed={filter === opt}
172
+ className={
173
+ filter === opt
174
+ ? 'cve-filter-button is-active'
175
+ : 'cve-filter-button'
176
+ }
177
+ onClick={() => setFilter(opt)}
178
+ >
179
+ {__(opt)}
180
+ </button>
181
+ ))}
182
+ </div>
183
+ </FormGroup>
184
+ </div>
185
+
186
+ <div className="cve-modal-results">
187
+ {sortedFindings.length === 0 ? (
188
+ <Text component={TextVariants.small} ouiaId="cve-findings-empty">
189
+ {__('No findings for selected filter')}
190
+ </Text>
191
+ ) : (
192
+ <Table
193
+ variant="compact"
194
+ aria-label="CVE findings table"
195
+ ouiaId="cve-findings-table"
196
+ >
197
+ <Thead>
198
+ <Tr ouiaId="cve-findings-header">
199
+ {columns.map(col => (
200
+ <Th
201
+ key={col.key}
202
+ className={`cve-col-${col.key} cve-sortable`}
203
+ aria-label={
204
+ col.key === 'severity' ? __('Severity') : undefined
205
+ }
206
+ onClick={() => onSort(col.key)}
207
+ >
208
+ {col.key === 'severity' ? (
209
+ <SeverityIcon severity="high" />
210
+ ) : (
211
+ col.label
212
+ )}
213
+ {sortBy.column === col.key && (
214
+ <span className="cve-sort-indicator">
215
+ {sortBy.direction === SortByDirection.asc
216
+ ? ' ▲'
217
+ : ' ▼'}
218
+ </span>
219
+ )}
220
+ </Th>
221
+ ))}
222
+ </Tr>
223
+ </Thead>
224
+ <Tbody>
225
+ {sortedFindings.map((finding, index) => (
226
+ <Tr
227
+ key={findingIdentity(finding)}
228
+ ouiaId={`cve-findings-row-${index}`}
229
+ >
230
+ <Td dataLabel={__('Severity')}>
231
+ <span
232
+ className="cve-summary cve-summary--icon-only"
233
+ title={finding.severity}
234
+ aria-label={finding.severity}
235
+ >
236
+ <SeverityIcon
237
+ severity={(finding.severity || '').toLowerCase()}
238
+ />
239
+ </span>
240
+ </Td>
241
+ <Td dataLabel={__('Published')}>
242
+ {formatDateTime(finding.published)}
243
+ </Td>
244
+ <Td dataLabel={__('Package')}>{finding.name}</Td>
245
+ <Td dataLabel={__('Affected version')}>
246
+ {finding.version}
247
+ </Td>
248
+ <Td dataLabel={__('Fixed version')} title={finding.fixed}>
249
+ <span className="cve-truncate">{finding.fixed}</span>
250
+ </Td>
251
+ {showKatelloFixColumn && (
252
+ <Td
253
+ dataLabel={__('Fix availability')}
254
+ title={katelloFixTitle(finding.katello_fix)}
255
+ >
256
+ <CveKatelloFixLink
257
+ fix={finding.katello_fix}
258
+ hostName={hostName}
259
+ />
260
+ </Td>
261
+ )}
262
+ <Td dataLabel={__('Status')}>
263
+ {finding.status || __('open')}
264
+ </Td>
265
+ <Td dataLabel={__('CVE')}>
266
+ <CveLink id={finding.id} url={finding.url} />
267
+ </Td>
268
+ <Td dataLabel={__('Title')} title={finding.title}>
269
+ <span className="cve-truncate">{finding.title}</span>
270
+ </Td>
271
+ </Tr>
272
+ ))}
273
+ </Tbody>
274
+ </Table>
275
+ )}
276
+ </div>
277
+ </>
278
+ )}
279
+ </div>
280
+ );
281
+ };
282
+
283
+ CveFindingsView.propTypes = {
284
+ scan: PropTypes.shape({
285
+ id: PropTypes.number,
286
+ scanned_at: PropTypes.string,
287
+ scanner: PropTypes.string,
288
+ source: PropTypes.string,
289
+ total: PropTypes.number,
290
+ findings: PropTypes.arrayOf(PropTypes.object),
291
+ error: PropTypes.shape({
292
+ message: PropTypes.string,
293
+ }),
294
+ }),
295
+ status: PropTypes.string,
296
+ initialFilter: PropTypes.string,
297
+ hostName: PropTypes.string,
298
+ };
299
+
300
+ CveFindingsView.defaultProps = {
301
+ scan: undefined,
302
+ status: undefined,
303
+ initialFilter: 'all',
304
+ hostName: undefined,
305
+ };
306
+
307
+ export default CveFindingsView;
@@ -0,0 +1,59 @@
1
+ /* eslint-disable import/no-unresolved */
2
+ import React from 'react';
3
+ import PropTypes from 'prop-types';
4
+ import {
5
+ katelloFixDetailsUrl,
6
+ katelloFixInstallUrl,
7
+ katelloFixLabel,
8
+ katelloFixTitle,
9
+ } from './cve_helpers';
10
+
11
+ const katelloFixClassName = fix =>
12
+ `cve-katello-fix cve-katello-fix--${(
13
+ fix?.status || 'unknown'
14
+ ).toLowerCase()}`;
15
+
16
+ const CveKatelloFixLink = ({ fix, hostName }) => {
17
+ const label = katelloFixLabel(fix);
18
+ const title = katelloFixTitle(fix);
19
+ const installUrl = katelloFixInstallUrl({ hostName, fix });
20
+ const detailsUrl = katelloFixDetailsUrl(fix);
21
+ const className = katelloFixClassName(fix);
22
+
23
+ if (installUrl) {
24
+ return (
25
+ <a className={className} href={installUrl} title={title}>
26
+ {label}
27
+ </a>
28
+ );
29
+ }
30
+
31
+ if ((fix?.status || '').toLowerCase() === 'applicable' && detailsUrl) {
32
+ return (
33
+ <a className={className} href={detailsUrl} title={title}>
34
+ {label}
35
+ </a>
36
+ );
37
+ }
38
+
39
+ return (
40
+ <span className={className} title={title}>
41
+ {label}
42
+ </span>
43
+ );
44
+ };
45
+
46
+ CveKatelloFixLink.propTypes = {
47
+ fix: PropTypes.shape({
48
+ status: PropTypes.string,
49
+ errata: PropTypes.arrayOf(PropTypes.object),
50
+ }),
51
+ hostName: PropTypes.string,
52
+ };
53
+
54
+ CveKatelloFixLink.defaultProps = {
55
+ fix: undefined,
56
+ hostName: undefined,
57
+ };
58
+
59
+ export default CveKatelloFixLink;
@@ -0,0 +1,28 @@
1
+ /* eslint-disable import/no-unresolved */
2
+ import React from 'react';
3
+ import PropTypes from 'prop-types';
4
+
5
+ const SAFE_URL = /^https?:\/\//i;
6
+
7
+ const CveLink = ({ id, url }) => {
8
+ if (url && SAFE_URL.test(url)) {
9
+ return (
10
+ <a href={url} target="_blank" rel="noreferrer">
11
+ {id}
12
+ </a>
13
+ );
14
+ }
15
+
16
+ return <>{id}</>;
17
+ };
18
+
19
+ CveLink.propTypes = {
20
+ id: PropTypes.string.isRequired,
21
+ url: PropTypes.string,
22
+ };
23
+
24
+ CveLink.defaultProps = {
25
+ url: undefined,
26
+ };
27
+
28
+ export default CveLink;