inspec_tools 2.0.7 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,6 +15,7 @@ module InspecTools
15
15
  autoload :Summary, 'inspec_tools/summary'
16
16
  autoload :Threshold, 'inspec_tools/threshold'
17
17
  autoload :XLSXTool, 'inspec_tools/xlsx_tool'
18
+ autoload :GenerateMap, 'inspec_tools/generate_map'
18
19
  end
19
20
 
20
21
  # rubocop:disable Style/GuardClause
@@ -23,7 +24,7 @@ module InspecPlugins
23
24
  class CliCommand < Inspec.plugin(2, :cli_command) # rubocop:disable Metrics/ClassLength
24
25
  POSSIBLE_LOG_LEVELS = %w{debug info warn error fatal}.freeze
25
26
 
26
- class_option :log_directory, type: :string, aliases: :l, desc: 'Provie log location'
27
+ class_option :log_directory, type: :string, aliases: :l, desc: 'Provide log location'
27
28
  class_option :log_level, type: :string, desc: "Set the logging level: #{POSSIBLE_LOG_LEVELS}"
28
29
 
29
30
  subcommand_desc 'tools [COMMAND]', 'Runs inspec_tools commands through Inspec'
@@ -54,12 +55,18 @@ module InspecPlugins
54
55
 
55
56
  desc 'inspec2xccdf', 'inspec2xccdf translates an inspec profile and attributes files to an xccdf file'
56
57
  long_desc InspecTools::Help.text(:inspec2xccdf)
57
- option :inspec_json, required: true, aliases: '-j'
58
- option :attributes, required: true, aliases: '-a'
59
- option :output, required: true, aliases: '-o'
58
+ option :inspec_json, required: true, aliases: '-j',
59
+ desc: 'path to InSpec JSON file created'
60
+ option :attributes, required: true, aliases: '-a',
61
+ desc: 'path to yml file that provides the required attributes for the XCCDF document. These attributes are parts of XCCDF document which do not fit into the InSpec schema.'
62
+ option :output, required: true, aliases: '-o',
63
+ desc: 'name or path to create the XCCDF and title to give the XCCDF'
64
+ option :metadata, required: false, type: :string, aliases: '-m',
65
+ desc: 'path to JSON file with additional host metadata for the XCCDF file'
60
66
  def inspec2xccdf
61
67
  json = File.read(options[:inspec_json])
62
- inspec_tool = InspecTools::Inspec.new(json)
68
+ metadata = options[:metadata] ? JSON.parse(File.read(options[:metadata])) : {}
69
+ inspec_tool = InspecTools::Inspec.new(json, metadata)
63
70
  attr_hsh = YAML.load_file(options[:attributes])
64
71
  xccdf = inspec_tool.to_xccdf(attr_hsh)
65
72
  File.write(options[:output], xccdf)
@@ -136,26 +143,8 @@ module InspecPlugins
136
143
 
137
144
  desc 'generate_map', 'Generates mapping template from CSV to Inspec Controls'
138
145
  def generate_map
139
- template = '
140
- # Setting csv_header to true will skip the csv file header
141
- skip_csv_header: true
142
- width : 80
143
-
144
-
145
- control.id: 0
146
- control.title: 15
147
- control.desc: 16
148
- control.tags:
149
- severity: 1
150
- rid: 8
151
- stig_id: 3
152
- cci: 2
153
- check: 12
154
- fix: 10
155
- '
156
- myfile = File.new('mapping.yml', 'w')
157
- myfile.puts template
158
- myfile.close
146
+ generator = InspecTools::GenerateMap.new
147
+ generator.generate_example('mapping.yml')
159
148
  end
160
149
 
161
150
  desc 'generate_ckl_metadata', 'Generate metadata file that can be passed to inspec2ckl'
@@ -6,6 +6,7 @@ require 'digest'
6
6
 
7
7
  require_relative '../utilities/inspec_util'
8
8
  require_relative '../utilities/cis_to_nist'
9
+ require_relative '../utilities/mapping_validator'
9
10
 
10
11
  # rubocop:disable Metrics/AbcSize
11
12
  # rubocop:disable Metrics/PerceivedComplexity
