abide_dev_utils 0.11.0 → 0.12.1

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +18 -31
  3. data/lib/abide_dev_utils/cem/benchmark.rb +335 -136
  4. data/lib/abide_dev_utils/cem/generate/coverage_report.rb +380 -0
  5. data/lib/abide_dev_utils/cem/generate/reference.rb +238 -35
  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 +64 -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 +193 -63
  43. metadata +27 -3
  44. data/lib/abide_dev_utils/cem/coverage_report.rb +0 -348
@@ -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
@@ -1,8 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'json'
4
+ require 'shellwords'
5
+ require 'timeout'
4
6
  require 'yaml'
5
7
  require 'abide_dev_utils/markdown'
8
+ require 'abide_dev_utils/output'
6
9
  require 'abide_dev_utils/ppt'
7
10
  require 'abide_dev_utils/cem/benchmark'
8
11
 
@@ -27,7 +30,8 @@ module AbideDevUtils
27
30
  benchmarks = AbideDevUtils::CEM::Benchmark.benchmarks_from_puppet_module(pupmod)
28
31
  case data.fetch(:format, 'markdown')
29
32
  when 'markdown'
30
- MarkdownGenerator.new(benchmarks).generate(doc_title)
33
+ file = data[:out_file] || 'REFERENCE.md'
34
+ MarkdownGenerator.new(benchmarks, pupmod.name, file: file).generate(doc_title)
31
35
  else
32
36
  raise "Format #{data[:format]} is unsupported! Only `markdown` format supported"
33
37
  end
@@ -55,59 +59,258 @@ module AbideDevUtils
55
59
 
56
60
  # Generates a markdown reference doc
57
61
  class MarkdownGenerator
58
- def initialize(benchmarks)
62
+ SPECIAL_CONTROL_IDS = %w[dependent cem_options cem_protected].freeze
63
+
64
+ def initialize(benchmarks, module_name, file: 'REFERENCE.md')
59
65
  @benchmarks = benchmarks
60
- @md = AbideDevUtils::Markdown.new('REFERENCE.md')
66
+ @module_name = module_name
67
+ @file = file
68
+ @md = AbideDevUtils::Markdown.new(@file)
61
69
  end
62
70
 
63
71
  def generate(doc_title = 'Reference')
64
72
  md.add_title(doc_title)
65
73
  benchmarks.each do |benchmark|
74
+ progress_bar = AbideDevUtils::Output.progress(title: "Generating Markdown for #{benchmark.title_key}",
75
+ total: benchmark.controls.length)
66
76
  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)}")
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}"
89
86
  end
90
87
  end
88
+ AbideDevUtils::Output.simple("Saving markdown to #{@file}")
91
89
  md.to_file
92
90
  end
93
91
 
94
92
  private
95
93
 
96
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
97
138
 
98
- def config_example(module_name, control, params_array)
99
- out_str = ["#{module_name}::config:", ' control_configs:', " \"#{control}\":"]
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 = []
100
208
  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
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)
108
215
  out_str << "#{indent}#{param_hash[:name]}: #{val}"
109
216
  end
110
- out_str.join("\n")
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}>>"
111
314
  end
112
315
  end
113
316
  end