foreman_openscap 7.1.0 → 8.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/foreman_openscap/locale/cs_CZ/foreman_openscap.js +517 -421
  3. data/app/assets/javascripts/foreman_openscap/locale/de/foreman_openscap.js +909 -813
  4. data/app/assets/javascripts/foreman_openscap/locale/en/foreman_openscap.js +517 -421
  5. data/app/assets/javascripts/foreman_openscap/locale/en_GB/foreman_openscap.js +549 -453
  6. data/app/assets/javascripts/foreman_openscap/locale/es/foreman_openscap.js +917 -821
  7. data/app/assets/javascripts/foreman_openscap/locale/fr/foreman_openscap.js +917 -821
  8. data/app/assets/javascripts/foreman_openscap/locale/gl/foreman_openscap.js +557 -461
  9. data/app/assets/javascripts/foreman_openscap/locale/it/foreman_openscap.js +603 -507
  10. data/app/assets/javascripts/foreman_openscap/locale/ja/foreman_openscap.js +908 -812
  11. data/app/assets/javascripts/foreman_openscap/locale/ka/foreman_openscap.js +1398 -0
  12. data/app/assets/javascripts/foreman_openscap/locale/ko/foreman_openscap.js +705 -609
  13. data/app/assets/javascripts/foreman_openscap/locale/pt_BR/foreman_openscap.js +917 -821
  14. data/app/assets/javascripts/foreman_openscap/locale/ru/foreman_openscap.js +707 -611
  15. data/app/assets/javascripts/foreman_openscap/locale/sv_SE/foreman_openscap.js +557 -461
  16. data/app/assets/javascripts/foreman_openscap/locale/zh_CN/foreman_openscap.js +908 -812
  17. data/app/assets/javascripts/foreman_openscap/locale/zh_TW/foreman_openscap.js +705 -609
  18. data/app/assets/javascripts/foreman_openscap/reports.js +5 -0
  19. data/app/controllers/arf_reports_controller.rb +23 -2
  20. data/app/helpers/arf_reports_helper.rb +17 -4
  21. data/app/models/concerns/foreman_openscap/host_extensions.rb +2 -0
  22. data/app/models/foreman_openscap/arf_report.rb +11 -2
  23. data/app/views/arf_reports/_output.html.erb +5 -1
  24. data/app/views/job_templates/run_openscap_remediation_-_ansible_default.erb +27 -0
  25. data/app/views/job_templates/run_openscap_remediation_-_script_default.erb +24 -0
  26. data/config/routes.rb +1 -0
  27. data/db/migrate/20230912122310_add_fixes_to_message.rb +5 -0
  28. data/lib/foreman_openscap/engine.rb +18 -2
  29. data/lib/foreman_openscap/version.rb +1 -1
  30. data/lib/tasks/foreman_openscap_tasks.rake +5 -16
  31. data/locale/Makefile +3 -3
  32. data/locale/cs_CZ/foreman_openscap.edit.po +144 -16
  33. data/locale/cs_CZ/foreman_openscap.po +97 -1
  34. data/locale/de/LC_MESSAGES/foreman_openscap.mo +0 -0
  35. data/locale/de/foreman_openscap.edit.po +145 -17
  36. data/locale/de/foreman_openscap.po +98 -2
  37. data/locale/en/foreman_openscap.edit.po +144 -16
  38. data/locale/en/foreman_openscap.po +97 -1
  39. data/locale/en_GB/foreman_openscap.edit.po +144 -16
  40. data/locale/en_GB/foreman_openscap.po +97 -1
  41. data/locale/es/LC_MESSAGES/foreman_openscap.mo +0 -0
  42. data/locale/es/foreman_openscap.edit.po +145 -17
  43. data/locale/es/foreman_openscap.po +98 -2
  44. data/locale/foreman_openscap.pot +168 -16
  45. data/locale/fr/LC_MESSAGES/foreman_openscap.mo +0 -0
  46. data/locale/fr/foreman_openscap.edit.po +145 -17
  47. data/locale/fr/foreman_openscap.po +98 -2
  48. data/locale/gl/foreman_openscap.edit.po +144 -16
  49. data/locale/gl/foreman_openscap.po +97 -1
  50. data/locale/it/foreman_openscap.edit.po +144 -16
  51. data/locale/it/foreman_openscap.po +97 -1
  52. data/locale/ja/LC_MESSAGES/foreman_openscap.mo +0 -0
  53. data/locale/ja/foreman_openscap.edit.po +145 -17
  54. data/locale/ja/foreman_openscap.po +98 -2
  55. data/locale/ka/LC_MESSAGES/foreman_openscap.mo +0 -0
  56. data/locale/ka/foreman_openscap.edit.po +1863 -0
  57. data/locale/ka/foreman_openscap.po +1405 -0
  58. data/locale/ka/foreman_openscap.po.time_stamp +0 -0
  59. data/locale/ko/LC_MESSAGES/foreman_openscap.mo +0 -0
  60. data/locale/ko/foreman_openscap.edit.po +145 -17
  61. data/locale/ko/foreman_openscap.po +98 -2
  62. data/locale/pt_BR/LC_MESSAGES/foreman_openscap.mo +0 -0
  63. data/locale/pt_BR/foreman_openscap.edit.po +145 -17
  64. data/locale/pt_BR/foreman_openscap.po +98 -2
  65. data/locale/ru/LC_MESSAGES/foreman_openscap.mo +0 -0
  66. data/locale/ru/foreman_openscap.edit.po +145 -17
  67. data/locale/ru/foreman_openscap.po +98 -2
  68. data/locale/sv_SE/foreman_openscap.edit.po +144 -16
  69. data/locale/sv_SE/foreman_openscap.po +97 -1
  70. data/locale/zh_CN/LC_MESSAGES/foreman_openscap.mo +0 -0
  71. data/locale/zh_CN/foreman_openscap.edit.po +145 -17
  72. data/locale/zh_CN/foreman_openscap.po +98 -2
  73. data/locale/zh_TW/LC_MESSAGES/foreman_openscap.mo +0 -0
  74. data/locale/zh_TW/foreman_openscap.edit.po +145 -17
  75. data/locale/zh_TW/foreman_openscap.po +98 -2
  76. data/webpack/components/EmptyState.js +6 -2
  77. data/webpack/components/OpenscapRemediationWizard/OpenscapRemediationSelectors.js +16 -0
  78. data/webpack/components/OpenscapRemediationWizard/OpenscapRemediationWizardContext.js +4 -0
  79. data/webpack/components/OpenscapRemediationWizard/ViewSelectedHostsLink.js +38 -0
  80. data/webpack/components/OpenscapRemediationWizard/WizardHeader.js +43 -0
  81. data/webpack/components/OpenscapRemediationWizard/constants.js +14 -0
  82. data/webpack/components/OpenscapRemediationWizard/helpers.js +33 -0
  83. data/webpack/components/OpenscapRemediationWizard/index.js +160 -0
  84. data/webpack/components/OpenscapRemediationWizard/steps/Finish.js +131 -0
  85. data/webpack/components/OpenscapRemediationWizard/steps/ReviewHosts.js +217 -0
  86. data/webpack/components/OpenscapRemediationWizard/steps/ReviewRemediation.js +176 -0
  87. data/webpack/components/OpenscapRemediationWizard/steps/ReviewRemediation.scss +4 -0
  88. data/webpack/components/OpenscapRemediationWizard/steps/SnippetSelect.js +160 -0
  89. data/webpack/components/OpenscapRemediationWizard/steps/index.js +4 -0
  90. data/webpack/index.js +8 -3
  91. data/webpack/testHelper.js +6 -5
  92. metadata +24 -3
