abide_dev_utils 0.9.7 → 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +7 -1
  4. data/Gemfile.lock +82 -64
  5. data/Rakefile +28 -0
  6. data/abide_dev_utils.gemspec +3 -1
  7. data/lib/abide_dev_utils/cem/benchmark.rb +291 -0
  8. data/lib/abide_dev_utils/cem/coverage_report.rb +348 -0
  9. data/lib/abide_dev_utils/cem/generate/reference.rb +116 -0
  10. data/lib/abide_dev_utils/cem/generate.rb +10 -0
  11. data/lib/abide_dev_utils/cem/mapping/mapper.rb +155 -0
  12. data/lib/abide_dev_utils/cem.rb +74 -0
  13. data/lib/abide_dev_utils/cli/cem.rb +153 -0
  14. data/lib/abide_dev_utils/cli/jira.rb +1 -1
  15. data/lib/abide_dev_utils/cli/xccdf.rb +15 -1
  16. data/lib/abide_dev_utils/cli.rb +2 -0
  17. data/lib/abide_dev_utils/errors/cem.rb +22 -0
  18. data/lib/abide_dev_utils/errors/general.rb +8 -2
  19. data/lib/abide_dev_utils/errors/ppt.rb +4 -0
  20. data/lib/abide_dev_utils/errors.rb +6 -0
  21. data/lib/abide_dev_utils/files.rb +34 -0
  22. data/lib/abide_dev_utils/markdown.rb +104 -0
  23. data/lib/abide_dev_utils/ppt/facter_utils.rb +140 -0
  24. data/lib/abide_dev_utils/ppt/hiera.rb +297 -0
  25. data/lib/abide_dev_utils/ppt/puppet_module.rb +74 -0
  26. data/lib/abide_dev_utils/ppt.rb +3 -5
  27. data/lib/abide_dev_utils/validate.rb +14 -0
  28. data/lib/abide_dev_utils/version.rb +1 -1
  29. data/lib/abide_dev_utils/xccdf/diff/benchmark/number_title.rb +270 -0
  30. data/lib/abide_dev_utils/xccdf/diff/benchmark/profile.rb +104 -0
  31. data/lib/abide_dev_utils/xccdf/diff/benchmark/property.rb +127 -0
  32. data/lib/abide_dev_utils/xccdf/diff/benchmark/property_existence.rb +47 -0
  33. data/lib/abide_dev_utils/xccdf/diff/benchmark.rb +267 -0
  34. data/lib/abide_dev_utils/xccdf/diff/utils.rb +30 -0
  35. data/lib/abide_dev_utils/xccdf/diff.rb +233 -0
  36. data/lib/abide_dev_utils/xccdf/parser/objects/digest_object.rb +118 -0
  37. data/lib/abide_dev_utils/xccdf/parser/objects/numbered_object.rb +104 -0
  38. data/lib/abide_dev_utils/xccdf/parser/objects.rb +741 -0
  39. data/lib/abide_dev_utils/xccdf/parser.rb +52 -0
  40. data/lib/abide_dev_utils/xccdf.rb +14 -124
  41. data/new_diff.rb +48 -0
  42. metadata +60 -9
  43. data/lib/abide_dev_utils/ppt/coverage.rb +0 -86
