smart_proxy_openscap 0.4.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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