diff --git a/.rubocop.yml b/.rubocop.yml index 383565f11..1fde39066 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -18,3 +18,11 @@ Style/HashSyntax: Metrics/LineLength: Enabled: false + +Style/CollectionMethods: + PreferredMethods: + find: find + detect: find + +Style/FrozenStringLiteralComment: + Enabled: false diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 16bec61c1..014aa6afd 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -258,3 +258,6 @@ Performance/Count: Style/StringLiteralsInInterpolation: Enabled: false + +Style/FrozenStringLiteralComment: + Enabled: false \ No newline at end of file diff --git a/app/assets/javascripts/foreman_openscap/policy_edit.js b/app/assets/javascripts/foreman_openscap/policy_edit.js index e54831ca3..c051eb53a 100644 --- a/app/assets/javascripts/foreman_openscap/policy_edit.js +++ b/app/assets/javascripts/foreman_openscap/policy_edit.js @@ -13,6 +13,21 @@ function scap_content_selected(element){ }) } +function tailoring_file_selected(element) { + var attrs = attribute_hash(['tailoring_file_id']); + var url = $(element).attr('data-url'); + tfm.tools.showSpinner(); + $.ajax({ + data: attrs, + type: 'post', + url: url, + complete: function() { reloadOnAjaxComplete($(element));}, + success: function(request) { + $('#tailoring_file_profile_select').html(request); + } + }) +} + function previous_step(previous) { $('#policy_current_step').val(previous); $('#new_policy').submit(); diff --git a/app/controllers/api/v2/compliance/policies_controller.rb b/app/controllers/api/v2/compliance/policies_controller.rb index d8f992b25..23677792a 100644 --- a/app/controllers/api/v2/compliance/policies_controller.rb +++ b/app/controllers/api/v2/compliance/policies_controller.rb @@ -4,7 +4,7 @@ class PoliciesController < ::Api::V2::BaseController include Foreman::Controller::SmartProxyAuth include Foreman::Controller::Parameters::PolicyApi - add_smart_proxy_filters :content, :features => 'Openscap' + add_smart_proxy_filters [:content, :tailoring], :features => 'Openscap' before_filter :find_resource, :except => %w(index create) @@ -83,6 +83,16 @@ def content :filename => @scap_content.original_filename end + api :GET, '/compliance/policies/:id/tailoring', N_("Show a policy's Tailoring file") + param :id, :identifier, :required => true + + def tailoring + @tailoring_file = @policy.tailoring_file + send_data @tailoring_file.scap_file, + :type => 'application/xml', + :filename => @tailoring_file.original_filename + end + private def find_resource not_found and return if params[:id].blank? @@ -91,7 +101,7 @@ def find_resource def action_permission case params[:action] - when 'content' + when 'content', 'tailoring' :view else super diff --git a/app/controllers/concerns/foreman/controller/parameters/policy_api.rb b/app/controllers/concerns/foreman/controller/parameters/policy_api.rb index 201355c6d..dbb544be8 100644 --- a/app/controllers/concerns/foreman/controller/parameters/policy_api.rb +++ b/app/controllers/concerns/foreman/controller/parameters/policy_api.rb @@ -4,8 +4,8 @@ module Foreman::Controller::Parameters::PolicyApi class_methods do def filter_params_list [:description, :name, :period, :scap_content_id, :scap_content_profile_id, - :weekday, :day_of_month, :cron_line, :location_ids => [], :organization_ids => [], - :hostgroup_ids => []] + :weekday, :day_of_month, :cron_line, :tailoring_file_id, :tailoring_file_profile_id, + :location_ids => [], :organization_ids => [], :hostgroup_ids => []] end def policy_params_filter diff --git a/app/controllers/concerns/foreman/controller/parameters/tailoring_file.rb b/app/controllers/concerns/foreman/controller/parameters/tailoring_file.rb new file mode 100644 index 000000000..1a4192227 --- /dev/null +++ b/app/controllers/concerns/foreman/controller/parameters/tailoring_file.rb @@ -0,0 +1,15 @@ +module Foreman::Controller::Parameters::TailoringFile + extend ActiveSupport::Concern + + class_methods do + def tailoring_file_params_filter + Foreman::ParameterFilter.new(::ForemanOpenscap::TailoringFile).tap do |filter| + filter.permit :name, :scap_file, :original_filename, :location_ids => [], :organization_ids => [] + end + end + end + + def tailoring_file_params + self.class.tailoring_file_params_filter.filter_params(params, parameter_filter_context) + end +end diff --git a/app/controllers/policies_controller.rb b/app/controllers/policies_controller.rb index 2f5070131..4bab78026 100644 --- a/app/controllers/policies_controller.rb +++ b/app/controllers/policies_controller.rb @@ -4,16 +4,16 @@ class PoliciesController < ApplicationController before_filter :find_by_id, :only => [:show, :edit, :update, :parse, :destroy] before_filter :find_multiple, :only => [:select_multiple_hosts, :update_multiple_hosts, :disassociate_multiple_hosts, :remove_policy_from_multiple_hosts] + before_filter :find_tailoring_file, :only => [:tailoring_file_selected] def model_of_controller ::ForemanOpenscap::Policy end def index - @policies = resource_base - .search_for(params[:search], :order => params[:order]) - .paginate(:page => params[:page], :per_page => params[:per_page]) - .includes(:scap_content, :scap_content_profile) + @policies = resource_base.search_for(params[:search], :order => params[:order]) + .paginate(:page => params[:page], :per_page => params[:per_page]) + .includes(:scap_content, :scap_content_profile, :tailoring_file) if @policies.empty? && ForemanOpenscap::ScapContent.unconfigured? redirect_to scap_contents_path end @@ -70,6 +70,11 @@ def scap_content_selected end end + def tailoring_file_selected + @policy ||= ::ForemanOpenscap::Policy.new + render :partial => 'tailoring_file_selected', :locals => { :policy => @policy, :tailoring_file => @tailoring_file } + end + def select_multiple_hosts; end def update_multiple_hosts @@ -114,6 +119,10 @@ def find_by_id @policy = resource_base.find(params[:id]) end + def find_tailoring_file + @tailoring_file = ForemanOpenscap::TailoringFile.find(params[:tailoring_file_id]) if params[:tailoring_file_id].present? + end + def find_multiple # Lets search by name or id and make sure one of them exists first if params[:host_ids].present? diff --git a/app/controllers/tailoring_files_controller.rb b/app/controllers/tailoring_files_controller.rb new file mode 100644 index 000000000..07e97885f --- /dev/null +++ b/app/controllers/tailoring_files_controller.rb @@ -0,0 +1,60 @@ +class TailoringFilesController < ApplicationController + include Foreman::Controller::AutoCompleteSearch + include Foreman::Controller::Parameters::TailoringFile + + before_filter :find_tailoring_file, :only => [:destroy, :update, :edit] + before_filter :handle_file_upload, :only => [:create, :update] + + def model_of_controller + ::ForemanOpenscap::TailoringFile + end + + def index + @tailoring_files = resource_base.search_for(params[:search], :order => params[:order]) + .paginate(:page => params[:page], :per_page => params[:per_page]) + end + + def new + @tailoring_file = ::ForemanOpenscap::TailoringFile.new + end + + def create + @tailoring_file = ForemanOpenscap::TailoringFile.new(tailoring_file_params) + if @tailoring_file.save + process_success + else + process_error + end + end + + def edit + end + + def update + if @tailoring_file.update_attributes(tailoring_file_params) + process_success + else + process_error + end + end + + def destroy + if @tailoring_file.destroy + process_success + else + process_error :object => @tailoring_file + end + end + + private + + def find_tailoring_file + @tailoring_file = resource_base.find(params[:id]) + end + + def handle_file_upload + return unless params[:tailoring_file] && raw_file = params[:tailoring_file][:scap_file] + params[:tailoring_file][:original_filename] = raw_file.original_filename + params[:tailoring_file][:scap_file] = raw_file.tempfile.read if raw_file.respond_to?(:tempfile) && raw_file.tempfile.respond_to?(:read) + end +end diff --git a/app/helpers/policies_helper.rb b/app/helpers/policies_helper.rb index 2fea77f83..2a31f7ba0 100644 --- a/app/helpers/policies_helper.rb +++ b/app/helpers/policies_helper.rb @@ -5,6 +5,14 @@ def profiles_selection return [] end + def policy_profile_from_scap_content(policy) + policy.scap_content_profile.nil? ? "Default" : policy.scap_content_profile.title + end + + def effective_policy_profile(policy) + policy.tailoring_file ? policy.tailoring_file_profile.title : policy_profile_from_scap_content(policy) + end + def scap_content_selector(form) scap_contents = ::ForemanOpenscap::ScapContent.all if scap_contents.length > 1 @@ -38,6 +46,21 @@ def scap_content_profile_selector(form) end end + def tailoring_file_selector(form) + select_f form, :tailoring_file_id, ForemanOpenscap::TailoringFile.all, :id, :name, + { :include_blank => _('Choose Tailoring File') }, + { :label => _('Tailoring File'), + :onchange => 'tailoring_file_selected(this)', + :'data-url' => method_path('tailoring_file_selected') } + end + + def tailoring_file_profile_selector(form, tailoring_file) + select_f form, :tailoring_file_profile_id, tailoring_file.scap_content_profiles, :id, :title, + { :selected => tailoring_file.scap_content_profiles.first.id }, + { :label => _("XCCDF Profile in Tailoring File"), + :help_inline => _("This profile will be used to override the one from scap content") } + end + def submit_or_cancel_policy(form, overwrite = nil, args = { }) args[:cancel_path] ||= send("#{controller_name}_path") content_tag(:div, :class => "clearfix") do diff --git a/app/lib/proxy_api/openscap.rb b/app/lib/proxy_api/openscap.rb index 52c900ad2..126936afa 100644 --- a/app/lib/proxy_api/openscap.rb +++ b/app/lib/proxy_api/openscap.rb @@ -10,8 +10,12 @@ def fetch_policies_for_scap_content(scap_file) parse(post(scap_file, "scap_content/policies")) end - def validate_scap_content(scap_file) - parse(post(scap_file, "scap_content/validator")) + def fetch_profiles_for_tailoring_file(scap_file) + parse(post(scap_file, "tailoring_file/profiles")) + end + + def validate_scap_file(scap_file, type) + parse(post(scap_file, "scap_file/validator/#{type}")) end def policy_html_guide(scap_file, policy) diff --git a/app/models/concerns/foreman_openscap/data_stream_content.rb b/app/models/concerns/foreman_openscap/data_stream_content.rb new file mode 100644 index 000000000..3fde870a3 --- /dev/null +++ b/app/models/concerns/foreman_openscap/data_stream_content.rb @@ -0,0 +1,43 @@ +module ForemanOpenscap + module DataStreamContent + require 'digest/sha2' + + extend ActiveSupport::Concern + + included do + validates :digest, :presence => true, :uniqueness => true + validates :scap_file, :presence => true + + validates_with ForemanOpenscap::DataStreamValidator + + after_save :create_profiles + + before_validation :redigest, :if => lambda { |ds_content| ds_content.persisted? && ds_content.scap_file_changed? } + before_destroy ActiveRecord::Base::EnsureNotUsedBy.new(:policies) + end + + def proxy_url + @proxy_url ||= SmartProxy.with_features('Openscap').find do |proxy| + available = ProxyAPI::AvailableProxy.new(:url => proxy.url) + available.available? + end.try(:url) + @proxy_url + end + + def digest + self[:digest] ||= Digest::SHA256.hexdigest(scap_file.to_s) + end + + private + + def redigest + self[:digest] = Digest::SHA256.hexdigest(scap_file.to_s) + end + + def create_profiles + fetch_profiles.each do |key, title| + ScapContentProfile.where(:profile_id => key, :title => title, "#{self.class.to_s.demodulize.underscore}_id".to_sym => id).first_or_create + end + end + end +end diff --git a/app/models/foreman_openscap/policy.rb b/app/models/foreman_openscap/policy.rb index 09830adc5..b29a97291 100644 --- a/app/models/foreman_openscap/policy.rb +++ b/app/models/foreman_openscap/policy.rb @@ -6,6 +6,8 @@ class Policy < ActiveRecord::Base belongs_to :scap_content belongs_to :scap_content_profile + belongs_to :tailoring_file + belongs_to :tailoring_file_profile, :class_name => ForemanOpenscap::ScapContentProfile has_many :policy_arf_reports has_many :arf_reports, :through => :policy_arf_reports, :dependent => :destroy has_many :asset_policies @@ -166,9 +168,11 @@ def unassign_hosts(hosts) def to_enc { 'id' => self.id, - 'profile_id' => self.scap_content_profile.try(:profile_id) || '', + 'profile_id' => profile_for_scan, 'content_path' => "/var/lib/openscap/content/#{self.scap_content.digest}.xml", - 'download_path' => "/compliance/policies/#{self.id}/content" # default to proxy path + 'tailoring_path' => tailoring_file ? "/var/lib/openscap/tailoring/#{self.tailoring_file.digest}.xml" : '', + 'download_path' => "/compliance/policies/#{self.id}/content", # default to proxy path + 'tailoring_download_path' => "/compliance/policies/#{self.id}/tailoring" }.merge(period_enc) end @@ -283,6 +287,16 @@ def assign_policy_to_hostgroups end end + def profile_for_scan + if tailoring_file_profile + tailoring_file_profile.profile_id + elsif scap_content_profile + scap_content_profile.profile_id + else + '' + end + end + def find_scap_puppetclass Puppetclass.find_by_name(SCAP_PUPPET_CLASS) end diff --git a/app/models/foreman_openscap/scap_content.rb b/app/models/foreman_openscap/scap_content.rb index ca3c6d89b..e2586c3af 100644 --- a/app/models/foreman_openscap/scap_content.rb +++ b/app/models/foreman_openscap/scap_content.rb @@ -1,56 +1,13 @@ -require 'digest/sha2' - module ForemanOpenscap - class DataStreamValidator < ActiveModel::Validator - def validate(scap_content) - return unless scap_content.scap_file_changed? - - unless SmartProxy.with_features('Openscap').any? - scap_content.errors.add(:base, _('No proxy with OpenSCAP features')) - return false - end - - if scap_content.proxy_url.nil? - scap_content.errors.add(:base, _('No available proxy to validate SCAP content')) - return false - end - - begin - api = ProxyAPI::Openscap.new(:url => scap_content.proxy_url) - errors = api.validate_scap_content(scap_content.scap_file) - if errors && errors['errors'].any? - errors['errors'].each { |error| scap_content.errors.add(:scap_file, _(error)) } - return false - end - rescue *ProxyAPI::AvailableProxy::HTTP_ERRORS => e - scap_content.errors.add(:base, _('No available proxy to validate. Returned with error: %s') % e) - return false - end - - - unless (scap_content.scap_content_profiles.map(&:profile_id) - scap_content.fetch_profiles.keys).empty? - scap_content.errors.add(:scap_file, _('Changed file does not include existing SCAP content profiles')) - return false - end - end - end - class ScapContent < ActiveRecord::Base include Authorizable include Taxonomix + include DataStreamContent has_many :scap_content_profiles, :dependent => :destroy has_many :policies - before_destroy EnsureNotUsedBy.new(:policies) - - validates_with DataStreamValidator validates :title, :presence => true, :length => { :maximum => 255 } - validates :digest, :presence => true - validates :scap_file, :presence => true - - after_save :create_profiles - before_validation :redigest, :if => lambda { |scap_content| scap_content.persisted? && scap_content.scap_file_changed? } scoped_search :on => :title, :complete_value => true scoped_search :on => :original_filename, :complete_value => true, :rename => :filename @@ -77,8 +34,9 @@ def to_label title end - def digest - self[:digest] ||= Digest::SHA256.hexdigest "#{scap_file}" + def as_json(*args) + hash = super + hash["scap_file"] = scap_file.to_s.encode('utf-8', :invalid => :replace, :undef => :replace, :replace => '_') end def fetch_profiles @@ -86,31 +44,5 @@ def fetch_profiles profiles = api.fetch_policies_for_scap_content(scap_file) profiles end - - def proxy_url - @proxy_url ||= SmartProxy.with_features('Openscap').find do |proxy| - available = ProxyAPI::AvailableProxy.new(:url => proxy.url) - available.available? - end.try(:url) - @proxy_url - end - - def as_json(*args) - hash = super - hash["scap_file"] = scap_file.to_s.encode('utf-8', :invalid => :replace, :undef => :replace, :replace => '_') - end - - private - - def create_profiles - profiles = fetch_profiles - profiles.each {|key, title| - scap_content_profiles.where(:profile_id => key, :title => title).first_or_create - } - end - - def redigest - self[:digest] = Digest::SHA256.hexdigest "#{scap_file}" - end end end diff --git a/app/models/foreman_openscap/scap_content_profile.rb b/app/models/foreman_openscap/scap_content_profile.rb index 16551ceb2..8492caea9 100644 --- a/app/models/foreman_openscap/scap_content_profile.rb +++ b/app/models/foreman_openscap/scap_content_profile.rb @@ -2,5 +2,7 @@ module ForemanOpenscap class ScapContentProfile < ActiveRecord::Base belongs_to :scap_content has_many :policies + belongs_to :tailoring_file + has_many :tailoring_file_policies, :class_name => ForemanOpenscap::Policy end end diff --git a/app/models/foreman_openscap/tailoring_file.rb b/app/models/foreman_openscap/tailoring_file.rb new file mode 100644 index 000000000..513e5b790 --- /dev/null +++ b/app/models/foreman_openscap/tailoring_file.rb @@ -0,0 +1,20 @@ +module ForemanOpenscap + class TailoringFile < ActiveRecord::Base + include Authorizable + include Taxonomix + include DataStreamContent + + has_many :policies + has_many :scap_content_profiles, :dependent => :destroy + validates :name, :presence => true, :uniqueness => true, :length => { :maximum => 255 } + validates :original_filename, :presence => :true + + scoped_search :on => :name, :complete_value => true + scoped_search :on => :original_filename, :complete_value => true, :rename => :filename + + def fetch_profiles + api = ProxyAPI::Openscap.new(:url => proxy_url) + api.fetch_profiles_for_tailoring_file(scap_file) + end + end +end diff --git a/app/validators/foreman_openscap/data_stream_validator.rb b/app/validators/foreman_openscap/data_stream_validator.rb new file mode 100644 index 000000000..0ab8a117c --- /dev/null +++ b/app/validators/foreman_openscap/data_stream_validator.rb @@ -0,0 +1,44 @@ +module ForemanOpenscap + class DataStreamValidator < ActiveModel::Validator + def validate(data_stream_content) + return unless data_stream_content.scap_file_changed? + + content_type = data_type(data_stream_content) + + unless SmartProxy.with_features('Openscap').any? + data_stream_content.errors.add(:base, _('No proxy with OpenSCAP features')) + return false + end + + if data_stream_content.proxy_url.nil? + data_stream_content.errors.add(:base, _('No available proxy to validate SCAP data stream file')) + return false + end + + begin + api = ProxyAPI::Openscap.new(:url => data_stream_content.proxy_url) + errors = api.validate_scap_file(data_stream_content.scap_file, content_type) + if errors && errors['errors'].any? + errors['errors'].each { |error| data_stream_content.errors.add(:scap_file, _(error)) } + return false + end + rescue *ProxyAPI::AvailableProxy::HTTP_ERRORS => e + data_stream_content.errors.add(:base, _('No available proxy to validate. Returned with error: %s') % e) + return false + end + + is_scap_content = content_type == 'scap_content' + + if is_scap_content && !(data_stream_content.scap_content_profiles.map(&:profile_id) - data_stream_content.fetch_profiles.keys).empty? + data_stream_content.errors.add(:scap_file, _('Changed file does not include existing SCAP content profiles')) + return false + end + end + + private + + def data_type(data_stream_content) + data_stream_content.class.to_s.demodulize.underscore + end + end +end diff --git a/app/views/policies/_form.html.erb b/app/views/policies/_form.html.erb index 24db89079..25a0ef605 100644 --- a/app/views/policies/_form.html.erb +++ b/app/views/policies/_form.html.erb @@ -26,6 +26,15 @@ <%= scap_content_profile_selector(f) %> + + <%= tailoring_file_selector(f) %> + + + <% if @policy.tailoring_file %> + <%= render 'tailoring_file_selected', :f => f, :policy => @policy, :tailoring_file => @policy.tailoring_file %> + <% end %> + +
<%= select_f(f, :period, %w[Weekly Monthly Custom], :downcase, :to_s, diff --git a/app/views/policies/_list.html.erb b/app/views/policies/_list.html.erb index a20fc4640..fd4dd2c52 100644 --- a/app/views/policies/_list.html.erb +++ b/app/views/policies/_list.html.erb @@ -1,8 +1,10 @@ - - - + + + + + <% for policy in @policies %> @@ -17,7 +19,17 @@ <% end %> + +
NameContentProfile<%= _('Name') %><%= _('Content') %><%= _('Profile') %><%= _('Tailoring File') %><%= _('Effective Profile') %>
- <%= policy.scap_content_profile.nil? ? "Default" : policy.scap_content_profile.title %> + <%= policy_profile_from_scap_content policy %> + + <% if policy.tailoring_file %> + <%= link_to_if_authorized policy.tailoring_file.name, hash_for_edit_tailoring_file_path(:id => policy.tailoring_file_id) %> + <% else %> + <%= _('None') %> + <% end%> + + <%= effective_policy_profile policy %> <%= action_buttons( diff --git a/app/views/policies/_tailoring_file_selected.html.erb b/app/views/policies/_tailoring_file_selected.html.erb new file mode 100644 index 000000000..a7e073c82 --- /dev/null +++ b/app/views/policies/_tailoring_file_selected.html.erb @@ -0,0 +1,5 @@ +<%= fields_for policy do |f| %> + <% if tailoring_file %> + <%= tailoring_file_profile_selector(f, tailoring_file) %> + <% end %> +<% end %> diff --git a/app/views/policies/steps/_scap_content_form.html.erb b/app/views/policies/steps/_scap_content_form.html.erb index d6c0ddb7a..b30d2bb7a 100644 --- a/app/views/policies/steps/_scap_content_form.html.erb +++ b/app/views/policies/steps/_scap_content_form.html.erb @@ -1,7 +1,10 @@
<%= wizard_header @policy.step_index, *@policy.steps %> - <%= scap_content_selector(f) %> + <%= scap_content_selector(f) %> + + <%= tailoring_file_selector(f) %> + <%= scap_content_profile_selector(f) %> diff --git a/app/views/tailoring_files/_form.html.erb b/app/views/tailoring_files/_form.html.erb new file mode 100644 index 000000000..a81057056 --- /dev/null +++ b/app/views/tailoring_files/_form.html.erb @@ -0,0 +1,25 @@ +<%= form_for @tailoring_file, + :url => (@tailoring_file.id? ? + tailoring_file_path(:id => @tailoring_file.id) : tailoring_files_path), + :html => { :multipart => true } do |f| %> + + <%= base_errors_for @tailoring_file %> + + +
+
+ <%= text_f(f, :name) %> + <%= file_field_f f, :scap_file, :help_block => _("Upload DataStream Tailoring file") %> +
+ <%= render 'taxonomies/loc_org_tabs', :f => f, :obj => @tailoring_file %> + <%= submit_or_cancel f %> +
+<% end %> diff --git a/app/views/tailoring_files/_list.html.erb b/app/views/tailoring_files/_list.html.erb new file mode 100644 index 000000000..3c11b84da --- /dev/null +++ b/app/views/tailoring_files/_list.html.erb @@ -0,0 +1,24 @@ + + + + + + + <% @tailoring_files.each do |file| %> + + + + + + <% end %> +
<%= _('Name')%><%= _('Filename') %>
+ <%= file.name %> + + <%= file.original_filename %> + + <%= action_buttons( + display_link_if_authorized(_("Edit"), hash_for_edit_tailoring_file_path(:id => file.id)), + display_delete_if_authorized(hash_for_tailoring_file_path(:id => file.id), + :confirm => _("Delete tailoring file %s?") % file.name) + ) %> +
diff --git a/app/views/tailoring_files/edit.html.erb b/app/views/tailoring_files/edit.html.erb new file mode 100644 index 000000000..cf2727ebc --- /dev/null +++ b/app/views/tailoring_files/edit.html.erb @@ -0,0 +1,3 @@ +<% title _("Edit Tailoring File") %> + +<%= render :partial => 'form' %> diff --git a/app/views/tailoring_files/index.html.erb b/app/views/tailoring_files/index.html.erb new file mode 100644 index 000000000..ab49db656 --- /dev/null +++ b/app/views/tailoring_files/index.html.erb @@ -0,0 +1,3 @@ +<% title _("Tailoring Files") %> +<% title_actions(display_link_if_authorized(_("Upload New Tailoring file"), hash_for_new_tailoring_file_path, :class => 'btn btn-default')) %> +<%= render :partial => 'list' %> diff --git a/app/views/tailoring_files/new.html.erb b/app/views/tailoring_files/new.html.erb new file mode 100644 index 000000000..c57280642 --- /dev/null +++ b/app/views/tailoring_files/new.html.erb @@ -0,0 +1,3 @@ +<% title _("Upload new Tailoring File") %> + +<%= render :partial => 'form' %> diff --git a/app/views/tailoring_files/welcome.html.erb b/app/views/tailoring_files/welcome.html.erb new file mode 100644 index 000000000..9183b01bc --- /dev/null +++ b/app/views/tailoring_files/welcome.html.erb @@ -0,0 +1,15 @@ +<% content_for(:title, _("Tailoring Files")) %> +
+
+ <%= icon_text("key", "", :kind => "fa") %> +
+

<%= _('Tailoring Files') %>

+

<%= _('It may sometimes be required to adjust the security policy to your specific needs. ') %>
+ <%= (_('In Foreman, tailoring_files represent the custom modifications to default XCCDF profiles and they can be applied to hosts + via %s') % link_to('compliance policies', policies_path)).html_safe %> +

+ +
+ <%= new_link(_('New Tailoring File'), {}, { :class => "btn-lg" }) %> +
+
\ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 757bc558f..e094fcfc0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -25,6 +25,7 @@ collection do get 'auto_complete_search' post 'scap_content_selected' + post 'tailoring_file_selected' get 'select_multiple_hosts' post 'update_multiple_hosts' get 'disassociate_multiple_hosts' @@ -38,6 +39,12 @@ end end + resources :tailoring_files do + collection do + get 'auto_complete_search' + end + end + resources :hosts, :only => [:show], :as => :compliance_hosts, :controller => :compliance_hosts end @@ -53,6 +60,7 @@ resources :policies, :except => [:new, :edit] do member do get 'content' + get 'tailoring' end end resources :arf_reports, :only => [:index, :show, :destroy] diff --git a/db/migrate/20161109155255_create_tailoring_files.rb b/db/migrate/20161109155255_create_tailoring_files.rb new file mode 100644 index 000000000..34300329c --- /dev/null +++ b/db/migrate/20161109155255_create_tailoring_files.rb @@ -0,0 +1,23 @@ +class CreateTailoringFiles < ActiveRecord::Migration + def up + create_table :foreman_openscap_tailoring_files do |t| + t.string :name, :unique => true, :null => false + t.text :scap_file + t.string :original_filename, :null => false + t.datetime :created_at + t.datetime :updated_at + t.string :digest + end + + add_column :foreman_openscap_policies, :tailoring_file_id, :integer, :references => :tailoring_file + add_column :foreman_openscap_policies, :tailoring_file_profile_id, :integer, :references => :scap_content_profile + add_column :foreman_openscap_scap_content_profiles, :tailoring_file_id, :integer, :references => :tailoring_file + end + + def down + remove_column :foreman_openscap_policies, :tailoring_file_id + remove_column :foreman_openscap_policies, :tailoring_file_profile_id + remove_column :foreman_openscap_scap_content_profiles, :tailoring_file_id + drop_table :foreman_openscap_tailoring_files + end +end diff --git a/lib/foreman_openscap/engine.rb b/lib/foreman_openscap/engine.rb index 5f4f0aa21..b375328e5 100644 --- a/lib/foreman_openscap/engine.rb +++ b/lib/foreman_openscap/engine.rb @@ -90,6 +90,9 @@ class Engine < ::Rails::Engine :resource_type => 'ForemanOpenscap::ScapContent' permission :edit_hosts, { :hosts => [:openscap_proxy_changed] }, :resource_type => "Host" permission :edit_hostgroups, { :hostgroups => [:openscap_proxy_changed] }, :resource_type => "Hostgroup" + permission :view_tailoring_files, { :tailoring_files => [:index] }, :resource_type => 'ForemanOpenscap::TailoringFile' + permission :edit_tailoring_files, { :tailoring_files => [:new, :create] }, :resource_type => 'ForemanOpenscap::TailoringFile' + permission :destroy_tailoring_files, { :tailoring_files => [:destroy] }, :resource_type => 'ForemanOpenscap::TailoringFile' end role "Compliance viewer", [:view_arf_reports, :view_policies, :view_scap_contents] @@ -109,6 +112,10 @@ class Engine < ::Rails::Engine menu :top_menu, :compliance_reports, :caption => N_('Reports'), :url_hash => {:controller => :arf_reports, :action => :index}, :parent => :hosts_menu + menu :top_menu, :compliance_files, :caption => N_('Tailoring Files'), + :url_hash => {:controller => :tailoring_files, :action => :index}, + :parent => :hosts_menu + # add dashboard widget widget 'compliance_host_reports_widget', diff --git a/test/factories/policy_factory.rb b/test/factories/policy_factory.rb index 2fe46a480..9be9502be 100644 --- a/test/factories/policy_factory.rb +++ b/test/factories/policy_factory.rb @@ -5,6 +5,8 @@ weekday 'monday' scap_content scap_content_profile + tailoring_file + tailoring_file_profile nil day_of_month nil cron_line nil hosts [] diff --git a/test/factories/scap_content_related.rb b/test/factories/scap_content_related.rb index e388ee279..4f5026b10 100644 --- a/test/factories/scap_content_related.rb +++ b/test/factories/scap_content_related.rb @@ -12,4 +12,11 @@ f.profile_id 'xccdf_org.test.common_test_profile' f.title 'test Profile for testing' end + + factory :tailoring_file, :class => ForemanOpenscap::TailoringFile do |f| + f.name 'some tailoring file' + f.original_filename 'original tailoring filename' + f.scap_file { File.new("#{ForemanOpenscap::Engine.root}/test/files/tailoring_files/ssg-firefox-ds-tailoring.xml", 'rb').read } + f.scap_content_profiles [] + end end diff --git a/test/files/tailoring_files/ssg-firefox-ds-tailoring.xml b/test/files/tailoring_files/ssg-firefox-ds-tailoring.xml new file mode 100644 index 000000000..c8cc37ebf --- /dev/null +++ b/test/files/tailoring_files/ssg-firefox-ds-tailoring.xml @@ -0,0 +1,31 @@ + + + + 1 + + Upstream Firefox STIG [CUSTOMIZED] + This profile is developed under the DoD consensus model and DISA FSO Vendor STIG process, +serving as the upstream development environment for the Firefox STIG. + +As a result of the upstream/downstream relationship between the SCAP Security Guide project +and the official DISA FSO STIG baseline, users should expect variance between SSG and DISA FSO content. +For official DISA FSO STIG content, refer to http://iase.disa.mil/stigs/app-security/browser-guidance/Pages/index.aspx. + +While this profile is packaged by Red Hat as part of the SCAP Security Guide package, please note +that commercial support of this SCAP content is NOT available. This profile is provided as example +SCAP content with no endorsement for suitability or production readiness. Support for this +profile is provided by the upstream SCAP Security Guide community on a best-effort basis. The +upstream project homepage is https://fedorahosted.org/scap-security-guide/. + + + + + + + + + + + + + diff --git a/test/functional/api/v2/compliance/policies_controller_test.rb b/test/functional/api/v2/compliance/policies_controller_test.rb index f9fbf18e1..0db1fa3d9 100644 --- a/test/functional/api/v2/compliance/policies_controller_test.rb +++ b/test/functional/api/v2/compliance/policies_controller_test.rb @@ -66,4 +66,11 @@ class Api::V2::Compliance::PoliciesControllerTest < ActionController::TestCase assert(@response.header['Content-Type'], 'application/xml') assert_response :success end + + test "should return xml of a tailoring file" do + policy = FactoryGirl.create(:policy, :tailoring_file => FactoryGirl.create(:tailoring_file)) + get :tailoring, { :id => policy.id }, set_session_user + assert(@response.header['Content-Type'], 'application/xml') + assert_response :success + end end diff --git a/test/functional/tailoring_files_controller_test.rb b/test/functional/tailoring_files_controller_test.rb new file mode 100644 index 000000000..60548bd9d --- /dev/null +++ b/test/functional/tailoring_files_controller_test.rb @@ -0,0 +1,38 @@ +require 'test_plugin_helper' + +class TailoringFilesControllerTest < ActionController::TestCase + setup do + @tailoring_file = FactoryGirl.create(:tailoring_file) + @scap_file = File.new("#{ForemanOpenscap::Engine.root}/test/files/tailoring_files/ssg-firefox-ds-tailoring.xml", 'rb') + end + + test 'index' do + get :index, {}, set_session_user + assert_template 'index' + end + + test 'new' do + get :new, {}, set_session_user + assert_template 'new' + end + + test 'edit' do + get :edit, { :id => @tailoring_file.id }, set_session_user + assert_template 'edit' + end + + test 'create' do + uploaded_file = ActionDispatch::Http::UploadedFile.new(:tempfile => @scap_file, + :content_type => 'text/xml') + uploaded_file.original_filename = 'uploaded-tailoring-file.xml' + post :create, { :tailoring_file => { :name => 'some_file', :scap_file => uploaded_file }}, set_session_user + assert_redirected_to tailoring_files_url + end + + test 'destroy' do + tf = ForemanOpenscap::TailoringFile.first + delete :destroy, { :id => tf.id }, set_session_user + assert_redirected_to tailoring_files_url + refute ForemanOpenscap::TailoringFile.exists?(tf.id) + end +end diff --git a/test/test_plugin_helper.rb b/test/test_plugin_helper.rb index d15edbe71..fff98701e 100644 --- a/test/test_plugin_helper.rb +++ b/test/test_plugin_helper.rb @@ -13,6 +13,13 @@ def skip_scap_callback end end +module ScapFileValidation + def stub_file_validation + ForemanOpenscap::DataStreamValidator.any_instance.stubs(:validate) + ForemanOpenscap::TailoringFile.any_instance.stubs(:fetch_profiles).returns({ 'test_profile_key' => 'test_profile_title' }) + end +end + class ActionMailer::TestCase include ScapClientPuppetclass setup :skip_scap_callback @@ -20,8 +27,9 @@ class ActionMailer::TestCase class ActionController::TestCase include ScapClientPuppetclass + include ScapFileValidation - setup :add_smart_proxy, :skip_scap_callback + setup :add_smart_proxy, :skip_scap_callback, :stub_file_validation private @@ -38,8 +46,9 @@ def add_smart_proxy class ActiveSupport::TestCase include ScapClientPuppetclass + include ScapFileValidation - setup :add_smart_proxy, :skip_scap_callback + setup :add_smart_proxy, :skip_scap_callback, :stub_file_validation private diff --git a/test/unit/tailoring_file_test.rb b/test/unit/tailoring_file_test.rb new file mode 100644 index 000000000..bfb63508d --- /dev/null +++ b/test/unit/tailoring_file_test.rb @@ -0,0 +1,15 @@ +require 'test_plugin_helper' + +class TailoringFileTest < ActiveSupport::TestCase + setup do + @scap_file = File.new("#{ForemanOpenscap::Engine.root}/test/files/tailoring_files/ssg-firefox-ds-tailoring.xml", 'rb').read + end + + test 'should create tailoring file' do + ForemanOpenscap::DataStreamValidator.any_instance.stubs(:validate) + FactoryGirl.create(:openscap_proxy) + ForemanOpenscap::TailoringFile.any_instance.stubs(:fetch_profiles).returns({ 'test_profile_key' => 'test_profile_title' }) + tailoring_file = ForemanOpenscap::TailoringFile.create(:name => 'test_file', :scap_file => @scap_file, :original_filename => 'original name') + assert tailoring_file.valid? + end +end