abide_dev_utils 0.10.1 → 0.11.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +7 -1
  4. data/Gemfile.lock +25 -19
  5. data/Rakefile +28 -0
  6. data/abide_dev_utils.gemspec +1 -0
  7. data/lib/abide_dev_utils/cem/benchmark.rb +490 -0
  8. data/lib/abide_dev_utils/cem/generate/coverage_report.rb +380 -0
  9. data/lib/abide_dev_utils/cem/generate/reference.rb +319 -0
  10. data/lib/abide_dev_utils/cem/generate.rb +11 -0
  11. data/lib/abide_dev_utils/cem/hiera_data/mapping_data/map_data.rb +110 -0
  12. data/lib/abide_dev_utils/cem/hiera_data/mapping_data/mixins.rb +46 -0
  13. data/lib/abide_dev_utils/cem/hiera_data/mapping_data.rb +146 -0
  14. data/lib/abide_dev_utils/cem/hiera_data/resource_data/control.rb +127 -0
  15. data/lib/abide_dev_utils/cem/hiera_data/resource_data/parameters.rb +90 -0
  16. data/lib/abide_dev_utils/cem/hiera_data/resource_data/resource.rb +102 -0
  17. data/lib/abide_dev_utils/cem/hiera_data/resource_data.rb +310 -0
  18. data/lib/abide_dev_utils/cem/hiera_data.rb +7 -0
  19. data/lib/abide_dev_utils/cem/mapping/mapper.rb +282 -0
  20. data/lib/abide_dev_utils/cem/validate/resource_data.rb +33 -0
  21. data/lib/abide_dev_utils/cem/validate.rb +10 -0
  22. data/lib/abide_dev_utils/cem.rb +1 -0
  23. data/lib/abide_dev_utils/cli/cem.rb +98 -0
  24. data/lib/abide_dev_utils/dot_number_comparable.rb +75 -0
  25. data/lib/abide_dev_utils/errors/cem.rb +32 -0
  26. data/lib/abide_dev_utils/errors/general.rb +8 -2
  27. data/lib/abide_dev_utils/errors/ppt.rb +4 -0
  28. data/lib/abide_dev_utils/errors.rb +6 -0
  29. data/lib/abide_dev_utils/markdown.rb +104 -0
  30. data/lib/abide_dev_utils/ppt/class_utils.rb +1 -1
  31. data/lib/abide_dev_utils/ppt/code_gen/data_types.rb +64 -0
  32. data/lib/abide_dev_utils/ppt/code_gen/generate.rb +15 -0
  33. data/lib/abide_dev_utils/ppt/code_gen/resource.rb +59 -0
  34. data/lib/abide_dev_utils/ppt/code_gen/resource_types/base.rb +93 -0
  35. data/lib/abide_dev_utils/ppt/code_gen/resource_types/class.rb +17 -0
  36. data/lib/abide_dev_utils/ppt/code_gen/resource_types/manifest.rb +16 -0
  37. data/lib/abide_dev_utils/ppt/code_gen/resource_types/parameter.rb +16 -0
  38. data/lib/abide_dev_utils/ppt/code_gen/resource_types/strings.rb +13 -0
  39. data/lib/abide_dev_utils/ppt/code_gen/resource_types.rb +6 -0
  40. data/lib/abide_dev_utils/ppt/code_gen.rb +15 -0
  41. data/lib/abide_dev_utils/ppt/code_introspection.rb +102 -0
  42. data/lib/abide_dev_utils/ppt/facter_utils.rb +140 -0
  43. data/lib/abide_dev_utils/ppt/hiera.rb +300 -0
  44. data/lib/abide_dev_utils/ppt/puppet_module.rb +75 -0
  45. data/lib/abide_dev_utils/ppt.rb +6 -5
  46. data/lib/abide_dev_utils/validate.rb +14 -0
  47. data/lib/abide_dev_utils/version.rb +1 -1
  48. data/lib/abide_dev_utils/xccdf/parser/helpers.rb +146 -0
  49. data/lib/abide_dev_utils/xccdf/parser/objects.rb +87 -144
  50. data/lib/abide_dev_utils/xccdf/parser.rb +5 -0
  51. data/lib/abide_dev_utils/xccdf/utils.rb +89 -0
  52. data/lib/abide_dev_utils/xccdf.rb +3 -0
  53. metadata +50 -3
  54. data/lib/abide_dev_utils/ppt/coverage.rb +0 -86
