From 796d88b2a665500445103b78527b6bf954d2e31c Mon Sep 17 00:00:00 2001 From: Ondrej Prazak Date: Fri, 11 Nov 2016 09:46:25 +0100 Subject: [PATCH] Fixes #14158 - Add tailoring file for scans --- lib/smart_proxy_openscap/fetch_file.rb | 50 ++++++++++++++++ .../fetch_scap_content.rb | 57 +++---------------- .../fetch_tailoring_file.rb | 16 ++++++ lib/smart_proxy_openscap/openscap_api.rb | 25 +++++++- .../openscap_content_parser.rb | 28 +++++++-- lib/smart_proxy_openscap/openscap_lib.rb | 1 + lib/smart_proxy_openscap/openscap_plugin.rb | 3 +- settings.d/openscap.yml.example | 3 + smart_proxy_openscap.gemspec | 2 +- test/data/tailoring.xml | 31 ++++++++++ test/fetch_tailoring_api_test.rb | 36 ++++++++++++ test/scap_content_parser_api_test.rb | 19 ++++++- 12 files changed, 211 insertions(+), 60 deletions(-) create mode 100644 lib/smart_proxy_openscap/fetch_file.rb create mode 100644 lib/smart_proxy_openscap/fetch_tailoring_file.rb create mode 100644 test/data/tailoring.xml create mode 100644 test/fetch_tailoring_api_test.rb diff --git a/lib/smart_proxy_openscap/fetch_file.rb b/lib/smart_proxy_openscap/fetch_file.rb new file mode 100644 index 00000000..ca20d9eb --- /dev/null +++ b/lib/smart_proxy_openscap/fetch_file.rb @@ -0,0 +1,50 @@ +module Proxy::OpenSCAP + class FetchFile + include ::Proxy::Log + + private + + def create_store_dir(store_dir) + logger.info "Creating directory to store SCAP file: #{store_dir}" + FileUtils.mkdir_p(store_dir) # will fail silently if exists + rescue Errno::EACCES => e + logger.error "No permission to create directory #{store_dir}" + raise e + rescue StandardError => e + logger.error "Could not create '#{store_dir}' directory: #{e.message}" + raise e + end + + def policy_content_file(policy_scap_file) + return nil if !File.file?(policy_scap_file) || File.zero?(policy_scap_file) + File.open(policy_scap_file, 'rb').read + end + + def save_or_serve_scap_file(policy_id, policy_scap_file, file_download_path) + lock = Proxy::FileLock::try_locking(policy_scap_file) + response = fetch_scap_content_xml(policy_id, policy_scap_file, file_download_path) + if lock.nil? + return response + else + begin + File.open(policy_scap_file, 'wb') do |file| + file << response + end + ensure + Proxy::FileLock::unlock(lock) + end + scap_file = policy_content_file(policy_scap_file) + raise FileNotFound if scap_file.nil? + return scap_file + end + end + + def fetch_scap_content_xml(policy_id, policy_scap_file, file_download_path) + foreman_request = Proxy::HttpRequest::ForemanRequest.new + req = foreman_request.request_factory.create_get(file_download_path) + response = foreman_request.send_request(req) + response.value + response.body + end + end +end diff --git a/lib/smart_proxy_openscap/fetch_scap_content.rb b/lib/smart_proxy_openscap/fetch_scap_content.rb index 7eba02db..281caa45 100644 --- a/lib/smart_proxy_openscap/fetch_scap_content.rb +++ b/lib/smart_proxy_openscap/fetch_scap_content.rb @@ -1,58 +1,17 @@ +require 'smart_proxy_openscap/fetch_file' + module Proxy::OpenSCAP - class FetchScapContent - include ::Proxy::Log + class FetchScapContent < FetchFile + def get_policy_content(policy_id) policy_store_dir = File.join(Proxy::OpenSCAP.fullpath(Proxy::OpenSCAP::Plugin.settings.contentdir), policy_id.to_s) policy_scap_file = File.join(policy_store_dir, "#{policy_id}_scap_content.xml") - begin - logger.info "Creating directory to store SCAP file: #{policy_store_dir}" - FileUtils.mkdir_p(policy_store_dir) # will fail silently if exists - rescue Errno::EACCES => e - logger.error "No permission to create directory #{policy_store_dir}" - raise e - rescue StandardError => e - logger.error "Could not create '#{policy_store_dir}' directory: #{e.message}" - raise e - end - - scap_file = policy_content_file(policy_scap_file) - scap_file ||= save_or_serve_scap_file(policy_id, policy_scap_file) - scap_file - end + file_download_path = "api/v2/compliance/policies/#{policy_id}/content" - private + create_store_dir policy_store_dir - def policy_content_file(policy_scap_file) - return nil if !File.file?(policy_scap_file) || File.zero?(policy_scap_file) - File.open(policy_scap_file, 'rb').read - end - - def save_or_serve_scap_file(policy_id, policy_scap_file) - lock = Proxy::FileLock::try_locking(policy_scap_file) - response = fetch_scap_content_xml(policy_id, policy_scap_file) - if lock.nil? - return response - else - begin - File.open(policy_scap_file, 'wb') do |file| - file << response - end - ensure - Proxy::FileLock::unlock(lock) - end - scap_file = policy_content_file(policy_scap_file) - raise FileNotFound if scap_file.nil? - return scap_file - end - end - - def fetch_scap_content_xml(policy_id, policy_scap_file) - foreman_request = Proxy::HttpRequest::ForemanRequest.new - policy_content_path = "api/v2/compliance/policies/#{policy_id}/content" - req = foreman_request.request_factory.create_get(policy_content_path) - response = foreman_request.send_request(req) - response.value - response.body + scap_file = policy_content_file(policy_scap_file) + scap_file ||= save_or_serve_scap_file(policy_id, policy_scap_file, file_download_path) end end end diff --git a/lib/smart_proxy_openscap/fetch_tailoring_file.rb b/lib/smart_proxy_openscap/fetch_tailoring_file.rb new file mode 100644 index 00000000..d6b65a1b --- /dev/null +++ b/lib/smart_proxy_openscap/fetch_tailoring_file.rb @@ -0,0 +1,16 @@ +require 'smart_proxy_openscap/fetch_file' + +module Proxy::OpenSCAP + class FetchTailoringFile < FetchFile + def get_tailoring_file(policy_id) + store_dir = File.join(Proxy::OpenSCAP.fullpath(Proxy::OpenSCAP::Plugin.settings.tailoringdir), policy_id.to_s) + policy_tailoring_file = File.join(store_dir, "#{policy_id}_tailoring_file.xml") + file_download_path = "api/v2/compliance/policies/#{policy_id}/tailoring" + + create_store_dir store_dir + + scap_file = policy_content_file(policy_tailoring_file) + scap_file ||= save_or_serve_scap_file(policy_id, policy_tailoring_file, file_download_path) + end + end +end diff --git a/lib/smart_proxy_openscap/openscap_api.rb b/lib/smart_proxy_openscap/openscap_api.rb index 88b607a6..8b3e4eab 100644 --- a/lib/smart_proxy_openscap/openscap_api.rb +++ b/lib/smart_proxy_openscap/openscap_api.rb @@ -87,6 +87,17 @@ class Api < ::Sinatra::Base end end + get "/policies/:policy_id/tailoring" do + content_type 'application/xml' + begin + Proxy::OpenSCAP::FetchTailoringFile.new.get_tailoring_file(params[:policy_id]) + rescue *HTTP_ERRORS => e + log_halt e.response.code.to_i, "File not found on Foreman. Wrong policy id?" + rescue StandardError => e + log_halt 500, "Error occurred: #{e.message}" + end + end + post "/scap_content/policies" do begin Proxy::OpenSCAP::ContentParser.new(request.body.string).extract_policies @@ -97,9 +108,19 @@ class Api < ::Sinatra::Base end end - post "/scap_content/validator" do + post "/tailoring_file/profiles" do + begin + Proxy::OpenSCAP::ContentParser.new(request.body.string).get_profiles + rescue *HTTP_ERRORS => e + log_halt 500, e.message + rescue StandardError => e + log_halt 500, "Error occurred: #{e.message}" + end + end + + post "/scap_file/validator/:type" do begin - Proxy::OpenSCAP::ContentParser.new(request.body.string).validate + Proxy::OpenSCAP::ContentParser.new(request.body.string, params[:type]).validate rescue *HTTP_ERRORS => e log_halt 500, e.message rescue StandardError => e diff --git a/lib/smart_proxy_openscap/openscap_content_parser.rb b/lib/smart_proxy_openscap/openscap_content_parser.rb index df88f009..fb34e82f 100644 --- a/lib/smart_proxy_openscap/openscap_content_parser.rb +++ b/lib/smart_proxy_openscap/openscap_content_parser.rb @@ -1,12 +1,21 @@ require 'openscap/ds/sds' require 'openscap/source' require 'openscap/xccdf/benchmark' +require 'openscap/xccdf/tailoring' module Proxy::OpenSCAP class ContentParser - def initialize(scap_content) + def initialize(scap_file, type = 'scap_content') OpenSCAP.oscap_init - @source = OpenSCAP::Source.new(:content => scap_content) + @source = OpenSCAP::Source.new(:content => scap_file) + @type = type + end + + def allowed_types + { + 'tailoring_file' => 'XCCDF Tailoring', + 'scap_content' => 'SCAP Source Datastream' + } end def extract_policies @@ -19,11 +28,20 @@ def extract_policies policies.to_json end + def get_profiles + tailoring = ::OpenSCAP::Xccdf::Tailoring.new(@source, nil) + profiles = tailoring.profiles.inject({}) do |memo, (key, profile)| + memo.tap { |obj| obj[key] = profile.title } + end + tailoring.destroy + profiles.to_json + end + def validate errors = [] - allowed_type = 'SCAP Source Datastream' - if @source.type != allowed_type - errors << "Uploaded file is not #{allowed_type}" + + if @source.type != allowed_types[@type] + errors << "Uploaded file is #{@source.type}, unexpected file type" end begin diff --git a/lib/smart_proxy_openscap/openscap_lib.rb b/lib/smart_proxy_openscap/openscap_lib.rb index 6aaa5a13..d120243d 100644 --- a/lib/smart_proxy_openscap/openscap_lib.rb +++ b/lib/smart_proxy_openscap/openscap_lib.rb @@ -21,6 +21,7 @@ require 'smart_proxy_openscap/openscap_report_parser' require 'smart_proxy_openscap/spool_forwarder' require 'smart_proxy_openscap/storage_fs' +require 'smart_proxy_openscap/fetch_tailoring_file' module Proxy::OpenSCAP extend ::Proxy::Log diff --git a/lib/smart_proxy_openscap/openscap_plugin.rb b/lib/smart_proxy_openscap/openscap_plugin.rb index d71f1b23..6e857cee 100644 --- a/lib/smart_proxy_openscap/openscap_plugin.rb +++ b/lib/smart_proxy_openscap/openscap_plugin.rb @@ -21,6 +21,7 @@ class Plugin < ::Proxy::Plugin :openscap_send_log_file => File.join(APP_ROOT, 'logs/openscap-send.log'), :contentdir => File.join(APP_ROOT, 'openscap/content'), :reportsdir => File.join(APP_ROOT, 'openscap/reports'), - :failed_dir => File.join(APP_ROOT, 'openscap/failed') + :failed_dir => File.join(APP_ROOT, 'openscap/failed'), + :tailoringdir => File.join(APP_ROOT, 'openscap/tailoring') end end diff --git a/settings.d/openscap.yml.example b/settings.d/openscap.yml.example index 5a840d68..d1947b90 100644 --- a/settings.d/openscap.yml.example +++ b/settings.d/openscap.yml.example @@ -12,6 +12,9 @@ # So we will not request the XML from Foreman each time #:contentdir: /var/lib/openscap/content +# Directory where OpenSCAP tailoring XML files are stored +#:tailoringdir: /var/lib/openscap/tailoring + # Directory where OpenSCAP report XML are stored # So Foreman can request arf xml reports #:reportsdir: /usr/share/foreman-proxy/openscap/reports diff --git a/smart_proxy_openscap.gemspec b/smart_proxy_openscap.gemspec index 6d019c34..a353109c 100644 --- a/smart_proxy_openscap.gemspec +++ b/smart_proxy_openscap.gemspec @@ -19,5 +19,5 @@ Gem::Specification.new do |s| s.add_development_dependency('rack-test') s.add_development_dependency('mocha') s.add_development_dependency('webmock') - s.add_dependency 'openscap', '>= 0.4.3' + s.add_dependency 'openscap', '>= 0.4.7' end diff --git a/test/data/tailoring.xml b/test/data/tailoring.xml new file mode 100644 index 00000000..c8cc37eb --- /dev/null +++ b/test/data/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/fetch_tailoring_api_test.rb b/test/fetch_tailoring_api_test.rb new file mode 100644 index 00000000..305e5376 --- /dev/null +++ b/test/fetch_tailoring_api_test.rb @@ -0,0 +1,36 @@ +require 'test_helper' +require 'smart_proxy_openscap' +require 'smart_proxy_openscap/openscap_api' + +ENV['RACK_ENV'] = 'test' + +class FetchTailoringApiTest < Test::Unit::TestCase + include Rack::Test::Methods + + def setup + @foreman_url = 'https://foreman.example.com' + Proxy::SETTINGS.stubs(:foreman_url).returns(@foreman_url) + @results_path = ("#{Dir.getwd}/test/test_run_files") + FileUtils.mkdir_p(@results_path) + Proxy::OpenSCAP::Plugin.settings.stubs(:tailoringdir).returns(@results_path) + @tailoring_file = File.new("#{Dir.getwd}/test/data/tailoring.xml").read + @policy_id = 1 + end + + def teardown + FileUtils.rm_rf(Dir.glob("#{@results_path}/*")) + end + + def app + ::Proxy::OpenSCAP::Api.new + end + + def test_get_tailoring_file_from_file + FileUtils.mkdir("#{@results_path}/#{@policy_id}") + FileUtils.cp("#{Dir.getwd}/test/data/tailoring.xml", "#{@results_path}/#{@policy_id}/#{@policy_id}_tailoring_file.xml") + get "/policies/#{@policy_id}/tailoring" + assert_equal("application/xml;charset=utf-8", last_response.header["Content-Type"], "Response header should be application/xml") + assert_equal(@tailoring_file.length, last_response.length, "Scap content should be equal") + assert(last_response.successful?, "Response should be success") + end +end diff --git a/test/scap_content_parser_api_test.rb b/test/scap_content_parser_api_test.rb index bdcde97b..5aeae856 100644 --- a/test/scap_content_parser_api_test.rb +++ b/test/scap_content_parser_api_test.rb @@ -9,6 +9,7 @@ def setup @foreman_url = 'https://foreman.example.com' Proxy::SETTINGS.stubs(:foreman_url).returns(@foreman_url) @scap_content = File.new("#{Dir.getwd}/test/data/ssg-rhel7-ds.xml").read + @tailoring_file = File.new("#{Dir.getwd}/test/data/tailoring.xml").read end def app @@ -31,7 +32,7 @@ def test_invalid_scap_content_policies end def test_scap_content_validator - post '/scap_content/validator', @scap_content, 'CONTENT_TYPE' => 'text/xml' + post '/scap_file/validator/scap_content', @scap_content, 'CONTENT_TYPE' => 'text/xml' result = JSON.parse(last_response.body) assert_empty(result['errors']) assert(last_response.successful?) @@ -39,7 +40,7 @@ def test_scap_content_validator def test_invalid_scap_content_validator Proxy::OpenSCAP::ContentParser.any_instance.stubs(:validate).returns({:errors => 'Invalid SCAP file type'}.to_json) - post '/scap_content/validator', @scap_content, 'CONTENT_TYPE' => 'text/xml' + post '/scap_file/validator/scap_content', @scap_content, 'CONTENT_TYPE' => 'text/xml' result = JSON.parse(last_response.body) refute_empty(result['errors']) assert(last_response.successful?) @@ -51,4 +52,18 @@ def test_scap_content_guide assert(result['html'].start_with?('')) assert(last_response.successful?) end + + def test_validate_tailoring_file + post '/scap_file/validator/tailoring_file', @tailoring_file, 'CONTENT_TYPE' => 'text/xml' + result = JSON.parse(last_response.body) + assert_empty(result['errors']) + assert(last_response.successful?) + end + + def test_get_profiles_from_tailoring_file + post '/tailoring_file/profiles', @tailoring_file, 'CONTENT_TYPE' => 'text/xml' + result = JSON.parse(last_response.body) + assert_equal 1, result.keys.length + assert(last_response.successful?) + end end