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 +4 -4
- data/lib/inspec_tools/csv.rb +5 -1
- data/lib/inspec_tools/inspec.rb +478 -4
- metadata +4 -6
- data/lib/utilities/xccdf/from_inspec.rb +0 -90
- data/lib/utilities/xccdf/to_xccdf.rb +0 -387
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6a8881f5571a113e746fedd8cb616f9ab58aae58ee2ef7938a01487a7131e0cb
|
4
|
+
data.tar.gz: 6202a3eb0e4816476eb2c2cb5fa2ab525f9cbad4a5a078044ef04f19c0bcf38e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 68612dd71958e27d66f97c6eb4acf48f30b5434697c6b8d07e2d4e17a9c4f49dcd37f71421dba153e07ccf8f1c030f4208fcb8afc825ba2eac4ddde18d192bd6
|
7
|
+
data.tar.gz: 5e061df82cdadae6b19bd0e04a69d044d8cec7cbe013e3955eb2c6f71a68558a12883cd3a353eb5964e27f3f533d2091d5a77045baf89292fef9414d763f540a
|
data/lib/inspec_tools/csv.rb
CHANGED
@@ -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
|
-
|
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?
|
data/lib/inspec_tools/inspec.rb
CHANGED
@@ -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/
|
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 =
|
42
|
+
data = parse_data_for_xccdf(@json)
|
42
43
|
@verbose = verbose
|
44
|
+
@benchmark = HappyMapperTools::Benchmark::Benchmark.new
|
43
45
|
|
44
|
-
|
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.
|
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-
|
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:
|
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
|