smart_proxy_openscap 0.7.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +41 -0
  3. data/.rubocop_todo.yml +111 -0
  4. data/.travis.yml +14 -0
  5. data/COPYING +674 -0
  6. data/Gemfile +14 -0
  7. data/README.md +107 -0
  8. data/Rakefile +16 -0
  9. data/bin/smart-proxy-arf-html +7 -0
  10. data/bin/smart-proxy-arf-json +7 -0
  11. data/bin/smart-proxy-openscap-send +61 -0
  12. data/bin/smart-proxy-policy-guide +7 -0
  13. data/bin/smart-proxy-scap-profiles +7 -0
  14. data/bin/smart-proxy-scap-validation +7 -0
  15. data/bundler.d/openscap.rb +6 -0
  16. data/extra/rubygem-smart_proxy_openscap.spec +101 -0
  17. data/extra/smart-proxy-openscap-send.cron +2 -0
  18. data/lib/smart_proxy_openscap.rb +14 -0
  19. data/lib/smart_proxy_openscap/arf_html.rb +22 -0
  20. data/lib/smart_proxy_openscap/arf_json.rb +114 -0
  21. data/lib/smart_proxy_openscap/arf_parser.rb +39 -0
  22. data/lib/smart_proxy_openscap/content_parser.rb +30 -0
  23. data/lib/smart_proxy_openscap/fetch_file.rb +60 -0
  24. data/lib/smart_proxy_openscap/fetch_scap_content.rb +17 -0
  25. data/lib/smart_proxy_openscap/fetch_tailoring_file.rb +17 -0
  26. data/lib/smart_proxy_openscap/foreman_forwarder.rb +40 -0
  27. data/lib/smart_proxy_openscap/http_config.ru +20 -0
  28. data/lib/smart_proxy_openscap/openscap_api.rb +187 -0
  29. data/lib/smart_proxy_openscap/openscap_exception.rb +9 -0
  30. data/lib/smart_proxy_openscap/openscap_html_generator.rb +38 -0
  31. data/lib/smart_proxy_openscap/openscap_import_api.rb +32 -0
  32. data/lib/smart_proxy_openscap/openscap_lib.rb +67 -0
  33. data/lib/smart_proxy_openscap/openscap_plugin.rb +27 -0
  34. data/lib/smart_proxy_openscap/policy_guide.rb +23 -0
  35. data/lib/smart_proxy_openscap/policy_parser.rb +33 -0
  36. data/lib/smart_proxy_openscap/profiles_parser.rb +32 -0
  37. data/lib/smart_proxy_openscap/scap_profiles.rb +52 -0
  38. data/lib/smart_proxy_openscap/scap_validation.rb +35 -0
  39. data/lib/smart_proxy_openscap/shell_wrapper.rb +77 -0
  40. data/lib/smart_proxy_openscap/spool_forwarder.rb +79 -0
  41. data/lib/smart_proxy_openscap/storage.rb +47 -0
  42. data/lib/smart_proxy_openscap/storage_fs.rb +102 -0
  43. data/lib/smart_proxy_openscap/version.rb +15 -0
  44. data/settings.d/openscap.yml.example +33 -0
  45. data/smart_proxy_openscap.gemspec +23 -0
  46. data/test/data/arf_report +0 -0
  47. data/test/data/corrupted_arf_report +0 -0
  48. data/test/data/spool/cleanup_spool/arf/2c101b95-033f-4b15-b490-f50bf9090dae/1/1484313035/fa2f68ffb944c917332a284dc63ec7f8fa76990cb815ddcad3318b5d9457f8a1 +0 -0
  49. data/test/data/spool/cleanup_spool/arf/e20b9695-f655-401a-9dda-8cca7a47a8c0/1/1484309984/fa2f68ffb944c917332a284dc63ec7f8fa76990cb815ddcad3318b5d9457f8a1 +0 -0
  50. data/test/data/spool/corrupted_spool/arf/e20b9695-f655-401a-9dda-8cca7a47a8c0/1/1484309984/a4dfba5db27b21795e6fa401b8dce7a70faeb25b7963891f07f6f4baaf052afb +0 -0
  51. data/test/data/spool/corrupted_spool/arf/e20b9695-f655-401a-9dda-8cca7a47a8c0/1/1484313035/fa2f68ffb944c917332a284dc63ec7f8fa76990cb815ddcad3318b5d9457f8a1 +0 -0
  52. data/test/data/spool/valid_spool/arf/e20b9695-f655-401a-9dda-8cca7a47a8c0/1/1484309984/fa2f68ffb944c917332a284dc63ec7f8fa76990cb815ddcad3318b5d9457f8a1 +0 -0
  53. data/test/data/spool/valid_spool/arf/e20b9695-f655-401a-9dda-8cca7a47a8c0/1/1484313035/fa2f68ffb944c917332a284dc63ec7f8fa76990cb815ddcad3318b5d9457f8a1 +0 -0
  54. data/test/data/ssg-rhel7-ds.xml +20271 -0
  55. data/test/data/tailoring.xml +31 -0
  56. data/test/fetch_scap_api_test.rb +73 -0
  57. data/test/fetch_tailoring_api_test.rb +37 -0
  58. data/test/get_report_xml_html_test.rb +58 -0
  59. data/test/post_report_api_test.rb +86 -0
  60. data/test/scap_content_parser_api_test.rb +69 -0
  61. data/test/script_class_test.rb +96 -0
  62. data/test/spool_forwarder_test.rb +84 -0
  63. data/test/test_helper.rb +13 -0
  64. metadata +180 -0