@@ -0,0 +1,291 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'abide_dev_utils/errors'
4
+ require 'abide_dev_utils/ppt/facter_utils'
5
+ require 'abide_dev_utils/cem/mapping/mapper'
6
+
7
+ module AbideDevUtils
8
+ module CEM
9
+ # Repesents a benchmark for purposes of organizing data for markdown representation
10
+ class Benchmark
11
+ attr_reader :osname, :major_version, :os_facts, :osfamily, :hiera_conf, :module_name, :framework, :rules
12
+
13
+ def initialize(osname, major_version, hiera_conf, module_name, framework: 'cis')
14
+ @osname = osname
15
+ @major_version = major_version
16
+ @os_facts = AbideDevUtils::Ppt::FacterUtils.recursive_facts_for_os(@osname, @major_version)
17
+ @osfamily = @os_facts['os']['family']
18
+ @hiera_conf = hiera_conf
19
+ @module_name = module_name
20
+ @framework = framework
21
+ @rules = {}
22
+ @map_cache = {}
23
+ @rules_in_map = {}
24
+ load_rules
25
+ end
26
+
27
+ # Creates Benchmark objects from a Puppet module
28
+ # @param pupmod [AbideDevUtils::Ppt::PuppetModule] A PuppetModule instance
29
+ # @param skip_errors [Boolean] True skips errors and loads non-erroring benchmarks, false raises the error.
30
+ # @return [Array<AbideDevUtils::CEM::Benchmark>] Array of Benchmark instances
31
+ def self.benchmarks_from_puppet_module(pupmod, ignore_all_errors: false, ignore_framework_mismatch: true)
32
+ frameworks = pupmod.hiera_conf.local_hiera_files(hierarchy_name: 'Mapping Data').each_with_object([]) do |hf, ary|
33
+ parts = hf.path.split(pupmod.hiera_conf.default_datadir)[-1].split('/')
34
+ ary << parts[2] unless ary.include?(parts[2])
35
+ end
36
+ pupmod.supported_os.each_with_object([]) do |supp_os, ary|
37
+ osname, majver = supp_os.split('::')
38
+ if majver.is_a?(Array)
39
+ majver.sort.each do |v|
40
+ frameworks.each do |fw|
41
+ benchmark = Benchmark.new(osname, v, pupmod.hiera_conf, pupmod.name(strip_namespace: true), framework: fw)
42
+ ary << benchmark
43
+ rescue StandardError => e
44
+ raise e unless ignore_all_errors || (e.instance_of?(AbideDevUtils::Errors::MappingDataFrameworkMismatchError) && ignore_framework_mismatch)
45
+ end
46
+ end
47
+ else
48
+ frameworks.each do |fw|
49
+ benchmark = Benchmark.new(osname, majver, pupmod.hiera_conf, pupmod.name(strip_namespace: true), framework: fw)
50
+ ary << benchmark
51
+ rescue StandardError => e
52
+ raise e unless ignore_all_errors || (e.instance_of?(AbideDevUtils::Errors::MappingDataFrameworkMismatchError) && ignore_framework_mismatch)
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ def mapper
59
+ @mapper ||= AbideDevUtils::CEM::Mapping::Mapper.new(module_name, framework, load_mapping_data)
60
+ end
61
+
62
+ def map_data
63
+ mapper.map_data
64
+ end
65
+
66
+ def resource_data
67
+ @resource_data ||= load_resource_data
68
+ end
69
+
70
+ def title
71
+ mapper.title
72
+ end
73
+
74
+ def version
75
+ mapper.version
76
+ end
77
+
78
+ def title_key
79
+ @title_key ||= "#{title} #{version}"
80
+ end
81
+
82
+ def add_rule(rule_hash)
83
+ @rules << rule_hash
84
+ end
85
+
86
+ def rules_in_map(mtype, level: nil, profile: nil)
87
+ real_mtype = map_type(mtype)
88
+ cache_key = [real_mtype, level, profile].compact.join('-')
89
+ return @rules_in_map[cache_key] if @rules_in_map.key?(cache_key)
90
+
91
+ all_rim = mapper.each_with_array_like(real_mtype) do |(lvl, profs), arr|
92
+ next if lvl == 'benchmark' || (!level.nil? && lvl != level)
93
+
94
+ profs.each do |prof, maps|
95
+ next if !profile.nil? && prof != profile
96
+
97
+ # CIS and STIG differ in that STIG does not have profiles
98
+ control_ids = maps.respond_to?(:keys) ? maps.keys : prof
99
+ arr << control_ids
100
+ end
101
+ end
102
+ @rules_in_map[cache_key] = all_rim.flatten.uniq
103
+ @rules_in_map[cache_key]
104
+ end
105
+
106
+ def map(control_id, level: nil, profile: nil)
107
+ mapper.get(control_id, level: level, profile: profile)
108
+ end
109
+
110
+ def map_type(control_id)
111
+ mapper.map_type(control_id)
112
+ end
113
+
114
+ private
115
+
116
+ def load_rules
117
+ resource_data["#{module_name}::resources"].each do |_, rdata|
118
+ unless rdata.key?('controls')
119
+ puts "Controls key not found in #{rdata}"
120
+ next
121
+ end
122
+ rdata['controls'].each do |control, control_data|
123
+ rule_title = map(control)
124
+ next if rule_title.nil? || rule_title.empty?
125
+
126
+ rule_title.find { |id| map_type(id) == 'title' }
127
+ alternate_ids = map(rule_title)
128
+
129
+ next unless rule_title.is_a?(String)
130
+
131
+ @rules[rule_title] = {} unless @rules&.key?(rule_title)
132
+ @rules[rule_title]['number'] = alternate_ids.find { |id| map_type(id) == 'number' }
133
+ @rules[rule_title]['alternate_ids'] = alternate_ids
134
+ @rules[rule_title]['params'] = [] unless @rules[rule_title].key?('params')
135
+ @rules[rule_title]['level'] = [] unless @rules[rule_title].key?('level')
136
+ @rules[rule_title]['profile'] = [] unless @rules[rule_title].key?('profile')
137
+ param_hashes(control_data).each do |param_hash|
138
+ next if @rules[rule_title]['params'].include?(param_hash[:name])
139
+
140
+ @rules[rule_title]['params'] << param_hash
141
+ end
142
+ levels, profiles = find_levels_and_profiles(control)
143
+ unless @rules[rule_title]['level'] == levels
144
+ @rules[rule_title]['level'] = @rules[rule_title]['level'] | levels
145
+ end
146
+ unless @rules[rule_title]['profile'] == profiles
147
+ @rules[rule_title]['profile'] = @rules[rule_title]['profile'] | profiles
148
+ end
149
+ @rules[rule_title]['resource'] = rdata['type']
150
+ end
151
+ end
152
+ @rules = sort_rules
153
+ end
154
+
155
+ def param_hashes(control_data)
156
+ return [] if control_data.nil? || control_data.empty?
157
+
158
+ p_hashes = []
159
+ if !control_data.respond_to?(:each) && control_data == 'no_params'
160
+ p_hashes << no_params
161
+ else
162
+ control_data.each do |param, param_val|
163
+ p_hashes << {
164
+ name: param,
165
+ type: ruby_class_to_puppet_type(param_val.class.to_s),
166
+ default: param_val,
167
+ }
168
+ end
169
+ end
170
+ p_hashes
171
+ end
172
+
173
+ def no_params
174
+ { name: 'No parameters', type: nil, default: nil }
175
+ end
176
+
177
+ # We sort the rules by their control number so they
178
+ # appear in the REFERENCE in benchmark order
179
+ def sort_rules
180
+ sorted = @rules.dup.sort_by do |_, v|
181
+ control_num_to_int(v['number'])
182
+ end
183
+ sorted.to_h
184
+ end
185
+
186
+ # In order to sort the rules by their control number,
187
+ # we need to convert the control number to an integer.
188
+ # This is a rough conversion, but should be sufficient
189
+ # for the purposes of sorting. The multipliers are
190
+ # the 20th, 15th, 10th, and 5th numbers in the Fibonacci
191
+ # sequence, then 1 after that. The reason for this is to
192
+ # ensure a "spiraled" wieghting of the sections in the control
193
+ # number, with 1st section having the most sorting weight, 2nd
194
+ # having second most, etc. However, the differences in the multipliers
195
+ # are such that it would be difficult for the product of a lesser-weighted
196
+ # section to be greater than a greater-weighted section.
197
+ def control_num_to_int(control_num)
198
+ multipliers = [6765, 610, 55, 5, 1]
199
+ nsum = 0
200
+ midx = 0
201
+ control_num.split('.').each do |num|
202
+ multiplier = midx >= multipliers.length ? 1 : multipliers[midx]
203
+ nsum += num.to_i * multiplier
204
+ midx += 1
205
+ end
206
+ nsum
207
+ end
208
+
209
+ def find_levels_and_profiles(control_id)
210
+ levels = []
211
+ profiles = []
212
+ mapper.each_like(control_id) do |lvl, profile_hash|
213
+ next if lvl == 'benchmark'
214
+
215
+ profile_hash.each do |prof, _|
216
+ unless map(control_id, level: lvl, profile: prof).nil?
217
+ levels << lvl
218
+ profiles << prof
219
+ end
220
+ end
221
+ end
222
+ [levels, profiles]
223
+ end
224
+
225
+ def ruby_class_to_puppet_type(class_name)
226
+ pup_type = class_name.split('::').last.capitalize
227
+ case pup_type
228
+ when %r{(Trueclass|Falseclass)}
229
+ 'Boolean'
230
+ when %r{(String|Pathname)}
231
+ 'String'
232
+ when %r{(Integer|Fixnum)}
233
+ 'Integer'
234
+ when %r{(Float|Double)}
235
+ 'Float'
236
+ else
237
+ pup_type
238
+ end
239
+ end
240
+
241
+ def load_mapping_data
242
+ files = case module_name
243
+ when /_windows$/
244
+ cem_windows_mapping_files
245
+ when /_linux$/
246
+ cem_linux_mapping_files
247
+ else
248
+ raise "Module name '#{module_name}' is not a CEM module"
249
+ end
250
+ validate_mapping_files_framework(files).each_with_object({}) do |f, h|
251
+ next unless f.path.include?(framework)
252
+
253
+ h[File.basename(f.path, '.yaml')] = YAML.load_file(f.path)
254
+ end
255
+ end
256
+
257
+ def cem_linux_mapping_files
258
+ facts = [['os.name', osname], ['os.release.major', major_version]]
259
+ mapping_files = hiera_conf.local_hiera_files_with_facts(*facts, hierarchy_name: 'Mapping Data')
260
+ raise AbideDevUtils::Errors::MappingFilesNotFoundError, facts if mapping_files.nil? || mapping_files.empty?
261
+
262
+ mapping_files
263
+ end
264
+
265
+ def cem_windows_mapping_files
266
+ facts = ['os.release.major', major_version]
267
+ mapping_files = hiera_conf.local_hiera_files_with_fact(facts[0], facts[1], hierarchy_name: 'Mapping Data')
268
+ raise AbideDevUtils::Errors::MappingFilesNotFoundError, facts if mapping_files.nil? || mapping_files.empty?
269
+
270
+ mapping_files
271
+ end
272
+
273
+ def validate_mapping_files_framework(files)
274
+ validated_files = files.select { |f| f.path_parts.include?(framework) }
275
+ if validated_files.nil? || validated_files.empty?
276
+ raise AbideDevUtils::Errors::MappingDataFrameworkMismatchError, framework
277
+ end
278
+
279
+ validated_files
280
+ end
281
+
282
+ def load_resource_data
283
+ facts = [['os.family', osfamily], ['os.name', osname], ['os.release.major', major_version]]
284
+ rdata_files = hiera_conf.local_hiera_files_with_facts(*facts, hierarchy_name: 'Resource Data')
285
+ raise AbideDevUtils::Errors::ResourceDataNotFoundError, facts if rdata_files.nil? || rdata_files.empty?
286
+
287
+ YAML.load_file(rdata_files[0].path)
288
+ end
289
+ end
290
+ end
291
+ end
@@ -0,0 +1,348 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+ require 'json'
5
+ require 'pathname'
6
+ require 'yaml'
7
+ require 'abide_dev_utils/ppt'
8
+ require 'abide_dev_utils/validate'
9
+ require 'abide_dev_utils/cem/benchmark'
10
+
11
+ module AbideDevUtils
12
+ module CEM
13
+ # Methods and objects used to construct a report of what CEM enforces versus what
14
+ # the various compliance frameworks expect to be enforced.
15
+ module CoverageReport
16
+ # def self.generate(outfile: 'cem_coverage.yaml', **_filters)
17
+ # pupmod = AbideDevUtils::Ppt::PuppetModule.new
18
+ # # filter = Filter.new(pupmod, **filters)
19
+ # benchmarks = AbideDevUtils::CEM::Benchmark.benchmarks_from_puppet_module(pupmod)
20
+ # Report.new(benchmarks).generate(outfile: outfile)
21
+ # end
22
+
23
+ def self.basic_coverage(format_func: :to_yaml, ignore_benchmark_errors: false)
24
+ pupmod = AbideDevUtils::Ppt::PuppetModule.new
25
+ # filter = Filter.new(pupmod, **filters)
26
+ benchmarks = AbideDevUtils::CEM::Benchmark.benchmarks_from_puppet_module(pupmod,
27
+ ignore_all_errors: ignore_benchmark_errors)
28
+ benchmarks.map do |b|
29
+ AbideDevUtils::CEM::CoverageReport::BenchmarkReport.new(b).basic_coverage.send(format_func)
30
+ end
31
+ end
32
+
33
+ class Filter
34
+ KEY_FACT_MAP = {
35
+ os_family: 'os.family',
36
+ os_name: 'os.name',
37
+ os_release_major: 'os.release.major',
38
+ }.freeze
39
+
40
+ attr_reader(*KEY_FACT_MAP.keys)
41
+
42
+ def initialize(pupmod, **filters)
43
+ @pupmod = pupmod
44
+ @benchmark = filters[:benchmark]
45
+ @profile = filters[:profile]
46
+ @level = filters[:level]
47
+ KEY_FACT_MAP.each_key do |k|
48
+ instance_variable_set "@#{k}", filters[k]
49
+ end
50
+ end
51
+
52
+ def resource_data
53
+ @resource_data ||= find_resource_data
54
+ end
55
+
56
+ def mapping_data
57
+ @mapping_data ||= find_mapping_data
58
+ end
59
+
60
+ private
61
+
62
+ def find_resource_data
63
+ fact_array = fact_array_for(:os_family, :os_name, :os_release_major)
64
+ @pupmod.hiera_conf.local_hiera_files_with_facts(*fact_array, hierarchy_name: 'Resource Data').map do |f|
65
+ YAML.load_file(f.path)
66
+ end
67
+ rescue NoMethodError
68
+ @pupmod.hiera_conf.local_hiera_files(hierarchy_name: 'Resource Data').map { |f| YAML.load_file(f.path) }
69
+ end
70
+
71
+ def find_mapping_data
72
+ fact_array = fact_array_for(:os_name, :os_release_major)
73
+ begin
74
+ data_array = @pupmod.hiera_conf.local_hiera_files_with_facts(*fact_array, hierarchy_name: 'Mapping Data').map do |f|
75
+ YAML.load_file(f.path)
76
+ end
77
+ rescue NoMethodError
78
+ data_array = @pupmod.hiera_conf.local_hiera_files(hierarchy_name: 'Mapping Data').map { |f| YAML.load_file(f.path) }
79
+ end
80
+ filter_mapping_data_array_by_benchmark!(data_array)
81
+ filter_mapping_data_array_by_profile!(data_array)
82
+ filter_mapping_data_array_by_level!(data_array)
83
+ data_array
84
+ end
85
+
86
+ def filter_mapping_data_array_by_benchmark!(data_array)
87
+ return unless @benchmark
88
+
89
+ data_array.select! do |d|
90
+ d.keys.all? do |k|
91
+ k == 'benchmark' || k.match?(/::#{@benchmark}::/)
92
+ end
93
+ end
94
+ end
95
+
96
+ def filter_mapping_data_array_by_profile!(data_array)
97
+ return unless @profile
98
+
99
+ data_array.reject! { |d| nested_hash_value(d, @profile).nil? }
100
+ end
101
+
102
+ def filter_mapping_data_array_by_level!(data_array)
103
+ return unless @level
104
+
105
+ data_array.reject! { |d| nested_hash_value(d, @level).nil? }
106
+ end
107
+
108
+ def nested_hash_value(obj, key)
109
+ if obj.respond_to?(:key?) && obj.key?(key)
110
+ obj[key]
111
+ elsif obj.respond_to?(:each)
112
+ r = nil
113
+ obj.find { |*a| r = nested_hash_value(a.last, key) }
114
+ r
115
+ end
116
+ end
117
+
118
+ def filter_stig_mapping_data(data_array); end
119
+
120
+ def fact_array_for(*keys)
121
+ keys.each_with_object([]) { |(k, _), a| a << fact_filter_value(k) }.compact
122
+ end
123
+
124
+ def fact_filter_value(key)
125
+ value = instance_variable_get("@#{key}")
126
+ return if value.nil? || value.empty?
127
+
128
+ [KEY_FACT_MAP[key], value]
129
+ end
130
+ end
131
+
132
+ class OldReport
133
+ def initialize(benchmarks)
134
+ @benchmarks = benchmarks
135
+ end
136
+
137
+ def self.generate
138
+ coverage = {}
139
+ coverage['classes'] = {}
140
+ all_cap = ClassUtils.find_all_classes_and_paths(puppet_class_dir)
141
+ invalid_classes = find_invalid_classes(all_cap)
142
+ valid_classes = find_valid_classes(all_cap, invalid_classes)
143
+ coverage['classes']['invalid'] = invalid_classes
144
+ coverage['classes']['valid'] = valid_classes
145
+ hiera = YAML.safe_load(File.open(hiera_path))
146
+ profile&.gsub!(/^profile_/, '') unless profile.nil?
147
+
148
+ matcher = profile.nil? ? /^profile_/ : /^profile_#{profile}/
149
+ hiera.each do |k, v|
150
+ key_base = k.split('::')[-1]
151
+ coverage['benchmark'] = v if key_base == 'title'
152
+ next unless key_base.match?(matcher)
153
+
154
+ coverage[key_base] = generate_uncovered_data(v, valid_classes)
155
+ end
156
+ coverage
157
+ end
158
+
159
+ def self.generate_uncovered_data(ctrl_list, valid_classes)
160
+ out_hash = {}
161
+ out_hash[:num_total] = ctrl_list.length
162
+ out_hash[:uncovered] = []
163
+ out_hash[:covered] = []
164
+ ctrl_list.each do |c|
165
+ if valid_classes.include?(c)
166
+ out_hash[:covered] << c
167
+ else
168
+ out_hash[:uncovered] << c
169
+ end
170
+ end
171
+ out_hash[:num_covered] = out_hash[:covered].length
172
+ out_hash[:num_uncovered] = out_hash[:uncovered].length
173
+ out_hash[:coverage] = Float(
174
+ (Float(out_hash[:num_covered]) / Float(out_hash[:num_total])) * 100.0
175
+ ).floor(3)
176
+ out_hash
177
+ end
178
+
179
+ def self.find_valid_classes(all_cap, invalid_classes)
180
+ all_classes = all_cap.dup.transpose[0]
181
+ return [] if all_classes.nil?
182
+
183
+ return all_classes - invalid_classes unless invalid_classes.nil?
184
+
185
+ all_classes
186
+ end
187
+
188
+ def self.find_invalid_classes(all_cap)
189
+ invalid_classes = []
190
+ all_cap.each do |cap|
191
+ invalid_classes << cap[0] unless class_valid?(cap[1])
192
+ end
193
+ invalid_classes
194
+ end
195
+
196
+ def self.class_valid?(manifest_path)
197
+ compiler = Puppet::Pal::Compiler.new(nil)
198
+ ast = compiler.parse_file(manifest_path)
199
+ ast.body.body.statements.each do |s|
200
+ next unless s.respond_to?(:arguments)
201
+ next unless s.arguments.respond_to?(:each)
202
+
203
+ s.arguments.each do |i|
204
+ return false if i.value == 'Not implemented'
205
+ end
206
+ end
207
+ true
208
+ end
209
+ end
210
+
211
+ # Class manages organizing report data into various output formats
212
+ class ReportOutput
213
+ attr_reader :controls_in_resource_data, :rules_in_map, :timestamp,
214
+ :title
215
+
216
+ def initialize(benchmark, controls_in_resource_data, rules_in_map)
217
+ @benchmark = benchmark
218
+ @controls_in_resource_data = controls_in_resource_data
219
+ @rules_in_map = rules_in_map
220
+ @timestamp = DateTime.now.iso8601
221
+ @title = "Coverage Report for #{@benchmark.title_key}"
222
+ end
223
+
224
+ def uncovered
225
+ @uncovered ||= rules_in_map - controls_in_resource_data
226
+ end
227
+
228
+ def uncovered_count
229
+ @uncovered_count ||= uncovered.length
230
+ end
231
+
232
+ def covered
233
+ @covered ||= rules_in_map - uncovered
234
+ end
235
+
236
+ def covered_count
237
+ @covered_count ||= covered.length
238
+ end
239
+
240
+ def total_count
241
+ @total_count ||= rules_in_map.length
242
+ end
243
+
244
+ def percentage
245
+ @percentage ||= covered_count.to_f / total_count
246
+ end
247
+
248
+ def to_h
249
+ {
250
+ title: title,
251
+ timestamp: timestamp,
252
+ benchmark: benchmark_hash,
253
+ coverage: coverage_hash,
254
+ }
255
+ end
256
+
257
+ def to_json(opts = nil)
258
+ JSON.generate(to_h, opts)
259
+ end
260
+
261
+ def to_yaml
262
+ to_h.to_yaml
263
+ end
264
+
265
+ def benchmark_hash
266
+ {
267
+ title: @benchmark.title,
268
+ version: @benchmark.version,
269
+ framework: @benchmark.framework,
270
+ }
271
+ end
272
+
273
+ def coverage_hash
274
+ {
275
+ total_count: total_count,
276
+ uncovered_count: uncovered_count,
277
+ uncovered: uncovered,
278
+ covered_count: covered_count,
279
+ covered: covered,
280
+ percentage: percentage,
281
+ controls_in_resource_data: controls_in_resource_data,
282
+ rules_in_map: rules_in_map,
283
+ }
284
+ end
285
+ end
286
+
287
+ # Creates ReportOutput objects based on the given Benchmark
288
+ class BenchmarkReport
289
+ def initialize(benchmark)
290
+ @benchmark = benchmark
291
+ end
292
+
293
+ def controls_in_resource_data
294
+ @controls_in_resource_data ||= find_controls_in_resource_data
295
+ end
296
+
297
+ def controls_in_mapping_data
298
+ @controls_in_mapping_data ||= find_controls_in_mapping_data
299
+ end
300
+
301
+ def basic_coverage(level: nil, profile: nil)
302
+ map_type = @benchmark.map_type(controls_in_resource_data[0])
303
+ rules_in_map = @benchmark.rules_in_map(map_type, level: level, profile: profile)
304
+ AbideDevUtils::CEM::CoverageReport::ReportOutput.new(@benchmark, controls_in_resource_data, rules_in_map)
305
+ end
306
+
307
+ private
308
+
309
+ def find_controls_in_resource_data
310
+ controls = @benchmark.resource_data["#{@benchmark.module_name}::resources"].each_with_object([]) do |(rname, rval), arr|
311
+ arr << case rval['controls'].class.to_s
312
+ when 'Hash'
313
+ rval['controls'].keys
314
+ when 'Array'
315
+ rval['controls']
316
+ else
317
+ raise "Invalid controls type: #{rval['controls'].class}"
318
+ end
319
+ end
320
+ controls.flatten.uniq.select do |c|
321
+ case @benchmark.framework
322
+ when 'cis'
323
+ @benchmark.map_type(c) != 'vulnid'
324
+ when 'stig'
325
+ @benchmark.map_type(c) == 'vulnid'
326
+ else
327
+ raise "Cannot find controls for framework #{@benchmark.framework}"
328
+ end
329
+ end
330
+ end
331
+
332
+ def find_controls_in_mapping_data
333
+ controls = @benchmark.map_data[0].each_with_object([]) do |(_, mapping), arr|
334
+ mapping.each do |level, profs|
335
+ next if level == 'benchmark'
336
+
337
+ profs.each do |_, ctrls|
338
+ arr << ctrls.keys
339
+ arr << ctrls.values
340
+ end
341
+ end
342
+ end
343
+ controls.flatten.uniq
344
+ end
345
+ end
346
+ end
347
+ end
348
+ end