smart_proxy_openscap 0.4.1 → 0.5.0

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.
@@ -0,0 +1,56 @@
1
+ require 'openscap/ds/sds'
2
+ require 'openscap/source'
3
+ require 'openscap/xccdf/benchmark'
4
+
5
+ module Proxy::OpenSCAP
6
+ class ContentParser
7
+ def initialize(scap_content)
8
+ OpenSCAP.oscap_init
9
+ @source = OpenSCAP::Source.new(:content => scap_content)
10
+ end
11
+
12
+ def extract_policies
13
+ policies = {}
14
+ bench = benchmark_profiles
15
+ bench.profiles.each do |key, profile|
16
+ policies[key] = profile.title
17
+ end
18
+ bench.destroy
19
+ policies.to_json
20
+ end
21
+
22
+ def validate
23
+ errors = []
24
+ allowed_type = 'SCAP Source Datastream'
25
+ if @source.type != allowed_type
26
+ errors << "Uploaded file is not #{allowed_type}"
27
+ end
28
+
29
+ begin
30
+ @source.validate!
31
+ rescue OpenSCAP::OpenSCAPError
32
+ errors << "Invalid SCAP file type"
33
+ end
34
+ {:errors => errors}.to_json
35
+ end
36
+
37
+ def guide(policy)
38
+ sds = OpenSCAP::DS::Sds.new @source
39
+ sds.select_checklist
40
+ profile_id = policy ? nil : policy
41
+ html = sds.html_guide profile_id
42
+ sds.destroy
43
+ {:html => html.force_encoding('UTF-8')}.to_json
44
+ end
45
+
46
+ private
47
+
48
+ def benchmark_profiles
49
+ sds = ::OpenSCAP::DS::Sds.new(@source)
50
+ bench_source = sds.select_checklist!
51
+ benchmark = ::OpenSCAP::Xccdf::Benchmark.new(bench_source)
52
+ sds.destroy
53
+ benchmark
54
+ end
55
+ end
56
+ end
@@ -1,20 +1,7 @@
1
1
  module Proxy::OpenSCAP
2
- class OpenSCAPException < Exception
3
- attr_accessor :response
4
- attr_accessor :message
5
- def initialize(response = nil)
6
- @response = response
7
- @message = response.message if response
8
- end
9
-
10
- def http_code
11
- @response.code || 500
12
- end
13
-
14
- def http_body
15
- @response.body if @response
16
- end
17
- end
18
-
2
+ class OpenSCAPException < StandardError; end
3
+ class StoreReportError < StandardError; end
4
+ class StoreSpoolError < StandardError; end
5
+ class StoreFailedError < StandardError; end
19
6
  class FileNotFound < StandardError; end
20
7
  end
@@ -10,32 +10,21 @@
10
10
 
11
11
  require 'digest'
12
12
  require 'fileutils'
13
+ require 'pathname'
13
14
  require 'json'
14
15
  require 'proxy/error'
15
16
  require 'proxy/request'
17
+ require 'smart_proxy_openscap/fetch_scap_content'
18
+ require 'smart_proxy_openscap/foreman_forwarder'
19
+ require 'smart_proxy_openscap/openscap_content_parser'
16
20
  require 'smart_proxy_openscap/openscap_exception'
21
+ require 'smart_proxy_openscap/openscap_report_parser'
22
+ require 'smart_proxy_openscap/spool_forwarder'
23
+ require 'smart_proxy_openscap/storage_fs'
17
24
 
18
25
  module Proxy::OpenSCAP
19
26
  extend ::Proxy::Log
20
27
 
21
- def self.get_policy_content(policy_id)
22
- policy_store_dir = File.join(Proxy::OpenSCAP::Plugin.settings.contentdir, policy_id.to_s)
23
- policy_scap_file = File.join(policy_store_dir, "#{policy_id}_scap_content.xml")
24
- begin
25
- FileUtils.mkdir_p(policy_store_dir) # will fail silently if exists
26
- rescue Errno::EACCES => e
27
- logger.error "No permission to create directory #{policy_store_dir}"
28
- raise e
29
- rescue StandardError => e
30
- logger.error "Could not create '#{policy_store_dir}' directory: #{e.message}"
31
- raise e
32
- end
33
-
34
- scap_file = policy_content_file(policy_scap_file)
35
- scap_file ||= save_or_serve_scap_file(policy_id, policy_scap_file)
36
- scap_file
37
- end
38
-
39
28
  def self.common_name(request)
40
29
  client_cert = request.env['SSL_CLIENT_CERT']
41
30
  raise Proxy::Error::Unauthorized, "Client certificate required!" if client_cert.to_s.empty?
@@ -48,162 +37,17 @@ module Proxy::OpenSCAP
48
37
  cn = client_cert.subject.to_a.detect { |name, value| name == 'CN' }