@@ -19,7 +20,7 @@ module InspecTools
19
20
  def initialize(xlsx, mapping, name, verbose = false)
20
21
  @name = name
21
22
  @xlsx = xlsx
22
- @mapping = mapping
23
+ @mapping = Utils::MappingValidator.validate(mapping)
23
24
  @verbose = verbose
24
25
  @cis_to_nist = Utils::CisToNist.get_mapping('cis_to_nist_mapping')
25
26
  end
@@ -0,0 +1,13 @@
1
+ require 'nokogiri'
2
+
3
+ module Utils
4
+ class CciXml
5
+ def self.get_cci_list(cci_list_file)
6
+ path = File.expand_path(File.join(File.expand_path(__dir__), '..', 'data', cci_list_file))
7
+ raise "CCI list does not exist at #{path}" unless File.exist?(path)
8
+
9
+ cci_list = Nokogiri::XML(File.open(path))
10
+ cci_list.remove_namespaces!
11
+ end
12
+ end
13
+ end
@@ -13,15 +13,8 @@ require 'overrides/object'
13
13
  require 'overrides/string'
14
14
  require 'rubocop'
15
15
 
16
- # rubocop:disable Metrics/ClassLength
17
- # rubocop:disable Metrics/AbcSize
18
- # rubocop:disable Metrics/PerceivedComplexity
19
- # rubocop:disable Metrics/CyclomaticComplexity
20
- # rubocop:disable Metrics/MethodLength
21
-
22
16
  module Utils
23
- class InspecUtil
24
- DATA_NOT_FOUND_MESSAGE = 'N/A'.freeze
17
+ class InspecUtil # rubocop:disable Metrics/ClassLength
25
18
  WIDTH = 80
26
19
  IMPACT_SCORES = {
27
20
  'none' => 0.0,
@@ -31,56 +24,7 @@ module Utils
31
24
  'critical' => 0.9
32
25
  }.freeze
33
26
 
34
- def self.parse_data_for_xccdf(json)
35
- data = {}
36
-
37
- controls = []
38
- if json['profiles'].nil?
39
- controls = json['controls']
40
- elsif json['profiles'].length == 1
41
- controls = json['profiles'].last['controls']
42
- else
43
- json['profiles'].each do |profile|
44
- controls.concat(profile['controls'])
45
- end
46
- end
47
- c_data = {}
48
-
49
- controls.each do |control|
50
- c_id = control['id'].to_sym
51
- c_data[c_id] = {}
52
- c_data[c_id]['id'] = control['id'] || DATA_NOT_FOUND_MESSAGE
53
- c_data[c_id]['title'] = control['title'] || DATA_NOT_FOUND_MESSAGE
54
- c_data[c_id]['desc'] = control['desc'] || DATA_NOT_FOUND_MESSAGE
55
- c_data[c_id]['severity'] = control['tags']['severity'] || DATA_NOT_FOUND_MESSAGE
56
- c_data[c_id]['gid'] = control['tags']['gid'] || DATA_NOT_FOUND_MESSAGE
57
- c_data[c_id]['gtitle'] = control['tags']['gtitle'] || DATA_NOT_FOUND_MESSAGE
58
- c_data[c_id]['gdescription'] = control['tags']['gdescription'] || DATA_NOT_FOUND_MESSAGE
59
- c_data[c_id]['rid'] = control['tags']['rid'] || DATA_NOT_FOUND_MESSAGE
60
- c_data[c_id]['rversion'] = control['tags']['rversion'] || DATA_NOT_FOUND_MESSAGE
61
- c_data[c_id]['rweight'] = control['tags']['rweight'] || DATA_NOT_FOUND_MESSAGE
62
- c_data[c_id]['stig_id'] = control['tags']['stig_id'] || DATA_NOT_FOUND_MESSAGE
63
- c_data[c_id]['cci'] = control['tags']['cci'] || DATA_NOT_FOUND_MESSAGE
64
- c_data[c_id]['nist'] = control['tags']['nist'] || ['unmapped']
65
- c_data[c_id]['check'] = control['tags']['check'] || DATA_NOT_FOUND_MESSAGE
66
- c_data[c_id]['checkref'] = control['tags']['checkref'] || DATA_NOT_FOUND_MESSAGE
67
- c_data[c_id]['fix'] = control['tags']['fix'] || DATA_NOT_FOUND_MESSAGE
68
- c_data[c_id]['fixref'] = control['tags']['fixref'] || DATA_NOT_FOUND_MESSAGE
69
- c_data[c_id]['fix_id'] = control['tags']['fix_id'] || DATA_NOT_FOUND_MESSAGE
70
- c_data[c_id]['rationale'] = control['tags']['rationale'] || DATA_NOT_FOUND_MESSAGE
71
- c_data[c_id]['cis_family'] = control['tags']['cis_family'] || DATA_NOT_FOUND_MESSAGE
72
- c_data[c_id]['cis_rid'] = control['tags']['cis_rid'] || DATA_NOT_FOUND_MESSAGE
73
- c_data[c_id]['cis_level'] = control['tags']['cis_level'] || DATA_NOT_FOUND_MESSAGE
74
- c_data[c_id]['impact'] = control['impact'].to_s || DATA_NOT_FOUND_MESSAGE
75
- c_data[c_id]['code'] = control['code'].to_s || DATA_NOT_FOUND_MESSAGE
76
- end
77
-
78
- data['controls'] = c_data.values
79
- data['status'] = 'success'
80
- data
81
- end
82
-
83
- def self.parse_data_for_ckl(json)
27
+ def self.parse_data_for_ckl(json) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
84
28
  data = {}
