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.
- checksums.yaml +4 -4
- data/.rubocop.yml +41 -0
- data/.rubocop_todo.yml +111 -0
- data/Gemfile +8 -0
- data/README.md +58 -37
- data/Rakefile +14 -0
- data/bin/smart-proxy-openscap-send +1 -1
- data/lib/smart_proxy_openscap/fetch_scap_content.rb +58 -0
- data/lib/smart_proxy_openscap/foreman_forwarder.rb +38 -0
- data/lib/smart_proxy_openscap/openscap_api.rb +76 -17
- data/lib/smart_proxy_openscap/openscap_content_parser.rb +56 -0
- data/lib/smart_proxy_openscap/openscap_exception.rb +4 -17
- data/lib/smart_proxy_openscap/openscap_lib.rb +13 -169
- data/lib/smart_proxy_openscap/openscap_plugin.rb +3 -1
- data/lib/smart_proxy_openscap/openscap_report_parser.rb +93 -0
- data/lib/smart_proxy_openscap/openscap_version.rb +1 -1
- data/lib/smart_proxy_openscap/spool_forwarder.rb +64 -0
- data/lib/smart_proxy_openscap/storage.rb +45 -0
- data/lib/smart_proxy_openscap/storage_fs.rb +71 -0
- data/settings.d/openscap.yml.example +8 -0
- data/smart_proxy_openscap.gemspec +7 -1
- data/test/data/arf_report +0 -0
- data/test/data/ssg-rhel7-ds.xml +20271 -0
- data/test/fetch_scap_api_test.rb +71 -0
- data/test/get_report_xml_html_test.rb +52 -0
- data/test/post_report_api_test.rb +75 -0
- data/test/scap_content_parser_api_test.rb +54 -0
- data/test/test_helper.rb +11 -0
- metadata +93 -4
@@ -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 <
|
3
|
-
|
4
|
-
|
5
|
-
|
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
|
-
|
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.
|
77
|
-
|
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.
|
105
|
-
|
106
|
-
|
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
|
@@ -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
|