inspec_tools 2.0.4 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,8 +2,6 @@ require 'json'
2
2
  require 'yaml'
3
3
  require_relative '../utilities/inspec_util'
4
4
 
5
- # rubocop:disable Metrics/AbcSize
6
-
7
5
  # Impact Definitions
8
6
  CRITICAL = 0.9
9
7
  HIGH = 0.7
@@ -16,48 +14,118 @@ TALLYS = %i(total critical high medium low).freeze
16
14
  THRESHOLD_TEMPLATE = File.expand_path('../data/threshold.yaml', File.dirname(__FILE__))
17
15
 
18
16
  module InspecTools
17
+ # rubocop:disable Metrics/ClassLength
19
18
  class Summary
20
- def initialize(inspec_json)
21
- @json = JSON.parse(inspec_json)
19
+ attr_reader :json
20
+ attr_reader :json_full
21
+ attr_reader :json_counts
22
+ attr_reader :threshold_file
23
+ attr_reader :threshold_inline
24
+ attr_reader :summary
25
+ attr_reader :threshold
26
+
27
+ def initialize(**options)
28
+ options = options[:options]
29
+ @json = JSON.parse(File.read(options[:inspec_json]))
30
+ @json_full = false || options[:json_full]
31
+ @json_counts = false || options[:json_counts]
32
+ @threshold = parse_threshold(options[:threshold_inline], options[:threshold_file])
33
+ @threshold_provided = options[:threshold_inline] || options[:threshold_file]
34
+ @summary = compute_summary
22
35
  end
23
36
 
24
- def to_summary
25
- @data = Utils::InspecUtil.parse_data_for_ckl(@json)
26
- @summary = {}
27
- @data.keys.each do |control_id|
28
- current_control = @data[control_id]
29
- current_control[:compliance_status] = Utils::InspecUtil.control_status(current_control, true)
30
- current_control[:finding_details] = Utils::InspecUtil.control_finding_details(current_control, current_control[:compliance_status])
37
+ def output_summary
38
+ unless @json_full || @json_counts
39
+ puts "\nThreshold compliance: #{@threshold['compliance.min']}%"
40
+ puts "\nOverall compliance: #{@summary[:compliance]}%\n\n"
41
+ @summary[:status].keys.each do |category|
42
+ puts category
43
+ @summary[:status][category].keys.each do |impact|
44
+ puts "\t#{impact} : #{@summary[:status][category][impact]}"
45
+ end
46
+ end
31
47
  end
32
- compute_summary
33
- @summary
48
+
49
+ puts @summary.to_json if @json_full
50
+ puts @summary[:status].to_json if @json_counts
34
51
  end
35
52
 
36
- def threshold(threshold = nil)
37
- @summary = to_summary
38
- @threshold = Utils::InspecUtil.to_dotted_hash(YAML.load_file(THRESHOLD_TEMPLATE))
39
- parse_threshold(Utils::InspecUtil.to_dotted_hash(threshold))
40
- threshold_compliance
53
+ def results_meet_threshold?
54
+ raise 'Please provide threshold as a yaml file or inline yaml' unless @threshold_provided
55
+
56
+ compliance = true
57
+ failure = []
58
+ failure << check_max_compliance(@threshold['compliance.max'], @summary[:compliance], '', 'compliance')
59
+ failure << check_min_compliance(@threshold['compliance.min'], @summary[:compliance], '', 'compliance')
60
+
61
+ BUCKETS.each do |bucket|
62
+ TALLYS.each do |tally|
63
+ failure << check_min_compliance(@threshold["#{bucket}.#{tally}.min"], @summary[:status][bucket][tally], bucket, tally)
64
+ failure << check_max_compliance(@threshold["#{bucket}.#{tally}.max"], @summary[:status][bucket][tally], bucket, tally)
65
+ end
66
+ end
67
+
68
+ failure.reject!(&:nil?)
69
+ compliance = false if failure.length.positive?
70
+ output(compliance, failure)
71
+ compliance
41
72
  end