85
29
 
86
30
  # Parse for inspec profile results json
@@ -221,7 +165,7 @@ module Utils
221
165
  end
222
166
 
223
167
  private_class_method def self.string_to_impact(severity, use_cvss_terms)
224
- if /none|na|n\/a|not[_|(\s*)]?applicable/i.match?(severity)
168
+ if %r{none|na|n/a|not[_|(\s*)]?applicable}i.match?(severity)
225
169
  impact = 0.0 # Informative
226
170
  elsif /low|cat(egory)?\s*(iii|3)/i.match?(severity)
227
171
  impact = 0.3 # Low Impact
@@ -248,13 +192,10 @@ module Utils
248
192
  end
249
193
 
250
194
  IMPACT_SCORES.reverse_each do |name, impact_score|
251
- if name == 'critical' && value >= impact_score && use_cvss_terms
252
- return 'high'
253
- elsif value >= impact_score
254
- return name
255
- else
256
- next
257
- end
195
+ return 'high' if name == 'critical' && value >= impact_score && use_cvss_terms
196
+ return name if value >= impact_score
197
+
198
+ next
258
199
  end
259
200
  end
260
201
 
@@ -277,7 +218,7 @@ module Utils
277
218
  WordWrap.ww(str.to_s, width)
278
219
  end
279
220
 
280
- private_class_method def self.generate_controls(inspec_json)
221
+ private_class_method def self.generate_controls(inspec_json) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
281
222
  controls = []
282
223
  inspec_json['controls'].each do |json_control|
283
224
  control = ::Inspec::Object::Control.new
@@ -384,7 +325,7 @@ module Utils
384
325
  myfile.puts readme_contents
385
326
  end
386
327
 
387
- private_class_method def self.unpack_profile(directory, controls, separated, output_format)
328
+ private_class_method def self.unpack_profile(directory, controls, separated, output_format) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
388
329
  FileUtils.rm_rf(directory) if Dir.exist?(directory)
389
330
  Dir.mkdir directory unless Dir.exist?(directory)
390
331
  Dir.mkdir "#{directory}/controls" unless Dir.exist?("#{directory}/controls")
@@ -433,9 +374,3 @@ module Utils
433
374
  end
434
375
  end
435
376
  end
