inspec_tools 2.0.4 → 2.2.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/README.md +25 -17
- data/Rakefile +82 -8
- data/lib/data/cis_to_nist_critical_controls +0 -0
- data/lib/data/cis_to_nist_mapping +0 -0
- data/lib/happy_mapper_tools/benchmark.rb +83 -0
- data/lib/happy_mapper_tools/stig_attributes.rb +68 -48
- data/lib/inspec_tools/csv.rb +42 -39
- data/lib/inspec_tools/generate_map.rb +35 -0
- data/lib/inspec_tools/inspec.rb +19 -91
- data/lib/inspec_tools/pdf.rb +2 -13
- data/lib/inspec_tools/plugin_cli.rb +22 -53
- data/lib/inspec_tools/summary.rb +108 -76
- data/lib/inspec_tools/xccdf.rb +1 -0
- data/lib/inspec_tools/xlsx_tool.rb +4 -16
- data/lib/utilities/cci_xml.rb +13 -0
- data/lib/utilities/cis_to_nist.rb +11 -0
- data/lib/utilities/inspec_util.rb +10 -74
- data/lib/utilities/mapping_validator.rb +10 -0
- data/lib/utilities/xccdf/from_inspec.rb +89 -0
- data/lib/utilities/xccdf/to_xccdf.rb +388 -0
- data/lib/utilities/xccdf/xccdf_score.rb +116 -0
- metadata +48 -29
- data/CHANGELOG.md +0 -714
- data/lib/data/NIST_Map_02052020_CIS_Controls_Version_7.1_Implementation_Groups_1.2.xlsx +0 -0
- data/lib/data/NIST_Map_09212017B_CSC-CIS_Critical_Security_Controls_VER_6.1_Excel_9.1.2016.xlsx +0 -0
- data/lib/utilities/extract_nist_cis_mapping.rb +0 -57
@@ -0,0 +1,388 @@
|
|
1
|
+
require_relative 'xccdf_score'
|
2
|
+
|
3
|
+
module Utils
|
4
|
+
# Data conversions for Inspec output into XCCDF format.
|
5
|
+
class ToXCCDF # rubocop:disable Metrics/ClassLength
|
6
|
+
# @param attribute [Hash] XCCDF supplemental attributes
|
7
|
+
# @param data [Hash] Converted Inspec output data
|
8
|
+
def initialize(attribute, data)
|
9
|
+
@attribute = attribute
|
10
|
+
@data = data
|
11
|
+
@benchmark = HappyMapperTools::Benchmark::Benchmark.new
|
12
|
+
end
|
13
|
+
|
14
|
+
# Build entire XML document and produce final output
|
15
|
+
# @param metadata [Hash] Data representing a system under scan
|
16
|
+
def to_xml(metadata)
|
17
|
+
build_benchmark_header
|
18
|
+
build_groups
|
19
|
+
# Only populate results if a target is defined so that conformant XML is produced.
|
20
|
+
@benchmark.testresult = build_test_results(metadata) if metadata['fqdn']
|
21
|
+
@benchmark.to_xml
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
# Sets top level XCCDF Benchmark attributes
|
27
|
+
def build_benchmark_header
|
28
|
+
@benchmark.title = @attribute['benchmark.title']
|
29
|
+
@benchmark.id = @attribute['benchmark.id']
|
30
|
+
@benchmark.description = @attribute['benchmark.description']
|
31
|
+
@benchmark.version = @attribute['benchmark.version']
|
32
|
+
@benchmark.xmlns = 'http://checklists.nist.gov/xccdf/1.1'
|
33
|
+
|
34
|
+
@benchmark.status = HappyMapperTools::Benchmark::Status.new
|
35
|
+
@benchmark.status.status = @attribute['benchmark.status']
|
36
|
+
@benchmark.status.date = @attribute['benchmark.status.date']
|
37
|
+
|
38
|
+
if @attribute['benchmark.notice.id']
|
39
|
+
@benchmark.notice = HappyMapperTools::Benchmark::Notice.new
|
40
|
+
@benchmark.notice.id = @attribute['benchmark.notice.id']
|
41
|
+
end
|
42
|
+
|
43
|
+
if @attribute['benchmark.plaintext'] || @attribute['benchmark.plaintext.id']
|
44
|
+
@benchmark.plaintext = HappyMapperTools::Benchmark::Plaintext.new
|
45
|
+
@benchmark.plaintext.plaintext = @attribute['benchmark.plaintext']
|
46
|
+
@benchmark.plaintext.id = @attribute['benchmark.plaintext.id']
|
47
|
+
end
|
48
|
+
|
49
|
+
@benchmark.reference = HappyMapperTools::Benchmark::ReferenceBenchmark.new
|
50
|
+
@benchmark.reference.href = @attribute['reference.href']
|
51
|
+
@benchmark.reference.dc_publisher = @attribute['reference.dc.publisher']
|
52
|
+
@benchmark.reference.dc_source = @attribute['reference.dc.source']
|
53
|
+
end
|
54
|
+
|
55
|
+
# Translate join of Inspec results and input attributes to XCCDF Groups
|
56
|
+
def build_groups # rubocop:disable Metrics/AbcSize
|
57
|
+
group_array = []
|
58
|
+
@data['controls'].each do |control|
|
59
|
+
group = HappyMapperTools::Benchmark::Group.new
|
60
|
+
group.id = control['id']
|
61
|
+
group.title = control['gtitle']
|
62
|
+
group.description = "<GroupDescription>#{control['gdescription']}</GroupDescription>" if control['gdescription']
|
63
|
+
|
64
|
+
group.rule = HappyMapperTools::Benchmark::Rule.new
|
65
|
+
group.rule.id = control['rid']
|
66
|
+
group.rule.severity = control['severity']
|
67
|
+
group.rule.weight = control['rweight']
|
68
|
+
group.rule.version = control['rversion']
|
69
|
+
group.rule.title = control['title'].tr("\n", ' ') if control['title']
|
70
|
+
group.rule.description = "<VulnDiscussion>#{control['desc'].tr("\n", ' ')}</VulnDiscussion><FalsePositives></FalsePositives><FalseNegatives></FalseNegatives><Documentable>false</Documentable><Mitigations></Mitigations><SeverityOverrideGuidance></SeverityOverrideGuidance><PotentialImpacts></PotentialImpacts><ThirdPartyTools></ThirdPartyTools><MitigationControl></MitigationControl><Responsibility></Responsibility><IAControls></IAControls>"
|
71
|
+
|
72
|
+
if ['reference.dc.publisher', 'reference.dc.title', 'reference.dc.subject', 'reference.dc.type', 'reference.dc.identifier'].any? { |a| @attribute.key?(a) }
|
73
|
+
group.rule.reference = build_rule_reference
|
74
|
+
end
|
75
|
+
|
76
|
+
group.rule.ident = build_rule_idents(control['cci']) if control['cci']
|
77
|
+
|
78
|
+
group.rule.fixtext = HappyMapperTools::Benchmark::Fixtext.new
|
79
|
+
group.rule.fixtext.fixref = control['fix_id']
|
80
|
+
group.rule.fixtext.fixtext = control['fix']
|
81
|
+
|
82
|
+
group.rule.fix = build_rule_fix(control['fix_id']) if control['fix_id']
|
83
|
+
|
84
|
+
group.rule.check = HappyMapperTools::Benchmark::Check.new
|
85
|
+
group.rule.check.system = control['checkref']
|
86
|
+
|
87
|
+
# content_ref is optional for schema compliance
|
88
|
+
if @attribute['content_ref.name'] || @attribute['content_ref.href']
|
89
|
+
group.rule.check.content_ref = HappyMapperTools::Benchmark::ContentRef.new
|
90
|
+
group.rule.check.content_ref.name = @attribute['content_ref.name']
|
91
|
+
group.rule.check.content_ref.href = @attribute['content_ref.href']
|
92
|
+
end
|
93
|
+
|
94
|
+
group.rule.check.content = control['check']
|
95
|
+
|
96
|
+
group_array << group
|
97
|
+
end
|
98
|
+
@benchmark.group = group_array
|
99
|
+
end
|
100
|
+
|
101
|
+
# Construct a Benchmark Testresult from Inspec data. This must be called after all XML processing has occurred for profiles
|
102
|
+
# and groups.
|
103
|
+
# @param metadata [Hash]
|
104
|
+
# @return [TestResult]
|
105
|
+
def build_test_results(metadata)
|
106
|
+
test_result = HappyMapperTools::Benchmark::TestResult.new
|
107
|
+
test_result.version = @benchmark.version
|
108
|
+
populate_remark(test_result)
|
109
|
+
populate_target_facts(test_result, metadata)
|
110
|
+
populate_identity(test_result, metadata)
|
111
|
+
populate_results(test_result)
|
112
|
+
populate_score(test_result, @benchmark.group)
|
113
|
+
|
114
|
+
test_result
|
115
|
+
end
|
116
|
+
|
117
|
+
# Contruct a Rule / RuleResult fix element with the provided id.
|
118
|
+
def build_rule_fix(fix_id)
|
119
|
+
HappyMapperTools::Benchmark::Fix.new.tap { |f| f.id = fix_id }
|
120
|
+
end
|
121
|
+
|
122
|
+
# Construct rule identifiers for rule
|
123
|
+
# @param idents [Array]
|
124
|
+
def build_rule_idents(idents)
|
125
|
+
raise "#{idents} is not an Array type." unless idents.is_a?(Array)
|
126
|
+
|
127
|
+
# Each rule identifier is a different element
|
128
|
+
idents.map do |identifier|
|
129
|
+
ident = HappyMapperTools::Benchmark::Ident.new
|
130
|
+
ident.system = 'https://public.cyber.mil/stigs/cci/'
|
131
|
+
ident.ident = identifier
|
132
|
+
ident
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# Contruct a Rule reference element
|
137
|
+
def build_rule_reference
|
138
|
+
reference = HappyMapperTools::Benchmark::ReferenceGroup.new
|
139
|
+
reference.dc_publisher = @attribute['reference.dc.publisher']
|
140
|
+
reference.dc_title = @attribute['reference.dc.title']
|
141
|
+
reference.dc_subject = @attribute['reference.dc.subject']
|
142
|
+
reference.dc_type = @attribute['reference.dc.type']
|
143
|
+
reference.dc_identifier = @attribute['reference.dc.identifier']
|
144
|
+
reference
|
145
|
+
end
|
146
|
+
|
147
|
+
# Create a remark with contextual information about the Inspec version and profiles used
|
148
|
+
# @param result [HappyMapperTools::Benchmark::TestResult]
|
149
|
+
def populate_remark(result)
|
150
|
+
result.remark = "Results created using Inspec version #{@data['inspec_version']}.\n#{@data['profiles'].map { |p| "Profile: #{p['name']} Version: #{p['version']}" }.join("\n")}"
|
151
|
+
end
|
152
|
+
|
153
|
+
# Create all target specific information.
|
154
|
+
# @param result [HappyMapperTools::Benchmark::TestResult]
|
155
|
+
# @param metadata [Hash]
|
156
|
+
def populate_target_facts(result, metadata)
|
157
|
+
result.target = metadata['fqdn']
|
158
|
+
result.target_address = metadata['ip'] if metadata['ip']
|
159
|
+
|
160
|
+
all_facts = []
|
161
|
+
|
162
|
+
if metadata['mac']
|
163
|
+
fact = HappyMapperTools::Benchmark::Fact.new
|
164
|
+
fact.name = 'urn:xccdf:fact:asset:identifier:mac'
|
165
|
+
fact.type = 'string'
|
166
|
+
fact.fact = metadata['mac']
|
167
|
+
all_facts << fact
|
168
|
+
end
|
169
|
+
|
170
|
+
if metadata['ip']
|
171
|
+
fact = HappyMapperTools::Benchmark::Fact.new
|
172
|
+
fact.name = 'urn:xccdf:fact:asset:identifier:ipv4'
|
173
|
+
fact.type = 'string'
|
174
|
+
fact.fact = metadata['ip']
|
175
|
+
all_facts << fact
|
176
|
+
end
|
177
|
+
|
178
|
+
return unless all_facts.size.nonzero?
|
179
|
+
|
180
|
+
facts = HappyMapperTools::Benchmark::TargetFact.new
|
181
|
+
facts.fact = all_facts
|
182
|
+
result.target_facts = facts
|
183
|
+
end
|
184
|
+
|
185
|
+
# Build out the TestResult given all the control and result data.
|
186
|
+
def populate_results(test_result)
|
187
|
+
# Note: id is not an XCCDF 1.2 compliant identifier and will need to be updated when that support is added.
|
188
|
+
test_result.id = 'result_1'
|
189
|
+
test_result.starttime = run_start_time
|
190
|
+
test_result.endtime = run_end_time
|
191
|
+
|
192
|
+
# Build out individual results
|
193
|
+
all_rule_result = []
|
194
|
+
|
195
|
+
@data['controls'].each do |control|
|
196
|
+
next if control['results'].empty?
|
197
|
+
|
198
|
+
control_results =
|
199
|
+
control['results'].map do |result|
|
200
|
+
populate_rule_result(control, result, xccdf_status(result['status'], control['impact']))
|
201
|
+
end
|
202
|
+
|
203
|
+
# Consolidate results into single rule result do to lack of multiple=true attribute on Rule.
|
204
|
+
# 1. Select the unified result status
|
205
|
+
selected_status = control_results.reduce(control_results.first.result) { |f_status, rule_result| xccdf_and_result(f_status, rule_result.result) }
|
206
|
+
|
207
|
+
# 2. Only choose results with that status
|
208
|
+
# 3. Combine those results
|
209
|
+
all_rule_result << combine_results(control_results.select { |r| r.result == selected_status })
|
210
|
+
end
|
211
|
+
|
212
|
+
test_result.rule_result = all_rule_result
|
213
|
+
test_result
|
214
|
+
end
|
215
|
+
|
216
|
+
# Create rule-result from the control and Inspec result information
|
217
|
+
def populate_rule_result(control, result, result_status)
|
218
|
+
rule_result = HappyMapperTools::Benchmark::RuleResultType.new
|
219
|
+
|
220
|
+
rule_result.idref = control['rid']
|
221
|
+
rule_result.severity = control['severity']
|
222
|
+
rule_result.time = end_time(result['start_time'], result['run_time'])
|
223
|
+
rule_result.weight = control['rweight']
|
224
|
+
|
225
|
+
rule_result.result = result_status
|
226
|
+
rule_result.message = result_message(result, result_status) if result_message(result, result_status)
|
227
|
+
rule_result.instance = result['code_desc']
|
228
|
+
|
229
|
+
rule_result.ident = build_rule_idents(control['cci']) if control['cci']
|
230
|
+
|
231
|
+
# Fix information is only necessary when there are failed tests
|
232
|
+
rule_result.fix = build_rule_fix(control['fix_id']) if control['fix_id'] && result_status == 'fail'
|
233
|
+
|
234
|
+
rule_result.check = HappyMapperTools::Benchmark::Check.new
|
235
|
+
rule_result.check.system = control['checkref']
|
236
|
+
rule_result.check.content = result['code_desc']
|
237
|
+
rule_result
|
238
|
+
end
|
239
|
+
|
240
|
+
# Combines rule results with the same result into a single rule result.
|
241
|
+
def combine_results(rule_results) # rubocop:disable Metrics/AbcSize
|
242
|
+
return rule_results.first if rule_results.size == 1
|
243
|
+
|
244
|
+
# Can combine, result, idents (duplicate, take first instance), instance - combine into an array removing duplicates
|
245
|
+
# check.content - Only one value allowed, combine by joining with line feed. Prior to, make sure all values are unique.
|
246
|
+
|
247
|
+
rule_result = HappyMapperTools::Benchmark::RuleResultType.new
|
248
|
+
rule_result.idref = rule_results.first.idref
|
249
|
+
rule_result.severity = rule_results.first.severity
|
250
|
+
# Take latest time
|
251
|
+
rule_result.time = rule_results.reduce(rule_results.first.time) { |time, r| time > r.time ? time : r.time }
|
252
|
+
rule_result.weight = rule_results.first.weight
|
253
|
+
|
254
|
+
rule_result.result = rule_results.first.result
|
255
|
+
rule_result.message = rule_results.reduce([]) { |messages, r| r.message ? messages.push(r.message) : messages }
|
256
|
+
rule_result.instance = rule_results.reduce([]) { |instances, r| r.instance ? instances.push(r.instance) : instances }.join("\n")
|
257
|
+
|
258
|
+
rule_result.ident = rule_results.first.ident
|
259
|
+
rule_result.fix = rule_results.first.fix
|
260
|
+
|
261
|
+
if rule_results.first.check
|
262
|
+
rule_result.check = HappyMapperTools::Benchmark::Check.new
|
263
|
+
rule_result.check.system = rule_results.first.check.system
|
264
|
+
rule_result.check.content = rule_results.map { |r| r.check.content }.join("\n")
|
265
|
+
end
|
266
|
+
|
267
|
+
rule_result
|
268
|
+
end
|
269
|
+
|
270
|
+
# Add information about the the account and organization executing the tests.
|
271
|
+
def populate_identity(test_result, metadata)
|
272
|
+
if metadata['identity']
|
273
|
+
test_result.identity = HappyMapperTools::Benchmark::IdentityType.new
|
274
|
+
test_result.identity.authenticated = true
|
275
|
+
test_result.identity.identity = metadata['identity']['identity']
|
276
|
+
test_result.identity.privileged = metadata['identity']['privileged']
|
277
|
+
end
|
278
|
+
|
279
|
+
test_result.organization = metadata['organization'] if metadata['organization']
|
280
|
+
end
|
281
|
+
|
282
|
+
# Return the earliest time of execution.
|
283
|
+
def run_start_time
|
284
|
+
@data['controls'].map { |control| control['results'].map { |result| DateTime.parse(result['start_time']) } }.flatten.min
|
285
|
+
end
|
286
|
+
|
287
|
+
# Return the latest time of execution accounting for Inspec duration.
|
288
|
+
def run_end_time
|
289
|
+
end_times =
|
290
|
+
@data['controls'].map do |control|
|
291
|
+
control['results'].map { |result| end_time(result['start_time'], result['run_time']) }
|
292
|
+
end
|
293
|
+
|
294
|
+
end_times.flatten.max
|
295
|
+
end
|
296
|
+
|
297
|
+
# Calculate an end time given a start time and second duration
|
298
|
+
def end_time(start, duration)
|
299
|
+
DateTime.parse(start) + (duration / (24*60*60))
|
300
|
+
end
|
301
|
+
|
302
|
+
# Map the Inspec result status to appropriate XCCDF test result status.
|
303
|
+
# XCCDF options include: pass, fail, error, unknown, notapplicable, notchecked, notselected, informational, fixed
|
304
|
+
#
|
305
|
+
# @param inspec_status [String] The reported Inspec status from an individual test
|
306
|
+
# @param impact [String] A value of 0.0 - 1.0
|
307
|
+
# @return A valid Inspec status.
|
308
|
+
def xccdf_status(inspec_status, impact)
|
309
|
+
# Currently, there is no good way to map an Inspec result status to one of XCCDF status unknown or notselected.
|
310
|
+
case inspec_status
|
311
|
+
when 'failed'
|
312
|
+
'fail'
|
313
|
+
when 'passed'
|
314
|
+
'pass'
|
315
|
+
when 'skipped'
|
316
|
+
if impact.to_f.zero?
|
317
|
+
'notapplicable'
|
318
|
+
else
|
319
|
+
'notchecked'
|
320
|
+
end
|
321
|
+
else
|
322
|
+
# In the event Inspec adds a new unaccounted for status, mapping to XCCDF unknown.
|
323
|
+
'unknown'
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
# When more than one result occurs for a rule and the specification does not declare multiple, the result must be combined.
|
328
|
+
# This determines the appropriate result to be selected when there are two to compare.
|
329
|
+
# @param one [String] A rule-result status
|
330
|
+
# @param two [String] A rule-result status
|
331
|
+
# @return The result of the AND operation.
|
332
|
+
def xccdf_and_result(one, two) # rubocop:disable Metrics/CyclomaticComplexity
|
333
|
+
# From XCCDF specification truth table
|
334
|
+
# P = pass
|
335
|
+
# F = fail
|
336
|
+
# U = unknown
|
337
|
+
# E = error
|
338
|
+
# N = notapplicable
|
339
|
+
# K = notchecked
|
340
|
+
# S = notselected
|
341
|
+
# I = informational
|
342
|
+
|
343
|
+
case one
|
344
|
+
when 'pass'
|
345
|
+
%w{fail unknown}.any? { |s| s == two } ? two : one
|
346
|
+
when 'fail'
|
347
|
+
one
|
348
|
+
when 'unknown'
|
349
|
+
two == 'fail' ? two : one
|
350
|
+
when 'notapplicable'
|
351
|
+
%w{pass fail unknown}.any? { |s| s == two } ? two : one
|
352
|
+
when 'notchecked'
|
353
|
+
%w{pass fail unknown notapplicable}.any? { |s| s == two } ? two : one
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
# Builds the message information for rule results
|
358
|
+
# @param result [Hash] A single Inspec result
|
359
|
+
# @param xccdf_status [String] the xccdf calculated result status for the provided result
|
360
|
+
def result_message(result, xccdf_status)
|
361
|
+
return unless result['message'] || result['skip_message']
|
362
|
+
|
363
|
+
message = HappyMapperTools::Benchmark::MessageType.new
|
364
|
+
# Including the code of the check and the resulting message if there is one.
|
365
|
+
message.message = "#{result['code_desc'] ? result['code_desc'] + "\n\n" : ''}#{result['message'] || result['skip_message']}"
|
366
|
+
message.severity = result_message_severity(xccdf_status)
|
367
|
+
message
|
368
|
+
end
|
369
|
+
|
370
|
+
# All rule-result messages require a defined severity. This determines a value to use based upon the result XCCDF status.
|
371
|
+
def result_message_severity(xccdf_status)
|
372
|
+
case xccdf_status
|
373
|
+
when 'fail'
|
374
|
+
'error'
|
375
|
+
when 'notapplicable'
|
376
|
+
'warning'
|
377
|
+
else
|
378
|
+
'info'
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
# Set scores for all 4 required/recommended scoring systems.
|
383
|
+
def populate_score(test_result, groups)
|
384
|
+
score = Utils::XCCDFScore.new(groups, test_result.rule_result)
|
385
|
+
test_result.score = [score.default_score, score.flat_score, score.flat_unweighted_score, score.absolute_score]
|
386
|
+
end
|
387
|
+
end
|
388
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
module Utils
|
2
|
+
# Perform scoring calculations for the different types that is used in a TestResult score.
|
3
|
+
class XCCDFScore
|
4
|
+
# @param groups [Array[HappyMapperTools::Benchmark::Group]]
|
5
|
+
# @param rule_results [Array[RuleResultType]]
|
6
|
+
def initialize(groups, rule_results)
|
7
|
+
@groups = groups
|
8
|
+
@rule_results = rule_results
|
9
|
+
end
|
10
|
+
|
11
|
+
# Calculate and return the urn:xccdf:scoring:default score for the entire benchmark.
|
12
|
+
# @return ScoreType
|
13
|
+
def default_score
|
14
|
+
HappyMapperTools::Benchmark::ScoreType.new('urn:xccdf:scoring:default', 100, score_benchmark_default)
|
15
|
+
end
|
16
|
+
|
17
|
+
# urn:xccdf:scoring:flat
|
18
|
+
# @return ScoreType
|
19
|
+
def flat_score
|
20
|
+
results = score_benchmark_with_weights(true)
|
21
|
+
HappyMapperTools::Benchmark::ScoreType.new('urn:xccdf:scoring:flat', results[:max], results[:score])
|
22
|
+
end
|
23
|
+
|
24
|
+
# urn:xccdf:scoring:flat-unweighted
|
25
|
+
# @return ScoreType
|
26
|
+
def flat_unweighted_score
|
27
|
+
results = score_benchmark_with_weights(false)
|
28
|
+
HappyMapperTools::Benchmark::ScoreType.new('urn:xccdf:scoring:flat-unweighted', results[:max], results[:score])
|
29
|
+
end
|
30
|
+
|
31
|
+
# urn:xccdf:scoring:absolute
|
32
|
+
# @return ScoreType
|
33
|
+
def absolute_score
|
34
|
+
results = score_benchmark_with_weights(true)
|
35
|
+
HappyMapperTools::Benchmark::ScoreType.new('urn:xccdf:scoring:absolute', 1, (results[:max] == results[:score] && results[:max].positive? ? 1 : 0))
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
# Return the overall score for the default model
|
41
|
+
def score_benchmark_default
|
42
|
+
return 0.0 unless @groups
|
43
|
+
|
44
|
+
count = 0
|
45
|
+
cumulative_score = 0.0
|
46
|
+
|
47
|
+
@groups.each do |group|
|
48
|
+
# Default weighted scoring only provides value when more than one rule exists per group. This implementation
|
49
|
+
# is not currently supporting more than one rule per group so weight need not apply.
|
50
|
+
rule_score = score_default_rule(test_results(group.rule.id))
|
51
|
+
|
52
|
+
if rule_score[:rule_count].positive?
|
53
|
+
count += 1
|
54
|
+
cumulative_score += rule_score[:rule_score]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
return 0.0 unless count.positive?
|
59
|
+
|
60
|
+
(cumulative_score / count).round(2)
|
61
|
+
end
|
62
|
+
|
63
|
+
# @param weighted [Boolean] Indicate to apply with weights.
|
64
|
+
def score_benchmark_with_weights(weighted)
|
65
|
+
score = 0.0
|
66
|
+
max_score = 0.0
|
67
|
+
|
68
|
+
return { score: score, max: max_score } unless @groups
|
69
|
+
|
70
|
+
@groups.each do |group|
|
71
|
+
# Default weighted scoring only provides value when more than one rule exists per group. This implementation
|
72
|
+
# is not currently supporting more than one rule per group so weight need not apply.
|
73
|
+
rule_score = rule_counts_and_score(test_results(group.rule.id))
|
74
|
+
|
75
|
+
next unless rule_score[:rule_count].positive?
|
76
|
+
|
77
|
+
weight =
|
78
|
+
if weighted
|
79
|
+
group.rule.weight.nil? ? 1.0 : group.rule.weight.to_f
|
80
|
+
else
|
81
|
+
group.rule.weight.nil? || group.rule.weight.to_f != 0.0 ? 1.0 : 0.0
|
82
|
+
end
|
83
|
+
|
84
|
+
max_score += weight
|
85
|
+
score += (weight * rule_score[:rule_score]) / rule_score[:rule_count]
|
86
|
+
end
|
87
|
+
|
88
|
+
{ score: score.round(2), max: max_score }
|
89
|
+
end
|
90
|
+
|
91
|
+
def score_default_rule(results)
|
92
|
+
sum = rule_counts_and_score(results)
|
93
|
+
return sum if sum[:rule_count].zero?
|
94
|
+
|
95
|
+
sum[:rule_score] = (100 * sum[:rule_score]) / sum[:rule_count]
|
96
|
+
sum
|
97
|
+
end
|
98
|
+
|
99
|
+
# Perform basic summation of rule results and passing tests
|
100
|
+
def rule_counts_and_score(results)
|
101
|
+
excluded_results = %w{notapplicable notchecked informational notselected}
|
102
|
+
rule_count = results.count { |r| !excluded_results.include?(r.result) }
|
103
|
+
rule_score = results.count { |r| r.result == 'pass' }
|
104
|
+
|
105
|
+
{ rule_count: rule_count, rule_score: rule_score }
|
106
|
+
end
|
107
|
+
|
108
|
+
# Get all test results with the matching rule id
|
109
|
+
# @return [Array]
|
110
|
+
def test_results(id)
|
111
|
+
return [] unless @rule_results
|
112
|
+
|
113
|
+
@rule_results.select { |r| r.idref == id }
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|