@@ -0,0 +1,32 @@
1
+ module Proxy::OpenSCAP
2
+ class ImportApi < ::Sinatra::Base
3
+ include ::Proxy::Log
4
+ helpers ::Proxy::Helpers
5
+ authorize_with_trusted_hosts
6
+
7
+ require 'smart_proxy_openscap/openscap_lib'
8
+
9
+ post "/arf/:cname/:policy_id/:date" do
10
+ cn = params[:cname]
11
+ date = params[:date]
12
+ policy = params[:policy_id]
13
+ log_halt(500, "Insufficient data") if (cn.nil? || date.nil?)
14
+
15
+ post_to_foreman = ForemanForwarder.new.post_arf_report(cn, policy, date, request.body.string, Proxy::OpenSCAP::Plugin.settings.timeout)
16
+ begin
17
+ Proxy::OpenSCAP::StorageFS.new(Proxy::OpenSCAP::Plugin.settings.reportsdir, cn, post_to_foreman['id'], date).store_archive(request.body.string)
18
+ rescue Proxy::OpenSCAP::StoreReportError => e
19
+ Proxy::OpenSCAP::StorageFS.new(Proxy::OpenSCAP::Plugin.settings.failed_dir, cn, post_to_foreman['id'], date).store_failed(request.body.string)
20
+ logger.error "Failed to save Report in reports directory (#{Proxy::OpenSCAP::Plugin.settings.reportsdir}). Failed with: #{e.message}.
21
+ Saving file in #{Proxy::OpenSCAP::Plugin.settings.failed_dir}. Please copy manually to #{Proxy::OpenSCAP::Plugin.settings.reportsdir}"
22
+ rescue *HTTP_ERRORS => e
23
+ ### If the upload to foreman fails then store it in the spooldir
24
+ logger.error "Failed to upload to Foreman, saving in spool. Failed with: #{e.message}"
25
+ Proxy::OpenSCAP::StorageFS.new(Proxy::OpenSCAP::Plugin.settings.spooldir, cn, policy, date).store_spool(request.body.string)
26
+ rescue Proxy::OpenSCAP::StoreSpoolError => e
27
+ log_halt 500, e.message
28
+ end
29
+ {:success => true, :arf_id => post_to_foreman['id']}.to_json
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,67 @@
1
+ #
2
+ # Copyright (c) 2014 Red Hat Inc.
3
+ #
4
+ # This software is licensed to you under the GNU General Public License,
5
+ # version 3 (GPLv3). There is NO WARRANTY for this software, express or
6
+ # implied, including the implied warranties of MERCHANTABILITY or FITNESS
7
+ # FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv3
8
+ # along with this software; if not, see http://www.gnu.org/licenses/gpl.txt
9
+ #
10
+
11
+ require 'digest'
12
+ require 'fileutils'
13
+ require 'pathname'
14
+ require 'json'
15
+ require 'proxy/error'
16
+ require 'yaml'
17
+ require 'ostruct'
18
+ require 'proxy/request'
19
+ require 'smart_proxy_openscap/fetch_scap_content'
20
+ require 'smart_proxy_openscap/foreman_forwarder'
21
+ require 'smart_proxy_openscap/content_parser'
22
+ require 'smart_proxy_openscap/openscap_exception'
23
+ require 'smart_proxy_openscap/arf_parser'
24
+ require 'smart_proxy_openscap/spool_forwarder'
25
+ require 'smart_proxy_openscap/openscap_html_generator'
26
+ require 'smart_proxy_openscap/fetch_tailoring_file'
27
+ require 'smart_proxy_openscap/policy_parser'
28
+ require 'smart_proxy_openscap/profiles_parser'
29
+
30
+ module Proxy::OpenSCAP
31
+ extend ::Proxy::Log
32
+
33
+ def self.plugin_settings
34
+ @@settings ||= OpenStruct.new(read_settings)
35
+ end
36
+
37
+ def self.read_settings
38
+ ::Proxy::OpenSCAP::Plugin.default_settings.merge(
39
+ YAML.load_file(File.join(::Proxy::SETTINGS.settings_directory, ::Proxy::OpenSCAP::Plugin.settings_file)))
40
+ end
41
+
42
+ def self.common_name(request)
43
+ client_cert = request.env['SSL_CLIENT_CERT']
44
+ raise Proxy::Error::Unauthorized, "Client certificate required!" if client_cert.to_s.empty?
45
+
46
+ begin
47
+ client_cert = OpenSSL::X509::Certificate.new(client_cert)
48
+ rescue OpenSSL::OpenSSLError => e
49
+ raise Proxy::Error::Unauthorized, e.message
50
+ end
51
+ cn = client_cert.subject.to_a.detect { |name, value| name == 'CN' }
52
+ cn = cn[1] unless cn.nil?
53
+ raise Proxy::Error::Unauthorized, "Common Name not found in the certificate" unless cn
54
+ cn
55
+ end
56
+
57
+ def self.send_spool_to_foreman(loaded_settings)
58
+ arf_dir = File.join(loaded_settings.spooldir, "/arf")
59
+ return unless File.exist? arf_dir
60
+ SpoolForwarder.new(loaded_settings).post_arf_from_spool(arf_dir)
61
+ end
62
+
63
+ def self.fullpath(path = Proxy::OpenSCAP::Plugin.settings.contentdir)
64
+ pathname = Pathname.new(path)
65
+ pathname.relative? ? pathname.expand_path(Sinatra::Base.root).to_s : path
66
+ end
67
+ end
@@ -0,0 +1,27 @@
1
+ #
2
+ # Copyright (c) 2014--2015 Red Hat Inc.
3
+ #
4
+ # This software is licensed to you under the GNU General Public License,
5
+ # version 3 (GPLv3). There is NO WARRANTY for this software, express or
6
+ # implied, including the implied warranties of MERCHANTABILITY or FITNESS
7
+ # FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv3
8
+ # along with this software; if not, see http://www.gnu.org/licenses/gpl.txt
9
+ #
10
+
11
+ require 'smart_proxy_openscap/version'
12
+
13
+ module Proxy::OpenSCAP
14
+ class Plugin < ::Proxy::Plugin
15
+ plugin :openscap, Proxy::OpenSCAP::VERSION
16
+
17
+ http_rackup_path File.expand_path("http_config.ru", File.expand_path("../", __FILE__))
18
+ https_rackup_path File.expand_path("http_config.ru", File.expand_path("../", __FILE__))
19
+
20
+ default_settings :spooldir => '/var/spool/foreman-proxy/openscap',
21
+ :openscap_send_log_file => File.join(APP_ROOT, 'logs/openscap-send.log'),
22
+ :contentdir => File.join(APP_ROOT, 'openscap/content'),
23
+ :reportsdir => File.join(APP_ROOT, 'openscap/reports'),
24
+ :failed_dir => File.join(APP_ROOT, 'openscap/failed'),
25
+ :tailoring_dir => File.join(APP_ROOT, 'openscap/tailoring')
26
+ end
27
+ end
@@ -0,0 +1,23 @@
1
+ require 'openscap'
2
+ require 'openscap/source'
3
+ require 'openscap/ds/sds'
4
+ require 'json'
5
+
6
+ module Proxy
7
+ module OpenSCAP
8
+ class PolicyGuide
9
+ def generate_guide(in_file, out_file, policy=nil)
10
+ ::OpenSCAP.oscap_init
11
+ source = ::OpenSCAP::Source.new in_file
12
+ sds = ::OpenSCAP::DS::Sds.new source
13
+ sds.select_checklist
14
+ html = sds.html_guide policy
15
+ File.write(out_file, { :html => html.force_encoding('UTF-8') }.to_json)
16
+ ensure
17
+ sds.destroy if sds
18
+ source.destroy if source
19
+ ::OpenSCAP.oscap_cleanup
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,33 @@
1
+ require 'smart_proxy_openscap/shell_wrapper'
2
+
3
+ module Proxy
4
+ module OpenSCAP
5
+ class PolicyParser < ShellWrapper
6
+
7
+ def initialize(policy)
8
+ @script_name = "smart-proxy-policy-guide"
9
+ @policy = policy
10
+ end
11
+
12
+ def guide(scap_file)
13
+ execute_shell_command scap_file
14
+ end
15
+
16
+ def in_filename
17
+ super
18
+ end
19
+
20
+ def out_filename
21
+ "#{in_filename}json-"
22
+ end
23
+
24
+ def failure_message
25
+ "Failure when running script which renders policy guide"
26
+ end
27
+
28
+ def command(in_file, out_file)
29
+ "#{script_location} #{in_file.path} #{out_file.path} #{@policy}"
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,32 @@
1
+ require 'smart_proxy_openscap/shell_wrapper'
2
+
3
+ module Proxy
4
+ module OpenSCAP
5
+ class ProfilesParser < ShellWrapper
6
+ def initialize(type)
7
+ @type = type
8
+ @script_name = 'smart-proxy-scap-profiles'
9
+ end
10
+
11
+ def profiles(scap_file)
12
+ execute_shell_command scap_file
13
+ end
14
+
15
+ def out_filename
16
+ "#{in_filename}json-"
17
+ end
18
+
19
+ def in_filename
20
+ "#{super}-#{@type}-profiles-"
21
+ end
22
+
23
+ def failure_message
24
+ "Failure when running script which extracts profiles from scap file"
25
+ end
26
+
27
+ def command(in_file, out_file)
28
+ "#{script_location} #{in_file.path} #{out_file.path} #{@type}"
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,52 @@
1
+ require 'openscap'
2
+ require 'openscap/ds/sds'
3
+ require 'openscap/source'
4
+ require 'openscap/xccdf/benchmark'
5
+ require 'openscap/xccdf/tailoring'
6
+ require 'json'
7
+
8
+ module Proxy
9
+ module OpenSCAP
10
+ class ScapProfiles
11
+ def profiles(in_file, out_file, type)
12
+ ::OpenSCAP.oscap_init
13
+ source = ::OpenSCAP::Source.new(in_file)
14
+ json = type == 'scap_content' ? scap_content_profiles(source) : tailoring_profiles(source)
15
+ File.write out_file, json
16
+ ensure
17
+ source.destroy if source
18
+ ::OpenSCAP.oscap_cleanup
19
+ end
20
+
21
+ def scap_content_profiles(source)
22
+ bench = benchmark_profiles source
23
+ profiles = collect_profiles bench
24
+ profiles.to_json
25
+ ensure
26
+ bench.destroy if bench
27
+ end
28
+
29
+ def tailoring_profiles(source)
30
+ tailoring = ::OpenSCAP::Xccdf::Tailoring.new(source, nil)
31
+ profiles = collect_profiles tailoring
32
+ profiles.to_json
33
+ ensure
34
+ tailoring.destroy if tailoring
35
+ end
36
+
37
+ def collect_profiles(profile_source)
38
+ profile_source.profiles.inject({}) do |memo, (key, profile)|
39
+ memo.tap { |hash| hash[key] = profile.title.strip }
40
+ end
41
+ end
42
+
43
+ def benchmark_profiles(source)
44
+ sds = ::OpenSCAP::DS::Sds.new(source)
45
+ bench_source = sds.select_checklist!
46
+ benchmark = ::OpenSCAP::Xccdf::Benchmark.new(bench_source)
47
+ ensure
48
+ sds.destroy if sds
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,35 @@
1
+ require 'json'
2
+ require 'openscap'
3
+ require 'openscap/source'
4
+
5
+ module Proxy
6
+ module OpenSCAP
7
+ class ScapValidation
8
+ def allowed_types
9
+ {
10
+ 'tailoring_file' => 'XCCDF Tailoring',
11
+ 'scap_content' => 'SCAP Source Datastream'
12
+ }
13
+ end
14
+
15
+ def validate(in_file, out_file, type)
16
+ errors = []
17
+ ::OpenSCAP.oscap_init
18
+ source = ::OpenSCAP::Source.new(in_file)
19
+ if source.type != allowed_types[type]
20
+ errors << "Uploaded file is #{source.type}, unexpected file type"
21
+ end
22
+
23
+ begin
24
+ source.validate!
25
+ rescue ::OpenSCAP::OpenSCAPError
26
+ errors << "Invalid SCAP file type"
27
+ end
28
+ File.write out_file, { :errors => errors }.to_json
29
+ ensure
30
+ source.destroy if source
31
+ ::OpenSCAP.oscap_cleanup
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,77 @@
1
+ require 'tempfile'
2
+
3
+ module Proxy
4
+ module OpenSCAP
5
+ class ShellWrapper
6
+ include ::Proxy::Log
7
+
8
+ attr_reader :script_name
9
+
10
+ def script_location
11
+ raise NotImplementedError, 'Must have @script_name' unless script_name
12
+ path = File.join(File.dirname(File.expand_path(__FILE__)), '../../bin', script_name)
13
+ return path if File.exist? path
14
+ script_name
15
+ end
16
+
17
+ def execute_shell_command(in_file_content = nil)
18
+ out_file = Tempfile.new(out_filename, "/var/tmp")
19
+ in_file = prepare_in_file in_file_content
20
+ comm = command(in_file, out_file)
21
+ logger.debug "Executing: #{comm}"
22
+ output = nil
23
+ begin
24
+ `#{comm}`
25
+ output = out_file.read
26
+ rescue => e
27
+ logger.debug failure_message
28
+ logger.debug e.message
29
+ logger.debug e.backtrace.join("\n\t")
30
+ ensure
31
+ close_unlink out_file, in_file
32
+ end
33
+ raise OpenSCAPException, exception_message if output.nil? || output.empty?
34
+ output
35
+ end
36
+
37
+ def close_unlink(*files)
38
+ files.compact.each do |file|
39
+ file.close
40
+ file.unlink
41
+ end
42
+ end
43
+
44
+ def prepare_in_file(in_file_content)
45
+ return unless in_file_content
46
+ file = Tempfile.new(in_filename, "/var/tmp")
47
+ file.write in_file_content
48
+ file.rewind
49
+ file
50
+ end
51
+
52
+ def in_filename
53
+ @in_filename ||= unique_filename
54
+ end
55
+
56
+ def out_filename
57
+ @out_filename ||= unique_filename
58
+ end
59
+
60
+ def unique_filename
61
+ SecureRandom.uuid
62
+ end
63
+
64
+ def command(in_file, out_file)
65
+ raise NotImplementedError, "Must be implemented"
66
+ end
67
+
68
+ def failure_message
69
+ raise NotImplementedError, "Must be implemented"
70
+ end
71
+
72
+ def exception_message
73
+ failure_message
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,79 @@
1
+ module Proxy::OpenSCAP
2
+ class SpoolForwarder
3
+ include ::Proxy::Log
4
+
5
+ def initialize(loaded_settings)
6
+ @loaded_settings = loaded_settings
7
+ end
8
+
9
+ def post_arf_from_spool(arf_dir)
10
+ Dir.foreach(arf_dir) do |cname|
11
+ next if cname == '.' || cname == '..'
12
+ cname_dir = File.join(arf_dir, cname)
13
+ forward_cname_dir(cname, cname_dir) if File.directory?(cname_dir)
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def forward_cname_dir(cname, cname_dir)
20
+ Dir.foreach(cname_dir) do |policy_id|
21
+ next if policy_id == '.' || policy_id == '..'
22
+ policy_dir = File.join(cname_dir, policy_id)
23
+ if File.directory?(policy_dir)
24
+ forward_policy_dir(cname, policy_id, policy_dir)
25
+ end
26
+ end
27
+ remove_if_empty(cname_dir)
28
+ end
29
+
30
+ def forward_policy_dir(cname, policy_id, policy_dir)
31
+ Dir.foreach(policy_dir) do |date|
32
+ next if date == '.' || date == '..'
33
+ date_dir = File.join(policy_dir, date)
34
+ if File.directory?(date_dir)
35
+ forward_date_dir(cname, policy_id, date, date_dir)
36
+ end
37
+ end
38
+ remove_if_empty(policy_dir)
39
+ end
40
+
41
+ def forward_date_dir(cname, policy_id, date, date_dir)
42
+ Dir.foreach(date_dir) do |arf|
43
+ next if arf == '.' || arf == '..'
44
+ arf_path = File.join(date_dir, arf)
45
+ if File.file?(arf_path)
46
+ logger.debug("Uploading #{arf} to Foreman")
47
+ forward_arf_file(cname, policy_id, date, arf_path)
48
+ end
49
+ end
50
+ remove_if_empty(date_dir)
51
+ end
52
+
53
+ def forward_arf_file(cname, policy_id, date, arf_file_path)
54
+ data = File.open(arf_file_path, 'rb') { |io| io.read }
55
+ post_to_foreman = ForemanForwarder.new.post_arf_report(cname, policy_id, date, data, @loaded_settings.timeout)
56
+ Proxy::OpenSCAP::StorageFS.new(@loaded_settings.reportsdir, cname, post_to_foreman['id'], date).store_archive(data)
57
+ File.delete arf_file_path
58
+ rescue Proxy::OpenSCAP::OpenSCAPException => e
59
+ logger.error "Failed to parse Arf Report at #{arf_file_path}, moving to #{@loaded_settings.corrupted_dir}"
60
+
61
+ Proxy::OpenSCAP::StorageFS.new(@loaded_settings.corrupted_dir, cname, policy_id, date).
62
+ move_corrupted(arf_file_path.split('/').last, @loaded_settings.spooldir)
63
+ rescue Proxy::OpenSCAP::ReportUploadError => e
64
+ logger.error "Failed to upload Arf Report at #{arf_file_path}, cause: #{e.message}, the report will be deleted."
65
+ File.delete arf_file_path
66
+ rescue StandardError => e
67
+ logger.error "smart-proxy-openscap-send failed to upload Compliance report for #{cname}, generated on #{Time.at date.to_i}. Cause: #{e}"
68
+ end
69
+
70
+ def remove_if_empty(dir)
71
+ begin
72
+ Dir.delete dir if Dir["#{dir}/*"].empty?
73
+ logger.debug "Removing directory: #{dir}"
74
+ rescue StandardError => e
75
+ logger.error "Could not remove directory: #{e.message}"
76
+ end
77
+ end
78
+ end
79
+ end