@@ -0,0 +1,160 @@
1
+ import React, { useState, useRef } from 'react';
2
+ import { useDispatch, useSelector } from 'react-redux';
3
+ import PropTypes from 'prop-types';
4
+ import { Button, Wizard } from '@patternfly/react-core';
5
+
6
+ import { sprintf, translate as __ } from 'foremanReact/common/I18n';
7
+ import { API_OPERATIONS, get } from 'foremanReact/redux/API';
8
+
9
+ import OpenscapRemediationWizardContext from './OpenscapRemediationWizardContext';
10
+ import {
11
+ selectLogResponse,
12
+ selectLogError,
13
+ selectLogStatus,
14
+ } from './OpenscapRemediationSelectors';
15
+ import { REPORT_LOG_REQUEST_KEY, FAIL_RULE_SEARCH } from './constants';
16
+ import { SnippetSelect, ReviewHosts, ReviewRemediation, Finish } from './steps';
17
+
18
+ const OpenscapRemediationWizard = ({
19
+ report_id: reportId,
20
+ host: { id: hostId, name: hostName },
21
+ supported_remediation_snippets: supportedJobSnippets,
22
+ }) => {
23
+ const dispatch = useDispatch();
24
+ const log = useSelector(state => selectLogResponse(state))?.log;
25
+ const logStatus = useSelector(state => selectLogStatus(state));
26
+ const logError = useSelector(state => selectLogError(state));
27
+
28
+ const fixes = JSON.parse(log?.message?.fixes || null) || [];
29
+ const source = log?.source?.value || '';
30
+ const title = log?.message?.value || '';
31
+ const defaultHostIdsParam = `id ^ (${hostId})`;
32
+ const defaultFailedHostsSearch = `${FAIL_RULE_SEARCH} = ${source}`;
33
+
34
+ const [isRemediationWizardOpen, setIsRemediationWizardOpen] = useState(false);
35
+ const [snippet, setSnippet] = useState('');
36
+ const [method, setMethod] = useState('job');
37
+ const [hostIdsParam, setHostIdsParam] = useState(defaultHostIdsParam);
38
+ const [isRebootSelected, setIsRebootSelected] = useState(false);
39
+ const [isAllHostsSelected, setIsAllHostsSelected] = useState(false);
40
+
41
+ const savedHostSelectionsRef = useRef({});
42
+
43
+ const onModalButtonClick = e => {
44
+ e.preventDefault();
45
+ const logId = e.target.getAttribute('data-log-id');
46
+ dispatch(
47
+ get({
48
+ type: API_OPERATIONS.GET,
49
+ key: REPORT_LOG_REQUEST_KEY,
50
+ url: `/compliance/arf_reports/${reportId}/show_log`,
51
+ params: { log_id: logId },
52
+ })
53
+ );
54
+ setIsRemediationWizardOpen(true);
55
+ };
56
+
57
+ const onWizardClose = () => {
58
+ // Reset to defaults
59
+ setHostIdsParam(defaultHostIdsParam);
60
+ setSnippet('');
61
+ setMethod('job');
62
+ setIsRebootSelected(false);
63
+ setIsRemediationWizardOpen(false);
64
+ savedHostSelectionsRef.current = {};
65
+ };
66
+
67
+ const reviewHostsStep = {
68
+ id: 2,
69
+ name: __('Review hosts'),
70
+ component: <ReviewHosts />,
71
+ canJumpTo: Boolean(snippet && method === 'job'),
72
+ enableNext: Boolean(snippet && method),
73
+ };
74
+ const steps = [
75
+ {
76
+ id: 1,
77
+ name: __('Select snippet'),
78
+ component: <SnippetSelect />,
79
+ canJumpTo: true,
80
+ enableNext: Boolean(snippet && method),
81
+ },
82
+ ...(snippet && method === 'job' ? [reviewHostsStep] : []),
83
+ {
84
+ id: 3,
85
+ name: __('Review remediation'),
86
+ component: <ReviewRemediation />,
87
+ canJumpTo: Boolean(snippet && method),
88
+ enableNext: method === 'job',
89
+ nextButtonText: __('Run'),
90
+ },
91
+ {
92
+ id: 4,
93
+ name: __('Done'),
94
+ component: <Finish onClose={onWizardClose} />,
95
+ isFinishedStep: true,
96
+ },
97
+ ];
98
+
99
+ return (
100
+ <>
101
+ {isRemediationWizardOpen && (
102
+ <OpenscapRemediationWizardContext.Provider
103
+ value={{
104
+ fixes,
105
+ snippet,
106
+ setSnippet,
107
+ method,
108
+ setMethod,
109
+ hostName,
110
+ source,
111
+ logStatus,
112
+ logError,
113
+ supportedJobSnippets,
114
+ isRebootSelected,
115
+ setIsRebootSelected,
116
+ hostId,
117
+ hostIdsParam,
118
+ setHostIdsParam,
119
+ isAllHostsSelected,
120
+ setIsAllHostsSelected,
121
+ defaultFailedHostsSearch,
122
+ savedHostSelectionsRef,
123
+ }}
124
+ >
125
+ <Wizard
126
+ title={title}
127
+ description={sprintf(__('Remediate %s rule'), source)}
128
+ isOpen={isRemediationWizardOpen}
129
+ steps={steps}
130
+ onClose={onWizardClose}
131
+ />
132
+ </OpenscapRemediationWizardContext.Provider>
133
+ )}
134
+ <Button
135
+ id="openscapRemediationWizardButton"
136
+ variant="link"
137
+ isInline
138
+ component="span"
139
+ onClick={e => onModalButtonClick(e)}
140
+ />
141
+ </>
142
+ );
143
+ };
144
+
145
+ OpenscapRemediationWizard.propTypes = {
146
+ report_id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
147
+ host: PropTypes.shape({
148
+ id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
149
+ name: PropTypes.string,
150
+ }),
151
+ supported_remediation_snippets: PropTypes.array,
152
+ };
153
+
154
+ OpenscapRemediationWizard.defaultProps = {
155
+ report_id: '',
156
+ host: {},
157
+ supported_remediation_snippets: [],
158
+ };
159
+
160
+ export default OpenscapRemediationWizard;
@@ -0,0 +1,131 @@
1
+ /* eslint-disable camelcase */
2
+ import React, { useContext } from 'react';
3
+ import PropTypes from 'prop-types';
4
+ import { Button, Bullseye } from '@patternfly/react-core';
5
+ import { ExternalLinkSquareAltIcon } from '@patternfly/react-icons';
6
+
7
+ import { translate as __ } from 'foremanReact/common/I18n';
8
+ import { foremanUrl } from 'foremanReact/common/helpers';
9
+ import { STATUS } from 'foremanReact/constants';
10
+ import { useAPI } from 'foremanReact/common/hooks/API/APIHooks';
11
+ import Loading from 'foremanReact/components/Loading';
12
+ import PermissionDenied from 'foremanReact/components/PermissionDenied';
13
+
14
+ import OpenscapRemediationWizardContext from '../OpenscapRemediationWizardContext';
15
+ import EmptyState from '../../EmptyState';
16
+ import ViewSelectedHostsLink from '../ViewSelectedHostsLink';
17
+ import { errorMsg, findFixBySnippet } from '../helpers';
18
+
19
+ import {
20
+ JOB_INVOCATION_PATH,
21
+ JOB_INVOCATION_API_PATH,
22
+ JOB_INVOCATION_API_REQUEST_KEY,
23
+ SNIPPET_SH,
24
+ SNIPPET_ANSIBLE,
25
+ } from '../constants';
26
+
27
+ const Finish = ({ onClose }) => {
28
+ const {
29
+ fixes,
30
+ snippet,
31
+ isRebootSelected,
32
+ hostIdsParam,
33
+ isAllHostsSelected,
34
+ defaultFailedHostsSearch,
35
+ } = useContext(OpenscapRemediationWizardContext);
36
+
37
+ const snippetText = findFixBySnippet(fixes, snippet)?.full_text;
38
+
39
+ const remediationJobParams = () => {
40
+ let feature = 'script_run_openscap_remediation';
41
+ const inputs = {};
42
+ switch (snippet) {
43
+ case SNIPPET_ANSIBLE:
44
+ feature = 'ansible_run_openscap_remediation';
45
+ inputs.tasks = snippetText;
46
+ inputs.reboot = isRebootSelected;
47
+ break;
48
+ case SNIPPET_SH:
49
+ default:
50
+ feature = 'script_run_openscap_remediation';
51
+ inputs.command = snippetText;
52
+ inputs.reboot = isRebootSelected;
53
+ }
54
+
55
+ return {
56
+ job_invocation: {
57
+ feature,
58
+ inputs,
59
+ search_query: isAllHostsSelected
60
+ ? defaultFailedHostsSearch
61
+ : hostIdsParam,
62
+ },
63
+ };
64
+ };
65
+
66
+ const response = useAPI('post', JOB_INVOCATION_API_PATH, {
67
+ key: JOB_INVOCATION_API_REQUEST_KEY,
68
+ params: remediationJobParams(),
69
+ });
70
+ const {
71
+ response: { response: { status: statusCode, data } = {} },
72
+ status = STATUS.PENDING,
73
+ } = response;
74
+
75
+ const linkToJob = (
76
+ <Button
77
+ variant="link"
78
+ icon={<ExternalLinkSquareAltIcon />}
79
+ iconPosition="right"
80
+ target="_blank"
81
+ component="a"
82
+ href={foremanUrl(`${JOB_INVOCATION_PATH}/${response?.response?.id}`)}
83
+ >
84
+ {__('Job details')}
85
+ </Button>
86
+ );
87
+ const closeBtn = <Button onClick={onClose}>{__('Close')}</Button>;
88
+ const errorComponent =
89
+ statusCode === 403 ? (
90
+ <PermissionDenied
91
+ missingPermissions={data?.error?.missing_permissions}
92
+ primaryButton={closeBtn}
93
+ />
94
+ ) : (
95
+ <EmptyState
96
+ error
97
+ title={__('Error!')}
98
+ body={errorMsg(data)}
99
+ primaryButton={closeBtn}
100
+ />
101
+ );
102
+ const body =
103
+ status === STATUS.RESOLVED ? (
104
+ <EmptyState
105
+ title={__(
106
+ 'The job has started on selected host(s), you can check the status on the job details page.'
107
+ )}
108
+ body={
109
+ <>
110
+ {linkToJob}
111
+ <ViewSelectedHostsLink
112
+ isAllHostsSelected={isAllHostsSelected}
113
+ hostIdsParam={hostIdsParam}
114
+ defaultFailedHostsSearch={defaultFailedHostsSearch}
115
+ />
116
+ </>
117
+ }
118
+ primaryButton={closeBtn}
119
+ />
120
+ ) : (
121
+ errorComponent
122
+ );
123
+
124
+ return <Bullseye>{status === STATUS.PENDING ? <Loading /> : body}</Bullseye>;
125
+ };
126
+
127
+ Finish.propTypes = {
128
+ onClose: PropTypes.func.isRequired,
129
+ };
130
+
131
+ export default Finish;
@@ -0,0 +1,217 @@
1
+ import React, { useContext, useState, useEffect } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import {
4
+ Spinner,
5
+ Toolbar,
6
+ ToolbarContent,
7
+ ToolbarGroup,
8
+ ToolbarItem,
9
+ } from '@patternfly/react-core';
10
+ import { Td } from '@patternfly/react-table';
11
+ import { toArray } from 'lodash';
12
+
13
+ import { foremanUrl } from 'foremanReact/common/helpers';
14
+ import { translate as __ } from 'foremanReact/common/I18n';
15
+ import SelectAllCheckbox from 'foremanReact/components/PF4/TableIndexPage/Table/SelectAllCheckbox';
16
+ import { Table } from 'foremanReact/components/PF4/TableIndexPage/Table/Table';
17
+ import { useBulkSelect } from 'foremanReact/components/PF4/TableIndexPage/Table/TableHooks';
18
+ import { getPageStats } from 'foremanReact/components/PF4/TableIndexPage/Table/helpers';
19
+ import { STATUS } from 'foremanReact/constants';
20
+ import { useAPI } from 'foremanReact/common/hooks/API/APIHooks';
21
+
22
+ import OpenscapRemediationWizardContext from '../OpenscapRemediationWizardContext';
23
+ import WizardHeader from '../WizardHeader';
24
+ import { HOSTS_API_PATH, HOSTS_API_REQUEST_KEY } from '../constants';
25
+
26
+ const ReviewHosts = () => {
27
+ const {
28
+ hostId,
29
+ setHostIdsParam,
30
+ defaultFailedHostsSearch,
31
+ setIsAllHostsSelected,
32
+ savedHostSelectionsRef,
33
+ } = useContext(OpenscapRemediationWizardContext);
34
+
35
+ const defaultParams = {
36
+ search: defaultFailedHostsSearch,
37
+ };
38
+ const defaultHostsArry = [hostId];
39
+
40
+ const [params, setParams] = useState(defaultParams);
41
+
42
+ const response = useAPI('get', `${HOSTS_API_PATH}?include_permissions=true`, {
43
+ key: HOSTS_API_REQUEST_KEY,
44
+ params: defaultParams,
45
+ });
46
+ const {
47
+ response: {
48
+ search: apiSearchQuery,
49
+ results,
50
+ per_page: perPage,
51
+ page,
52
+ subtotal,
53
+ message: errorMessage,
54
+ },
55
+ status = STATUS.PENDING,
56
+ setAPIOptions,
57
+ } = response;
58
+
59
+ const subtotalCount = Number(subtotal ?? 0);
60
+
61
+ const setParamsAndAPI = newParams => {
62
+ setParams(newParams);
63
+ setAPIOptions({ key: HOSTS_API_REQUEST_KEY, params: newParams });
64
+ };
65
+
66
+ const { pageRowCount } = getPageStats({
67
+ total: subtotalCount,
68
+ page,
69
+ perPage,
70
+ });
71
+ const { fetchBulkParams, ...selectAllOptions } = useBulkSelect({
72
+ results,
73
+ metadata: { total: subtotalCount, page },
74
+ initialSearchQuery: apiSearchQuery || defaultFailedHostsSearch,
75
+ isSelectable: () => true,
76
+ defaultArry: defaultHostsArry,
77
+ initialArry: toArray(
78
+ savedHostSelectionsRef.current.inclusionSet || defaultHostsArry
79
+ ),
80
+ initialExclusionArry: toArray(
81
+ savedHostSelectionsRef.current.exclusionSet || []
82
+ ),
83
+ initialSelectAllMode: savedHostSelectionsRef.current.selectAllMode || false,
84
+ });
85
+ const {
86
+ selectPage,
87
+ selectedCount,
88
+ selectOne,
89
+ selectNone,
90
+ selectDefault,
91
+ selectAll,
92
+ areAllRowsOnPageSelected,
93
+ areAllRowsSelected,
94
+ isSelected,
95
+ inclusionSet,
96
+ exclusionSet,
97
+ selectAllMode,
98
+ } = selectAllOptions;
99
+
100
+ useEffect(() => {
101
+ if (selectedCount) {
102
+ setHostIdsParam(fetchBulkParams());
103
+ savedHostSelectionsRef.current = {
104
+ inclusionSet,
105
+ exclusionSet,
106
+ selectAllMode,
107
+ };
108
+ }
109
+ }, [selectedCount, fetchBulkParams, setHostIdsParam]);
110
+
111
+ const selectionToolbar = (
112
+ <ToolbarItem key="selectAll">
113
+ <SelectAllCheckbox
114
+ {...{
115
+ selectAll: () => {
116
+ selectAll(true);
117
+ setIsAllHostsSelected(true);
118
+ },
119
+ selectPage: () => {
120
+ selectPage();
121
+ setIsAllHostsSelected(false);
122
+ },
123
+ selectDefault: () => {
124
+ selectDefault();
125
+ setIsAllHostsSelected(false);
126
+ },
127
+ selectNone: () => {
128
+ selectNone();
129
+ setIsAllHostsSelected(false);
130
+ },
131
+ selectedCount,
132
+ pageRowCount,
133
+ }}
134
+ totalCount={subtotalCount}
135
+ selectedDefaultCount={1} // The default host (hostId) is always selected
136
+ areAllRowsOnPageSelected={areAllRowsOnPageSelected()}
137
+ areAllRowsSelected={areAllRowsSelected()}
138
+ />
139
+ </ToolbarItem>
140
+ );
141
+
142
+ const RowSelectTd = ({ rowData }) => (
143
+ <Td
144
+ select={{
145
+ rowIndex: rowData.id,
146
+ onSelect: (_event, isSelecting) => {
147
+ selectOne(isSelecting, rowData.id, rowData);
148
+ // If at least one was unselected, then it's not all selected
149
+ if (!isSelecting) setIsAllHostsSelected(false);
150
+ },
151
+ isSelected: rowData.id === hostId || isSelected(rowData.id),
152
+ disable: rowData.id === hostId || false,
153
+ }}
154
+ />
155
+ );
156
+ RowSelectTd.propTypes = {
157
+ rowData: PropTypes.object.isRequired,
158
+ };
159
+
160
+ const columns = {
161
+ name: {
162
+ title: __('Name'),
163
+ wrapper: ({ id, name }) => <a href={foremanUrl(`hosts/${id}`)}>{name}</a>,
164
+ isSorted: true,
165
+ },
166
+ operatingsystem_name: {
167
+ title: __('OS'),
168
+ },
169
+ };
170
+
171
+ return (
172
+ <>
173
+ <WizardHeader
174
+ title={__('Review hosts')}
175
+ description={__(
176
+ 'By default, remediation is applied to the current host. Optionally, remediate any additional hosts that fail the rule.'
177
+ )}
178
+ />
179
+ <Toolbar ouiaId="table-toolbar" className="table-toolbar">
180
+ <ToolbarContent>
181
+ <ToolbarGroup>
182
+ {selectionToolbar}
183
+ {status === STATUS.PENDING && (
184
+ <ToolbarItem>
185
+ <Spinner size="sm" />
186
+ </ToolbarItem>
187
+ )}
188
+ </ToolbarGroup>
189
+ </ToolbarContent>
190
+ </Toolbar>
191
+ <Table
192
+ ouiaId="hosts-review-table"
193
+ isEmbedded
194
+ params={params}
195
+ setParams={setParamsAndAPI}
196
+ itemCount={subtotalCount}
197
+ results={results}
198
+ url={HOSTS_API_PATH}
199
+ refreshData={() =>
200
+ setAPIOptions({
201
+ key: HOSTS_API_REQUEST_KEY,
202
+ params: { defaultFailedHostsSearch },
203
+ })
204
+ }
205
+ columns={columns}
206
+ errorMessage={
207
+ status === STATUS.ERROR && errorMessage ? errorMessage : null
208
+ }
209
+ isPending={status === STATUS.PENDING}
210
+ showCheckboxes
211
+ rowSelectTd={RowSelectTd}
212
+ />
213
+ </>
214
+ );
215
+ };
216
+
217
+ export default ReviewHosts;
@@ -0,0 +1,176 @@
1
+ /* eslint-disable camelcase */
2
+ import React, { useContext, useState } from 'react';
3
+ import { some } from 'lodash';
4
+ import {
5
+ CodeBlock,
6
+ CodeBlockAction,
7
+ CodeBlockCode,
8
+ ClipboardCopyButton,
9
+ Button,
10
+ Grid,
11
+ GridItem,
12
+ Alert,
13
+ Checkbox,
14
+ } from '@patternfly/react-core';
15
+ import { ExternalLinkSquareAltIcon } from '@patternfly/react-icons';
16
+
17
+ import { translate as __ } from 'foremanReact/common/I18n';
18
+ import { foremanUrl } from 'foremanReact/common/helpers';
19
+ import { getHostsPageUrl } from 'foremanReact/Root/Context/ForemanContext';
20
+
21
+ import OpenscapRemediationWizardContext from '../OpenscapRemediationWizardContext';
22
+ import WizardHeader from '../WizardHeader';
23
+ import ViewSelectedHostsLink from '../ViewSelectedHostsLink';
24
+ import { HOSTS_PATH, FAIL_RULE_SEARCH } from '../constants';
25
+ import { findFixBySnippet } from '../helpers';
26
+
27
+ import './ReviewRemediation.scss';
28
+
29
+ const ReviewRemediation = () => {
30
+ const {
31
+ fixes,
32
+ snippet,
33
+ method,
34
+ hostName,
35
+ source,
36
+ isRebootSelected,
37
+ setIsRebootSelected,
38
+ isAllHostsSelected,
39
+ hostIdsParam,
40
+ defaultFailedHostsSearch,
41
+ } = useContext(OpenscapRemediationWizardContext);
42
+ const [copied, setCopied] = useState(false);
43
+ const selectedFix = findFixBySnippet(fixes, snippet);
44
+ const snippetText = selectedFix?.full_text;
45
+ // can be one of null, "true", "false"
46
+ // if null, it may indicate that reboot might be needed
47
+ const checkForReboot = () => !some(fixes, { reboot: 'false' });
48
+ const isRebootRequired = () => some(fixes, { reboot: 'true' });
49
+
50
+ const copyToClipboard = (e, text) => {
51
+ navigator.clipboard.writeText(text.toString());
52
+ };
53
+
54
+ const onCopyClick = (e, text) => {
55
+ copyToClipboard(e, text);
56
+ setCopied(true);
57
+ };
58
+
59
+ const description =
60
+ method === 'manual'
61
+ ? __('Review the remediation snippet and apply it to the host manually.')
62
+ : __(
63
+ 'Review the remediation snippet that will be applied to selected host(s).'
64
+ );
65
+
66
+ const rebootAlertTitle = isRebootRequired()
67
+ ? __('A reboot is required after applying remediation.')
68
+ : __('A reboot might be required after applying remediation.');
69
+
70
+ const actions = (
71
+ <React.Fragment>
72
+ <CodeBlockAction>
73
+ <ClipboardCopyButton
74
+ id="basic-copy-button"
75
+ textId="code-content"
76
+ aria-label="Copy to clipboard"
77
+ onClick={e => onCopyClick(e, snippetText)}
78
+ exitDelay={copied ? 1500 : 600}
79
+ maxWidth="110px"
80
+ variant="plain"
81
+ onTooltipHidden={() => setCopied(false)}
82
+ >
83
+ {copied
84
+ ? __('Successfully copied to clipboard!')
85
+ : __('Copy to clipboard')}
86
+ </ClipboardCopyButton>
87
+ </CodeBlockAction>
88
+ </React.Fragment>
89
+ );
90
+
91
+ return (
92
+ <>
93
+ <WizardHeader
94
+ title={__('Review remediation')}
95
+ description={description}
96
+ />
97
+ <Grid hasGutter>
98
+ <br />
99
+ <GridItem>
100
+ <Alert
101
+ ouiaId="review-alert"
102
+ variant="danger"
103
+ title={`${__(
104
+ 'Do not implement any of the recommended remedial actions or scripts without first testing them in a non-production environment.'
105
+ )}
106
+ ${__('Remediation might render the system non-functional.')}`}
107
+ />
108
+ </GridItem>
109
+ <GridItem md={12} span={4} rowSpan={1}>
110
+ <ViewSelectedHostsLink
111
+ isAllHostsSelected={isAllHostsSelected}
112
+ hostIdsParam={hostIdsParam}
113
+ defaultFailedHostsSearch={defaultFailedHostsSearch}
114
+ />
115
+ </GridItem>
116
+ <GridItem md={12} span={4} rowSpan={1}>
117
+ <Button
118
+ variant="link"
119
+ icon={<ExternalLinkSquareAltIcon />}
120
+ iconPosition="right"
121
+ target="_blank"
122
+ component="a"
123
+ href={foremanUrl(`${getHostsPageUrl(true)}/${hostName}`)}
124
+ >
125
+ {hostName}
126
+ </Button>{' '}
127
+ </GridItem>
128
+ <GridItem md={12} span={8} rowSpan={1}>
129
+ <Button
130
+ variant="link"
131
+ icon={<ExternalLinkSquareAltIcon />}
132
+ iconPosition="right"
133
+ target="_blank"
134
+ component="a"
135
+ href={foremanUrl(
136
+ `${HOSTS_PATH}/?search=${FAIL_RULE_SEARCH} = ${source}`
137
+ )}
138
+ >
139
+ {__('Other hosts failing this rule')}
140
+ </Button>
141
+ </GridItem>
142
+ {checkForReboot() ? (
143
+ <>
144
+ <GridItem span={12} rowSpan={1}>
145
+ <Alert
146
+ ouiaId="reboot-alert"
147
+ variant={isRebootRequired() ? 'warning' : 'info'}
148
+ title={rebootAlertTitle}
149
+ />
150
+ </GridItem>
151
+ {method === 'manual' ? null : (
152
+ <GridItem span={4} rowSpan={1}>
153
+ <Checkbox
154
+ id="reboot-checkbox"
155
+ label={__('Reboot the system(s)')}
156
+ name="reboot-checkbox"
157
+ isChecked={isRebootSelected}
158
+ onChange={selected => setIsRebootSelected(selected)}
159
+ />
160
+ </GridItem>
161
+ )}
162
+ </>
163
+ ) : null}
164
+ <GridItem>
165
+ <CodeBlock actions={actions}>
166
+ <CodeBlockCode id="code-content" className="remediation-code">
167
+ {snippetText}
168
+ </CodeBlockCode>
169
+ </CodeBlock>
170
+ </GridItem>
171
+ </Grid>
172
+ </>
173
+ );
174
+ };
175
+
176
+ export default ReviewRemediation;
@@ -0,0 +1,4 @@
1
+ pre.remediation-code {
2
+ border: none;
3
+ border-radius: none;
4
+ }