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.
- checksums.yaml +4 -4
- data/Gemfile.lock +18 -31
- data/lib/abide_dev_utils/cem/benchmark.rb +335 -136
- data/lib/abide_dev_utils/cem/generate/coverage_report.rb +380 -0
- data/lib/abide_dev_utils/cem/generate/reference.rb +238 -35
- data/lib/abide_dev_utils/cem/generate.rb +5 -4
- data/lib/abide_dev_utils/cem/hiera_data/mapping_data/map_data.rb +110 -0
- data/lib/abide_dev_utils/cem/hiera_data/mapping_data/mixins.rb +46 -0
- data/lib/abide_dev_utils/cem/hiera_data/mapping_data.rb +146 -0
- data/lib/abide_dev_utils/cem/hiera_data/resource_data/control.rb +127 -0
- data/lib/abide_dev_utils/cem/hiera_data/resource_data/parameters.rb +90 -0
- data/lib/abide_dev_utils/cem/hiera_data/resource_data/resource.rb +102 -0
- data/lib/abide_dev_utils/cem/hiera_data/resource_data.rb +310 -0
- data/lib/abide_dev_utils/cem/hiera_data.rb +7 -0
- data/lib/abide_dev_utils/cem/mapping/mapper.rb +161 -34
- data/lib/abide_dev_utils/cem/validate/resource_data.rb +33 -0
- data/lib/abide_dev_utils/cem/validate.rb +10 -0
- data/lib/abide_dev_utils/cem.rb +0 -1
- data/lib/abide_dev_utils/cli/cem.rb +20 -2
- data/lib/abide_dev_utils/dot_number_comparable.rb +75 -0
- data/lib/abide_dev_utils/errors/cem.rb +10 -0
- data/lib/abide_dev_utils/ppt/class_utils.rb +1 -1
- data/lib/abide_dev_utils/ppt/code_gen/data_types.rb +64 -0
- data/lib/abide_dev_utils/ppt/code_gen/generate.rb +15 -0
- data/lib/abide_dev_utils/ppt/code_gen/resource.rb +59 -0
- data/lib/abide_dev_utils/ppt/code_gen/resource_types/base.rb +93 -0
- data/lib/abide_dev_utils/ppt/code_gen/resource_types/class.rb +17 -0
- data/lib/abide_dev_utils/ppt/code_gen/resource_types/manifest.rb +16 -0
- data/lib/abide_dev_utils/ppt/code_gen/resource_types/parameter.rb +16 -0
- data/lib/abide_dev_utils/ppt/code_gen/resource_types/strings.rb +13 -0
- data/lib/abide_dev_utils/ppt/code_gen/resource_types.rb +6 -0
- data/lib/abide_dev_utils/ppt/code_gen.rb +15 -0
- data/lib/abide_dev_utils/ppt/code_introspection.rb +102 -0
- data/lib/abide_dev_utils/ppt/hiera.rb +4 -1
- data/lib/abide_dev_utils/ppt/puppet_module.rb +2 -1
- data/lib/abide_dev_utils/ppt.rb +3 -0
- data/lib/abide_dev_utils/version.rb +1 -1
- data/lib/abide_dev_utils/xccdf/parser/helpers.rb +146 -0
- data/lib/abide_dev_utils/xccdf/parser/objects.rb +87 -144
- data/lib/abide_dev_utils/xccdf/parser.rb +5 -0
- data/lib/abide_dev_utils/xccdf/utils.rb +89 -0
- data/lib/abide_dev_utils/xccdf.rb +193 -63
- metadata +27 -3
- 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
|
-
|
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
|
-
|
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
|
-
@
|
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.
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
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
|
99
|
-
|
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
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
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.
|
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
|