Skip to content

Commit

Permalink
Fixes #14158 - Add tailoring file for scans
Browse files Browse the repository at this point in the history
  • Loading branch information
Ondrej Prazak committed Nov 18, 2016
1 parent c3d10e2 commit 796d88b
Show file tree
Hide file tree
Showing 12 changed files with 211 additions and 60 deletions.
50 changes: 50 additions & 0 deletions 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
57 changes: 8 additions & 49 deletions 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
16 changes: 16 additions & 0 deletions 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
25 changes: 23 additions & 2 deletions lib/smart_proxy_openscap/openscap_api.rb
Expand Up @@ -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
Expand All @@ -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
Expand Down
28 changes: 23 additions & 5 deletions 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
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions lib/smart_proxy_openscap/openscap_lib.rb
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion lib/smart_proxy_openscap/openscap_plugin.rb
Expand Up @@ -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
3 changes: 3 additions & 0 deletions settings.d/openscap.yml.example
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion smart_proxy_openscap.gemspec
Expand Up @@ -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
31 changes: 31 additions & 0 deletions test/data/tailoring.xml
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<xccdf:Tailoring xmlns:xccdf="http://checklists.nist.gov/xccdf/1.2" id="xccdf_scap-workbench_tailoring_default">
<xccdf:benchmark href="/usr/share/xml/scap/ssg/content/ssg-firefox-ds.xml"/>
<xccdf:version time="2016-11-10T11:24:26">1</xccdf:version>
<xccdf:Profile id="xccdf_org.ssgproject.content_profile_stig-firefox-upstream_customized" extends="xccdf_org.ssgproject.content_profile_stig-firefox-upstream">
<xccdf:title xmlns:xhtml="http://www.w3.org/1999/xhtml" xml:lang="en-US">Upstream Firefox STIG [CUSTOMIZED]</xccdf:title>
<xccdf:description xmlns:xhtml="http://www.w3.org/1999/xhtml" xml:lang="en-US">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/.
</xccdf:description>
<xccdf:select idref="xccdf_org.ssgproject.content_rule_firefox_preferences-non-secure_page_warning" selected="true"/>
<xccdf:select idref="xccdf_org.ssgproject.content_rule_firefox_preferences-javascript_status_bar_text" selected="true"/>
<xccdf:select idref="xccdf_org.ssgproject.content_rule_firefox_preferences-javascript_context_menus" selected="true"/>
<xccdf:select idref="xccdf_org.ssgproject.content_rule_firefox_preferences-javascript_status_bar_changes" selected="true"/>
<xccdf:select idref="xccdf_org.ssgproject.content_rule_firefox_preferences-javascript_window_resizing" selected="true"/>
<xccdf:select idref="xccdf_org.ssgproject.content_rule_firefox_preferences-javascript_window_changes" selected="true"/>
<xccdf:select idref="xccdf_org.ssgproject.content_rule_firefox_preferences-auto-update_of_firefox" selected="false"/>
<xccdf:select idref="xccdf_org.ssgproject.content_rule_firefox_preferences-autofill_passwords" selected="false"/>
<xccdf:select idref="xccdf_org.ssgproject.content_rule_firefox_preferences-autofill_forms" selected="false"/>
<xccdf:select idref="xccdf_org.ssgproject.content_rule_firefox_preferences-addons_plugin_updates" selected="false"/>
</xccdf:Profile>
</xccdf:Tailoring>
36 changes: 36 additions & 0 deletions 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
19 changes: 17 additions & 2 deletions test/scap_content_parser_api_test.rb
Expand Up @@ -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
Expand All @@ -31,15 +32,15 @@ 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?)
end

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?)
Expand All @@ -51,4 +52,18 @@ def test_scap_content_guide
assert(result['html'].start_with?('<!DOCTYPE html>'))
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

0 comments on commit 796d88b

Please sign in to comment.