inspec_tools 3.0.0 → 3.1.0.pre1

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