inspec_tools 3.0.0 → 3.1.0.pre1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5b88a75ec28d6491c6f8070b70402898b70d0971eb1d467aaab58e8f1671c50f
4
- data.tar.gz: acad2a4ee5d9c23e972e98b175575915213ea55bb8546b942191ba037392d0a8
3
+ metadata.gz: 6a8881f5571a113e746fedd8cb616f9ab58aae58ee2ef7938a01487a7131e0cb
4
+ data.tar.gz: 6202a3eb0e4816476eb2c2cb5fa2ab525f9cbad4a5a078044ef04f19c0bcf38e
5
5
  SHA512:
6
- metadata.gz: aab806365dba6a0878a2351cd6e433655ddcd8dcfbf5136228a5e2b79b1110709a9f714d13e801b7cbf836564a82f72b462f1cd4e4ba319d78f8c9d35f0b6950
7
- data.tar.gz: 245322d7e8d84eb755aa644dbc4fe8cba3098e467c2f055a56dd66a4ee05fc4ff30beabecf13fea9eed060e26199f9b9b6da87dbcb1cc2ff0658ee959beefef6
6
+ metadata.gz: 68612dd71958e27d66f97c6eb4acf48f30b5434697c6b8d07e2d4e17a9c4f49dcd37f71421dba153e07ccf8f1c030f4208fcb8afc825ba2eac4ddde18d192bd6
7
+ data.tar.gz: 5e061df82cdadae6b19bd0e04a69d044d8cec7cbe013e3955eb2c6f71a68558a12883cd3a353eb5964e27f3f533d2091d5a77045baf89292fef9414d763f540a
@@ -74,7 +74,11 @@ module InspecTools
74
74
  control['tags']['nist'] = nist unless nist.nil? || nist.include?(nil)
75
75
  @mapping['control.tags'].each do |tag|
76
76
  if tag.first == 'cci'
77
- control['tags'][tag.first] = cci_number
77
+ if cci_number.is_a? Array
78
+ control['tags'][tag.first] = cci_number
79
+ else
80
+ control['tags'][tag.first] = [cci_number]
81
+ end
78
82
  next
79
83
  end
80
84
  control['tags'][tag.first] = row[tag.last] unless row[tag.last].nil?
@@ -9,11 +9,12 @@ require_relative '../happy_mapper_tools/stig_checklist'
9
9
  require_relative '../happy_mapper_tools/benchmark'
10
10
  require_relative '../utilities/inspec_util'
11
11
  require_relative 'csv'
12
- require_relative '../utilities/xccdf/from_inspec'
13
- require_relative '../utilities/xccdf/to_xccdf'
12
+ require_relative '../utilities/xccdf/xccdf_score'
14
13
 
15
14
  module InspecTools
16
15
  class Inspec
16
+ DATA_NOT_FOUND_MESSAGE = 'N/A'.freeze
17
+
17
18
  def initialize(inspec_json, metadata = {})
18
19
  @json = JSON.parse(inspec_json)
19
20
  @metadata = metadata
@@ -38,10 +39,11 @@ module InspecTools
38
39
  # @param attributes [Hash] Optional input attributes
39
40
  # @return [String] XML formatted String
40
41
  def to_xccdf(attributes, verbose = false)
41
- data = Utils::FromInspec.new.parse_data_for_xccdf(@json)
42
+ data = parse_data_for_xccdf(@json)
42
43
  @verbose = verbose
44
+ @benchmark = HappyMapperTools::Benchmark::Benchmark.new
43
45
 
44
- Utils::ToXCCDF.new(attributes || {}, data).to_xml(@metadata)
46
+ to_xml(@metadata, attributes, data)
45
47
  end
46
48
 
47
49
  ####
@@ -80,6 +82,478 @@ module InspecTools
80
82
  find_topmost_profile_name(index + 1, parent_name)
81
83
  end
82
84
 
