foreman_openscap 7.1.0 → 8.0.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/app/assets/javascripts/foreman_openscap/locale/cs_CZ/foreman_openscap.js +517 -421
- data/app/assets/javascripts/foreman_openscap/locale/de/foreman_openscap.js +909 -813
- data/app/assets/javascripts/foreman_openscap/locale/en/foreman_openscap.js +517 -421
- data/app/assets/javascripts/foreman_openscap/locale/en_GB/foreman_openscap.js +549 -453
- data/app/assets/javascripts/foreman_openscap/locale/es/foreman_openscap.js +917 -821
- data/app/assets/javascripts/foreman_openscap/locale/fr/foreman_openscap.js +917 -821
- data/app/assets/javascripts/foreman_openscap/locale/gl/foreman_openscap.js +557 -461
- data/app/assets/javascripts/foreman_openscap/locale/it/foreman_openscap.js +603 -507
- data/app/assets/javascripts/foreman_openscap/locale/ja/foreman_openscap.js +908 -812
- data/app/assets/javascripts/foreman_openscap/locale/ka/foreman_openscap.js +1398 -0
- data/app/assets/javascripts/foreman_openscap/locale/ko/foreman_openscap.js +705 -609
- data/app/assets/javascripts/foreman_openscap/locale/pt_BR/foreman_openscap.js +917 -821
- data/app/assets/javascripts/foreman_openscap/locale/ru/foreman_openscap.js +707 -611
- data/app/assets/javascripts/foreman_openscap/locale/sv_SE/foreman_openscap.js +557 -461
- data/app/assets/javascripts/foreman_openscap/locale/zh_CN/foreman_openscap.js +908 -812
- data/app/assets/javascripts/foreman_openscap/locale/zh_TW/foreman_openscap.js +705 -609
- data/app/assets/javascripts/foreman_openscap/reports.js +5 -0
- data/app/controllers/arf_reports_controller.rb +23 -2
- data/app/helpers/arf_reports_helper.rb +17 -4
- data/app/models/concerns/foreman_openscap/host_extensions.rb +2 -0
- data/app/models/foreman_openscap/arf_report.rb +11 -2
- data/app/views/arf_reports/_output.html.erb +5 -1
- data/app/views/job_templates/run_openscap_remediation_-_ansible_default.erb +27 -0
- data/app/views/job_templates/run_openscap_remediation_-_script_default.erb +24 -0
- data/config/routes.rb +1 -0
- data/db/migrate/20230912122310_add_fixes_to_message.rb +5 -0
- data/lib/foreman_openscap/engine.rb +18 -2
- data/lib/foreman_openscap/version.rb +1 -1
- data/lib/tasks/foreman_openscap_tasks.rake +5 -16
- data/locale/Makefile +3 -3
- data/locale/cs_CZ/foreman_openscap.edit.po +144 -16
- data/locale/cs_CZ/foreman_openscap.po +97 -1
- data/locale/de/LC_MESSAGES/foreman_openscap.mo +0 -0
- data/locale/de/foreman_openscap.edit.po +145 -17
- data/locale/de/foreman_openscap.po +98 -2
- data/locale/en/foreman_openscap.edit.po +144 -16
- data/locale/en/foreman_openscap.po +97 -1
- data/locale/en_GB/foreman_openscap.edit.po +144 -16
- data/locale/en_GB/foreman_openscap.po +97 -1
- data/locale/es/LC_MESSAGES/foreman_openscap.mo +0 -0
- data/locale/es/foreman_openscap.edit.po +145 -17
- data/locale/es/foreman_openscap.po +98 -2
- data/locale/foreman_openscap.pot +168 -16
- data/locale/fr/LC_MESSAGES/foreman_openscap.mo +0 -0
- data/locale/fr/foreman_openscap.edit.po +145 -17
- data/locale/fr/foreman_openscap.po +98 -2
- data/locale/gl/foreman_openscap.edit.po +144 -16
- data/locale/gl/foreman_openscap.po +97 -1
- data/locale/it/foreman_openscap.edit.po +144 -16
- data/locale/it/foreman_openscap.po +97 -1
- data/locale/ja/LC_MESSAGES/foreman_openscap.mo +0 -0
- data/locale/ja/foreman_openscap.edit.po +145 -17
- data/locale/ja/foreman_openscap.po +98 -2
- data/locale/ka/LC_MESSAGES/foreman_openscap.mo +0 -0
- data/locale/ka/foreman_openscap.edit.po +1863 -0
- data/locale/ka/foreman_openscap.po +1405 -0
- data/locale/ka/foreman_openscap.po.time_stamp +0 -0
- data/locale/ko/LC_MESSAGES/foreman_openscap.mo +0 -0
- data/locale/ko/foreman_openscap.edit.po +145 -17
- data/locale/ko/foreman_openscap.po +98 -2
- data/locale/pt_BR/LC_MESSAGES/foreman_openscap.mo +0 -0
- data/locale/pt_BR/foreman_openscap.edit.po +145 -17
- data/locale/pt_BR/foreman_openscap.po +98 -2
- data/locale/ru/LC_MESSAGES/foreman_openscap.mo +0 -0
- data/locale/ru/foreman_openscap.edit.po +145 -17
- data/locale/ru/foreman_openscap.po +98 -2
- data/locale/sv_SE/foreman_openscap.edit.po +144 -16
- data/locale/sv_SE/foreman_openscap.po +97 -1
- data/locale/zh_CN/LC_MESSAGES/foreman_openscap.mo +0 -0
- data/locale/zh_CN/foreman_openscap.edit.po +145 -17
- data/locale/zh_CN/foreman_openscap.po +98 -2
- data/locale/zh_TW/LC_MESSAGES/foreman_openscap.mo +0 -0
- data/locale/zh_TW/foreman_openscap.edit.po +145 -17
- data/locale/zh_TW/foreman_openscap.po +98 -2
- data/webpack/components/EmptyState.js +6 -2
- data/webpack/components/OpenscapRemediationWizard/OpenscapRemediationSelectors.js +16 -0
- data/webpack/components/OpenscapRemediationWizard/OpenscapRemediationWizardContext.js +4 -0
- data/webpack/components/OpenscapRemediationWizard/ViewSelectedHostsLink.js +38 -0
- data/webpack/components/OpenscapRemediationWizard/WizardHeader.js +43 -0
- data/webpack/components/OpenscapRemediationWizard/constants.js +14 -0
- data/webpack/components/OpenscapRemediationWizard/helpers.js +33 -0
- data/webpack/components/OpenscapRemediationWizard/index.js +160 -0
- data/webpack/components/OpenscapRemediationWizard/steps/Finish.js +131 -0
- data/webpack/components/OpenscapRemediationWizard/steps/ReviewHosts.js +217 -0
- data/webpack/components/OpenscapRemediationWizard/steps/ReviewRemediation.js +176 -0
- data/webpack/components/OpenscapRemediationWizard/steps/ReviewRemediation.scss +4 -0
- data/webpack/components/OpenscapRemediationWizard/steps/SnippetSelect.js +160 -0
- data/webpack/components/OpenscapRemediationWizard/steps/index.js +4 -0
- data/webpack/index.js +8 -3
- data/webpack/testHelper.js +6 -5
- 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;
|