42
73
 
43
74
  private
44
75
 
76
+ def check_min_compliance(min, data, bucket, tally)
77
+ expected_to_string(bucket, tally, 'min', min, data) if min != -1 and data < min
78
+ end
79
+
80
+ def check_max_compliance(max, data, bucket, tally)
81
+ expected_to_string(bucket, tally, 'max', max, data) if max != -1 and data > max
82
+ end
83
+
84
+ def output(passed_threshold, what_failed)
85
+ if passed_threshold
86
+ puts "Overall compliance threshold of #{@threshold['compliance.min']}\% met. Current compliance at #{@summary[:compliance]}\%"
87
+ else
88
+ puts 'Compliance threshold was not met: '
89
+ puts what_failed.join("\n")
90
+ end
91
+ end
92
+
93
+ def expected_to_string(bucket, tally, maxmin, value, got)
94
+ return "Expected #{bucket}.#{tally}.#{maxmin}:#{value} got:#{got}" unless bucket.empty? || bucket.nil?
95
+
96
+ "Expected #{tally}.#{maxmin}:#{value}\% got:#{got}\%"
97
+ end
98
+
99
+ def parse_threshold(threshold_inline, threshold_file)
100
+ threshold = Utils::InspecUtil.to_dotted_hash(YAML.load_file(THRESHOLD_TEMPLATE))
101
+ threshold.merge!(Utils::InspecUtil.to_dotted_hash(YAML.load_file(threshold_file))) if threshold_file
102
+ threshold.merge!(Utils::InspecUtil.to_dotted_hash(YAML.safe_load(threshold_inline))) if threshold_inline
103
+ threshold
104
+ end
105
+
45
106
  def compute_summary
46
- @summary[:buckets] = {}
47
- @summary[:buckets][:failed] = select_by_status(@data, 'Open')
48
- @summary[:buckets][:passed] = select_by_status(@data, 'NotAFinding')
49
- @summary[:buckets][:no_impact] = select_by_status(@data, 'Not_Applicable')
50
- @summary[:buckets][:skipped] = select_by_status(@data, 'Not_Reviewed')
51
- @summary[:buckets][:error] = select_by_status(@data, 'Profile_Error')
52
-
53
- @summary[:status] = {}
54
- @summary[:status][:failed] = tally_by_impact(@summary[:buckets][:failed])
55
- @summary[:status][:passed] = tally_by_impact(@summary[:buckets][:passed])
56
- @summary[:status][:no_impact] = tally_by_impact(@summary[:buckets][:no_impact])
57
- @summary[:status][:skipped] = tally_by_impact(@summary[:buckets][:skipped])
58
- @summary[:status][:error] = tally_by_impact(@summary[:buckets][:error])
59
-
60
- @summary[:compliance] = compute_compliance
107
+ data = Utils::InspecUtil.parse_data_for_ckl(@json)
108
+
109
+ data.keys.each do |control_id|
110
+ current_control = data[control_id]
111
+ current_control[:compliance_status] = Utils::InspecUtil.control_status(current_control, true)
112
+ current_control[:finding_details] = Utils::InspecUtil.control_finding_details(current_control, current_control[:compliance_status])
113
+ end
114
+
115
+ summary = {}
116
+ summary[:buckets] = {}
117
+ summary[:buckets][:failed] = select_by_status(data, 'Open')
118
+ summary[:buckets][:passed] = select_by_status(data, 'NotAFinding')
119
+ summary[:buckets][:no_impact] = select_by_status(data, 'Not_Applicable')
120
+ summary[:buckets][:skipped] = select_by_status(data, 'Not_Reviewed')
121
+ summary[:buckets][:error] = select_by_status(data, 'Profile_Error')
122
+
123
+ summary[:status] = {}
124
+ %i(failed passed no_impact skipped error).each do |key|
125
+ summary[:status][key] = tally_by_impact(summary[:buckets][key])
126
+ end
127
+ summary[:compliance] = compute_compliance(summary)
128
+ summary
61
129
  end