85
+ # Build entire XML document and produce final output
86
+ # @param metadata [Hash] Data representing a system under scan
87
+ def to_xml(metadata, attributes, data)
88
+ attributes = {} if attributes.nil?
89
+ build_benchmark_header(attributes)
90
+ build_groups(attributes, data)
91
+ # Only populate results if a target is defined so that conformant XML is produced.
92
+ @benchmark.testresult = build_test_results(metadata, data) if metadata['fqdn']
93
+ @benchmark.to_xml
94
+ end
95
+
96
+ def build_benchmark_header(attributes)
97
+ @benchmark.title = attributes['benchmark.title']
98
+ @benchmark.id = attributes['benchmark.id']
99
+ @benchmark.description = attributes['benchmark.description']
100
+ @benchmark.version = attributes['benchmark.version']
101
+ @benchmark.xmlns = 'http://checklists.nist.gov/xccdf/1.1'
102
+
103
+ @benchmark.status = HappyMapperTools::Benchmark::Status.new
104
+ @benchmark.status.status = attributes['benchmark.status']
105
+ @benchmark.status.date = attributes['benchmark.status.date']
106
+
107
+ if attributes['benchmark.notice.id']
108
+ @benchmark.notice = HappyMapperTools::Benchmark::Notice.new
109
+ @benchmark.notice.id = attributes['benchmark.notice.id']
110
+ end
111
+
112
+ if attributes['benchmark.plaintext'] || attributes['benchmark.plaintext.id']
113
+ @benchmark.plaintext = HappyMapperTools::Benchmark::Plaintext.new
114
+ @benchmark.plaintext.plaintext = attributes['benchmark.plaintext']
115
+ @benchmark.plaintext.id = attributes['benchmark.plaintext.id']
116
+ end
117
+
118
+ @benchmark.reference = HappyMapperTools::Benchmark::ReferenceBenchmark.new
119
+ @benchmark.reference.href = attributes['reference.href']
120
+ @benchmark.reference.dc_publisher = attributes['reference.dc.publisher']
121
+ @benchmark.reference.dc_source = attributes['reference.dc.source']
122
+ end
123
+
124
+ # Translate join of Inspec results and input attributes to XCCDF Groups
125
+ def build_groups(attributes, data)
126
+ group_array = []
127
+ data['controls'].each do |control|
128
+ group = HappyMapperTools::Benchmark::Group.new
129
+ group.id = control['id']
130
+ group.title = control['gtitle']
131
+ group.description = "<GroupDescription>#{control['gdescription']}</GroupDescription>" if control['gdescription']
132
+
133
+ group.rule = HappyMapperTools::Benchmark::Rule.new
134
+ group.rule.id = control['rid']
135
+ group.rule.severity = control['severity']
136
+ group.rule.weight = control['rweight']
137
+ group.rule.version = control['rversion']
138
+ group.rule.title = control['title'].tr("\n", ' ') if control['title']
139
+ group.rule.description = "<VulnDiscussion>#{control['desc']}</VulnDiscussion><FalsePositives></FalsePositives><FalseNegatives></FalseNegatives><Documentable>false</Documentable><Mitigations>#{control['rationale']}</Mitigations><SeverityOverrideGuidance></SeverityOverrideGuidance><PotentialImpacts></PotentialImpacts><ThirdPartyTools></ThirdPartyTools><MitigationControl></MitigationControl><Responsibility></Responsibility><IAControls></IAControls>"
140
+
141
+ if ['reference.dc.publisher', 'reference.dc.title', 'reference.dc.subject', 'reference.dc.type', 'reference.dc.identifier'].any? { |a| attributes.key?(a) }
142
+ group.rule.reference = build_rule_reference(attributes)
143
+ end
144
+
145
+ group.rule.ident = build_rule_idents(control['cci']) if control['cci']
146
+ group.rule.ident += build_rule_idents(control['legacy']) if control['legacy']
147
+
148
+ group.rule.fixtext = HappyMapperTools::Benchmark::Fixtext.new
149
+ group.rule.fixtext.fixref = control['fix_id']
150
+ group.rule.fixtext.fixtext = control['fix']
151
+
152
+ group.rule.fix = build_rule_fix(control['fix_id']) if control['fix_id']
153
+
154
+ group.rule.check = HappyMapperTools::Benchmark::Check.new
155
+ group.rule.check.system = control['checkref']
156
+
157
+ # content_ref is optional for schema compliance
158
+ if attributes['content_ref.name'] || attributes['content_ref.href']
159
+ group.rule.check.content_ref = HappyMapperTools::Benchmark::ContentRef.new
160
+ group.rule.check.content_ref.name = attributes['content_ref.name']
161
+ group.rule.check.content_ref.href = attributes['content_ref.href']
162
+ end
163
+
164
+ group.rule.check.content = control['check']
165
+
166
+ group_array << group
167
+ end
168
+ @benchmark.group = group_array
169
+ end
170
+
171
+ # Construct a Benchmark Testresult from Inspec data. This must be called after all XML processing has occurred for profiles
172
+ # and groups.
173
+ # @param metadata [Hash]
174
+ # @return [TestResult]
175
+ def build_test_results(metadata, data)
176
+ test_result = HappyMapperTools::Benchmark::TestResult.new
177
+ test_result.version = @benchmark.version
178
+ test_result = populate_remark(test_result, data)
179
+ test_result = populate_target_facts(test_result, metadata)
180
+ test_result = populate_identity(test_result, metadata)
181
+ test_result = populate_results(test_result, data)
182
+ populate_score(test_result, @benchmark.group)
183
+ end
184
+
185
+ # Contruct a Rule / RuleResult fix element with the provided id.
186
+ def build_rule_fix(fix_id)
187
+ HappyMapperTools::Benchmark::Fix.new.tap { |f| f.id = fix_id }
188
+ end
189
+
190
+ # Construct rule identifiers for rule
191
+ # @param idents [Array]
192
+ def build_rule_idents(idents)
193
+ raise "#{idents} is not an Array type." unless idents.is_a?(Array)
194
+
195
+ # Each rule identifier is a different element
196
+ idents.map do |identifier|
197
+ HappyMapperTools::Benchmark::Ident.new identifier
198
+ end
199
+ end
200
+
201
+ # Contruct a Rule reference element
202
+ def build_rule_reference(attributes)
203
+ reference = HappyMapperTools::Benchmark::ReferenceGroup.new
204
+ reference.dc_publisher = attributes['reference.dc.publisher']
205
+ reference.dc_title = attributes['reference.dc.title']
206
+ reference.dc_subject = attributes['reference.dc.subject']
207
+ reference.dc_type = attributes['reference.dc.type']
208
+ reference.dc_identifier = attributes['reference.dc.identifier']
209
+ reference
210
+ end
211
+
212
+ # Create a remark with contextual information about the Inspec version and profiles used
213
+ # @param result [HappyMapperTools::Benchmark::TestResult]
214
+ def populate_remark(result, data)
215
+ result.remark = "Results created using Inspec version #{data['inspec_version']}."
216
+ result.remark += "\n#{data['profiles'].map { |p| "Profile: #{p['name']} Version: #{p['version']}" }.join("\n")}" if data['profiles']
217
+ result
218
+ end
219
+
220
+ # Create all target specific information.
221
+ # @param result [HappyMapperTools::Benchmark::TestResult]
222
+ # @param metadata [Hash]
223
+ def populate_target_facts(result, metadata)
224
+ result.target = metadata['fqdn']
225
+ result.target_address = metadata['ip'] if metadata['ip']
226
+
227
+ all_facts = []
228
+
229
+ if metadata['mac']
230
+ fact = HappyMapperTools::Benchmark::Fact.new
231
+ fact.name = 'urn:xccdf:fact:asset:identifier:mac'
232
+ fact.type = 'string'
233
+ fact.fact = metadata['mac']
234
+ all_facts << fact
235
+ end
236
+
237
+ if metadata['ip']
238
+ fact = HappyMapperTools::Benchmark::Fact.new
239
+ fact.name = 'urn:xccdf:fact:asset:identifier:ipv4'
240
+ fact.type = 'string'
241
+ fact.fact = metadata['ip']
242
+ all_facts << fact
243
+ end
244
+
245
+ return result unless all_facts.size.nonzero?
246
+
247
+ facts = HappyMapperTools::Benchmark::TargetFact.new
248
+ facts.fact = all_facts
249
+ result.target_facts = facts
250
+ result
251
+ end
252
+
253
+ # Add information about the the account and organization executing the tests.
254
+ def populate_identity(test_result, metadata)
255
+ if metadata['identity']
256
+ test_result.identity = HappyMapperTools::Benchmark::IdentityType.new
257
+ test_result.identity.authenticated = true
258
+ test_result.identity.identity = metadata['identity']['identity']
259
+ test_result.identity.privileged = metadata['identity']['privileged']
260
+ end
261
+
262
+ test_result.organization = metadata['organization'] if metadata['organization']
263
+ test_result
264
+ end
265
+
266
+ # Build out the TestResult given all the control and result data.
267
+ def populate_results(test_result, data)
268
+ # NOTE: id is not an XCCDF 1.2 compliant identifier and will need to be updated when that support is added.
269
+ test_result.id = 'result_1'
270
+ test_result.starttime = run_start_time(data)
271
+ test_result.endtime = run_end_time(data)
272
+
273
+ # Build out individual results
274
+ all_rule_result = []
275
+
276
+ data['controls'].each do |control|
277
+ next if control['results'].nil? || control['results'].empty?
278
+
279
+ control_results =
280
+ control['results'].map do |result|
281
+ populate_rule_result(control, result, xccdf_status(result['status'], control['impact']))
282
+ end
283
+
284
+ # Consolidate results into single rule result do to lack of multiple=true attribute on Rule.
285
+ # 1. Select the unified result status
286
+ selected_status = control_results.reduce(control_results.first.result) { |f_status, rule_result| xccdf_and_result(f_status, rule_result.result) }
287
+
288
+ # 2. Only choose results with that status
289
+ # 3. Combine those results
290
+ all_rule_result << combine_results(control_results.select { |r| r.result == selected_status })
291
+ end
292
+
293
+ test_result.rule_result = all_rule_result
294
+ test_result
295
+ end
296
+
297
+ # Return the earliest time of execution.
298
+ def run_start_time(data)
299
+ start_times =
300
+ data['controls'].map do |control|
301
+ next if control['results'].nil?
302
+
303
+ control['results'].map { |result| DateTime.parse(result['start_time']) }
304
+ end
305
+ start_times.flatten.min
306
+ end
307
+
308
+ # Return the latest time of execution accounting for Inspec duration.
309
+ def run_end_time(data)
310
+ end_times =
311
+ data['controls'].map do |control|
312
+ next if control['results'].nil?
313
+
314
+ control['results'].map { |result| end_time(result['start_time'], result['run_time']) }
315
+ end
316
+ end_times.flatten.max
317
+ end
318
+
319
+ # Create rule-result from the control and Inspec result information
320
+ def populate_rule_result(control, result, result_status)
321
+ rule_result = HappyMapperTools::Benchmark::RuleResultType.new
322
+
323
+ rule_result.idref = control['rid']
324
+ rule_result.severity = control['severity']
325
+ rule_result.time = end_time(result['start_time'], result['run_time'])
326
+ rule_result.weight = control['rweight']
327
+
328
+ rule_result.result = result_status
329
+ rule_result.message = result_message(result, result_status) if result_message(result, result_status)
330
+ rule_result.instance = result['code_desc']
331
+
332
+ rule_result.ident = build_rule_idents(control['cci']) if control['cci']
333
+ rule_result.ident += build_rule_idents(control['legacy']) if control['legacy']
334
+
335
+ # Fix information is only necessary when there are failed tests
336
+ rule_result.fix = build_rule_fix(control['fix_id']) if control['fix_id'] && result_status == 'fail'
337
+
338
+ rule_result.check = HappyMapperTools::Benchmark::Check.new
339
+ rule_result.check.system = control['checkref']
340
+ rule_result.check.content = result['code_desc']
341
+ rule_result
342
+ end
343
+
344
+ # Map the Inspec result status to appropriate XCCDF test result status.
345
+ # XCCDF options include: pass, fail, error, unknown, notapplicable, notchecked, notselected, informational, fixed
346
+ #
347
+ # @param inspec_status [String] The reported Inspec status from an individual test
348
+ # @param impact [String] A value of 0.0 - 1.0
349
+ # @return A valid Inspec status.
350
+ def xccdf_status(inspec_status, impact)
351
+ # Currently, there is no good way to map an Inspec result status to one of XCCDF status unknown or notselected.
352
+ case inspec_status
353
+ when 'failed'
354
+ 'fail'
355
+ when 'passed'
356
+ 'pass'
357
+ when 'skipped'
358
+ if impact.to_f.zero?
359
+ 'notapplicable'
360
+ else
361
+ 'notchecked'
362
+ end
363
+ else
364
+ # In the event Inspec adds a new unaccounted for status, mapping to XCCDF unknown.
365
+ 'unknown'
366
+ end
367
+ end
368
+
369
+ # When more than one result occurs for a rule and the specification does not declare multiple, the result must be combined.
370
+ # This determines the appropriate result to be selected when there are two to compare.
371
+ # @param one [String] A rule-result status
372
+ # @param two [String] A rule-result status
373
+ # @return The result of the AND operation.
374
+ def xccdf_and_result(one, two)
375
+ # From XCCDF specification truth table
376
+ # P = pass
377
+ # F = fail
378
+ # U = unknown
379
+ # E = error
380
+ # N = notapplicable
381
+ # K = notchecked
382
+ # S = notselected
383
+ # I = informational
384
+
385
+ case one
386
+ when 'pass'
387
+ %w{fail unknown}.any? { |s| s == two } ? two : one
388
+ when 'fail'
389
+ one
390
+ when 'unknown'
391
+ two == 'fail' ? two : one
392
+ when 'notapplicable'
393
+ %w{pass fail unknown}.any? { |s| s == two } ? two : one
394
+ when 'notchecked'
395
+ %w{pass fail unknown notapplicable}.any? { |s| s == two } ? two : one
396
+ end
397
+ end
398
+
399
+ # Combines rule results with the same result into a single rule result.
400
+ def combine_results(rule_results)
401
+ return rule_results.first if rule_results.size == 1
402
+
403
+ # Can combine, result, idents (duplicate, take first instance), instance - combine into an array removing duplicates
404
+ # check.content - Only one value allowed, combine by joining with line feed. Prior to, make sure all values are unique.
405
+
406
+ rule_result = HappyMapperTools::Benchmark::RuleResultType.new
407
+ rule_result.idref = rule_results.first.idref
408
+ rule_result.severity = rule_results.first.severity
409
+ # Take latest time
410
+ rule_result.time = rule_results.reduce(rule_results.first.time) { |time, r| time > r.time ? time : r.time }
411
+ rule_result.weight = rule_results.first.weight
412
+
413
+ rule_result.result = rule_results.first.result
414
+ rule_result.message = rule_results.reduce([]) { |messages, r| r.message ? messages.push(r.message) : messages }
415
+ rule_result.instance = rule_results.reduce([]) { |instances, r| r.instance ? instances.push(r.instance) : instances }.join("\n")
416
+
417
+ rule_result.ident = rule_results.first.ident
418
+ rule_result.fix = rule_results.first.fix
419
+
420
+ if rule_results.first.check
421
+ rule_result.check = HappyMapperTools::Benchmark::Check.new
422
+ rule_result.check.system = rule_results.first.check.system
423
+ rule_result.check.content = rule_results.map { |r| r.check.content }.join("\n")
424
+ end
425
+
426
+ rule_result
427
+ end
428
+
429
+ # Calculate an end time given a start time and second duration
430
+ def end_time(start, duration)
431
+ DateTime.parse(start) + (duration / (24*60*60))
432
+ end
433
+
434
+ # Builds the message information for rule results
435
+ # @param result [Hash] A single Inspec result
436
+ # @param xccdf_status [String] the xccdf calculated result status for the provided result
437
+ def result_message(result, xccdf_status)
438
+ return unless result['message'] || result['skip_message']
439
+
440
+ message = HappyMapperTools::Benchmark::MessageType.new
441
+ # Including the code of the check and the resulting message if there is one.
442
+ message.message = "#{result['code_desc'] ? "#{result['code_desc']}\n\n" : ''}#{result['message'] || result['skip_message']}"
443
+ message.severity = result_message_severity(xccdf_status)
444
+ message
445
+ end
446
+
447
+ # All rule-result messages require a defined severity. This determines a value to use based upon the result XCCDF status.
448
+ def result_message_severity(xccdf_status)
449
+ case xccdf_status
450
+ when 'fail'
451
+ 'error'
452
+ when 'notapplicable'
453
+ 'warning'
454
+ else
455
+ 'info'
456
+ end
457
+ end
458
+
459
+ # Convert raw Inspec result json into format acceptable for XCCDF transformation.
460
+ def parse_data_for_xccdf(json) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
461
+ data = {}
462
+
463
+ controls = []
464
+ if json['profiles'].nil?
465
+ controls = json['controls']
466
+ elsif json['profiles'].length == 1
467
+ controls = json['profiles'].last['controls']
468
+ else
469
+ json['profiles'].each do |profile|
470
+ controls.concat(profile['controls'])
471
+ end
472
+ end
473
+ c_data = {}
474
+
475
+ controls.each do |control|
476
+ c_id = control['id'].to_sym
477
+ c_data[c_id] = {}
478
+ c_data[c_id]['id'] = control['id'] || DATA_NOT_FOUND_MESSAGE
479
+ c_data[c_id]['title'] = control['title'] if control['title'] # Optional attribute
480
+ c_data[c_id]['desc'] = control['desc'] || DATA_NOT_FOUND_MESSAGE
481
+ c_data[c_id]['severity'] = control['tags']['severity'] || 'unknown'
482
+ c_data[c_id]['gid'] = control['tags']['gid'] || control['id']
483
+ c_data[c_id]['gtitle'] = control['tags']['gtitle'] if control['tags']['gtitle'] # Optional attribute
484
+ c_data[c_id]['gdescription'] = control['tags']['gdescription'] if control['tags']['gdescription'] # Optional attribute
485
+ c_data[c_id]['rid'] = control['tags']['rid'] || "r_#{c_data[c_id]['gid']}"
486
+ c_data[c_id]['rversion'] = control['tags']['rversion'] if control['tags']['rversion'] # Optional attribute
487
+ c_data[c_id]['rweight'] = control['tags']['rweight'] if control['tags']['rweight'] # Optional attribute where N/A is not schema compliant
488
+ c_data[c_id]['stig_id'] = control['tags']['stig_id'] || DATA_NOT_FOUND_MESSAGE
489
+ c_data[c_id]['cci'] = control['tags']['cci'] if control['tags']['cci'] # Optional attribute
490
+ c_data[c_id]['legacy'] = control['tags']['legacy'] if control['tags']['legacy'] # Optional attribute
491
+ c_data[c_id]['nist'] = control['tags']['nist'] || ['unmapped']
492
+
493
+ # new (post-2020) inspec output places check, fix, and rationale fields in a descriptions block
494
+ if control['descriptions'].is_a?(Hash) && control['descriptions'].key?('check') && control['descriptions'].key?('fix') && control['descriptions'].key?('rationale')
495
+ c_data[c_id]['check'] = control['descriptions']['check'] || DATA_NOT_FOUND_MESSAGE
496
+ c_data[c_id]['fix'] = control['descriptions']['fix'] || DATA_NOT_FOUND_MESSAGE
497
+ c_data[c_id]['rationale'] = control['descriptions']['rationale'] || DATA_NOT_FOUND_MESSAGE
498
+ else
499
+ c_data[c_id]['check'] = control['tags']['check'] || DATA_NOT_FOUND_MESSAGE
500
+ c_data[c_id]['fix'] = control['tags']['fix'] || DATA_NOT_FOUND_MESSAGE
501
+ c_data[c_id]['rationale'] = control['tags']['rationale'] || DATA_NOT_FOUND_MESSAGE
502
+ end
503
+ c_data[c_id]['checkref'] = control['tags']['checkref'] || DATA_NOT_FOUND_MESSAGE
504
+ c_data[c_id]['fix_id'] = control['tags']['fix_id'] if control['tags']['fix_id'] # Optional attribute where N/A is not schema compliant
505
+ c_data[c_id]['cis_family'] = control['tags']['cis_family'] || DATA_NOT_FOUND_MESSAGE
506
+ c_data[c_id]['cis_rid'] = control['tags']['cis_rid'] || DATA_NOT_FOUND_MESSAGE
507
+ c_data[c_id]['cis_level'] = control['tags']['cis_level'] || DATA_NOT_FOUND_MESSAGE
508
+ c_data[c_id]['impact'] = control['impact'].to_s || DATA_NOT_FOUND_MESSAGE
509
+ c_data[c_id]['code'] = control['code'].to_s || DATA_NOT_FOUND_MESSAGE
510
+ c_data[c_id]['results'] = parse_results_for_xccdf(control['results']) if control['results']
511
+ end
512
+
513
+ data['controls'] = c_data.values
514
+ data['profiles'] = parse_profiles_for_xccdf(json['profiles'])
515
+ data['status'] = 'success'
516
+ # If generator exists this is a more up-to-date inspec.json so look for version in the new location, else old location
517
+ data['inspec_version'] = json['generator'].nil? ? json['version'] : json['generator']['version']
518
+ data
519
+ end
520
+
521
+ # Set scores for all 4 required/recommended scoring systems.
522
+ def populate_score(test_result, groups)
523
+ score = Utils::XCCDFScore.new(groups, test_result.rule_result)
524
+ test_result.score = [score.default_score, score.flat_score, score.flat_unweighted_score, score.absolute_score]
525
+ test_result
526
+ end
527
+
528
+ # Convert profile information for result processing
529
+ # @param profiles [Array[Hash]] - The profiles section of the JSON output
530
+ def parse_profiles_for_xccdf(profiles)
531
+ return [] unless profiles
532
+
533
+ profiles.map do |profile|
534
+ data = {}
535
+ data['name'] = profile['name']
536
+ data['version'] = profile['version']
537
+ data
538
+ end
539
+ end
540
+
541
+ # Convert the test result data to a parseable Hash for downstream processing
542
+ # @param results [Array[Hash]] - The results section of the JSON output
543
+ def parse_results_for_xccdf(results)
544
+ results.map do |result|
545
+ data = {}
546
+ data['status'] = result['status']
547
+ data['code_desc'] = result['code_desc']
548
+ data['run_time'] = result['run_time']
549
+ data['start_time'] = result['start_time']
550
+ data['resource'] = result['resource']
551
+ data['message'] = result['message']
552
+ data['skip_message'] = result['skip_message']
553
+ data
554
+ end
555
+ end
556
+
83
557
  ###
