Revision 85d4141b
Added by Oleh Fedorenko about 1 month ago
app/assets/javascripts/foreman_openscap/reports.js | ||
---|---|---|
showDetails.is(':visible') ? $(event).find('span').attr('class', 'glyphicon glyphicon-collapse-up') : $(event).find('span').attr('class', 'glyphicon glyphicon-collapse-down');
|
||
}
|
||
|
||
function showRemediationWizard(log_id) {
|
||
var wizard_button = $('#openscapRemediationWizardButton');
|
||
wizard_button.attr('data-log-id', log_id);
|
||
wizard_button.click();
|
||
}
|
app/controllers/arf_reports_controller.rb | ||
---|---|---|
include Foreman::Controller::AutoCompleteSearch
|
||
include ForemanOpenscap::ArfReportsControllerCommonExtensions
|
||
|
||
before_action :find_arf_report, :only => %i[show show_html destroy parse_html parse_bzip download_html]
|
||
before_action :find_arf_report, :only => %i[show show_html destroy parse_html parse_bzip download_html show_log]
|
||
before_action :find_multiple, :only => %i[delete_multiple submit_delete_multiple]
|
||
|
||
def model_of_controller
|
||
... | ... | |
end
|
||
end
|
||
|
||
def show_log
|
||
return not_found unless @arf_report # TODO: use Message/Log model directly instead?
|
||
|
||
log = @arf_report.logs.find(params[:log_id])
|
||
return not_found unless log
|
||
|
||
respond_to do |format|
|
||
format.json do
|
||
render json: {
|
||
log: {
|
||
source: log.source,
|
||
message: {
|
||
value: log.message.value,
|
||
fixes: log.message.fixes,
|
||
}
|
||
},
|
||
}, status: :ok
|
||
end
|
||
end
|
||
end
|
||
|
||
private
|
||
|
||
def find_arf_report
|
||
... | ... | |
|
||
def action_permission
|
||
case params[:action]
|
||
when 'show_html', 'parse_html', 'parse_bzip', 'download_html'
|
||
when 'show_html', 'parse_html', 'parse_bzip', 'download_html', 'show_log'
|
||
:view
|
||
when 'delete_multiple', 'submit_delete_multiple'
|
||
:destroy
|
app/helpers/arf_reports_helper.rb | ||
---|---|---|
msg.html_safe
|
||
end
|
||
|
||
def host_search_by_rule_result_buttons(source)
|
||
action_buttons(display_link_if_authorized(_('Hosts failing this rule'), hash_for_hosts_path(:search => "fails_xccdf_rule = #{source}")),
|
||
display_link_if_authorized(_('Hosts passing this rule'), hash_for_hosts_path(:search => "passes_xccdf_rule = #{source}")),
|
||
display_link_if_authorized(_('Hosts othering this rule'), hash_for_hosts_path(:search => "others_xccdf_rule = #{source}")))
|
||
def host_search_by_rule_result_buttons(log)
|
||
buttons = [
|
||
display_link_if_authorized(_('Hosts failing this rule'), hash_for_hosts_path(:search => "fails_xccdf_rule = #{log.source}")),
|
||
display_link_if_authorized(_('Hosts passing this rule'), hash_for_hosts_path(:search => "passes_xccdf_rule = #{log.source}")),
|
||
display_link_if_authorized(_('Hosts othering this rule'), hash_for_hosts_path(:search => "others_xccdf_rule = #{log.source}")),
|
||
]
|
||
if log.result == 'fail' && log.message.fixes.present?
|
||
buttons << link_to_function_if_authorized(_('Remediation'), "showRemediationWizard(#{log.id})", hash_for_show_log_arf_report_path(id: log.report.id))
|
||
end
|
||
action_buttons(buttons)
|
||
end
|
||
|
||
def supported_remediation_snippets
|
||
snippets = []
|
||
snippets << 'urn:xccdf:fix:script:sh' if ForemanOpenscap.with_remote_execution?
|
||
snippets << 'urn:xccdf:fix:script:ansible' if ForemanOpenscap.with_ansible?
|
||
snippets
|
||
end
|
||
end
|
app/models/foreman_openscap/arf_report.rb | ||
---|---|---|
:severity => log[:severity],
|
||
:description => newline_to_space(log[:description]),
|
||
:rationale => newline_to_space(log[:rationale]),
|
||
:scap_references => references_links(log[:references])
|
||
:scap_references => references_links(log[:references]),
|
||
:fixes => fixes(log[:fixes])
|
||
}
|
||
else
|
||
msg = Message.new(:value => N_(log[:title]),
|
||
:severity => log[:severity],
|
||
:description => newline_to_space(log[:description]),
|
||
:rationale => newline_to_space(log[:rationale]),
|
||
:scap_references => references_links(log[:references]))
|
||
:scap_references => references_links(log[:references]),
|
||
:fixes => fixes(log[:fixes]))
|
||
end
|
||
msg.save!
|
||
end
|
||
... | ... | |
html_links.join(', ')
|
||
end
|
||
|
||
def self.fixes(raw_fixes)
|
||
return if raw_fixes.empty?
|
||
|
||
JSON.fast_generate(raw_fixes)
|
||
end
|
||
|
||
def self.update_msg_with_changes(msg, incoming_data)
|
||
msg.severity = incoming_data['severity']
|
||
msg.description = incoming_data['description']
|
||
msg.rationale = incoming_data['rationale']
|
||
msg.scap_references = incoming_data['references']
|
||
msg.value = incoming_data['title']
|
||
msg.fixes = fixes(incoming_data['fixes'])
|
||
|
||
return unless msg.changed?
|
||
msg.save
|
app/views/arf_reports/_output.html.erb | ||
---|---|---|
</td>
|
||
<td><%= log.source %></td>
|
||
<td><%= react_component 'RuleSeverity', { :severity => log.message.severity.downcase } %></td>
|
||
<td><%= host_search_by_rule_result_buttons(log.source) %></td>
|
||
<td><%= host_search_by_rule_result_buttons(log) %></td>
|
||
</tr>
|
||
<% end %>
|
||
<tr id='ntsh' <%= "style='display: none;'".html_safe if logs.size > 0%>>
|
||
... | ... | |
</tr>
|
||
</tbody>
|
||
</table>
|
||
<%= react_component 'OpenscapRemediationWizard',
|
||
{ report_id: @arf_report.id,
|
||
host: { name: @arf_report.host.name, id: @arf_report.host.id },
|
||
supported_remediation_snippets: supported_remediation_snippets } %>
|
app/views/job_templates/run_openscap_remediation_-_ansible_default.erb | ||
---|---|---|
<%#
|
||
name: Run OpenSCAP remediation - Ansible Default
|
||
job_category: OpenSCAP Ansible Commands
|
||
description_format: Run OpenSCAP remediation on given host. Please note, it is not meant to be used directly.
|
||
snippet: false
|
||
provider_type: Ansible
|
||
kind: job_template
|
||
model: JobTemplate
|
||
feature: ansible_run_openscap_remediation
|
||
template_inputs:
|
||
- name: tasks
|
||
description: Tasks to run on the host
|
||
input_type: user
|
||
required: true
|
||
- name: reboot
|
||
description: Indicate wether the host should be rebooted after all the remediation
|
||
input_type: user
|
||
required: false
|
||
%>
|
||
---
|
||
- hosts: all
|
||
tasks:
|
||
<%= indent(4) { input('tasks') } -%>
|
||
<% if truthy?(input('reboot')) %>
|
||
- name: Reboot the machine
|
||
reboot:
|
||
<% end %>
|
app/views/job_templates/run_openscap_remediation_-_script_default.erb | ||
---|---|---|
<%#
|
||
name: Run OpenSCAP remediation - Script Default
|
||
job_category: OpenSCAP
|
||
description_format: Run OpenSCAP remediation on given host. Please note, it is not meant to be used directly.
|
||
snippet: false
|
||
provider_type: script
|
||
kind: job_template
|
||
model: JobTemplate
|
||
feature: script_run_openscap_remediation
|
||
template_inputs:
|
||
- name: command
|
||
description: Command to run on the host
|
||
input_type: user
|
||
required: true
|
||
- name: reboot
|
||
description: Indicate wether the host should be rebooted after the remediation
|
||
input_type: user
|
||
required: false
|
||
%>
|
||
<%= input('command') %>
|
||
<% if truthy?(input('reboot')) -%>
|
||
echo "A reboot is required to finish the remediation. The system is going to reboot now."
|
||
<%= render_template('Power Action - Script Default', action: 'restart') %>
|
||
<% end -%>
|
config/routes.rb | ||
---|---|---|
get 'parse_html'
|
||
get 'parse_bzip'
|
||
get 'download_html'
|
||
get 'show_log'
|
||
end
|
||
collection do
|
||
get 'auto_complete_search'
|
db/migrate/20230912122310_add_fixes_to_message.rb | ||
---|---|---|
class AddFixesToMessage < ActiveRecord::Migration[6.1]
|
||
def change
|
||
add_column :messages, :fixes, :text
|
||
end
|
||
end
|
lib/foreman_openscap/engine.rb | ||
---|---|---|
|
||
initializer 'foreman_openscap.register_plugin', :before => :finisher_hook do |app|
|
||
Foreman::Plugin.register :foreman_openscap do
|
||
requires_foreman '>= 3.7'
|
||
requires_foreman '>= 3.11'
|
||
register_gettext
|
||
|
||
apipie_documented_controllers ["#{ForemanOpenscap::Engine.root}/app/controllers/api/v2/compliance/*.rb"]
|
||
... | ... | |
|
||
# Add permissions
|
||
security_block :foreman_openscap do
|
||
permission :view_arf_reports, { :arf_reports => %i[index show parse_html show_html
|
||
permission :view_arf_reports, { :arf_reports => %i[index show parse_html show_html show_log
|
||
parse_bzip auto_complete_search download_html],
|
||
'api/v2/compliance/arf_reports' => %i[index show download download_html],
|
||
:compliance_hosts => [:show] },
|
||
... | ... | |
:description => N_("Run OVAL scan")
|
||
}
|
||
|
||
ansible_remediation_options = {
|
||
:description => N_("Run OpenSCAP remediation with Ansible"),
|
||
:provided_inputs => ["tasks", "reboot"]
|
||
}
|
||
|
||
script_remediation_options = {
|
||
:description => N_("Run OpenSCAP remediation with Shell"),
|
||
:provided_inputs => ["command", "reboot"]
|
||
}
|
||
|
||
if Gem::Version.new(ForemanRemoteExecution::VERSION) >= Gem::Version.new('1.2.3')
|
||
options[:host_action_button] = true
|
||
oval_options[:host_action_button] = (!::Foreman.in_rake? && ActiveRecord::Base.connection.table_exists?(:settings)) ? (Setting.find_by(:name => 'lab_features')&.value || false) : false
|
||
... | ... | |
|
||
RemoteExecutionFeature.register(:foreman_openscap_run_scans, N_("Run OpenSCAP scan"), options)
|
||
RemoteExecutionFeature.register(:foreman_openscap_run_oval_scans, N_("Run OVAL scan"), oval_options)
|
||
RemoteExecutionFeature.register(:ansible_run_openscap_remediation, N_("Run OpenSCAP remediation with Ansible"), ansible_remediation_options)
|
||
RemoteExecutionFeature.register(:script_run_openscap_remediation, N_("Run OpenSCAP remediation with Shell"), script_remediation_options)
|
||
end
|
||
end
|
||
|
||
... | ... | |
def self.with_remote_execution?
|
||
RemoteExecutionFeature rescue false
|
||
end
|
||
|
||
def self.with_ansible?
|
||
ForemanAnsible rescue false
|
||
end
|
||
end
|
webpack/components/EmptyState.js | ||
---|---|---|
|
||
EmptyState.propTypes = {
|
||
title: PropTypes.string,
|
||
body: PropTypes.string,
|
||
error: PropTypes.oneOfType([PropTypes.shape({}), PropTypes.string]),
|
||
body: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
||
error: PropTypes.oneOfType([
|
||
PropTypes.shape({}),
|
||
PropTypes.string,
|
||
PropTypes.bool,
|
||
]),
|
||
search: PropTypes.bool,
|
||
lock: PropTypes.bool,
|
||
primaryButton: PropTypes.node,
|
webpack/components/OpenscapRemediationWizard/OpenscapRemediationSelectors.js | ||
---|---|---|
import {
|
||
selectAPIError,
|
||
selectAPIResponse,
|
||
selectAPIStatus,
|
||
} from 'foremanReact/redux/API/APISelectors';
|
||
import { STATUS } from 'foremanReact/constants';
|
||
import { REPORT_LOG_REQUEST_KEY } from './constants';
|
||
|
||
export const selectLogResponse = state =>
|
||
selectAPIResponse(state, REPORT_LOG_REQUEST_KEY);
|
||
|
||
export const selectLogStatus = state =>
|
||
selectAPIStatus(state, REPORT_LOG_REQUEST_KEY) || STATUS.PENDING;
|
||
|
||
export const selectLogError = state =>
|
||
selectAPIError(state, REPORT_LOG_REQUEST_KEY);
|
webpack/components/OpenscapRemediationWizard/OpenscapRemediationWizardContext.js | ||
---|---|---|
import { createContext } from 'react';
|
||
|
||
const OpenscapRemediationWizardContext = createContext({});
|
||
export default OpenscapRemediationWizardContext;
|
webpack/components/OpenscapRemediationWizard/ViewSelectedHostsLink.js | ||
---|---|---|
import React from 'react';
|
||
import PropTypes from 'prop-types';
|
||
|
||
import { ExternalLinkSquareAltIcon } from '@patternfly/react-icons';
|
||
import { Button } from '@patternfly/react-core';
|
||
|
||
import { translate as __ } from 'foremanReact/common/I18n';
|
||
import { foremanUrl } from 'foremanReact/common/helpers';
|
||
import { getHostsPageUrl } from 'foremanReact/Root/Context/ForemanContext';
|
||
|
||
const ViewSelectedHostsLink = ({
|
||
hostIdsParam,
|
||
isAllHostsSelected,
|
||
defaultFailedHostsSearch,
|
||
}) => {
|
||
const search = isAllHostsSelected ? defaultFailedHostsSearch : hostIdsParam;
|
||
const url = foremanUrl(`${getHostsPageUrl(false)}?search=${search}`);
|
||
return (
|
||
<Button
|
||
component="a"
|
||
variant="link"
|
||
icon={<ExternalLinkSquareAltIcon />}
|
||
iconPosition="right"
|
||
target="_blank"
|
||
href={url}
|
||
>
|
||
{__('View selected hosts')}
|
||
</Button>
|
||
);
|
||
};
|
||
|
||
ViewSelectedHostsLink.propTypes = {
|
||
isAllHostsSelected: PropTypes.bool.isRequired,
|
||
defaultFailedHostsSearch: PropTypes.string.isRequired,
|
||
hostIdsParam: PropTypes.string.isRequired,
|
||
};
|
||
|
||
export default ViewSelectedHostsLink;
|
webpack/components/OpenscapRemediationWizard/WizardHeader.js | ||
---|---|---|
import React from 'react';
|
||
import PropTypes from 'prop-types';
|
||
import {
|
||
Grid,
|
||
TextContent,
|
||
Text,
|
||
TextVariants,
|
||
Flex,
|
||
FlexItem,
|
||
} from '@patternfly/react-core';
|
||
|
||
const WizardHeader = ({ title, description }) => (
|
||
<Grid style={{ gridGap: '24px' }}>
|
||
{title && (
|
||
<TextContent>
|
||
<Text ouiaId="wizard-header-text" component={TextVariants.h2}>
|
||
{title}
|
||
</Text>
|
||
</TextContent>
|
||
)}
|
||
{description && (
|
||
<TextContent>
|
||
<Flex flex={{ default: 'inlineFlex' }}>
|
||
<FlexItem>
|
||
<TextContent>{description}</TextContent>
|
||
</FlexItem>
|
||
</Flex>
|
||
</TextContent>
|
||
)}
|
||
</Grid>
|
||
);
|
||
|
||
WizardHeader.propTypes = {
|
||
title: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
|
||
description: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
|
||
};
|
||
|
||
WizardHeader.defaultProps = {
|
||
title: undefined,
|
||
description: undefined,
|
||
};
|
||
|
||
export default WizardHeader;
|
webpack/components/OpenscapRemediationWizard/constants.js | ||
---|---|---|
export const OPENSCAP_REMEDIATION_MODAL_ID = 'openscapRemediationModal';
|
||
export const HOSTS_PATH = '/hosts';
|
||
export const FAIL_RULE_SEARCH = 'fails_xccdf_rule';
|
||
|
||
export const HOSTS_API_PATH = '/api/hosts';
|
||
export const HOSTS_API_REQUEST_KEY = 'HOSTS';
|
||
export const REPORT_LOG_REQUEST_KEY = 'ARF_REPORT_LOG';
|
||
|
||
export const JOB_INVOCATION_PATH = '/job_invocations';
|
||
export const JOB_INVOCATION_API_PATH = '/api/job_invocations';
|
||
export const JOB_INVOCATION_API_REQUEST_KEY = 'OPENSCAP_REX_JOB_INVOCATIONS';
|
||
|
||
export const SNIPPET_SH = 'urn:xccdf:fix:script:sh';
|
||
export const SNIPPET_ANSIBLE = 'urn:xccdf:fix:script:ansible';
|
webpack/components/OpenscapRemediationWizard/helpers.js | ||
---|---|---|
import { join, find, map, compact, includes, filter, isString } from 'lodash';
|
||
|
||
const getResponseErrorMsgs = ({ data } = {}) => {
|
||
if (data) {
|
||
const messages =
|
||
data.displayMessage || data.message || data.errors || data.error?.message;
|
||
return Array.isArray(messages) ? messages : [messages];
|
||
}
|
||
return [];
|
||
};
|
||
|
||
export const errorMsg = data => {
|
||
if (isString(data)) return data;
|
||
|
||
return join(getResponseErrorMsgs({ data }), '\n');
|
||
};
|
||
|
||
export const findFixBySnippet = (fixes, snippet) =>
|
||
find(fixes, fix => fix.system === snippet);
|
||
|
||
export const supportedRemediationSnippets = (
|
||
fixes,
|
||
meth,
|
||
supportedJobSnippets
|
||
) => {
|
||
if (meth === 'manual') return map(fixes, f => f.system);
|
||
return compact(
|
||
map(
|
||
filter(fixes, fix => includes(supportedJobSnippets, fix.system)),
|
||
f => f.system
|
||
)
|
||
);
|
||
};
|
webpack/components/OpenscapRemediationWizard/index.js | ||
---|---|---|
import React, { useState, useRef } from 'react';
|
||
import { useDispatch, useSelector } from 'react-redux';
|
||
import PropTypes from 'prop-types';
|
||
import { Button, Wizard } from '@patternfly/react-core';
|
||
|
||
import { sprintf, translate as __ } from 'foremanReact/common/I18n';
|
||
import { API_OPERATIONS, get } from 'foremanReact/redux/API';
|
||
|
||
import OpenscapRemediationWizardContext from './OpenscapRemediationWizardContext';
|
||
import {
|
||
selectLogResponse,
|
||
selectLogError,
|
||
selectLogStatus,
|
||
} from './OpenscapRemediationSelectors';
|
||
import { REPORT_LOG_REQUEST_KEY, FAIL_RULE_SEARCH } from './constants';
|
||
import { SnippetSelect, ReviewHosts, ReviewRemediation, Finish } from './steps';
|
||
|
||
const OpenscapRemediationWizard = ({
|
||
report_id: reportId,
|
||
host: { id: hostId, name: hostName },
|
||
supported_remediation_snippets: supportedJobSnippets,
|
||
}) => {
|
||
const dispatch = useDispatch();
|
||
const log = useSelector(state => selectLogResponse(state))?.log;
|
||
const logStatus = useSelector(state => selectLogStatus(state));
|
||
const logError = useSelector(state => selectLogError(state));
|
||
|
||
const fixes = JSON.parse(log?.message?.fixes || null) || [];
|
||
const source = log?.source?.value || '';
|
||
const title = log?.message?.value || '';
|
||
const defaultHostIdsParam = `id ^ (${hostId})`;
|
||
const defaultFailedHostsSearch = `${FAIL_RULE_SEARCH} = ${source}`;
|
||
|
||
const [isRemediationWizardOpen, setIsRemediationWizardOpen] = useState(false);
|
||
const [snippet, setSnippet] = useState('');
|
||
const [method, setMethod] = useState('job');
|
||
const [hostIdsParam, setHostIdsParam] = useState(defaultHostIdsParam);
|
||
const [isRebootSelected, setIsRebootSelected] = useState(false);
|
||
const [isAllHostsSelected, setIsAllHostsSelected] = useState(false);
|
||
|
||
const savedHostSelectionsRef = useRef({});
|
||
|
||
const onModalButtonClick = e => {
|
||
e.preventDefault();
|
||
const logId = e.target.getAttribute('data-log-id');
|
||
dispatch(
|
||
get({
|
||
type: API_OPERATIONS.GET,
|
||
key: REPORT_LOG_REQUEST_KEY,
|
||
url: `/compliance/arf_reports/${reportId}/show_log`,
|
||
params: { log_id: logId },
|
||
})
|
||
);
|
||
setIsRemediationWizardOpen(true);
|
||
};
|
||
|
||
const onWizardClose = () => {
|
||
// Reset to defaults
|
||
setHostIdsParam(defaultHostIdsParam);
|
||
setSnippet('');
|
||
setMethod('job');
|
||
setIsRebootSelected(false);
|
||
setIsRemediationWizardOpen(false);
|
||
savedHostSelectionsRef.current = {};
|
||
};
|
||
|
||
const reviewHostsStep = {
|
||
id: 2,
|
||
name: __('Review hosts'),
|
||
component: <ReviewHosts />,
|
||
canJumpTo: Boolean(snippet && method === 'job'),
|
||
enableNext: Boolean(snippet && method),
|
||
};
|
||
const steps = [
|
||
{
|
||
id: 1,
|
||
name: __('Select snippet'),
|
||
component: <SnippetSelect />,
|
||
canJumpTo: true,
|
||
enableNext: Boolean(snippet && method),
|
||
},
|
||
...(snippet && method === 'job' ? [reviewHostsStep] : []),
|
||
{
|
||
id: 3,
|
||
name: __('Review remediation'),
|
||
component: <ReviewRemediation />,
|
||
canJumpTo: Boolean(snippet && method),
|
||
enableNext: method === 'job',
|
||
nextButtonText: __('Run'),
|
||
},
|
||
{
|
||
id: 4,
|
||
name: __('Done'),
|
||
component: <Finish onClose={onWizardClose} />,
|
||
isFinishedStep: true,
|
||
},
|
||
];
|
||
|
||
return (
|
||
<>
|
||
{isRemediationWizardOpen && (
|
||
<OpenscapRemediationWizardContext.Provider
|
||
value={{
|
||
fixes,
|
||
snippet,
|
||
setSnippet,
|
||
method,
|
||
setMethod,
|
||
hostName,
|
||
source,
|
||
logStatus,
|
||
logError,
|
||
supportedJobSnippets,
|
||
isRebootSelected,
|
||
setIsRebootSelected,
|
||
hostId,
|
||
hostIdsParam,
|
||
setHostIdsParam,
|
||
isAllHostsSelected,
|
||
setIsAllHostsSelected,
|
||
defaultFailedHostsSearch,
|
||
savedHostSelectionsRef,
|
||
}}
|
||
>
|
||
<Wizard
|
||
title={title}
|
||
description={sprintf(__('Remediate %s rule'), source)}
|
||
isOpen={isRemediationWizardOpen}
|
||
steps={steps}
|
||
onClose={onWizardClose}
|
||
/>
|
||
</OpenscapRemediationWizardContext.Provider>
|
||
)}
|
||
<Button
|
||
id="openscapRemediationWizardButton"
|
||
variant="link"
|
||
isInline
|
||
component="span"
|
||
onClick={e => onModalButtonClick(e)}
|
||
/>
|
||
</>
|
||
);
|
||
};
|
||
|
||
OpenscapRemediationWizard.propTypes = {
|
||
report_id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||
host: PropTypes.shape({
|
||
id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||
name: PropTypes.string,
|
||
}),
|
||
supported_remediation_snippets: PropTypes.array,
|
||
};
|
||
|
||
OpenscapRemediationWizard.defaultProps = {
|
||
report_id: '',
|
||
host: {},
|
||
supported_remediation_snippets: [],
|
||
};
|
||
|
||
export default OpenscapRemediationWizard;
|
webpack/components/OpenscapRemediationWizard/steps/Finish.js | ||
---|---|---|
/* eslint-disable camelcase */
|
||
import React, { useContext } from 'react';
|
||
import PropTypes from 'prop-types';
|
||
import { Button, Bullseye } from '@patternfly/react-core';
|
||
import { ExternalLinkSquareAltIcon } from '@patternfly/react-icons';
|
||
|
||
import { translate as __ } from 'foremanReact/common/I18n';
|
||
import { foremanUrl } from 'foremanReact/common/helpers';
|
||
import { STATUS } from 'foremanReact/constants';
|
||
import { useAPI } from 'foremanReact/common/hooks/API/APIHooks';
|
||
import Loading from 'foremanReact/components/Loading';
|
||
import PermissionDenied from 'foremanReact/components/PermissionDenied';
|
||
|
||
import OpenscapRemediationWizardContext from '../OpenscapRemediationWizardContext';
|
||
import EmptyState from '../../EmptyState';
|
||
import ViewSelectedHostsLink from '../ViewSelectedHostsLink';
|
||
import { errorMsg, findFixBySnippet } from '../helpers';
|
||
|
||
import {
|
||
JOB_INVOCATION_PATH,
|
||
JOB_INVOCATION_API_PATH,
|
||
JOB_INVOCATION_API_REQUEST_KEY,
|
||
SNIPPET_SH,
|
||
SNIPPET_ANSIBLE,
|
||
} from '../constants';
|
||
|
||
const Finish = ({ onClose }) => {
|
||
const {
|
||
fixes,
|
||
snippet,
|
||
isRebootSelected,
|
||
hostIdsParam,
|
||
isAllHostsSelected,
|
||
defaultFailedHostsSearch,
|
||
} = useContext(OpenscapRemediationWizardContext);
|
||
|
||
const snippetText = findFixBySnippet(fixes, snippet)?.full_text;
|
||
|
||
const remediationJobParams = () => {
|
||
let feature = 'script_run_openscap_remediation';
|
||
const inputs = {};
|
||
switch (snippet) {
|
||
case SNIPPET_ANSIBLE:
|
||
feature = 'ansible_run_openscap_remediation';
|
||
inputs.tasks = snippetText;
|
||
inputs.reboot = isRebootSelected;
|
||
break;
|
||
case SNIPPET_SH:
|
||
default:
|
||
feature = 'script_run_openscap_remediation';
|
||
inputs.command = snippetText;
|
||
inputs.reboot = isRebootSelected;
|
||
}
|
||
|
||
return {
|
||
job_invocation: {
|
||
feature,
|
||
inputs,
|
||
search_query: isAllHostsSelected
|
||
? defaultFailedHostsSearch
|
||
: hostIdsParam,
|
||
},
|
||
};
|
||
};
|
||
|
||
const response = useAPI('post', JOB_INVOCATION_API_PATH, {
|
||
key: JOB_INVOCATION_API_REQUEST_KEY,
|
||
params: remediationJobParams(),
|
||
});
|
||
const {
|
||
response: { response: { status: statusCode, data } = {} },
|
||
status = STATUS.PENDING,
|
||
} = response;
|
||
|
||
const linkToJob = (
|
||
<Button
|
||
variant="link"
|
||
icon={<ExternalLinkSquareAltIcon />}
|
||
iconPosition="right"
|
||
target="_blank"
|
||
component="a"
|
||
href={foremanUrl(`${JOB_INVOCATION_PATH}/${response?.response?.id}`)}
|
||
>
|
||
{__('Job details')}
|
||
</Button>
|
||
);
|
||
const closeBtn = <Button onClick={onClose}>{__('Close')}</Button>;
|
||
const errorComponent =
|
||
statusCode === 403 ? (
|
||
<PermissionDenied
|
||
missingPermissions={data?.error?.missing_permissions}
|
||
primaryButton={closeBtn}
|
||
/>
|
||
) : (
|
||
<EmptyState
|
||
error
|
||
title={__('Error!')}
|
||
body={errorMsg(data)}
|
||
primaryButton={closeBtn}
|
||
/>
|
||
);
|
||
const body =
|
||
status === STATUS.RESOLVED ? (
|
||
<EmptyState
|
||
title={__(
|
||
'The job has started on selected host(s), you can check the status on the job details page.'
|
||
)}
|
||
body={
|
||
<>
|
||
{linkToJob}
|
||
<ViewSelectedHostsLink
|
||
isAllHostsSelected={isAllHostsSelected}
|
||
hostIdsParam={hostIdsParam}
|
||
defaultFailedHostsSearch={defaultFailedHostsSearch}
|
||
/>
|
||
</>
|
||
}
|
||
primaryButton={closeBtn}
|
||
/>
|
||
) : (
|
||
errorComponent
|
||
);
|
||
|
||
return <Bullseye>{status === STATUS.PENDING ? <Loading /> : body}</Bullseye>;
|
||
};
|
||
|
||
Finish.propTypes = {
|
||
onClose: PropTypes.func.isRequired,
|
||
};
|
||
|
||
export default Finish;
|
webpack/components/OpenscapRemediationWizard/steps/ReviewHosts.js | ||
---|---|---|
import React, { useContext, useState, useEffect } from 'react';
|
||
import PropTypes from 'prop-types';
|
||
import {
|
||
Spinner,
|
||
Toolbar,
|
||
ToolbarContent,
|
||
ToolbarGroup,
|
||
ToolbarItem,
|
||
} from '@patternfly/react-core';
|
||
import { Td } from '@patternfly/react-table';
|
||
import { toArray } from 'lodash';
|
||
|
||
import { foremanUrl } from 'foremanReact/common/helpers';
|
||
import { translate as __ } from 'foremanReact/common/I18n';
|
||
import SelectAllCheckbox from 'foremanReact/components/PF4/TableIndexPage/Table/SelectAllCheckbox';
|
||
import { Table } from 'foremanReact/components/PF4/TableIndexPage/Table/Table';
|
||
import { useBulkSelect } from 'foremanReact/components/PF4/TableIndexPage/Table/TableHooks';
|
||
import { getPageStats } from 'foremanReact/components/PF4/TableIndexPage/Table/helpers';
|
||
import { STATUS } from 'foremanReact/constants';
|
||
import { useAPI } from 'foremanReact/common/hooks/API/APIHooks';
|
||
|
||
import OpenscapRemediationWizardContext from '../OpenscapRemediationWizardContext';
|
||
import WizardHeader from '../WizardHeader';
|
||
import { HOSTS_API_PATH, HOSTS_API_REQUEST_KEY } from '../constants';
|
||
|
||
const ReviewHosts = () => {
|
||
const {
|
||
hostId,
|
||
setHostIdsParam,
|
||
defaultFailedHostsSearch,
|
||
setIsAllHostsSelected,
|
||
savedHostSelectionsRef,
|
||
} = useContext(OpenscapRemediationWizardContext);
|
||
|
||
const defaultParams = {
|
||
search: defaultFailedHostsSearch,
|
||
};
|
||
const defaultHostsArry = [hostId];
|
||
|
||
const [params, setParams] = useState(defaultParams);
|
||
|
||
const response = useAPI('get', `${HOSTS_API_PATH}?include_permissions=true`, {
|
||
key: HOSTS_API_REQUEST_KEY,
|
||
params: defaultParams,
|
||
});
|
||
const {
|
||
response: {
|
||
search: apiSearchQuery,
|
||
results,
|
||
per_page: perPage,
|
||
page,
|
||
subtotal,
|
||
message: errorMessage,
|
||
},
|
||
status = STATUS.PENDING,
|
||
setAPIOptions,
|
||
} = response;
|
||
|
||
const subtotalCount = Number(subtotal ?? 0);
|
||
|
||
const setParamsAndAPI = newParams => {
|
||
setParams(newParams);
|
||
setAPIOptions({ key: HOSTS_API_REQUEST_KEY, params: newParams });
|
||
};
|
||
|
||
const { pageRowCount } = getPageStats({
|
||
total: subtotalCount,
|
||
page,
|
||
perPage,
|
||
});
|
||
const { fetchBulkParams, ...selectAllOptions } = useBulkSelect({
|
||
results,
|
||
metadata: { total: subtotalCount, page },
|
||
initialSearchQuery: apiSearchQuery || defaultFailedHostsSearch,
|
||
isSelectable: () => true,
|
||
defaultArry: defaultHostsArry,
|
||
initialArry: toArray(
|
||
savedHostSelectionsRef.current.inclusionSet || defaultHostsArry
|
||
),
|
||
initialExclusionArry: toArray(
|
||
savedHostSelectionsRef.current.exclusionSet || []
|
||
),
|
||
initialSelectAllMode: savedHostSelectionsRef.current.selectAllMode || false,
|
||
});
|
||
const {
|
||
selectPage,
|
||
selectedCount,
|
||
selectOne,
|
||
selectNone,
|
||
selectDefault,
|
||
selectAll,
|
||
areAllRowsOnPageSelected,
|
||
areAllRowsSelected,
|
||
isSelected,
|
||
inclusionSet,
|
||
exclusionSet,
|
||
selectAllMode,
|
||
} = selectAllOptions;
|
||
|
||
useEffect(() => {
|
||
if (selectedCount) {
|
||
setHostIdsParam(fetchBulkParams());
|
||
savedHostSelectionsRef.current = {
|
||
inclusionSet,
|
||
exclusionSet,
|
||
selectAllMode,
|
||
};
|
||
}
|
||
}, [selectedCount, fetchBulkParams, setHostIdsParam]);
|
||
|
||
const selectionToolbar = (
|
||
<ToolbarItem key="selectAll">
|
||
<SelectAllCheckbox
|
||
{...{
|
||
selectAll: () => {
|
||
selectAll(true);
|
||
setIsAllHostsSelected(true);
|
||
},
|
||
selectPage: () => {
|
||
selectPage();
|
||
setIsAllHostsSelected(false);
|
||
},
|
||
selectDefault: () => {
|
||
selectDefault();
|
||
setIsAllHostsSelected(false);
|
||
},
|
||
selectNone: () => {
|
||
selectNone();
|
||
setIsAllHostsSelected(false);
|
||
},
|
||
selectedCount,
|
||
pageRowCount,
|
||
}}
|
||
totalCount={subtotalCount}
|
||
selectedDefaultCount={1} // The default host (hostId) is always selected
|
||
areAllRowsOnPageSelected={areAllRowsOnPageSelected()}
|
||
areAllRowsSelected={areAllRowsSelected()}
|
||
/>
|
||
</ToolbarItem>
|
||
);
|
||
|
||
const RowSelectTd = ({ rowData }) => (
|
||
<Td
|
||
select={{
|
||
rowIndex: rowData.id,
|
||
onSelect: (_event, isSelecting) => {
|
||
selectOne(isSelecting, rowData.id, rowData);
|
||
// If at least one was unselected, then it's not all selected
|
||
if (!isSelecting) setIsAllHostsSelected(false);
|
||
},
|
||
isSelected: rowData.id === hostId || isSelected(rowData.id),
|
||
disable: rowData.id === hostId || false,
|
||
}}
|
||
/>
|
||
);
|
||
RowSelectTd.propTypes = {
|
||
rowData: PropTypes.object.isRequired,
|
||
};
|
||
|
||
const columns = {
|
||
name: {
|
||
title: __('Name'),
|
||
wrapper: ({ id, name }) => <a href={foremanUrl(`hosts/${id}`)}>{name}</a>,
|
||
isSorted: true,
|
||
},
|
||
operatingsystem_name: {
|
||
title: __('OS'),
|
||
},
|
||
};
|
||
|
||
return (
|
||
<>
|
||
<WizardHeader
|
||
title={__('Review hosts')}
|
||
description={__(
|
||
'By default, remediation is applied to the current host. Optionally, remediate any additional hosts that fail the rule.'
|
||
)}
|
||
/>
|
||
<Toolbar ouiaId="table-toolbar" className="table-toolbar">
|
||
<ToolbarContent>
|
||
<ToolbarGroup>
|
||
{selectionToolbar}
|
||
{status === STATUS.PENDING && (
|
||
<ToolbarItem>
|
||
<Spinner size="sm" />
|
||
</ToolbarItem>
|
||
)}
|
||
</ToolbarGroup>
|
||
</ToolbarContent>
|
||
</Toolbar>
|
||
<Table
|
||
ouiaId="hosts-review-table"
|
||
isEmbedded
|
||
params={params}
|
||
setParams={setParamsAndAPI}
|
||
itemCount={subtotalCount}
|
||
results={results}
|
||
url={HOSTS_API_PATH}
|
||
refreshData={() =>
|
||
setAPIOptions({
|
||
key: HOSTS_API_REQUEST_KEY,
|
||
params: { defaultFailedHostsSearch },
|
||
})
|
||
}
|
||
columns={columns}
|
||
errorMessage={
|
||
status === STATUS.ERROR && errorMessage ? errorMessage : null
|
||
}
|
||
isPending={status === STATUS.PENDING}
|
||
showCheckboxes
|
||
rowSelectTd={RowSelectTd}
|
||
/>
|
||
</>
|
||
);
|
||
};
|
||
|
||
export default ReviewHosts;
|
webpack/components/OpenscapRemediationWizard/steps/ReviewRemediation.js | ||
---|---|---|
/* eslint-disable camelcase */
|
||
import React, { useContext, useState } from 'react';
|
||
import { some } from 'lodash';
|
||
import {
|
||
CodeBlock,
|
||
CodeBlockAction,
|
||
CodeBlockCode,
|
||
ClipboardCopyButton,
|
||
Button,
|
||
Grid,
|
||
GridItem,
|
||
Alert,
|
||
Checkbox,
|
||
} from '@patternfly/react-core';
|
||
import { ExternalLinkSquareAltIcon } from '@patternfly/react-icons';
|
||
|
||
import { translate as __ } from 'foremanReact/common/I18n';
|
||
import { foremanUrl } from 'foremanReact/common/helpers';
|
||
import { getHostsPageUrl } from 'foremanReact/Root/Context/ForemanContext';
|
||
|
||
import OpenscapRemediationWizardContext from '../OpenscapRemediationWizardContext';
|
||
import WizardHeader from '../WizardHeader';
|
||
import ViewSelectedHostsLink from '../ViewSelectedHostsLink';
|
||
import { HOSTS_PATH, FAIL_RULE_SEARCH } from '../constants';
|
||
import { findFixBySnippet } from '../helpers';
|
||
|
||
import './ReviewRemediation.scss';
|
||
|
||
const ReviewRemediation = () => {
|
||
const {
|
||
fixes,
|
||
snippet,
|
||
method,
|
||
hostName,
|
||
source,
|
||
isRebootSelected,
|
||
setIsRebootSelected,
|
||
isAllHostsSelected,
|
||
hostIdsParam,
|
||
defaultFailedHostsSearch,
|
||
} = useContext(OpenscapRemediationWizardContext);
|
||
const [copied, setCopied] = useState(false);
|
||
const selectedFix = findFixBySnippet(fixes, snippet);
|
||
const snippetText = selectedFix?.full_text;
|
||
// can be one of null, "true", "false"
|
||
// if null, it may indicate that reboot might be needed
|
||
const checkForReboot = () => !some(fixes, { reboot: 'false' });
|
||
const isRebootRequired = () => some(fixes, { reboot: 'true' });
|
||
|
||
const copyToClipboard = (e, text) => {
|
||
navigator.clipboard.writeText(text.toString());
|
||
};
|
||
|
||
const onCopyClick = (e, text) => {
|
||
copyToClipboard(e, text);
|
||
setCopied(true);
|
||
};
|
||
|
||
const description =
|
||
method === 'manual'
|
||
? __('Review the remediation snippet and apply it to the host manually.')
|
||
: __(
|
||
'Review the remediation snippet that will be applied to selected host(s).'
|
||
);
|
||
|
||
const rebootAlertTitle = isRebootRequired()
|
||
? __('A reboot is required after applying remediation.')
|
||
: __('A reboot might be required after applying remediation.');
|
||
|
||
const actions = (
|
||
<React.Fragment>
|
||
<CodeBlockAction>
|
||
<ClipboardCopyButton
|
||
id="basic-copy-button"
|
||
textId="code-content"
|
||
aria-label="Copy to clipboard"
|
||
onClick={e => onCopyClick(e, snippetText)}
|
||
exitDelay={copied ? 1500 : 600}
|
||
maxWidth="110px"
|
||
variant="plain"
|
||
onTooltipHidden={() => setCopied(false)}
|
||
>
|
||
{copied
|
||
? __('Successfully copied to clipboard!')
|
||
: __('Copy to clipboard')}
|
||
</ClipboardCopyButton>
|
||
</CodeBlockAction>
|
||
</React.Fragment>
|
||
);
|
||
|
||
return (
|
||
<>
|
||
<WizardHeader
|
||
title={__('Review remediation')}
|
||
description={description}
|
||
/>
|
||
<Grid hasGutter>
|
||
<br />
|
||
<GridItem>
|
||
<Alert
|
||
ouiaId="review-alert"
|
||
variant="danger"
|
||
title={`${__(
|
||
'Do not implement any of the recommended remedial actions or scripts without first testing them in a non-production environment.'
|
||
)}
|
||
${__('Remediation might render the system non-functional.')}`}
|
||
/>
|
||
</GridItem>
|
||
<GridItem md={12} span={4} rowSpan={1}>
|
||
<ViewSelectedHostsLink
|
||
isAllHostsSelected={isAllHostsSelected}
|
||
hostIdsParam={hostIdsParam}
|
||
defaultFailedHostsSearch={defaultFailedHostsSearch}
|
||
/>
|
||
</GridItem>
|
||
<GridItem md={12} span={4} rowSpan={1}>
|
||
<Button
|
||
variant="link"
|
||
icon={<ExternalLinkSquareAltIcon />}
|
||
iconPosition="right"
|
||
target="_blank"
|
||
component="a"
|
||
href={foremanUrl(`${getHostsPageUrl(true)}/${hostName}`)}
|
||
>
|
||
{hostName}
|
||
</Button>{' '}
|
||
</GridItem>
|
||
<GridItem md={12} span={8} rowSpan={1}>
|
||
<Button
|
||
variant="link"
|
||
icon={<ExternalLinkSquareAltIcon />}
|
||
iconPosition="right"
|
||
target="_blank"
|
||
component="a"
|
||
href={foremanUrl(
|
||
`${HOSTS_PATH}/?search=${FAIL_RULE_SEARCH} = ${source}`
|
||
)}
|
||
>
|
||
{__('Other hosts failing this rule')}
|
||
</Button>
|
||
</GridItem>
|
||
{checkForReboot() ? (
|
||
<>
|
||
<GridItem span={12} rowSpan={1}>
|
||
<Alert
|
||
ouiaId="reboot-alert"
|
||
variant={isRebootRequired() ? 'warning' : 'info'}
|
||
title={rebootAlertTitle}
|
||
/>
|
||
</GridItem>
|
||
{method === 'manual' ? null : (
|
||
<GridItem span={4} rowSpan={1}>
|
||
<Checkbox
|
||
id="reboot-checkbox"
|
||
label={__('Reboot the system(s)')}
|
||
name="reboot-checkbox"
|
||
isChecked={isRebootSelected}
|
||
onChange={selected => setIsRebootSelected(selected)}
|
||
/>
|
||
</GridItem>
|
||
)}
|
||
</>
|
||
) : null}
|
||
<GridItem>
|
||
<CodeBlock actions={actions}>
|
||
<CodeBlockCode id="code-content" className="remediation-code">
|
||
{snippetText}
|
||
</CodeBlockCode>
|
||
</CodeBlock>
|
||
</GridItem>
|
||
</Grid>
|
||
</>
|
||
);
|
||
};
|
||
|
||
export default ReviewRemediation;
|
webpack/components/OpenscapRemediationWizard/steps/ReviewRemediation.scss | ||
---|---|---|
pre.remediation-code {
|
||
border: none;
|
||
border-radius: none;
|
||
}
|
webpack/components/OpenscapRemediationWizard/steps/SnippetSelect.js | ||
---|---|---|
import React, { useContext } from 'react';
|
||
import { map, split, capitalize, join, slice, isEmpty } from 'lodash';
|
||
import {
|
||
Form,
|
||
FormGroup,
|
||
FormSelect,
|
||
FormSelectOption,
|
||
Radio,
|
||
Alert,
|
||
} from '@patternfly/react-core';
|
||
|
||
import { translate as __ } from 'foremanReact/common/I18n';
|
||
import { STATUS } from 'foremanReact/constants';
|
||
import Loading from 'foremanReact/components/Loading';
|
||
|
||
import OpenscapRemediationWizardContext from '../OpenscapRemediationWizardContext';
|
||
import WizardHeader from '../WizardHeader';
|
||
import EmptyState from '../../EmptyState';
|
||
import { errorMsg, supportedRemediationSnippets } from '../helpers';
|
||
|
||
const SnippetSelect = () => {
|
||
const {
|
||
fixes,
|
||
snippet,
|
||
setSnippet,
|
||
method,
|
||
setMethod,
|
||
logStatus,
|
||
logError,
|
||
supportedJobSnippets,
|
||
} = useContext(OpenscapRemediationWizardContext);
|
||
|
||
const snippetNameMap = {
|
||
'urn:xccdf:fix:script:ansible': 'Ansible',
|
||
'urn:xccdf:fix:script:puppet': 'Puppet',
|
||
'urn:xccdf:fix:script:sh': 'Shell',
|
||
'urn:xccdf:fix:script:kubernetes': 'Kubernetes',
|
||
'urn:redhat:anaconda:pre': 'Anaconda',
|
||
'urn:redhat:osbuild:blueprint': 'OSBuild Blueprint',
|
||
};
|
||
|
||
const snippetName = system => {
|
||
const mapped = snippetNameMap[system];
|
||
if (mapped) return mapped;
|
||
|
||
return join(
|
||
map(slice(split(system, ':'), -2), n => capitalize(n)),
|
||
' '
|
||
);
|
||
};
|
||
|
||
const resetSnippet = meth => {
|
||
const snip = supportedRemediationSnippets(
|
||
fixes,
|
||
meth,
|
||
supportedJobSnippets
|
||
)[0];
|
||
setSnippet(snip);
|
||
return snip;
|
||
};
|
||
|
||
const setMethodResetSnippet = meth => {
|
||
setMethod(meth);
|
||
resetSnippet(meth);
|
||
};
|
||
|
||
const body =
|
||
logStatus === STATUS.RESOLVED ? (
|
||
<Form>
|
||
<FormGroup
|
||
label={__('Method')}
|
||
type="string"
|
||
fieldId="method"
|
||
isRequired={false}
|
||
>
|
||
<Radio
|
||
label={__('Remote job')}
|
||
id="job"
|
||
name="job"
|
||
ouiaId="job"
|
||
aria-label="job"
|
||
isChecked={method === 'job'}
|
||
onChange={() => setMethodResetSnippet('job')}
|
||
/>
|
||
<Radio
|
||
label={__('Manual')}
|
||
id="manual"
|
||
name="manual"
|
||
ouiaId="manual"
|
||
aria-label="manual"
|
||
isChecked={method === 'manual'}
|
||
onChange={() => setMethodResetSnippet('manual')}
|
||
/>
|
||
</FormGroup>
|
||
{isEmpty(
|
||
supportedRemediationSnippets(fixes, method, supportedJobSnippets)
|
||
) ? (
|
||
<Alert
|
||
ouiaId="snippet-alert"
|
||
variant="info"
|
||
title={__(
|
||
'There is no job to remediate with. Please remediate manually.'
|
||
)}
|
||
/>
|
||
) : (
|
||
<FormGroup
|
||
label={__('Snippet')}
|
||
type="string"
|
||
fieldId="snippet"
|
||
isRequired
|
||
>
|
||
<FormSelect
|
||
ouiaId="snippet-select"
|
||
isRequired
|
||
value={snippet}
|
||
onChange={value => setSnippet(value)}
|
||
aria-label="FormSelect Input"
|
||
>
|
||
<FormSelectOption
|
||
isDisabled
|
||
key={0}
|
||
value=""
|
||
label={__('Select snippet')}
|
||
/>
|
||
{map(
|
||
supportedRemediationSnippets(
|
||
fixes,
|
||
method,
|
||
supportedJobSnippets
|
||
),
|
||
fix => (
|
||
<FormSelectOption
|
||
key={fix}
|
||
value={fix}
|
||
label={snippetName(fix)}
|
||
/>
|
||
)
|
||
)}
|
||
</FormSelect>
|
||
</FormGroup>
|
||
)}
|
||
</Form>
|
||
) : (
|
||
<EmptyState error title={__('Error!')} body={errorMsg(logError)} />
|
||
);
|
||
|
||
return (
|
||
<>
|
||
<WizardHeader
|
||
title={__('Select remediation method')}
|
||
description={__(
|
||
'You can remediate by running a remote job or you can display a snippet for manual remediation.'
|
||
)}
|
||
/>
|
||
{logStatus === STATUS.PENDING ? <Loading /> : body}
|
||
</>
|
||
);
|
||
};
|
||
|
||
export default SnippetSelect;
|
webpack/components/OpenscapRemediationWizard/steps/index.js | ||
---|---|---|
export { default as SnippetSelect } from './SnippetSelect';
|
||
export { default as ReviewHosts } from './ReviewHosts';
|
||
export { default as ReviewRemediation } from './ReviewRemediation';
|
||
export { default as Finish } from './Finish';
|
webpack/index.js | ||
---|---|---|
import componentRegistry from 'foremanReact/components/componentRegistry';
|
||
|
||
import RuleSeverity from './components/RuleSeverity';
|
||
import OpenscapRemediationWizard from './components/OpenscapRemediationWizard';
|
||
|
||
componentRegistry.register({
|
||
name: 'RuleSeverity',
|
||
type: RuleSeverity,
|
||
const components = [
|
||
{ name: 'RuleSeverity', type: RuleSeverity },
|
||
{ name: 'OpenscapRemediationWizard', type: OpenscapRemediationWizard },
|
||
];
|
||
|
||
components.forEach(component => {
|
||
componentRegistry.register(component);
|
||
});
|
Also available in: Unified diff
Fixes #36738 - Add remediation wizard (#546)