abide_dev_utils 0.9.7 → 0.11.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.
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