abide_dev_utils 0.10.1 → 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.
@@ -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
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'yaml'
5
+ require 'abide_dev_utils/markdown'
6
+ require 'abide_dev_utils/ppt'
7
+ require 'abide_dev_utils/cem/benchmark'
8
+
9
+ module AbideDevUtils
10
+ module CEM
11
+ module Generate
12
+ # Holds objects and methods for generating a reference doc
13
+ module Reference
14
+ MAPPING_PATH_KEY = 'Mapping Data'
15
+ RESOURCE_DATA_PATH_KEY = 'Resource Data'
16
+
17
+ def self.generate(data = {})
18
+ pupmod = AbideDevUtils::Ppt::PuppetModule.new
19
+ doc_title = case pupmod.name
20
+ when 'puppetlabs-cem_linux'
21
+ 'CEM Linux Reference'
22
+ when 'puppetlabs-cem_windows'
23
+ 'CEM Windows Reference'
24
+ else
25
+ 'Reference'
26
+ end
27
+ benchmarks = AbideDevUtils::CEM::Benchmark.benchmarks_from_puppet_module(pupmod)
28
+ case data.fetch(:format, 'markdown')
29
+ when 'markdown'
30
+ MarkdownGenerator.new(benchmarks).generate(doc_title)
31
+ else
32
+ raise "Format #{data[:format]} is unsupported! Only `markdown` format supported"
33
+ end
34
+ end
35
+
36
+ def self.generate_markdown
37
+ AbideDevUtils::Markdown.new('REFERENCE.md').generate
38
+ end
39
+
40
+ def self.config_example(control, params_array)
41
+ out_str = ['cem_windows::config:', ' control_configs:', " \"#{control}\":"]
42
+ indent = ' '
43
+ params_array.each do |param_hash|
44
+ val = case param_hash[:type]
45
+ when 'String'
46
+ "'#{param_hash[:default]}'"
47
+ else
48
+ param_hash[:default]
49
+ end
50
+
51
+ out_str << "#{indent}#{param_hash[:name]}: #{val}"
52
+ end
53
+ out_str.join("\n")
54
+ end
55
+
56
+ # Generates a markdown reference doc
57
+ class MarkdownGenerator
58
+ def initialize(benchmarks)
59
+ @benchmarks = benchmarks
60
+ @md = AbideDevUtils::Markdown.new('REFERENCE.md')
61
+ end
62
+
63
+ def generate(doc_title = 'Reference')
64
+ md.add_title(doc_title)
65
+ benchmarks.each do |benchmark|
66
+ md.add_h1(benchmark.title_key)
67
+ benchmark.rules.each do |title, rule|
68
+ md.add_h2("#{rule['number']} #{title}")
69
+ md.add_ul('Parameters:')
70
+ rule['params'].each do |p|
71
+ md.add_ul("#{md.code(p[:name])} - [ #{md.code(p[:type])} ] - #{md.italic('Default:')} #{md.code(p[:default])}", indent: 1)
72
+ end
73
+ md.add_ul('Config Example:')
74
+ example = config_example(benchmark.module_name, title, rule['params'])
75
+ md.add_code_block(example, language: 'yaml')
76
+ md.add_ul('Supported Levels:')
77
+ rule['level'].each do |l|
78
+ md.add_ul(md.code(l), indent: 1)
79
+ end
80
+ md.add_ul('Supported Profiles:')
81
+ rule['profile'].each do |l|
82
+ md.add_ul(md.code(l), indent: 1)
83
+ end
84
+ md.add_ul('Alternate Config IDs:')
85
+ rule['alternate_ids'].each do |l|
86
+ md.add_ul(md.code(l), indent: 1)
87
+ end
88
+ md.add_ul("Resource: #{md.code(rule['resource'].capitalize)}")
89
+ end
90
+ end
91
+ md.to_file
92
+ end
93
+
94
+ private
95
+
96
+ attr_reader :benchmarks, :md
97
+
98
+ def config_example(module_name, control, params_array)
99
+ out_str = ["#{module_name}::config:", ' control_configs:', " \"#{control}\":"]
100
+ indent = ' '
101
+ params_array.each do |param_hash|
102
+ val = case param_hash[:type]
103
+ when 'String'
104
+ "'#{param_hash[:default]}'"
105
+ else
106
+ param_hash[:default]
107
+ end
108
+ out_str << "#{indent}#{param_hash[:name]}: #{val}"
109
+ end
110
+ out_str.join("\n")
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AbideDevUtils
4
+ module CEM
5
+ # Holds objects and methods for `abide cem generate` subcommands
6
+ module Generate
7
+ require 'abide_dev_utils/cem/generate/reference'
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AbideDevUtils
4
+ module CEM
5
+ module Mapping
6
+ # Handles interacting with mapping data
7
+ class Mapper
8
+ MAP_TYPES = %w[hiera_title_num number hiera_title vulnid title].freeze
9
+
10
+ attr_reader :module_name, :framework, :map_data
11
+
12
+ def initialize(module_name, framework, map_data)
13
+ @module_name = module_name
14
+ @framework = framework
15
+ load_framework(@framework)
16
+ @map_data = map_data
17
+ @cache = {}
18
+ @rule_cache = {}
19
+ end
20
+
21
+ def title
22
+ @title ||= benchmark_data['title']
23
+ end
24
+
25
+ def version
26
+ @version ||= benchmark_data['version']
27
+ end
28
+
29
+ def each_like(identifier)
30
+ mtype, mtop = map_type_and_top_key(identifier)
31
+ map_data[mtype][mtop].each { |key, val| yield key, val }
32
+ end
33
+
34
+ def each_with_array_like(identifier)
35
+ mtype, mtop = map_type_and_top_key(identifier)
36
+ map_data[mtype][mtop].each_with_object([]) { |(key, val), ary| yield [key, val], ary }
37
+ end
38
+
39
+ def get(control_id, level: nil, profile: nil)
40
+ return cache_get(control_id, level, profile) if cached?(control_id, level, profile)
41
+
42
+ value = get_map(control_id, level: level, profile: profile)
43
+ return if value.nil? || value.empty?
44
+
45
+ cache_set(value, control_id, level, profile)
46
+ value
47
+ end
48
+
49
+ def map_type(control_id)
50
+ return control_id if MAP_TYPES.include?(control_id)
51
+
52
+ case control_id
53
+ when %r{^c[0-9_]+$}
54
+ 'hiera_title_num'
55
+ when %r{^[0-9][0-9.]*$}
56
+ 'number'
57
+ when %r{^[a-z][a-z0-9_]+$}
58
+ 'hiera_title'
59
+ when %r{^V-[0-9]{6}$}
60
+ 'vulnid'
61
+ else
62
+ 'title'
63
+ end
64
+ end
65
+
66
+ private
67
+
68
+ def load_framework(framework)
69
+ case framework.downcase
70
+ when 'cis'
71
+ self.class.include AbideDevUtils::CEM::Mapping::MixinCIS
72
+ extend AbideDevUtils::CEM::Mapping::MixinCIS
73
+ when 'stig'
74
+ self.class.include AbideDevUtils::CEM::Mapping::MixinSTIG
75
+ extend AbideDevUtils::CEM::Mapping::MixinSTIG
76
+ else
77
+ raise "Invalid framework: #{framework}"
78
+ end
79
+ end
80
+
81
+ def map_type_and_top_key(identifier)
82
+ mtype = MAP_TYPES.include?(identifier) ? identifier : map_type(identifier)
83
+ [mtype, map_top_key(mtype)]
84
+ end
85
+
86
+ def cached?(control_id, *args)
87
+ @cache.key?(cache_key(control_id, *args))
88
+ end
89
+
90
+ def cache_get(control_id, *args)
91
+ ckey = cache_key(control_id, *args)
92
+ @cache[ckey] if cached?(control_id, *args)
93
+ end
94
+
95
+ def cache_set(value, control_id, *args)
96
+ @cache[cache_key(control_id, *args)] = value unless value.nil?
97
+ end
98
+
99
+ def default_map_type
100
+ @default_map_type ||= (framework == 'stig' ? 'vulnid' : map_data.keys.first)
101
+ end
102
+
103
+ def benchmark_data
104
+ @benchmark_data ||= map_data[default_map_type][map_top_key(default_map_type)]['benchmark']
105
+ end
106
+
107
+ def cache_key(control_id, *args)
108
+ args.unshift(control_id).compact.join('-')
109
+ end
110
+
111
+ def map_top_key(mtype)
112
+ [module_name, 'mappings', framework, mtype].join('::')
113
+ end
114
+ end
115
+
116
+ # Mixin module used by Mapper to implement CIS-specific mapping behavior
117
+ module MixinCIS
118
+ def get_map(control_id, level: nil, profile: nil, **_)
119
+ mtype, mtop = map_type_and_top_key(control_id)
120
+ return if mtype == 'vulnid'
121
+
122
+ return map_data[mtype][mtop][level][profile][control_id] unless level.nil? || profile.nil?
123
+
124
+ map_data[mtype][mtop].each do |lvl, profile_hash|
125
+ next if lvl == 'benchmark'
126
+
127
+ profile_hash.each do |prof, control_hash|
128
+ return map_data[mtype][mtop][lvl][prof][control_id] if control_hash.key?(control_id)
129
+ end
130
+ end
131
+ end
132
+ end
133
+
134
+ # Mixin module used by Mapper to implement STIG-specific mapping behavior
135
+ module MixinSTIG
136
+ def get_map(control_id, level: nil, **_)
137
+ mtype, mtop = map_type_and_top_key(control_id)
138
+ return map_data[mtype][mtop][level][control_id] unless level.nil?
139
+
140
+ begin
141
+ map_data[mtype][mtop].each do |lvl, control_hash|
142
+ next if lvl == 'benchmark'
143
+
144
+ return control_hash[control_id] if control_hash.key?(control_id)
145
+ end
146
+ rescue NoMethodError => e
147
+ require 'pry'
148
+ binding.pry
149
+ #raise "Control ID: #{control_id}, Level: #{level}, #{e.message}"
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'abide_dev_utils/xccdf'
4
+ require 'abide_dev_utils/cem/coverage_report'
5
+ require 'abide_dev_utils/cem/generate'
4
6
 
5
7
  module AbideDevUtils
6
8
  # Methods for working with Compliance Enforcement Modules (CEM)