84
558
  # This method converts an inspec json to an array of arrays
85
559
  #
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: inspec_tools
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0
4
+ version: 3.1.0.pre1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Thew
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: exe
13
13
  cert_chain: []
14
- date: 2021-07-28 00:00:00.000000000 Z
14
+ date: 2021-08-13 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: colorize
@@ -350,8 +350,6 @@ files:
350
350
  - lib/utilities/mapping_validator.rb
351
351
  - lib/utilities/parser.rb
352
352
  - lib/utilities/text_cleaner.rb
353
- - lib/utilities/xccdf/from_inspec.rb
354
- - lib/utilities/xccdf/to_xccdf.rb
355
353
  - lib/utilities/xccdf/xccdf_score.rb
356
354
  homepage: https://inspec-tools.mitre.org/
357
355
  licenses:
@@ -368,9 +366,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
368
366
  version: '2.7'
369
367
  required_rubygems_version: !ruby/object:Gem::Requirement
370
368
  requirements:
371
- - - ">="
369
+ - - ">"
372
370
  - !ruby/object:Gem::Version
373
- version: '0'
371
+ version: 1.3.1
374
372
  requirements: []
375
373
  rubygems_version: 3.2.22
376
374
  signing_key:
@@ -1,90 +0,0 @@
1
- module Utils
2
- # Data transformation from Inspec result output into usable data for XCCDF conversions.
3
- class FromInspec
4
- DATA_NOT_FOUND_MESSAGE = 'N/A'.freeze
5
-
6
- # Convert raw Inspec result json into format acceptable for XCCDF transformation.
7
- def parse_data_for_xccdf(json)
8
- data = {}
9
-
10
- controls = []
11
- if json['profiles'].nil?
12
- controls = json['controls']
13
- elsif json['profiles'].length == 1
14
- controls = json['profiles'].last['controls']
15
- else
16
- json['profiles'].each do |profile|
17
- controls.concat(profile['controls'])
18
- end
19
- end
20
- c_data = {}
21
-
22
- controls.each do |control|
23
- c_id = control['id'].to_sym
24
- c_data[c_id] = {}
25
- c_data[c_id]['id'] = control['id'] || DATA_NOT_FOUND_MESSAGE
26
- c_data[c_id]['title'] = control['title'] if control['title'] # Optional attribute
27
- c_data[c_id]['desc'] = control['desc'] || DATA_NOT_FOUND_MESSAGE
28
- c_data[c_id]['severity'] = control['tags']['severity'] || 'unknown'
29
- c_data[c_id]['gid'] = control['tags']['gid'] || control['id']
30
- c_data[c_id]['gtitle'] = control['tags']['gtitle'] if control['tags']['gtitle'] # Optional attribute
31
- c_data[c_id]['gdescription'] = control['tags']['gdescription'] if control['tags']['gdescription'] # Optional attribute
32
- c_data[c_id]['rid'] = control['tags']['rid'] || "r_#{c_data[c_id]['gid']}"
33
- c_data[c_id]['rversion'] = control['tags']['rversion'] if control['tags']['rversion'] # Optional attribute
34
- c_data[c_id]['rweight'] = control['tags']['rweight'] if control['tags']['rweight'] # Optional attribute where N/A is not schema compliant
35
- c_data[c_id]['stig_id'] = control['tags']['stig_id'] || DATA_NOT_FOUND_MESSAGE
36
- c_data[c_id]['cci'] = control['tags']['cci'] if control['tags']['cci'] # Optional attribute
37
- c_data[c_id]['legacy'] = control['tags']['legacy'] if control['tags']['legacy'] # Optional attribute
38
- c_data[c_id]['nist'] = control['tags']['nist'] || ['unmapped']
39
- c_data[c_id]['check'] = control['tags']['check'] || DATA_NOT_FOUND_MESSAGE
40
- c_data[c_id]['checkref'] = control['tags']['checkref'] || DATA_NOT_FOUND_MESSAGE
41
- c_data[c_id]['fix'] = control['tags']['fix'] || DATA_NOT_FOUND_MESSAGE
42
- c_data[c_id]['fix_id'] = control['tags']['fix_id'] if control['tags']['fix_id'] # Optional attribute where N/A is not schema compliant
43
- c_data[c_id]['rationale'] = control['tags']['rationale'] || DATA_NOT_FOUND_MESSAGE
44
- c_data[c_id]['cis_family'] = control['tags']['cis_family'] || DATA_NOT_FOUND_MESSAGE
45
- c_data[c_id]['cis_rid'] = control['tags']['cis_rid'] || DATA_NOT_FOUND_MESSAGE
46
- c_data[c_id]['cis_level'] = control['tags']['cis_level'] || DATA_NOT_FOUND_MESSAGE
47
- c_data[c_id]['impact'] = control['impact'].to_s || DATA_NOT_FOUND_MESSAGE
48
- c_data[c_id]['code'] = control['code'].to_s || DATA_NOT_FOUND_MESSAGE
49
- c_data[c_id]['results'] = parse_results_for_xccdf(control['results']) if control['results']
50
- end
51
-
52
- data['controls'] = c_data.values
53
- data['profiles'] = parse_profiles_for_xccdf(json['profiles'])
54
- data['status'] = 'success'
55
- data['inspec_version'] = json['version']
56
- data
57
- end
58
-
59
- private
60
-
61
- # Convert profile information for result processing
62
- # @param profiles [Array[Hash]] - The profiles section of the JSON output
63
- def parse_profiles_for_xccdf(profiles)
64
- return [] unless profiles
65
-
66
- profiles.map do |profile|
67
- data = {}
68
- data['name'] = profile['name']
69
- data['version'] = profile['version']
70
- data
71
- end
72
- end
73
-
74
- # Convert the test result data to a parseable Hash for downstream processing
75
- # @param results [Array[Hash]] - The results section of the JSON output
76
- def parse_results_for_xccdf(results)
77
- results.map do |result|
78
- data = {}
79
- data['status'] = result['status']
80
- data['code_desc'] = result['code_desc']
81
- data['run_time'] = result['run_time']
82
- data['start_time'] = result['start_time']
83
- data['resource'] = result['resource']
84
- data['message'] = result['message']
85
- data['skip_message'] = result['skip_message']
86
- data
87
- end
88
- end
89
- end
90
- end
@@ -1,387 +0,0 @@
1
- require_relative 'xccdf_score'
2
-
3
- module Utils
4
- # Data conversions for Inspec output into XCCDF format.
5
- class ToXCCDF
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
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']}</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
- group.rule.ident += build_rule_idents(control['legacy']) if control['legacy']
78
-
79
- group.rule.fixtext = HappyMapperTools::Benchmark::Fixtext.new
80
- group.rule.fixtext.fixref = control['fix_id']
81
- group.rule.fixtext.fixtext = control['fix']
82
-
83
- group.rule.fix = build_rule_fix(control['fix_id']) if control['fix_id']
84
-
85
- group.rule.check = HappyMapperTools::Benchmark::Check.new
86
- group.rule.check.system = control['checkref']
87
-
88
- # content_ref is optional for schema compliance
89
- if @attribute['content_ref.name'] || @attribute['content_ref.href']
90
- group.rule.check.content_ref = HappyMapperTools::Benchmark::ContentRef.new
91
- group.rule.check.content_ref.name = @attribute['content_ref.name']
92
- group.rule.check.content_ref.href = @attribute['content_ref.href']
93
- end
94
-
95
- group.rule.check.content = control['check']
96
-
97
- group_array << group
98
- end
99
- @benchmark.group = group_array
100
- end
101
-
102
- # Construct a Benchmark Testresult from Inspec data. This must be called after all XML processing has occurred for profiles
103
- # and groups.
104
- # @param metadata [Hash]
105
- # @return [TestResult]
106
- def build_test_results(metadata)
107
- test_result = HappyMapperTools::Benchmark::TestResult.new
108
- test_result.version = @benchmark.version
109
- populate_remark(test_result)
110
- populate_target_facts(test_result, metadata)
111
- populate_identity(test_result, metadata)
112
- populate_results(test_result)
113
- populate_score(test_result, @benchmark.group)
114
-
115
- test_result
116
- end
117
-
118
- # Contruct a Rule / RuleResult fix element with the provided id.
119
- def build_rule_fix(fix_id)
120
- HappyMapperTools::Benchmark::Fix.new.tap { |f| f.id = fix_id }
121
- end
122
-
123
- # Construct rule identifiers for rule
124
- # @param idents [Array]
125
- def build_rule_idents(idents)
126
- raise "#{idents} is not an Array type." unless idents.is_a?(Array)
127
-
128
- # Each rule identifier is a different element
129
- idents.map do |identifier|
130
- HappyMapperTools::Benchmark::Ident.new identifier
131
- end
132
- end
133
-
134
- # Contruct a Rule reference element
135
- def build_rule_reference
136
- reference = HappyMapperTools::Benchmark::ReferenceGroup.new
137
- reference.dc_publisher = @attribute['reference.dc.publisher']
138
- reference.dc_title = @attribute['reference.dc.title']
139
- reference.dc_subject = @attribute['reference.dc.subject']
140
- reference.dc_type = @attribute['reference.dc.type']
141
- reference.dc_identifier = @attribute['reference.dc.identifier']
142
- reference
143
- end
144
-
145
- # Create a remark with contextual information about the Inspec version and profiles used
146
- # @param result [HappyMapperTools::Benchmark::TestResult]
147
- def populate_remark(result)
148
- result.remark = "Results created using Inspec version #{@data['inspec_version']}.\n#{@data['profiles'].map { |p| "Profile: #{p['name']} Version: #{p['version']}" }.join("\n")}"
149
- end
150
-
151
- # Create all target specific information.
152
- # @param result [HappyMapperTools::Benchmark::TestResult]
153
- # @param metadata [Hash]
154
- def populate_target_facts(result, metadata)
155
- result.target = metadata['fqdn']
156
- result.target_address = metadata['ip'] if metadata['ip']
157
-
158
- all_facts = []
159
-
160
- if metadata['mac']
161
- fact = HappyMapperTools::Benchmark::Fact.new
162
- fact.name = 'urn:xccdf:fact:asset:identifier:mac'
163
- fact.type = 'string'
164
- fact.fact = metadata['mac']
165
- all_facts << fact
166
- end
167
-
168
- if metadata['ip']
169
- fact = HappyMapperTools::Benchmark::Fact.new
170
- fact.name = 'urn:xccdf:fact:asset:identifier:ipv4'
171
- fact.type = 'string'
172
- fact.fact = metadata['ip']
173
- all_facts << fact
174
- end
175
-
176
- return unless all_facts.size.nonzero?
177
-
178
- facts = HappyMapperTools::Benchmark::TargetFact.new
179
- facts.fact = all_facts
180
- result.target_facts = facts
181
- end
182
-
183
- # Build out the TestResult given all the control and result data.
184
- def populate_results(test_result)
185
- # NOTE: id is not an XCCDF 1.2 compliant identifier and will need to be updated when that support is added.
186
- test_result.id = 'result_1'
187
- test_result.starttime = run_start_time
188
- test_result.endtime = run_end_time
189
-
190
- # Build out individual results
191
- all_rule_result = []
192
-
193
- @data['controls'].each do |control|
194
- next if control['results'].empty?
195
-
196
- control_results =
197
- control['results'].map do |result|
198
- populate_rule_result(control, result, xccdf_status(result['status'], control['impact']))
199
- end
200
-
201
- # Consolidate results into single rule result do to lack of multiple=true attribute on Rule.
202
- # 1. Select the unified result status
203
- selected_status = control_results.reduce(control_results.first.result) { |f_status, rule_result| xccdf_and_result(f_status, rule_result.result) }
204
-
205
- # 2. Only choose results with that status
206
- # 3. Combine those results
207
- all_rule_result << combine_results(control_results.select { |r| r.result == selected_status })
208
- end
209
-
210
- test_result.rule_result = all_rule_result
211
- test_result
212
- end
213
-
214
- # Create rule-result from the control and Inspec result information
215
- def populate_rule_result(control, result, result_status)
216
- rule_result = HappyMapperTools::Benchmark::RuleResultType.new
217
-
218
- rule_result.idref = control['rid']
219
- rule_result.severity = control['severity']
220
- rule_result.time = end_time(result['start_time'], result['run_time'])
221
- rule_result.weight = control['rweight']
222
-
223
- rule_result.result = result_status
224
- rule_result.message = result_message(result, result_status) if result_message(result, result_status)
225
- rule_result.instance = result['code_desc']
226
-
227
- rule_result.ident = build_rule_idents(control['cci']) if control['cci']
228
- rule_result.ident += build_rule_idents(control['legacy']) if control['legacy']
229
-
230
- # Fix information is only necessary when there are failed tests
231
- rule_result.fix = build_rule_fix(control['fix_id']) if control['fix_id'] && result_status == 'fail'
232
-
233
- rule_result.check = HappyMapperTools::Benchmark::Check.new
234
- rule_result.check.system = control['checkref']
235
- rule_result.check.content = result['code_desc']
236
- rule_result
237
- end
238
-
239
- # Combines rule results with the same result into a single rule result.
240
- def combine_results(rule_results)
241
- return rule_results.first if rule_results.size == 1
242
-
243
- # Can combine, result, idents (duplicate, take first instance), instance - combine into an array removing duplicates
244
- # check.content - Only one value allowed, combine by joining with line feed. Prior to, make sure all values are unique.
245
-
246
- rule_result = HappyMapperTools::Benchmark::RuleResultType.new
247
- rule_result.idref = rule_results.first.idref
248
- rule_result.severity = rule_results.first.severity
249
- # Take latest time
250
- rule_result.time = rule_results.reduce(rule_results.first.time) { |time, r| time > r.time ? time : r.time }
251
- rule_result.weight = rule_results.first.weight
252
-
253
- rule_result.result = rule_results.first.result
254
- rule_result.message = rule_results.reduce([]) { |messages, r| r.message ? messages.push(r.message) : messages }
255
- rule_result.instance = rule_results.reduce([]) { |instances, r| r.instance ? instances.push(r.instance) : instances }.join("\n")
256
-
257
- rule_result.ident = rule_results.first.ident
258
- rule_result.fix = rule_results.first.fix
259
-
260
- if rule_results.first.check
261
- rule_result.check = HappyMapperTools::Benchmark::Check.new
262
- rule_result.check.system = rule_results.first.check.system
263
- rule_result.check.content = rule_results.map { |r| r.check.content }.join("\n")
264
- end
265
-
266
- rule_result
267
- end
268
-
269
- # Add information about the the account and organization executing the tests.
270
- def populate_identity(test_result, metadata)
271
- if metadata['identity']
272
- test_result.identity = HappyMapperTools::Benchmark::IdentityType.new
273
- test_result.identity.authenticated = true
274
- test_result.identity.identity = metadata['identity']['identity']
275
- test_result.identity.privileged = metadata['identity']['privileged']
276
- end
277
-
278
- test_result.organization = metadata['organization'] if metadata['organization']
279
- end
280
-
281
- # Return the earliest time of execution.
282
- def run_start_time
283
- @data['controls'].map { |control| control['results'].map { |result| DateTime.parse(result['start_time']) } }.flatten.min
284
- end
285
-
286
- # Return the latest time of execution accounting for Inspec duration.
287
- def run_end_time
288
- end_times =
289
- @data['controls'].map do |control|
290
- control['results'].map { |result| end_time(result['start_time'], result['run_time']) }
291
- end
292
-
293
- end_times.flatten.max
294
- end
295
-
296
- # Calculate an end time given a start time and second duration
297
- def end_time(start, duration)
298
- DateTime.parse(start) + (duration / (24*60*60))
299
- end
300
-
301
- # Map the Inspec result status to appropriate XCCDF test result status.
302
- # XCCDF options include: pass, fail, error, unknown, notapplicable, notchecked, notselected, informational, fixed
303
- #
304
- # @param inspec_status [String] The reported Inspec status from an individual test
305
- # @param impact [String] A value of 0.0 - 1.0
306
- # @return A valid Inspec status.
307
- def xccdf_status(inspec_status, impact)
308
- # Currently, there is no good way to map an Inspec result status to one of XCCDF status unknown or notselected.
309
- case inspec_status
310
- when 'failed'
311
- 'fail'
312
- when 'passed'
313
- 'pass'
314
- when 'skipped'
315
- if impact.to_f.zero?
316
- 'notapplicable'
317
- else
318
- 'notchecked'
319
- end
320
- else
321
- # In the event Inspec adds a new unaccounted for status, mapping to XCCDF unknown.
322
- 'unknown'
323
- end
324
- end
325
-
326
- # When more than one result occurs for a rule and the specification does not declare multiple, the result must be combined.
327
- # This determines the appropriate result to be selected when there are two to compare.
328
- # @param one [String] A rule-result status
329
- # @param two [String] A rule-result status
330
- # @return The result of the AND operation.
331
- def xccdf_and_result(one, two)
332
- # From XCCDF specification truth table
333
- # P = pass
334
- # F = fail
335
- # U = unknown
336
- # E = error
337
- # N = notapplicable
338
- # K = notchecked
339
- # S = notselected
340
- # I = informational
341
-
342
- case one
343
- when 'pass'
344
- %w{fail unknown}.any? { |s| s == two } ? two : one
345
- when 'fail'
346
- one
347
- when 'unknown'
348
- two == 'fail' ? two : one
349
- when 'notapplicable'
350
- %w{pass fail unknown}.any? { |s| s == two } ? two : one
351
- when 'notchecked'
352
- %w{pass fail unknown notapplicable}.any? { |s| s == two } ? two : one
353
- end
354
- end
355
-
356
- # Builds the message information for rule results
357
- # @param result [Hash] A single Inspec result
358
- # @param xccdf_status [String] the xccdf calculated result status for the provided result
359
- def result_message(result, xccdf_status)
360
- return unless result['message'] || result['skip_message']
361
-
362
- message = HappyMapperTools::Benchmark::MessageType.new
363
- # Including the code of the check and the resulting message if there is one.
364
- message.message = "#{result['code_desc'] ? "#{result['code_desc']}\n\n" : ''}#{result['message'] || result['skip_message']}"
365
- message.severity = result_message_severity(xccdf_status)
366
- message
367
- end
368
-
369
- # All rule-result messages require a defined severity. This determines a value to use based upon the result XCCDF status.
370
- def result_message_severity(xccdf_status)
371
- case xccdf_status
372
- when 'fail'
373
- 'error'
374
- when 'notapplicable'
375
- 'warning'
376
- else
377
- 'info'
378
- end
379
- end
380
-
381
- # Set scores for all 4 required/recommended scoring systems.
382
- def populate_score(test_result, groups)
383
- score = Utils::XCCDFScore.new(groups, test_result.rule_result)
384
- test_result.score = [score.default_score, score.flat_score, score.flat_unweighted_score, score.absolute_score]
385
- end
386
- end
387
- end