49
38
  cn = cn[1] unless cn.nil?
50
39
  raise Proxy::Error::Unauthorized, "Common Name not found in the certificate" unless cn
51
- return cn
52
- end
53
-
54
- def self.spool_arf_dir(common_name, policy_id)
55
- validate_policy_id(policy_id)
56
- date = Time.now.strftime("%Y-%m-%d")
57
- dir = Proxy::OpenSCAP::Plugin.settings.spooldir + "/arf/#{common_name}/#{policy_id}/#{date}/"
58
- begin
59
- FileUtils.mkdir_p dir
60
- rescue StandardError => e
61
- logger.error "Could not create '#{dir}' directory: #{e.message}"
62
- raise e
63
- end
64
- dir
65
- end
66
-
67
- def self.store_arf(spool_arf_dir, data)
68
- filename = Digest::SHA256.hexdigest data
69
- target_path = spool_arf_dir + filename
70
- File.open(target_path,'w') { |f| f.write(data) }
71
- return target_path
40
+ cn
72
41
  end
73
42
 
74
43
  def self.send_spool_to_foreman
75
44
  arf_dir = File.join(Proxy::OpenSCAP::Plugin.settings.spooldir, "/arf")
76
- return unless File.exists? arf_dir
77
- ForemanForwarder.new.do(arf_dir)
78
- end
79
-
80
- private
81
- def self.validate_policy_id(id)
82
- unless /[\d]+/ =~ id
83
- raise Proxy::Error::BadRequest, "Malformed policy ID"
84
- end
85
- end
86
-
87
- def self.fetch_scap_content_xml(policy_id, policy_scap_file)
88
- foreman_request = Proxy::HttpRequest::ForemanRequest.new
89
- policy_content_path = "/api/v2/compliance/policies/#{policy_id}/content"
90
- req = foreman_request.request_factory.create_get(policy_content_path)
91
- response = foreman_request.send_request(req)
92
- unless response.is_a? Net::HTTPSuccess
93
- raise OpenSCAPException.new(response)
94
- end
95
- response.body
96
- end
97
-
98
-
99
- def self.policy_content_file(policy_scap_file)
100
- return nil if !File.file?(policy_scap_file) || File.zero?(policy_scap_file)
101
- File.open(policy_scap_file, 'rb').read
45
+ return unless File.exist? arf_dir
46
+ SpoolForwarder.new.post_arf_from_spool(arf_dir)
102
47
  end
103
48
 
104
- def self.save_or_serve_scap_file(policy_id, policy_scap_file)
105
- lock = Proxy::FileLock::try_locking(policy_scap_file)
106
- response = fetch_scap_content_xml(policy_id, policy_scap_file)
107
- if lock.nil?
108
- return response
109
- else
110
- begin
111
- File.open(policy_scap_file, 'wb') do |file|
112
- file << response
113
- end
114
- ensure
115
- Proxy::FileLock::unlock(lock)
116
- end
117
- scap_file = policy_content_file(policy_scap_file)
118
- raise FileNotFound if scap_file.nil?
119
- return scap_file
120
- end
121
- end
122
-
123
- class ForemanForwarder < Proxy::HttpRequest::ForemanRequest
124
- def do(arf_dir)
125
- Dir.foreach(arf_dir) { |cname|
126
- cname_dir = File.join(arf_dir, cname)
127
- if File.directory? cname_dir and !(cname == '.' || cname == '..')
128
- forward_cname_dir(cname, cname_dir)
129
- end
130
- }
131
- end
132
-
133
- private
134
- def forward_cname_dir(cname, cname_dir)
135
- Dir.foreach(cname_dir) { |policy_id|
136
- policy_dir = File.join(cname_dir, policy_id)
137
- if File.directory? policy_dir and !(policy_id == '.' || policy_id == '..')
138
- forward_policy_dir(cname, policy_id, policy_dir)
139
- end
140
- }
141
- remove(cname_dir)
142
- end
143
-
144
- def forward_policy_dir(cname, policy_id, policy_dir)
145
- Dir.foreach(policy_dir) { |date|
146
- date_dir = File.join(policy_dir, date)
147
- if File.directory? date_dir and !(date == '.' || date == '..')
148
- forward_date_dir(cname, policy_id, date, date_dir)
149
- end
150
- }
151
- remove(policy_dir)
152
- end
153
-
154
- def forward_date_dir(cname, policy_id, date, date_dir)
155
- path = upload_path(cname, policy_id, date)
156
- Dir.foreach(date_dir) { |arf|
157
- arf_path = File.join(date_dir, arf)
158
- if File.file? arf_path and !(arf == '.' || arf == '..')
159
- logger.debug("Uploading #{arf} to #{path}")
160
- forward_arf_file(path, arf_path)
161
- end
162
- }
163
- remove(date_dir)
164
- end
165
-
166
- def upload_path(cname, policy_id, date)
167
- return "/api/v2/compliance/arf_reports/#{cname}/#{policy_id}/#{date}"
168
- end
169
-
170
- def forward_arf_file(foreman_api_path, arf_file_path)
171
- begin
172
- data = File.read(arf_file_path)
173
- response = send_request(foreman_api_path, data)
174
- # Raise an HTTP error if the response is not 2xx (success).
175
- response.value
176
- res = JSON.parse(response.body)
177
- raise StandardError, "Received result: #{res['result']}" unless res['result'] == 'OK'
178
- File.delete arf_file_path
179
- rescue StandardError => e
180
- logger.debug response.body if response
181
- raise e
182
- end
183
- end
184
-
185
- def remove(dir)
186
- begin
187
- Dir.delete dir
188
- rescue StandardError => e
189
- logger.error "Could not remove directory: #{e.message}"
190
- end
191
- end
192
-
193
- def send_request(path, body)
194
- # Override the parent method to set the right headers
195
- path = [uri.path, path].join('/') unless uri.path.empty?
196
- req = Net::HTTP::Post.new(URI.join(uri.to_s, path).path)
197
- # Well, this is unfortunate. We want to have content-type text/xml. We
198
- # also need the content-encoding to equal with x-bzip2. However, when
199
- # the Foreman's framework sees text/xml, it will rewrite it to application/xml.
200
- # What's worse, a framework will try to parse body as an utf8 string,
201
- # no matter what content-encoding says. Oh my.
202
- # Let's pass content-type arf-bzip2 and move forward.
203
- req.content_type = 'application/arf-bzip2'
204
- req['Content-Encoding'] = 'x-bzip2'
205
- req.body = body
206
- http.request(req)
207
- end
49
+ def self.fullpath(path = Proxy::OpenSCAP::Plugin.settings.contentdir)
50
+ pathname = Pathname.new(path)
51
+ pathname.relative? ? pathname.expand_path(Sinatra::Base.root).to_s : path
208
52
  end