436
-
437
- # rubocop:enable Metrics/ClassLength
438
- # rubocop:enable Metrics/AbcSize
439
- # rubocop:enable Metrics/PerceivedComplexity
440
- # rubocop:enable Metrics/CyclomaticComplexity
441
- # rubocop:enable Metrics/MethodLength
@@ -0,0 +1,10 @@
1
+ module Utils
2
+ class MappingValidator
3
+ def self.validate(mapping)
4
+ return mapping unless mapping.include?('control.tags') && mapping['control.tags'].include?('nist')
5
+
6
+ raise "Mapping file should not contain an entry for 'control.tags.nist'.
7
+ NIST tags will be autogenerated via 'control.tags.cci'."
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,89 @@
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) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
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]['nist'] = control['tags']['nist'] || ['unmapped']
38
+ c_data[c_id]['check'] = control['tags']['check'] || DATA_NOT_FOUND_MESSAGE
39
+ c_data[c_id]['checkref'] = control['tags']['checkref'] || DATA_NOT_FOUND_MESSAGE
40
+ c_data[c_id]['fix'] = control['tags']['fix'] || DATA_NOT_FOUND_MESSAGE
41
+ c_data[c_id]['fix_id'] = control['tags']['fix_id'] if control['tags']['fix_id'] # Optional attribute where N/A is not schema compliant
42
+ c_data[c_id]['rationale'] = control['tags']['rationale'] || DATA_NOT_FOUND_MESSAGE
43
+ c_data[c_id]['cis_family'] = control['tags']['cis_family'] || DATA_NOT_FOUND_MESSAGE
44
+ c_data[c_id]['cis_rid'] = control['tags']['cis_rid'] || DATA_NOT_FOUND_MESSAGE
45
+ c_data[c_id]['cis_level'] = control['tags']['cis_level'] || DATA_NOT_FOUND_MESSAGE
46
+ c_data[c_id]['impact'] = control['impact'].to_s || DATA_NOT_FOUND_MESSAGE
47
+ c_data[c_id]['code'] = control['code'].to_s || DATA_NOT_FOUND_MESSAGE
48
+ c_data[c_id]['results'] = parse_results_for_xccdf(control['results']) if control['results']
49
+ end
50
+
51
+ data['controls'] = c_data.values
52
+ data['profiles'] = parse_profiles_for_xccdf(json['profiles'])
53
+ data['status'] = 'success'
54
+ data['inspec_version'] = json['version']
55
+ data
56
+ end
57
+
58
+ private
59
+
60
+ # Convert profile information for result processing
61
+ # @param profiles [Array[Hash]] - The profiles section of the JSON output
62
+ def parse_profiles_for_xccdf(profiles)
63
+ return [] unless profiles
64
+
65
+ profiles.map do |profile|
66
+ data = {}
67
+ data['name'] = profile['name']
68
+ data['version'] = profile['version']
69
+ data
70
+ end
71
+ end
72
+
73
+ # Convert the test result data to a parseable Hash for downstream processing
74
+ # @param results [Array[Hash]] - The results section of the JSON output
75
+ def parse_results_for_xccdf(results)
76
+ results.map do |result|
77
+ data = {}
78
+ data['status'] = result['status']
79
+ data['code_desc'] = result['code_desc']
80
+ data['run_time'] = result['run_time']
81
+ data['start_time'] = result['start_time']
82
+ data['resource'] = result['resource']
83
+ data['message'] = result['message']
84
+ data['skip_message'] = result['skip_message']
85
+ data
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,388 @@
1
+ require_relative 'xccdf_score'
2
+
3
+ module Utils
4
+ # Data conversions for Inspec output into XCCDF format.
5
+ class ToXCCDF # rubocop:disable Metrics/ClassLength
6
+ # @param attribute [Hash] XCCDF supplemental attributes
7
+ # @param data [Hash] Converted Inspec output data
8
+ def initialize(attribute, data)
9
+ @attribute = attribute
10
+ @data = data
11
+ @benchmark = HappyMapperTools::Benchmark::Benchmark.new
12
+ end
13
+
14
+ # Build entire XML document and produce final output
15
+ # @param metadata [Hash] Data representing a system under scan
16
+ def to_xml(metadata)
17
+ build_benchmark_header
18
+ build_groups
19
+ # Only populate results if a target is defined so that conformant XML is produced.
20
+ @benchmark.testresult = build_test_results(metadata) if metadata['fqdn']
21
+ @benchmark.to_xml
22
+ end
23
+
24
+ private
25
+
26
+ # Sets top level XCCDF Benchmark attributes
27
+ def build_benchmark_header
28
+ @benchmark.title = @attribute['benchmark.title']
29
+ @benchmark.id = @attribute['benchmark.id']
30
+ @benchmark.description = @attribute['benchmark.description']
31
+ @benchmark.version = @attribute['benchmark.version']
32
+ @benchmark.xmlns = 'http://checklists.nist.gov/xccdf/1.1'
33
+
34
+ @benchmark.status = HappyMapperTools::Benchmark::Status.new
35
+ @benchmark.status.status = @attribute['benchmark.status']
36
+ @benchmark.status.date = @attribute['benchmark.status.date']
37
+
38
+ if @attribute['benchmark.notice.id']
39
+ @benchmark.notice = HappyMapperTools::Benchmark::Notice.new
40
+ @benchmark.notice.id = @attribute['benchmark.notice.id']
41
+ end
42
+
43
+ if @attribute['benchmark.plaintext'] || @attribute['benchmark.plaintext.id']
44
+ @benchmark.plaintext = HappyMapperTools::Benchmark::Plaintext.new
45
+ @benchmark.plaintext.plaintext = @attribute['benchmark.plaintext']
46
+ @benchmark.plaintext.id = @attribute['benchmark.plaintext.id']
47
+ end
48
+
49
+ @benchmark.reference = HappyMapperTools::Benchmark::ReferenceBenchmark.new
50
+ @benchmark.reference.href = @attribute['reference.href']
51
+ @benchmark.reference.dc_publisher = @attribute['reference.dc.publisher']
52
+ @benchmark.reference.dc_source = @attribute['reference.dc.source']
53
+ end
54
+
55
+ # Translate join of Inspec results and input attributes to XCCDF Groups
56
+ def build_groups # rubocop:disable Metrics/AbcSize
57
+ group_array = []
58
+ @data['controls'].each do |control|
59
+ group = HappyMapperTools::Benchmark::Group.new
60
+ group.id = control['id']
61
+ group.title = control['gtitle']
62
+ group.description = "<GroupDescription>#{control['gdescription']}</GroupDescription>" if control['gdescription']
63
+
64
+ group.rule = HappyMapperTools::Benchmark::Rule.new
65
+ group.rule.id = control['rid']
66
+ group.rule.severity = control['severity']
67
+ group.rule.weight = control['rweight']
68
+ group.rule.version = control['rversion']
69
+ group.rule.title = control['title'].tr("\n", ' ') if control['title']
70
+ group.rule.description = "<VulnDiscussion>#{control['desc'].tr("\n", ' ')}</VulnDiscussion><FalsePositives></FalsePositives><FalseNegatives></FalseNegatives><Documentable>false</Documentable><Mitigations></Mitigations><SeverityOverrideGuidance></SeverityOverrideGuidance><PotentialImpacts></PotentialImpacts><ThirdPartyTools></ThirdPartyTools><MitigationControl></MitigationControl><Responsibility></Responsibility><IAControls></IAControls>"
71
+
72
+ if ['reference.dc.publisher', 'reference.dc.title', 'reference.dc.subject', 'reference.dc.type', 'reference.dc.identifier'].any? { |a| @attribute.key?(a) }
73
+ group.rule.reference = build_rule_reference
74
+ end
75
+
76
+ group.rule.ident = build_rule_idents(control['cci']) if control['cci']
77
+
78
+ group.rule.fixtext = HappyMapperTools::Benchmark::Fixtext.new
79
+ group.rule.fixtext.fixref = control['fix_id']
80
+ group.rule.fixtext.fixtext = control['fix']
81
+
82
+ group.rule.fix = build_rule_fix(control['fix_id']) if control['fix_id']
83
+
84
+ group.rule.check = HappyMapperTools::Benchmark::Check.new
85
+ group.rule.check.system = control['checkref']
86
+
87
+ # content_ref is optional for schema compliance
88
+ if @attribute['content_ref.name'] || @attribute['content_ref.href']
89
+ group.rule.check.content_ref = HappyMapperTools::Benchmark::ContentRef.new
90
+ group.rule.check.content_ref.name = @attribute['content_ref.name']
91
+ group.rule.check.content_ref.href = @attribute['content_ref.href']
92
+ end
93
+
94
+ group.rule.check.content = control['check']
95
+
96
+ group_array << group
97
+ end
98
+ @benchmark.group = group_array
99
+ end
100
+
101
+ # Construct a Benchmark Testresult from Inspec data. This must be called after all XML processing has occurred for profiles
102
+ # and groups.
103
+ # @param metadata [Hash]
104
+ # @return [TestResult]
105
+ def build_test_results(metadata)
106
+ test_result = HappyMapperTools::Benchmark::TestResult.new
107
+ test_result.version = @benchmark.version
108
+ populate_remark(test_result)
109
+ populate_target_facts(test_result, metadata)
110
+ populate_identity(test_result, metadata)
111
+ populate_results(test_result)
112
+ populate_score(test_result, @benchmark.group)
113
+
114
+ test_result
115
+ end
116
+
117
+ # Contruct a Rule / RuleResult fix element with the provided id.
118
+ def build_rule_fix(fix_id)
119
+ HappyMapperTools::Benchmark::Fix.new.tap { |f| f.id = fix_id }
120
+ end
121
+
122
+ # Construct rule identifiers for rule
123
+ # @param idents [Array]
124
+ def build_rule_idents(idents)
125
+ raise "#{idents} is not an Array type." unless idents.is_a?(Array)
126
+
127
+ # Each rule identifier is a different element
128
+ idents.map do |identifier|
129
+ ident = HappyMapperTools::Benchmark::Ident.new
130
+ ident.system = 'https://public.cyber.mil/stigs/cci/'
131
+ ident.ident = identifier
132
+ ident
133
+ end
134
+ end
135
+
136
+ # Contruct a Rule reference element
137
+ def build_rule_reference
138
+ reference = HappyMapperTools::Benchmark::ReferenceGroup.new
139
+ reference.dc_publisher = @attribute['reference.dc.publisher']
140
+ reference.dc_title = @attribute['reference.dc.title']
141
+ reference.dc_subject = @attribute['reference.dc.subject']
142
+ reference.dc_type = @attribute['reference.dc.type']
143
+ reference.dc_identifier = @attribute['reference.dc.identifier']
144
+ reference
145
+ end
146
+
147
+ # Create a remark with contextual information about the Inspec version and profiles used
148
+ # @param result [HappyMapperTools::Benchmark::TestResult]
149
+ def populate_remark(result)
150
+ result.remark = "Results created using Inspec version #{@data['inspec_version']}.\n#{@data['profiles'].map { |p| "Profile: #{p['name']} Version: #{p['version']}" }.join("\n")}"
151
+ end
152
+
153
+ # Create all target specific information.
154
+ # @param result [HappyMapperTools::Benchmark::TestResult]
155
+ # @param metadata [Hash]
156
+ def populate_target_facts(result, metadata)
157
+ result.target = metadata['fqdn']
158
+ result.target_address = metadata['ip'] if metadata['ip']
159
+
160
+ all_facts = []
161
+
162
+ if metadata['mac']
163
+ fact = HappyMapperTools::Benchmark::Fact.new
164
+ fact.name = 'urn:xccdf:fact:asset:identifier:mac'
165
+ fact.type = 'string'
166
+ fact.fact = metadata['mac']
167
+ all_facts << fact
168
+ end
169
+
170
+ if metadata['ip']
171
+ fact = HappyMapperTools::Benchmark::Fact.new
172
+ fact.name = 'urn:xccdf:fact:asset:identifier:ipv4'
173
+ fact.type = 'string'
174
+ fact.fact = metadata['ip']
175
+ all_facts << fact
176
+ end
177
+
178
+ return unless all_facts.size.nonzero?
179
+
180
+ facts = HappyMapperTools::Benchmark::TargetFact.new
181
+ facts.fact = all_facts
182
+ result.target_facts = facts
183
+ end
184
+
185
+ # Build out the TestResult given all the control and result data.
186
+ def populate_results(test_result)
187
+ # Note: id is not an XCCDF 1.2 compliant identifier and will need to be updated when that support is added.
188
+ test_result.id = 'result_1'
189
+ test_result.starttime = run_start_time
190
+ test_result.endtime = run_end_time
191
+
192
+ # Build out individual results
193
+ all_rule_result = []
194
+
195
+ @data['controls'].each do |control|
196
+ next if control['results'].empty?
197
+
198
+ control_results =
199
+ control['results'].map do |result|
200
+ populate_rule_result(control, result, xccdf_status(result['status'], control['impact']))
201
+ end
202
+
203
+ # Consolidate results into single rule result do to lack of multiple=true attribute on Rule.
204
+ # 1. Select the unified result status
205
+ selected_status = control_results.reduce(control_results.first.result) { |f_status, rule_result| xccdf_and_result(f_status, rule_result.result) }
206
+
207
+ # 2. Only choose results with that status
208
+ # 3. Combine those results
209
+ all_rule_result << combine_results(control_results.select { |r| r.result == selected_status })
210
+ end
211
+
212
+ test_result.rule_result = all_rule_result
213
+ test_result
214
+ end
215
+
216
+ # Create rule-result from the control and Inspec result information
217
+ def populate_rule_result(control, result, result_status)
218
+ rule_result = HappyMapperTools::Benchmark::RuleResultType.new
219
+
220
+ rule_result.idref = control['rid']
221
+ rule_result.severity = control['severity']
222
+ rule_result.time = end_time(result['start_time'], result['run_time'])
223
+ rule_result.weight = control['rweight']
224
+
225
+ rule_result.result = result_status
226
+ rule_result.message = result_message(result, result_status) if result_message(result, result_status)
227
+ rule_result.instance = result['code_desc']
228
+
229
+ rule_result.ident = build_rule_idents(control['cci']) if control['cci']
230
+
231
+ # Fix information is only necessary when there are failed tests
232
+ rule_result.fix = build_rule_fix(control['fix_id']) if control['fix_id'] && result_status == 'fail'
233
+
234
+ rule_result.check = HappyMapperTools::Benchmark::Check.new
235
+ rule_result.check.system = control['checkref']
236
+ rule_result.check.content = result['code_desc']
237
+ rule_result
238
+ end
239
+
240
+ # Combines rule results with the same result into a single rule result.
241
+ def combine_results(rule_results) # rubocop:disable Metrics/AbcSize
242
+ return rule_results.first if rule_results.size == 1
243
+
244
+ # Can combine, result, idents (duplicate, take first instance), instance - combine into an array removing duplicates
245
+ # check.content - Only one value allowed, combine by joining with line feed. Prior to, make sure all values are unique.
246
+
247
+ rule_result = HappyMapperTools::Benchmark::RuleResultType.new
248
+ rule_result.idref = rule_results.first.idref
249
+ rule_result.severity = rule_results.first.severity
250
+ # Take latest time
251
+ rule_result.time = rule_results.reduce(rule_results.first.time) { |time, r| time > r.time ? time : r.time }
252
+ rule_result.weight = rule_results.first.weight
253
+
254
+ rule_result.result = rule_results.first.result
255
+ rule_result.message = rule_results.reduce([]) { |messages, r| r.message ? messages.push(r.message) : messages }
256
+ rule_result.instance = rule_results.reduce([]) { |instances, r| r.instance ? instances.push(r.instance) : instances }.join("\n")
257
+
258
+ rule_result.ident = rule_results.first.ident
259
+ rule_result.fix = rule_results.first.fix
260
+
261
+ if rule_results.first.check
262
+ rule_result.check = HappyMapperTools::Benchmark::Check.new
263
+ rule_result.check.system = rule_results.first.check.system
264
+ rule_result.check.content = rule_results.map { |r| r.check.content }.join("\n")
265
+ end
266
+
267
+ rule_result
268
+ end
269
+
270
+ # Add information about the the account and organization executing the tests.
271
+ def populate_identity(test_result, metadata)
272
+ if metadata['identity']
273
+ test_result.identity = HappyMapperTools::Benchmark::IdentityType.new
274
+ test_result.identity.authenticated = true
275
+ test_result.identity.identity = metadata['identity']['identity']
276
+ test_result.identity.privileged = metadata['identity']['privileged']
277
+ end
278
+
279
+ test_result.organization = metadata['organization'] if metadata['organization']
280
+ end
281
+
282
+ # Return the earliest time of execution.
283
+ def run_start_time
284
+ @data['controls'].map { |control| control['results'].map { |result| DateTime.parse(result['start_time']) } }.flatten.min
285
+ end
286
+
287
+ # Return the latest time of execution accounting for Inspec duration.
288
+ def run_end_time
289
+ end_times =
290
+ @data['controls'].map do |control|
291
+ control['results'].map { |result| end_time(result['start_time'], result['run_time']) }
292
+ end
293
+
294
+ end_times.flatten.max
295
+ end
296
+
297
+ # Calculate an end time given a start time and second duration
298
+ def end_time(start, duration)
299
+ DateTime.parse(start) + (duration / (24*60*60))
300
+ end
301
+
302
+ # Map the Inspec result status to appropriate XCCDF test result status.
303
+ # XCCDF options include: pass, fail, error, unknown, notapplicable, notchecked, notselected, informational, fixed
304
+ #
305
+ # @param inspec_status [String] The reported Inspec status from an individual test
306
+ # @param impact [String] A value of 0.0 - 1.0
307
+ # @return A valid Inspec status.
308
+ def xccdf_status(inspec_status, impact)
309
+ # Currently, there is no good way to map an Inspec result status to one of XCCDF status unknown or notselected.
310
+ case inspec_status
311
+ when 'failed'
312
+ 'fail'
313
+ when 'passed'
314
+ 'pass'
315
+ when 'skipped'
316
+ if impact.to_f.zero?
317
+ 'notapplicable'
318
+ else
319
+ 'notchecked'
320
+ end
321
+ else
322
+ # In the event Inspec adds a new unaccounted for status, mapping to XCCDF unknown.
323
+ 'unknown'
324
+ end
325
+ end
326
+
327
+ # When more than one result occurs for a rule and the specification does not declare multiple, the result must be combined.
328
+ # This determines the appropriate result to be selected when there are two to compare.
329
+ # @param one [String] A rule-result status
330
+ # @param two [String] A rule-result status
331
+ # @return The result of the AND operation.
332
+ def xccdf_and_result(one, two) # rubocop:disable Metrics/CyclomaticComplexity
333
+ # From XCCDF specification truth table
334
+ # P = pass
335
+ # F = fail
336
+ # U = unknown
337
+ # E = error
338
+ # N = notapplicable
339
+ # K = notchecked
340
+ # S = notselected
341
+ # I = informational
342
+
343
+ case one
344
+ when 'pass'
345
+ %w{fail unknown}.any? { |s| s == two } ? two : one
346
+ when 'fail'
347
+ one
348
+ when 'unknown'
349
+ two == 'fail' ? two : one
350
+ when 'notapplicable'
351
+ %w{pass fail unknown}.any? { |s| s == two } ? two : one
352
+ when 'notchecked'
353
+ %w{pass fail unknown notapplicable}.any? { |s| s == two } ? two : one
354
+ end
355
+ end
356
+
357
+ # Builds the message information for rule results
358
+ # @param result [Hash] A single Inspec result
359
+ # @param xccdf_status [String] the xccdf calculated result status for the provided result
360
+ def result_message(result, xccdf_status)
361
+ return unless result['message'] || result['skip_message']
362
+
363
+ message = HappyMapperTools::Benchmark::MessageType.new
364
+ # Including the code of the check and the resulting message if there is one.
365
+ message.message = "#{result['code_desc'] ? result['code_desc'] + "\n\n" : ''}#{result['message'] || result['skip_message']}"
366
+ message.severity = result_message_severity(xccdf_status)
367
+ message
368
+ end
369
+
370
+ # All rule-result messages require a defined severity. This determines a value to use based upon the result XCCDF status.
371
+ def result_message_severity(xccdf_status)
372
+ case xccdf_status
373
+ when 'fail'
374
+ 'error'
375
+ when 'notapplicable'
376
+ 'warning'
377
+ else
378
+ 'info'
379
+ end
380
+ end
381
+
382
+ # Set scores for all 4 required/recommended scoring systems.
383
+ def populate_score(test_result, groups)
384
+ score = Utils::XCCDFScore.new(groups, test_result.rule_result)
385
+ test_result.score = [score.default_score, score.flat_score, score.flat_unweighted_score, score.absolute_score]
386
+ end
387
+ end
388
+ end