@@ -0,0 +1,380 @@
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
+ module Generate
14
+ # Methods and objects used to construct a report of what CEM enforces versus what
15
+ # the various compliance frameworks expect to be enforced.
16
+ module CoverageReport
17
+ def self.generate(format_func: :to_h, opts: {})
18
+ opts = AbideDevUtils::CEM::CoverageReport::ReportOptions.new(opts)
19
+ pupmod = AbideDevUtils::Ppt::PuppetModule.new
20
+ benchmarks = AbideDevUtils::CEM::Benchmark.benchmarks_from_puppet_module(pupmod,
21
+ ignore_all_errors: opts.ignore_all_errors)
22
+ benchmarks.map do |b|
23
+ AbideDevUtils::CEM::CoverageReport::BenchmarkReport.new(b)
24
+ .send(report_type, **{ profile: profile, level: level })
25
+ .send(format_func)
26
+ end
27
+ end
28
+
29
+ class ReportOptions
30
+ DEFAULTS = {
31
+ benchmark: nil,
32
+ profile: nil,
33
+ level: nil,
34
+ format_func: :to_h,
35
+ ignore_all_errors: false,
36
+ xccdf_dir: nil,
37
+ }.freeze
38
+
39
+ attr_reader(*DEFAULTS.keys)
40
+
41
+ def initialize(opts = {})
42
+ @opts = DEFAULTS.merge(opts)
43
+ DEFAULTS.each_key do |k|
44
+ instance_variable_set "@#{k}", @opts[k]
45
+ end
46
+ end
47
+
48
+ def report_type
49
+ @report_type ||= (xccdf_dir.nil? ? :basic_coverage : :correlated_coverage)
50
+ end
51
+ end
52
+
53
+ class Filter
54
+ KEY_FACT_MAP = {
55
+ os_family: 'os.family',
56
+ os_name: 'os.name',
57
+ os_release_major: 'os.release.major',
58
+ }.freeze
59
+
60
+ attr_reader(*KEY_FACT_MAP.keys)
61
+
62
+ def initialize(pupmod, **filters)
63
+ @pupmod = pupmod
64
+ @benchmark = filters[:benchmark]
65
+ @profile = filters[:profile]
66
+ @level = filters[:level]
67
+ KEY_FACT_MAP.each_key do |k|
68
+ instance_variable_set "@#{k}", filters[k]
69
+ end
70
+ end
71
+
72
+ def resource_data
73
+ @resource_data ||= find_resource_data
74
+ end
75
+
76
+ def mapping_data
77
+ @mapping_data ||= find_mapping_data
78
+ end
79
+
80
+ private
81
+
82
+ def find_resource_data
83
+ fact_array = fact_array_for(:os_family, :os_name, :os_release_major)
84
+ @pupmod.hiera_conf.local_hiera_files_with_facts(*fact_array, hierarchy_name: 'Resource Data').map do |f|
85
+ YAML.load_file(f.path)
86
+ end
87
+ rescue NoMethodError
88
+ @pupmod.hiera_conf.local_hiera_files(hierarchy_name: 'Resource Data').map { |f| YAML.load_file(f.path) }
89
+ end
90
+
91
+ def find_mapping_data
92
+ fact_array = fact_array_for(:os_name, :os_release_major)
93
+ begin
94
+ data_array = @pupmod.hiera_conf.local_hiera_files_with_facts(*fact_array, hierarchy_name: 'Mapping Data').map do |f|
95
+ YAML.load_file(f.path)
96
+ end
97
+ rescue NoMethodError
98
+ data_array = @pupmod.hiera_conf.local_hiera_files(hierarchy_name: 'Mapping Data').map { |f| YAML.load_file(f.path) }
99
+ end
100
+ filter_mapping_data_array_by_benchmark!(data_array)
101
+ filter_mapping_data_array_by_profile!(data_array)
102
+ filter_mapping_data_array_by_level!(data_array)
103
+ data_array
104
+ end
105
+
106
+ def filter_mapping_data_array_by_benchmark!(data_array)
107
+ return unless @benchmark
108
+
109
+ data_array.select! do |d|
110
+ d.keys.all? do |k|
111
+ k == 'benchmark' || k.match?(/::#{@benchmark}::/)
112
+ end
113
+ end
114
+ end
115
+
116
+ def filter_mapping_data_array_by_profile!(data_array)
117
+ return unless @profile
118
+
119
+ data_array.reject! { |d| nested_hash_value(d, @profile).nil? }
120
+ end
121
+
122
+ def filter_mapping_data_array_by_level!(data_array)
123
+ return unless @level
124
+
125
+ data_array.reject! { |d| nested_hash_value(d, @level).nil? }
126
+ end
127
+
128
+ def nested_hash_value(obj, key)
129
+ if obj.respond_to?(:key?) && obj.key?(key)
130
+ obj[key]
131
+ elsif obj.respond_to?(:each)
132
+ r = nil
133
+ obj.find { |*a| r = nested_hash_value(a.last, key) }
134
+ r
135
+ end
136
+ end
137
+
138
+ def filter_stig_mapping_data(data_array); end
139
+
140
+ def fact_array_for(*keys)
141
+ keys.each_with_object([]) { |(k, _), a| a << fact_filter_value(k) }.compact
142
+ end
143
+
144
+ def fact_filter_value(key)
145
+ value = instance_variable_get("@#{key}")
146
+ return if value.nil? || value.empty?
147
+
148
+ [KEY_FACT_MAP[key], value]
149
+ end
150
+ end
151
+
152
+ class OldReport
153
+ def initialize(benchmarks)
154
+ @benchmarks = benchmarks
155
+ end
156
+
157
+ def self.generate
158
+ coverage = {}
159
+ coverage['classes'] = {}
160
+ all_cap = ClassUtils.find_all_classes_and_paths(puppet_class_dir)
161
+ invalid_classes = find_invalid_classes(all_cap)
162
+ valid_classes = find_valid_classes(all_cap, invalid_classes)
163
+ coverage['classes']['invalid'] = invalid_classes
164
+ coverage['classes']['valid'] = valid_classes
165
+ hiera = YAML.safe_load(File.open(hiera_path))
166
+ profile&.gsub!(/^profile_/, '') unless profile.nil?
167
+
168
+ matcher = profile.nil? ? /^profile_/ : /^profile_#{profile}/
169
+ hiera.each do |k, v|
170
+ key_base = k.split('::')[-1]
171
+ coverage['benchmark'] = v if key_base == 'title'
172
+ next unless key_base.match?(matcher)
173
+
174
+ coverage[key_base] = generate_uncovered_data(v, valid_classes)
175
+ end
176
+ coverage
177
+ end
178
+
179
+ def self.generate_uncovered_data(ctrl_list, valid_classes)
180
+ out_hash = {}
181
+ out_hash[:num_total] = ctrl_list.length
182
+ out_hash[:uncovered] = []
183
+ out_hash[:covered] = []
184
+ ctrl_list.each do |c|
185
+ if valid_classes.include?(c)
186
+ out_hash[:covered] << c
187
+ else
188
+ out_hash[:uncovered] << c
189
+ end
190
+ end
191
+ out_hash[:num_covered] = out_hash[:covered].length
192
+ out_hash[:num_uncovered] = out_hash[:uncovered].length
193
+ out_hash[:coverage] = Float(
194
+ (Float(out_hash[:num_covered]) / Float(out_hash[:num_total])) * 100.0
195
+ ).floor(3)
196
+ out_hash
197
+ end
198
+
199
+ def self.find_valid_classes(all_cap, invalid_classes)
200
+ all_classes = all_cap.dup.transpose[0]
201
+ return [] if all_classes.nil?
202
+
203
+ return all_classes - invalid_classes unless invalid_classes.nil?
204
+
205
+ all_classes
206
+ end
207
+
208
+ def self.find_invalid_classes(all_cap)
209
+ invalid_classes = []
210
+ all_cap.each do |cap|
211
+ invalid_classes << cap[0] unless class_valid?(cap[1])
212
+ end
213
+ invalid_classes
214
+ end
215
+
216
+ def self.class_valid?(manifest_path)
217
+ compiler = Puppet::Pal::Compiler.new(nil)
218
+ ast = compiler.parse_file(manifest_path)
219
+ ast.body.body.statements.each do |s|
220
+ next unless s.respond_to?(:arguments)
221
+ next unless s.arguments.respond_to?(:each)
222
+
223
+ s.arguments.each do |i|
224
+ return false if i.value == 'Not implemented'
225
+ end
226
+ end
227
+ true
228
+ end
229
+ end
230
+
231
+ # Class manages organizing report data into various output formats
232
+ class ReportOutput
233
+ attr_reader :controls_in_resource_data, :rules_in_map, :timestamp,
234
+ :title
235
+
236
+ def initialize(benchmark, controls_in_resource_data, rules_in_map)
237
+ @benchmark = benchmark
238
+ @controls_in_resource_data = controls_in_resource_data
239
+ @rules_in_map = rules_in_map
240
+ @timestamp = DateTime.now.iso8601
241
+ @title = "Coverage Report for #{@benchmark.title_key}"
242
+ end
243
+
244
+ def uncovered
245
+ @uncovered ||= rules_in_map - controls_in_resource_data
246
+ end
247
+
248
+ def uncovered_count
249
+ @uncovered_count ||= uncovered.length
250
+ end
251
+
252
+ def covered
253
+ @covered ||= rules_in_map - uncovered
254
+ end
255
+
256
+ def covered_count
257
+ @covered_count ||= covered.length
258
+ end
259
+
260
+ def total_count
261
+ @total_count ||= rules_in_map.length
262
+ end
263
+
264
+ def percentage
265
+ @percentage ||= covered_count.to_f / total_count
266
+ end
267
+
268
+ def to_h
269
+ {
270
+ title: title,
271
+ timestamp: timestamp,
272
+ benchmark: benchmark_hash,
273
+ coverage: coverage_hash,
274
+ }
275
+ end
276
+
277
+ def to_json(opts = nil)
278
+ JSON.generate(to_h, opts)
279
+ end
280
+
281
+ def to_yaml
282
+ to_h.to_yaml
283
+ end
284
+
285
+ def benchmark_hash
286
+ {
287
+ title: @benchmark.title,
288
+ version: @benchmark.version,
289
+ framework: @benchmark.framework,
290
+ }
291
+ end
292
+
293
+ def coverage_hash
294
+ {
295
+ total_count: total_count,
296
+ uncovered_count: uncovered_count,
297
+ uncovered: uncovered,
298
+ covered_count: covered_count,
299
+ covered: covered,
300
+ percentage: percentage,
301
+ controls_in_resource_data: controls_in_resource_data,
302
+ rules_in_map: rules_in_map,
303
+ }
304
+ end
305
+ end
306
+
307
+ # Creates ReportOutput objects based on the given Benchmark
308
+ class BenchmarkReport
309
+ def initialize(benchmark, opts = AbideDevUtils::CEM::CoverageReport::ReportOptions.new)
310
+ @benchmark = benchmark
311
+ @opts = opts
312
+ end
313
+
314
+ def controls_in_resource_data
315
+ @controls_in_resource_data ||= find_controls_in_resource_data
316
+ end
317
+
318
+ def controls_in_mapping_data
319
+ @controls_in_mapping_data ||= find_controls_in_mapping_data
320
+ end
321
+
322
+ def basic_coverage(level: @opts.level, profile: @opts.profile)
323
+ map_type = @benchmark.map_type(controls_in_resource_data[0])
324
+ rules_in_map = @benchmark.rules_in_map(map_type, level: level, profile: profile)
325
+ AbideDevUtils::CEM::CoverageReport::ReportOutput.new(@benchmark, controls_in_resource_data, rules_in_map)
326
+ end
327
+
328
+ # def correlated_coverage(level: @opts.level, profile: @opts.profile)
329
+ # correlation = ReportOutputCorrelation.new(basic_coverage(level: level, profile: profile))
330
+ # end
331
+
332
+ private
333
+
334
+ def find_controls_in_resource_data
335
+ controls = @benchmark.resource_data["#{@benchmark.module_name}::resources"].each_with_object([]) do |(rname, rval), arr|
336
+ arr << case rval['controls'].class.to_s
337
+ when 'Hash'
338
+ rval['controls'].keys
339
+ when 'Array'
340
+ rval['controls']
341
+ else
342
+ raise "Invalid controls type: #{rval['controls'].class}"
343
+ end
344
+ end
345
+ controls.flatten.uniq.select do |c|
346
+ case @benchmark.framework
347
+ when 'cis'
348
+ @benchmark.map_type(c) != 'vulnid'
349
+ when 'stig'
350
+ @benchmark.map_type(c) == 'vulnid'
351
+ else
352
+ raise "Cannot find controls for framework #{@benchmark.framework}"
353
+ end
354
+ end
355
+ end
356
+
357
+ def find_controls_in_mapping_data
358
+ controls = @benchmark.map_data[0].each_with_object([]) do |(_, mapping), arr|
359
+ mapping.each do |level, profs|
360
+ next if level == 'benchmark'
361
+
362
+ profs.each do |_, ctrls|
363
+ arr << ctrls.keys
364
+ arr << ctrls.values
365
+ end
366
+ end
367
+ end
368
+ controls.flatten.uniq
369
+ end
370
+ end
371
+
372
+ class ReportOutputCorrelation
373
+ def initialize(cov_rep)
374
+ @cov_rep = cov_rep
375
+ end
376
+ end
377
+ end
378
+ end
379
+ end
380
+ end
@@ -0,0 +1,319 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'shellwords'
5
+ require 'timeout'
6
+ require 'yaml'
7
+ require 'abide_dev_utils/markdown'
8
+ require 'abide_dev_utils/output'
9
+ require 'abide_dev_utils/ppt'
10
+ require 'abide_dev_utils/cem/benchmark'
11
+
12
+ module AbideDevUtils
13
+ module CEM
14
+ module Generate
15
+ # Holds objects and methods for generating a reference doc
16
+ module Reference
17
+ MAPPING_PATH_KEY = 'Mapping Data'
18
+ RESOURCE_DATA_PATH_KEY = 'Resource Data'
19
+
20
+ def self.generate(data = {})
21
+ pupmod = AbideDevUtils::Ppt::PuppetModule.new
22
+ doc_title = case pupmod.name
23
+ when 'puppetlabs-cem_linux'
24
+ 'CEM Linux Reference'
25
+ when 'puppetlabs-cem_windows'
26
+ 'CEM Windows Reference'
27
+ else
28
+ 'Reference'
29
+ end
30
+ benchmarks = AbideDevUtils::CEM::Benchmark.benchmarks_from_puppet_module(pupmod)
31
+ case data.fetch(:format, 'markdown')
32
+ when 'markdown'
33
+ file = data[:out_file] || 'REFERENCE.md'
34
+ MarkdownGenerator.new(benchmarks, pupmod.name, file: file).generate(doc_title)
35
+ else
36
+ raise "Format #{data[:format]} is unsupported! Only `markdown` format supported"
37
+ end
38
+ end
39
+
40
+ def self.generate_markdown
41
+ AbideDevUtils::Markdown.new('REFERENCE.md').generate
42
+ end
43
+
44
+ def self.config_example(control, params_array)
45
+ out_str = ['cem_windows::config:', ' control_configs:', " \"#{control}\":"]
46
+ indent = ' '
47
+ params_array.each do |param_hash|
48
+ val = case param_hash[:type]
49
+ when 'String'
50
+ "'#{param_hash[:default]}'"
51
+ else
52
+ param_hash[:default]
53
+ end
54
+
55
+ out_str << "#{indent}#{param_hash[:name]}: #{val}"
56
+ end
57
+ out_str.join("\n")
58
+ end
59
+
60
+ # Generates a markdown reference doc
61
+ class MarkdownGenerator
62
+ SPECIAL_CONTROL_IDS = %w[dependent cem_options cem_protected].freeze
63
+
64
+ def initialize(benchmarks, module_name, file: 'REFERENCE.md')
65
+ @benchmarks = benchmarks
66
+ @module_name = module_name
67
+ @file = file
68
+ @md = AbideDevUtils::Markdown.new(@file)
69
+ end
70
+
71
+ def generate(doc_title = 'Reference')
72
+ md.add_title(doc_title)
73
+ benchmarks.each do |benchmark|
74
+ progress_bar = AbideDevUtils::Output.progress(title: "Generating Markdown for #{benchmark.title_key}",
75
+ total: benchmark.controls.length)
76
+ md.add_h1(benchmark.title_key)
77
+ benchmark.controls.each do |control|
78
+ next if SPECIAL_CONTROL_IDS.include? control.id
79
+ next if benchmark.framework == 'stig' && control.id_map_type != 'vulnid'
80
+
81
+ control_md = ControlMarkdown.new(control, @md, @module_name, benchmark.framework)
82
+ control_md.generate!
83
+ progress_bar.increment
84
+ rescue StandardError => e
85
+ raise "Failed to generate markdown for control #{control.id}. Original message: #{e.message}"
86
+ end
87
+ end
88
+ AbideDevUtils::Output.simple("Saving markdown to #{@file}")
89
+ md.to_file
90
+ end
91
+
92
+ private
93
+
94
+ attr_reader :benchmarks, :md
95
+ end
96
+
97
+ class ConfigExampleError < StandardError; end
98
+
99
+ class ControlMarkdown
100
+ def initialize(control, md, module_name, framework, formatter: nil)
101
+ @control = control
102
+ @md = md
103
+ @module_name = module_name
104
+ @framework = framework
105
+ @formatter = formatter.nil? ? TypeExprValueFormatter : formatter
106
+ @control_data = {}
107
+ end
108
+
109
+ def generate!
110
+ heading_builder
111
+ control_params_builder
112
+ control_levels_builder
113
+ control_profiles_builder
114
+ config_example_builder
115
+ control_alternate_ids_builder
116
+ dependent_controls_builder
117
+ resource_reference_builder
118
+ end
119
+
120
+ private
121
+
122
+ def heading_builder
123
+ @md.add_h2("#{@control.number} - #{@control.title}")
124
+ end
125
+
126
+ def control_has_valid_params?
127
+ return true if @control.params? || @control.resource.cem_options? || @control.resource.cem_protected?
128
+ return true if @control.resource.manifest? && @control.resource.manifest.declaration.parameters?
129
+
130
+ false
131
+ end
132
+
133
+ def resource_param(ctrl_param)
134
+ return unless @control.resource.manifest?
135
+
136
+ @control.resource.manifest.declaration.parameters&.find { |x| x.name == "$#{ctrl_param[:name]}" }
137
+ end
138
+
139
+ def param_type_expr(ctrl_param, rsrc_param)
140
+ @control_data[ctrl_param[:name]] = {} unless @control_data.key?(ctrl_param[:name])
141
+ @control_data[ctrl_param[:name]][:type_expr] = rsrc_param&.type_expr? ? rsrc_param&.type_expr : ctrl_param[:type]
142
+ return unless @control_data[ctrl_param[:name]][:type_expr]
143
+
144
+ " - [ #{@md.code(@control_data[ctrl_param[:name]][:type_expr])} ]"
145
+ end
146
+
147
+ def param_default_value(ctrl_param, rsrc_param)
148
+ @control_data[ctrl_param[:name]] = {} unless @control_data.key?(ctrl_param[:name])
149
+ @control_data[ctrl_param[:name]][:default] = ctrl_param[:default] || rsrc_param&.value
150
+ return unless @control_data[ctrl_param[:name]][:default]
151
+
152
+ " - #{@md.italic('Default:')} #{@md.code(@control_data[ctrl_param[:name]][:default])}"
153
+ end
154
+
155
+ def control_params_builder
156
+ return unless control_has_valid_params?
157
+
158
+ @md.add_ul('Parameters:')
159
+ [@control.param_hashes, @control.resource.cem_options, @control.resource.cem_protected].each do |collection|
160
+ collection.each do |hsh|
161
+ rparam = resource_param(hsh)
162
+ str_array = [@md.code(hsh[:name]), param_type_expr(hsh, rparam), param_default_value(hsh, rparam)]
163
+ @md.add_ul(str_array.compact.join, indent: 1)
164
+ end
165
+ end
166
+ end
167
+
168
+ def control_levels_builder
169
+ return unless @control.levels
170
+
171
+ @md.add_ul('Supported Levels:')
172
+ @control.levels.each do |l|
173
+ @md.add_ul(@md.code(l), indent: 1)
174
+ end
175
+ end
176
+
177
+ def control_profiles_builder
178
+ return unless @control.profiles
179
+
180
+ @md.add_ul('Supported Profiles:')
181
+ @control.profiles.each do |l|
182
+ @md.add_ul(@md.code(l), indent: 1)
183
+ end
184
+ end
185
+
186
+ def control_alternate_ids_builder
187
+ return if @framework == 'stig'
188
+
189
+ @md.add_ul('Alternate Config IDs:')
190
+ @control.alternate_ids.each do |l|
191
+ @md.add_ul(@md.code(l), indent: 1)
192
+ end
193
+ end
194
+
195
+ def dependent_controls_builder
196
+ dep_ctrls = @control.resource.dependent_controls
197
+ return if dep_ctrls.nil? || dep_ctrls.empty?
198
+
199
+ @md.add_ul('Dependent controls:')
200
+ dep_ctrls.each do |ctrl|
201
+ puts "DEPENDENT: #{ctrl.id}"
202
+ @md.add_ul(@md.code(ctrl.display_title), indent: 1)
203
+ end
204
+ end
205
+
206
+ def config_example_builder
207
+ out_str = []
208
+ indent = ' '
209
+ @control.param_hashes.each do |param_hash|
210
+ next if param_hash[:name] == 'No parameters'
211
+
212
+ val = @formatter.format(@control_data[param_hash[:name]][:default],
213
+ @control_data[param_hash[:name]][:type_expr],
214
+ optional_strategy: :placeholder)
215
+ out_str << "#{indent}#{param_hash[:name]}: #{val}"
216
+ end
217
+ return if out_str.empty?
218
+
219
+ out_str.unshift(" #{@control.title.dump}:")
220
+ out_str.unshift(' control_configs:')
221
+ out_str.unshift("#{@module_name}::config:")
222
+ @md.add_ul('Hiera Configuration Example:')
223
+ @md.add_code_block(out_str.join("\n"), language: 'yaml')
224
+ rescue StandardError => e
225
+ err_msg = [
226
+ "Failed to generate config example for control #{@control.id}",
227
+ "Error: #{e.message}",
228
+ "Control: Data #{@control_data.inspect}",
229
+ e.backtrace.join("\n")
230
+ ].join("\n")
231
+ raise ConfigExampleError, err_msg
232
+ end
233
+
234
+ def resource_reference_builder
235
+ @md.add_ul("Resource: #{@md.code(@control.resource.to_reference)}")
236
+ end
237
+ end
238
+
239
+ # Holds methods for formmating values based on type expressions
240
+ class TypeExprValueFormatter
241
+ UNDEF_VAL = 'undef'
242
+
243
+ # Formats a value based on a type expression.
244
+ # @param value [Any] the value to format
245
+ # @param type_expr [String] the type expression to use for formatting
246
+ # @param optional_strategy [Symbol] the strategy to use for optional values
247
+ # @return [Any] the formatted value
248
+ def self.format(value, type_expr, optional_strategy: :undef)
249
+ return value if value == 'No parameters'
250
+
251
+ case type_expr
252
+ when /^(String|Stdlib::(Unix|Windows|Absolute)path|Enum)/
253
+ quote(value)
254
+ when /^Optional\[/
255
+ optional(value, type_expr, strategy: optional_strategy)
256
+ else
257
+ return type_expr_placeholder(type_expr) if value.nil?
258
+
259
+ quote(value)
260
+ end
261
+ end
262
+
263
+ # Escapes and quotes a string. If value is not a string, returns value.
264
+ # @param value [Any] the string to quote.
265
+ # @return [String] the quoted string.
266
+ # @return [Any] the value if it is not a string.
267
+ def self.quote(value)
268
+ if value.is_a?(String)
269
+ value.inspect
270
+ else
271
+ value
272
+ end
273
+ end
274
+
275
+ # Checks if a value is considered undef.
276
+ # @param value [Any] the value to check.
277
+ # @return [Boolean] true if value is considered undef (nil or 'undef').
278
+ def self.undef?(value)
279
+ value.nil? || value == UNDEF_VAL
280
+ end
281
+
282
+ # Returns the display representation of the value with an Optional type expression.
283
+ # If the value is not nil or 'undef', returns the quoted form of the value.
284
+ # @param value [Any] the value to format.
285
+ # @param type_expr [String] the type expression.
286
+ # @param strategy [Symbol] the strategy to use. Valid strategies are :undef and :placeholder.
287
+ # :undef will return 'undef' if the value is nil or 'undef'.
288
+ # :placeholder will return a peeled type expression placeholder if the value is nil or 'undef'.
289
+ # @return [String] the formatted value.
290
+ # @return [Any] the quoted value if it is not nil.
291
+ def self.optional(value, type_expr, strategy: :undef)
292
+ return UNDEF_VAL if undef?(value) && strategy == :undef
293
+ return type_expr_placeholder(peel_type_expr(type_expr)) if undef?(value) && strategy == :placeholder
294
+
295
+ quote(value)
296
+ end
297
+
298
+ # Returns a "peeled" type expression. Peeling a type expression removes the
299
+ # first layer of the type expression. For example, if the type expression is
300
+ # Optional[String], the peeled type expression is String.
301
+ # @param type_expr [String] the type expression to peel.
302
+ # @return [String] the peeled type expression.
303
+ def self.peel_type_expr(type_expr)
304
+ return type_expr unless type_expr.include?('[')
305
+
306
+ type_expr.match(/^[A-Z][a-z0-9_]*\[(?<peeled>[A-Za-z0-9:,_{}=>\[\]\\\s]+)\]$/)[:peeled]
307
+ end
308
+
309
+ # Formats the type expression as a placeholder.
310
+ # @param type_expr [String] The type expression to format.
311
+ # @return [String] The formatted type expression.
312
+ def self.type_expr_placeholder(type_expr)
313
+ "<<Type #{type_expr}>>"
314
+ end
315
+ end
316
+ end
317
+ end
318
+ end
319
+ end