62
130
 
63
131
  def select_by_impact(controls, impact)
@@ -78,49 +146,13 @@ module InspecTools
78
146
  tally
79
147
  end
80
148
 
81
- def compute_compliance
82
- (@summary[:status][:passed][:total]*100.0/
83
- (@summary[:status][:passed][:total]+
84
- @summary[:status][:failed][:total]+
85
- @summary[:status][:skipped][:total]+
86
- @summary[:status][:error][:total])).floor
87
- end
88
-
89
- def threshold_compliance
90
- compliance = true
91
- failure = []
92
- max = @threshold['compliance.max']
93
- min = @threshold['compliance.min']
94
- if max != -1 and @summary[:compliance] > max
95
- compliance = false
96
- failure << "Expected compliance.max:#{max} got:#{@summary[:compliance]}"
97
- end
98
- if min != -1 and @summary[:compliance] < min
99
- compliance = false
100
- failure << "Expected compliance.min:#{min} got:#{@summary[:compliance]}"
101
- end
102
- status = @summary[:status]
103
- BUCKETS.each do |bucket|
104
- TALLYS.each do |tally|
105
- max = @threshold["#{bucket}.#{tally}.max"]
106
- min = @threshold["#{bucket}.#{tally}.min"]
107
- if max != -1 and status[bucket][tally] > max
108
- compliance = false
109
- failure << "Expected #{bucket}.#{tally}.max:#{max} got:#{status[bucket][tally]}"
110
- end
111
- if min != -1 and status[bucket][tally] < min
112
- compliance = false
113
- failure << "Expected #{bucket}.#{tally}.min:#{min} got:#{status[bucket][tally]}"
114
- end
115
- end
116
- end
117
- puts failure.join("\n") unless compliance
118
- puts 'Compliance threshold met' if compliance
119
- compliance
120
- end
121
-
122
- def parse_threshold(new_threshold)
123
- @threshold.merge!(new_threshold)
149
+ def compute_compliance(summary)
150
+ (summary[:status][:passed][:total]*100.0/
151
+ (summary[:status][:passed][:total]+
152
+ summary[:status][:failed][:total]+
153
+ summary[:status][:skipped][:total]+
154
+ summary[:status][:error][:total])).floor
124
155
  end
125
156
  end
157
+ # rubocop:enable Metrics/ClassLength
126
158
  end
@@ -140,6 +140,7 @@ module InspecTools
140
140
  control['tags']['documentable'] = group.rule.description.documentable if group.rule.description.documentable != ''
141
141
  control['tags']['mitigations'] = group.rule.description.false_negatives if group.rule.description.mitigations != ''
142
142
  control['tags']['severity_override_guidance'] = group.rule.description.severity_override_guidance if group.rule.description.severity_override_guidance != ''
143
+ control['tags']['security_override_guidance'] = group.rule.description.security_override_guidance if group.rule.description.security_override_guidance != ''
143
144
  control['tags']['potential_impacts'] = group.rule.description.potential_impacts if group.rule.description.potential_impacts != ''
144
145
  control['tags']['third_party_tools'] = group.rule.description.third_party_tools if group.rule.description.third_party_tools != ''
145
146
  control['tags']['mitigation_controls'] = group.rule.description.mitigation_controls if group.rule.description.mitigation_controls != ''
@@ -3,9 +3,10 @@ require 'inspec-objects'
3
3
  require 'word_wrap'
4
4
  require 'yaml'
5
5
  require 'digest'
6
- require 'roo'
7
6
 
8
7
  require_relative '../utilities/inspec_util'
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
@@ -14,15 +15,14 @@ require_relative '../utilities/inspec_util'
14
15
  module InspecTools
15
16
  # Methods for converting from XLS to various formats
16
17
  class XLSXTool
17
- CIS_2_NIST_XLSX = Roo::Spreadsheet.open(File.join(File.dirname(__FILE__), '../data/NIST_Map_02052020_CIS_Controls_Version_7.1_Implementation_Groups_1.2.xlsx'))
18
18
  LATEST_NIST_REV = 'Rev_4'.freeze
