Skip to content

Commit

Permalink
Fixes #36738 - Add remediation wizard
Browse files Browse the repository at this point in the history
  • Loading branch information
ofedoren committed Oct 12, 2023
1 parent 0397578 commit 067f71c
Show file tree
Hide file tree
Showing 24 changed files with 1,150 additions and 15 deletions.
5 changes: 5 additions & 0 deletions app/assets/javascripts/foreman_openscap/reports.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,8 @@ function showReportDetails(log_id, event) {
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();
}
25 changes: 23 additions & 2 deletions app/controllers/arf_reports_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ class ArfReportsController < ApplicationController
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
Expand Down Expand Up @@ -72,6 +72,27 @@ def submit_delete_multiple
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
Expand Down Expand Up @@ -99,7 +120,7 @@ def find_multiple

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
Expand Down
21 changes: 17 additions & 4 deletions app/helpers/arf_reports_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,22 @@ def reported_info(arf_report)
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
13 changes: 11 additions & 2 deletions app/models/foreman_openscap/arf_report.rb
Original file line number Diff line number Diff line change
Expand Up @@ -131,14 +131,16 @@ def self.create_arf(asset, proxy, params)
: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
Expand Down Expand Up @@ -222,12 +224,19 @@ def self.references_links(references)
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
Expand Down
6 changes: 5 additions & 1 deletion app/views/arf_reports/_output.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
</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%>>
Expand All @@ -35,3 +35,7 @@
</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 } %>
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<%#
name: Run OpenSCAP remediation - Ansible Default
job_category: OpenSCAP Ansible Commands
description_format: Run OpenSCAP remediation on given host
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 %>
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<%#
name: Run OpenSCAP remediation - Script Default
job_category: OpenSCAP
description_format: Run OpenSCAP remediation on given host
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."
reboot
<% end -%>
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
get 'parse_html'
get 'parse_bzip'
get 'download_html'
get 'show_log'
end
collection do
get 'auto_complete_search'
Expand Down
5 changes: 5 additions & 0 deletions db/migrate/20230912122310_add_fixes_to_message.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddFixesToMessage < ActiveRecord::Migration[6.1]
def change
add_column :messages, :fixes, :text
end
end
20 changes: 18 additions & 2 deletions lib/foreman_openscap/engine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class Engine < ::Rails::Engine

initializer 'foreman_openscap.register_plugin', :before => :finisher_hook do |app|
Foreman::Plugin.register :foreman_openscap do
requires_foreman '>= 3.7'
requires_foreman '>= 3.9'
register_gettext

apipie_documented_controllers ["#{ForemanOpenscap::Engine.root}/app/controllers/api/v2/compliance/*.rb"]
Expand All @@ -58,7 +58,7 @@ class Engine < ::Rails::Engine

# 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] },
Expand Down Expand Up @@ -275,13 +275,25 @@ class Engine < ::Rails::Engine
: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
end

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

Expand All @@ -303,4 +315,8 @@ def self.use_relative_model_naming?
def self.with_remote_execution?
RemoteExecutionFeature rescue false
end

def self.with_ansible?
ForemanAnsible rescue false
end
end
2 changes: 1 addition & 1 deletion webpack/components/EmptyState.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ EmptyStateIcon.defaultProps = {

EmptyState.propTypes = {
title: PropTypes.string,
body: PropTypes.string,
body: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
error: PropTypes.oneOfType([PropTypes.shape({}), PropTypes.string]),
search: PropTypes.bool,
lock: PropTypes.bool,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import {
selectAPIError,
selectAPIResponse,
selectAPIStatus,
} from 'foremanReact/redux/API/APISelectors';
import { STATUS } from 'foremanReact/constants';
import {
JOB_INVOCATION_API_REQUEST_KEY,
REPORT_LOG_REQUEST_KEY,
} from './constants';

export const selectRemediationResponse = state =>
selectAPIResponse(state, JOB_INVOCATION_API_REQUEST_KEY) || {};

export const selectRemediationStatus = state =>
selectAPIStatus(state, JOB_INVOCATION_API_REQUEST_KEY) || STATUS.PENDING;

export const selectRemediationError = state =>
selectAPIError(state, JOB_INVOCATION_API_REQUEST_KEY);

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);
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { createContext } from 'react';

const OpenscapRemediationWizardContext = createContext({});
export default OpenscapRemediationWizardContext;
43 changes: 43 additions & 0 deletions webpack/components/OpenscapRemediationWizard/WizardHeader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
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;
15 changes: 15 additions & 0 deletions webpack/components/OpenscapRemediationWizard/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export const OPENSCAP_REMEDIATION_MODAL_ID = 'openscapRemediationModal';
export const NEW_HOSTS_PATH = '/new/hosts';
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';
31 changes: 31 additions & 0 deletions webpack/components/OpenscapRemediationWizard/helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { join, find, map, compact, includes, filter } 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 = error => {
join(getResponseErrorMsgs(error?.response || {}), '\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
)
);
};
Loading

0 comments on commit 067f71c

Please sign in to comment.