209
53
  end
@@ -19,6 +19,8 @@ module Proxy::OpenSCAP
19
19
 
20
20
  default_settings :spooldir => '/var/spool/foreman-proxy/openscap',
21
21
  :openscap_send_log_file => 'logs/openscap-send.log',
22
- :contentdir => 'openscap/content'
22
+ :contentdir => 'openscap/content',
23
+ :reportsdir => 'openscap/reports',
24
+ :failed_dir => 'openscap/failed'
23
25
  end
24
26
  end
@@ -0,0 +1,93 @@
1
+ # encoding=utf-8
2
+ require 'openscap'
3
+ require 'openscap/ds/arf'
4
+ require 'openscap/xccdf/testresult'
5
+ require 'openscap/xccdf/ruleresult'
6
+ require 'openscap/xccdf/rule'
7
+ require 'openscap/xccdf/fix'
8
+ require 'openscap/xccdf/benchmark'
9
+ require 'json'
10
+ module Proxy::OpenSCAP
11
+ class Parse
12
+ def initialize(arf_data)
13
+ OpenSCAP.oscap_init
14
+ size = arf_data.size
15
+ @arf_digest = Digest::SHA256.hexdigest(arf_data)
16
+ @arf = OpenSCAP::DS::Arf.new(:content => arf_data, :path => 'arf.xml.bz2', :length => size)
17
+ @results = @arf.test_result.rr
18
+ sds = @arf.report_request
19
+ bench_source = sds.select_checklist!
20
+ @bench = OpenSCAP::Xccdf::Benchmark.new(bench_source)
21
+ @items = @bench.items
22
+ end
23
+
24
+ def as_json
25
+ parse_report.to_json
26
+ end
27
+
28
+ private
29
+
30
+ def parse_report
31
+ report = {}
32
+ report[:logs] = []
33
+ passed = 0
34
+ failed = 0
35
+ othered = 0
36
+ @results.each do |rr_id, result|
37
+ next if result.result == 'notapplicable' || result.result == 'notselected'
38
+ # get rules and their results
39
+ rule_data = @items[rr_id]
40
+ report[:logs] << populate_result_data(rr_id, result.result, rule_data)
41
+ # create metrics for the results
42
+ case result.result
43
+ when 'pass', 'fixed'
44
+ passed += 1
45
+ when 'fail'
46
+ failed += 1
47
+ else
48
+ othered += 1
49
+ end
50
+ end
51
+ report[:digest] = @arf_digest
52
+ report[:metrics] = { :passed => passed, :failed => failed, :othered => othered }
53
+ report
54
+ end
55
+
56
+ def populate_result_data(result_id, rule_result, rule_data)
57
+ log = {}
58
+ log[:source] = ascii8bit_to_utf8(result_id)
59
+ log[:result] = ascii8bit_to_utf8(rule_result)
60
+ log[:title] = ascii8bit_to_utf8(rule_data.title)
61
+ log[:description] = ascii8bit_to_utf8(rule_data.description)
62
+ log[:rationale] = ascii8bit_to_utf8(rule_data.rationale)
63
+ log[:references] = hash_a8b(rule_data.references.map(&:to_hash))
64
+ log[:fixes] = hash_a8b(rule_data.fixes.map(&:to_hash))
65
+ log[:severity] = ascii8bit_to_utf8(rule_data.severity)
66
+ log
67
+ end
68
+
69
+ # Unfortunately openscap in ruby 1.9.3 outputs data in Ascii-8bit.
70
+ # We transform it to UTF-8 for easier json integration.
71
+
72
+ # :invalid ::
73
+ # If the value is invalid, #encode replaces invalid byte sequences in
74
+ # +str+ with the replacement character. The default is to raise the
75
+ # Encoding::InvalidByteSequenceError exception
76
+ # :undef ::
77
+ # If the value is undefined, #encode replaces characters which are
78
+ # undefined in the destination encoding with the replacement character.
79
+ # The default is to raise the Encoding::UndefinedConversionError.
80
+ # :replace ::
81
+ # Sets the replacement string to the given value. The default replacement
82
+ # string is "\uFFFD" for Unicode encoding forms, and "?" otherwise.
83
+ def ascii8bit_to_utf8(string)
84
+ string.to_s.encode('utf-8', :invalid => :replace, :undef => :replace, :replace => '_')
85
+ end
86
+
87
+ def hash_a8b(ary)
88
+ ary.map do |hash|
89
+ Hash[hash.map { |key, value| [ascii8bit_to_utf8(key), ascii8bit_to_utf8(value)] }]
90
+ end
91
+ end
92
+ end
93
+ end
@@ -10,6 +10,6 @@
10
10
 
