inspec_tools 2.0.4 → 2.2.0

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