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.
- 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
|