11
11
  module Proxy
12
12
  module OpenSCAP
13
- VERSION = '0.4.1'
13
+ VERSION = '0.5.0'
14
14
  end
15
15
  end
@@ -0,0 +1,64 @@
1
+ module Proxy::OpenSCAP
2
+ class SpoolForwarder
3
+ include ::Proxy::Log
4
+
5
+ def post_arf_from_spool(arf_dir)
6
+ Dir.foreach(arf_dir) do |cname|
7
+ next if cname == '.' || cname == '..'
8
+ cname_dir = File.join(arf_dir, cname)
9
+ forward_cname_dir(cname, cname_dir) if File.directory?(cname_dir)
10
+ end
11
+ end
12
+
13
+ private
14
+
15
+ def forward_cname_dir(cname, cname_dir)
16
+ Dir.foreach(cname_dir) do |policy_id|
17
+ next if policy_id == '.' || policy_id == '..'
18
+ policy_dir = File.join(cname_dir, policy_id)
19
+ if File.directory?(policy_dir)
20
+ forward_policy_dir(cname, policy_id, policy_dir)
21
+ end
22
+ end
23
+ remove(cname_dir)
24
+ end
25
+
26
+ def forward_policy_dir(cname, policy_id, policy_dir)
27
+ Dir.foreach(policy_dir) do |date|
28
+ next if date == '.' || date == '..'
29
+ date_dir = File.join(policy_dir, date)
30
+ if File.directory?(date_dir)
31
+ forward_date_dir(cname, policy_id, date, date_dir)
32
+ end
33
+ end
34
+ remove(policy_dir)
35
+ end
36
+
37
+ def forward_date_dir(cname, policy_id, date, date_dir)
38
+ Dir.foreach(date_dir) do |arf|
39
+ next if arf == '.' || arf == '..'
40
+ arf_path = File.join(date_dir, arf)
41
+ if File.file?(arf_path)
42
+ logger.debug("Uploading #{arf} to Foreman")
43
+ forward_arf_file(cname, policy_id, date, arf_path)
44
+ end
45
+ end
46
+ remove(date_dir)
47
+ end
48
+
49
+ def forward_arf_file(cname, policy_id, date, arf_file_path)
50
+ data = File.open(arf_file_path, 'rb') { |io| io.read }
51
+ post_to_foreman = ForemanForwarder.new.post_arf_report(cname, policy_id, date, data)
52
+ Proxy::OpenSCAP::StorageFS.new(Proxy::OpenSCAP::Plugin.settings.reportsdir, cname, post_to_foreman['id'], date).store_archive(data)
53
+ File.delete arf_file_path
54
+ end
55
+
56
+ def remove(dir)
57
+ begin
58
+ Dir.delete dir
59
+ rescue StandardError => e
60
+ logger.error "Could not remove directory: #{e.message}"
61
+ end
62
+ end
63
+ end
64
+ end