abide_dev_utils 0.10.1 → 0.11.0

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