19
19
 
20
20
  def initialize(xlsx, mapping, name, verbose = false)
21
21
  @name = name
22
22
  @xlsx = xlsx
23
- @mapping = mapping
23
+ @mapping = Utils::MappingValidator.validate(mapping)
24
24
  @verbose = verbose
25
- @cis_to_nist = get_cis_to_nist_control_mapping(CIS_2_NIST_XLSX)
25
+ @cis_to_nist = Utils::CisToNist.get_mapping('cis_to_nist_mapping')
26
26
  end
27
27
 
28
28
  def to_ckl
@@ -46,18 +46,6 @@ module InspecTools
46
46
 
47
47
  private
48
48
 
49
- def get_cis_to_nist_control_mapping(spreadsheet)
50
- cis_to_nist = {}
51
- spreadsheet.sheet(3).each do |row|
52
- if row[3].is_a? Numeric
53
- cis_to_nist[row[3].to_s] = row[0]
54
- else
55
- cis_to_nist[row[2].to_s] = row[0] unless (row[2] == '') || row[2].to_i.nil?
56
- end
57
- end
58
- cis_to_nist
59
- end
60
-
61
49
  def insert_json_metadata
62
50
  @profile['name'] = @name
63
51
  @profile['title'] = 'InSpec Profile'
@@ -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
@@ -0,0 +1,11 @@
1
+ module Utils
2
+ class CisToNist
3
+ def self.get_mapping(mapping_file)
4
+ path = File.expand_path(File.join(File.expand_path(__dir__), '..', 'data', mapping_file))
5
+ raise "CIS to NIST control mapping does not exist at #{path}. Has it been generated?" unless File.exist?(path)
6
+
7
+ mapping = File.open(path)
8
+ Marshal.load(mapping)
9
+ end
10
+ end
11
+ 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
@@ -315,6 +256,7 @@ module Utils
315
256
  control.add_tag(::Inspec::Object::Tag.new('documentable', json_control['tags']['documentable'])) unless json_control['tags']['documentable'].blank?
316
257
  control.add_tag(::Inspec::Object::Tag.new('mitigations', json_control['tags']['mitigations'])) unless json_control['tags']['mitigations'].blank?
317
258
  control.add_tag(::Inspec::Object::Tag.new('severity_override_guidance', json_control['tags']['severity_override_guidance'])) unless json_control['tags']['severity_override_guidance'].blank?
259
+ control.add_tag(::Inspec::Object::Tag.new('security_override_guidance', json_control['tags']['security_override_guidance'])) unless json_control['tags']['security_override_guidance'].blank?
318
260
  control.add_tag(::Inspec::Object::Tag.new('potential_impacts', json_control['tags']['potential_impacts'])) unless json_control['tags']['potential_impacts'].blank?
319
261
  control.add_tag(::Inspec::Object::Tag.new('third_party_tools', json_control['tags']['third_party_tools'])) unless json_control['tags']['third_party_tools'].blank?
320
262
  control.add_tag(::Inspec::Object::Tag.new('mitigation_controls', json_control['tags']['mitigation_controls'])) unless json_control['tags']['mitigation_controls'].blank?
@@ -383,7 +325,7 @@ module Utils
383
325
  myfile.puts readme_contents
384
326
  end
385
327
 
386
- 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
387
329
  FileUtils.rm_rf(directory) if Dir.exist?(directory)
388
330
  Dir.mkdir directory unless Dir.exist?(directory)
389
331
  Dir.mkdir "#{directory}/controls" unless Dir.exist?("#{directory}/controls")
@@ -432,9 +374,3 @@ module Utils
432
374
  end
433
375
  end
434
376
  end
435
-
436
- # rubocop:enable Metrics/ClassLength
437
- # rubocop:enable Metrics/AbcSize
438
- # rubocop:enable Metrics/PerceivedComplexity
439
- # rubocop:enable Metrics/CyclomaticComplexity
440
- # 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