smart_proxy_openscap 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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