abide_dev_utils 0.11.0 → 0.11.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -14
  3. data/lib/abide_dev_utils/cem/benchmark.rb +330 -136
  4. data/lib/abide_dev_utils/cem/generate/coverage_report.rb +380 -0
  5. data/lib/abide_dev_utils/cem/generate/reference.rb +157 -33
  6. data/lib/abide_dev_utils/cem/generate.rb +5 -4
  7. data/lib/abide_dev_utils/cem/hiera_data/mapping_data/map_data.rb +110 -0
  8. data/lib/abide_dev_utils/cem/hiera_data/mapping_data/mixins.rb +46 -0
  9. data/lib/abide_dev_utils/cem/hiera_data/mapping_data.rb +146 -0
  10. data/lib/abide_dev_utils/cem/hiera_data/resource_data/control.rb +127 -0
  11. data/lib/abide_dev_utils/cem/hiera_data/resource_data/parameters.rb +90 -0
  12. data/lib/abide_dev_utils/cem/hiera_data/resource_data/resource.rb +102 -0
  13. data/lib/abide_dev_utils/cem/hiera_data/resource_data.rb +310 -0
  14. data/lib/abide_dev_utils/cem/hiera_data.rb +7 -0
  15. data/lib/abide_dev_utils/cem/mapping/mapper.rb +161 -34
  16. data/lib/abide_dev_utils/cem/validate/resource_data.rb +33 -0
  17. data/lib/abide_dev_utils/cem/validate.rb +10 -0
  18. data/lib/abide_dev_utils/cem.rb +0 -1
  19. data/lib/abide_dev_utils/cli/cem.rb +20 -2
  20. data/lib/abide_dev_utils/dot_number_comparable.rb +75 -0
  21. data/lib/abide_dev_utils/errors/cem.rb +10 -0
  22. data/lib/abide_dev_utils/ppt/class_utils.rb +1 -1
  23. data/lib/abide_dev_utils/ppt/code_gen/data_types.rb +51 -0
  24. data/lib/abide_dev_utils/ppt/code_gen/generate.rb +15 -0
  25. data/lib/abide_dev_utils/ppt/code_gen/resource.rb +59 -0
  26. data/lib/abide_dev_utils/ppt/code_gen/resource_types/base.rb +93 -0
  27. data/lib/abide_dev_utils/ppt/code_gen/resource_types/class.rb +17 -0
  28. data/lib/abide_dev_utils/ppt/code_gen/resource_types/manifest.rb +16 -0
  29. data/lib/abide_dev_utils/ppt/code_gen/resource_types/parameter.rb +16 -0
  30. data/lib/abide_dev_utils/ppt/code_gen/resource_types/strings.rb +13 -0
  31. data/lib/abide_dev_utils/ppt/code_gen/resource_types.rb +6 -0
  32. data/lib/abide_dev_utils/ppt/code_gen.rb +15 -0
  33. data/lib/abide_dev_utils/ppt/code_introspection.rb +102 -0
  34. data/lib/abide_dev_utils/ppt/hiera.rb +4 -1
  35. data/lib/abide_dev_utils/ppt/puppet_module.rb +2 -1
  36. data/lib/abide_dev_utils/ppt.rb +3 -0
  37. data/lib/abide_dev_utils/version.rb +1 -1
  38. data/lib/abide_dev_utils/xccdf/parser/helpers.rb +146 -0
  39. data/lib/abide_dev_utils/xccdf/parser/objects.rb +87 -144
  40. data/lib/abide_dev_utils/xccdf/parser.rb +5 -0
  41. data/lib/abide_dev_utils/xccdf/utils.rb +89 -0
  42. data/lib/abide_dev_utils/xccdf.rb +3 -0
  43. metadata +27 -3
  44. data/lib/abide_dev_utils/cem/coverage_report.rb +0 -348
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'abide_dev_utils/validate'
4
+
5
+ module AbideDevUtils
6
+ module XCCDF
7
+ module Utils
8
+ # Class for working with directories that contain XCCDF files
9
+ class FileDir
10
+ CIS_FILE_NAME_PARTS_PATTERN = /^CIS_(?<subject>[A-Za-z0-9._()-]+)_Benchmark_v(?<version>[0-9.]+)-xccdf$/.freeze
11
+ def initialize(path)
12
+ @path = File.expand_path(path)
13
+ AbideDevUtils::Validate.directory(@path)
14
+ end
15
+
16
+ def files
17
+ @files ||= Dir.glob(File.join(@path, '*-xccdf.xml')).map { |f| FileNameData.new(f) }
18
+ end
19
+
20
+ def fuzzy_find(label, value)
21
+ files.find { |f| f.fuzzy_match?(label, value) }
22
+ end
23
+
24
+ def fuzzy_select(label, value)
25
+ files.select { |f| f.fuzzy_match?(label, value) }
26
+ end
27
+
28
+ def fuzzy_reject(label, value)
29
+ files.reject { |f| f.fuzzy_match?(label, value) }
30
+ end
31
+
32
+ def label?(label)
33
+ files.select { |f| f.has?(label) }
34
+ end
35
+
36
+ def no_label?(label)
37
+ files.reject { |f| f.has?(label) }
38
+ end
39
+ end
40
+
41
+ # Parses XCCDF file names into labeled parts
42
+ class FileNameData
43
+ CIS_PATTERN = /^CIS_(?<subject>[A-Za-z0-9._()-]+?)(?<stig>_STIG)?_Benchmark_v(?<version>[0-9.]+)-xccdf$/.freeze
44
+
45
+ attr_reader :path, :name, :labeled_parts
46
+
47
+ def initialize(path)
48
+ @path = path
49
+ @name = File.basename(path, '.xml')
50
+ @labeled_parts = File.basename(name, '.xml').match(CIS_PATTERN)&.named_captures
51
+ end
52
+
53
+ def subject
54
+ @subject ||= labeled_parts&.fetch('subject', nil)
55
+ end
56
+
57
+ def stig
58
+ @stig ||= labeled_parts&.fetch('subject', nil)
59
+ end
60
+
61
+ def version
62
+ @version ||= labeled_parts&.fetch('version', nil)
63
+ end
64
+
65
+ def has?(label)
66
+ val = send(label.to_sym)
67
+ !val.nil? && !val.empty?
68
+ end
69
+
70
+ def fuzzy_match?(label, value)
71
+ return false unless has?(label)
72
+
73
+ this_val = normalize_char_array(send(label.to_sym).chars)
74
+ other_val = normalize_char_array(value.chars)
75
+ other_val.each_with_index do |c, idx|
76
+ return false unless this_val[idx] == c
77
+ end
78
+ true
79
+ end
80
+
81
+ private
82
+
83
+ def normalize_char_array(char_array)
84
+ char_array.grep_v(/[^A-Za-z0-9]/).map(&:downcase)[3..]
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -70,6 +70,7 @@ module AbideDevUtils
70
70
  CIS_LEVEL_CODE = /(?:_|^)([Ll]evel_[0-9]|[Ll]1|[Ll]2|[NnBb][GgLl]|#{CIS_NEXT_GEN_WINDOWS})/.freeze
71
71
  CIS_CONTROL_PARTS = /#{CIS_CONTROL_NUMBER}#{CIS_LEVEL_CODE}?_+([A-Za-z].*)/.freeze
72
72
  CIS_PROFILE_PARTS = /#{CIS_LEVEL_CODE}[_-]+([A-Za-z].*)/.freeze
73
+ STIG_PROFILE_PARTS = /(STIG)/.freeze
73
74
 
74
75
  def xpath(path)
75
76
  @xml.xpath(path)
@@ -119,6 +120,8 @@ module AbideDevUtils
119
120
  end
120
121
 
121
122
  def profile_parts(profile)
123
+ return ['STIG', ''] if profile == 'STIG'
124
+
122
125
  parts = control_profile_text(profile).match(CIS_PROFILE_PARTS)
123
126
  raise AbideDevUtils::Errors::ProfilePartsError, profile if parts.nil?
124
127
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: abide_dev_utils
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.0
4
+ version: 0.11.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - abide-team
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-07-25 00:00:00.000000000 Z
11
+ date: 2022-08-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nokogiri
@@ -360,10 +360,20 @@ files:
360
360
  - lib/abide_dev_utils.rb
361
361
  - lib/abide_dev_utils/cem.rb
362
362
  - lib/abide_dev_utils/cem/benchmark.rb
363
- - lib/abide_dev_utils/cem/coverage_report.rb
364
363
  - lib/abide_dev_utils/cem/generate.rb
364
+ - lib/abide_dev_utils/cem/generate/coverage_report.rb
365
365
  - lib/abide_dev_utils/cem/generate/reference.rb
366
+ - lib/abide_dev_utils/cem/hiera_data.rb
367
+ - lib/abide_dev_utils/cem/hiera_data/mapping_data.rb
368
+ - lib/abide_dev_utils/cem/hiera_data/mapping_data/map_data.rb
369
+ - lib/abide_dev_utils/cem/hiera_data/mapping_data/mixins.rb
370
+ - lib/abide_dev_utils/cem/hiera_data/resource_data.rb
371
+ - lib/abide_dev_utils/cem/hiera_data/resource_data/control.rb
372
+ - lib/abide_dev_utils/cem/hiera_data/resource_data/parameters.rb
373
+ - lib/abide_dev_utils/cem/hiera_data/resource_data/resource.rb
366
374
  - lib/abide_dev_utils/cem/mapping/mapper.rb
375
+ - lib/abide_dev_utils/cem/validate.rb
376
+ - lib/abide_dev_utils/cem/validate/resource_data.rb
367
377
  - lib/abide_dev_utils/cli.rb
368
378
  - lib/abide_dev_utils/cli/abstract.rb
369
379
  - lib/abide_dev_utils/cli/cem.rb
@@ -375,6 +385,7 @@ files:
375
385
  - lib/abide_dev_utils/comply.rb
376
386
  - lib/abide_dev_utils/config.rb
377
387
  - lib/abide_dev_utils/constants.rb
388
+ - lib/abide_dev_utils/dot_number_comparable.rb
378
389
  - lib/abide_dev_utils/errors.rb
379
390
  - lib/abide_dev_utils/errors/base.rb
380
391
  - lib/abide_dev_utils/errors/cem.rb
@@ -393,6 +404,17 @@ files:
393
404
  - lib/abide_dev_utils/ppt.rb
394
405
  - lib/abide_dev_utils/ppt/api.rb
395
406
  - lib/abide_dev_utils/ppt/class_utils.rb
407
+ - lib/abide_dev_utils/ppt/code_gen.rb
408
+ - lib/abide_dev_utils/ppt/code_gen/data_types.rb
409
+ - lib/abide_dev_utils/ppt/code_gen/generate.rb
410
+ - lib/abide_dev_utils/ppt/code_gen/resource.rb
411
+ - lib/abide_dev_utils/ppt/code_gen/resource_types.rb
412
+ - lib/abide_dev_utils/ppt/code_gen/resource_types/base.rb
413
+ - lib/abide_dev_utils/ppt/code_gen/resource_types/class.rb
414
+ - lib/abide_dev_utils/ppt/code_gen/resource_types/manifest.rb
415
+ - lib/abide_dev_utils/ppt/code_gen/resource_types/parameter.rb
416
+ - lib/abide_dev_utils/ppt/code_gen/resource_types/strings.rb
417
+ - lib/abide_dev_utils/ppt/code_introspection.rb
396
418
  - lib/abide_dev_utils/ppt/facter_utils.rb
397
419
  - lib/abide_dev_utils/ppt/hiera.rb
398
420
  - lib/abide_dev_utils/ppt/new_obj.rb
@@ -411,9 +433,11 @@ files:
411
433
  - lib/abide_dev_utils/xccdf/diff/benchmark/property_existence.rb
412
434
  - lib/abide_dev_utils/xccdf/diff/utils.rb
413
435
  - lib/abide_dev_utils/xccdf/parser.rb
436
+ - lib/abide_dev_utils/xccdf/parser/helpers.rb
414
437
  - lib/abide_dev_utils/xccdf/parser/objects.rb
415
438
  - lib/abide_dev_utils/xccdf/parser/objects/digest_object.rb
416
439
  - lib/abide_dev_utils/xccdf/parser/objects/numbered_object.rb
440
+ - lib/abide_dev_utils/xccdf/utils.rb
417
441
  - new_diff.rb
418
442
  homepage: https://github.com/puppetlabs/abide_dev_utils
419
443
  licenses:
@@ -1,348 +0,0 @@
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