abide_dev_utils 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_validator'
4
+ require_relative '../../../validate'
5
+
6
+ module AbideDevUtils
7
+ module CEM
8
+ module Validate
9
+ module Strings
10
+ # Validates a Puppet Class from a Puppet Strings hash
11
+ class PuppetClassValidator < BaseValidator
12
+ def validate_puppet_class
13
+ check_text_or_summary
14
+ check_params
15
+ end
16
+
17
+ # @return [Hash] Hash of basic class data to be used in findings
18
+ def finding_data(**data)
19
+ data
20
+ end
21
+
22
+ private
23
+
24
+ # Checks if the class has a description or summary
25
+ def check_text_or_summary
26
+ valid_desc = AbideDevUtils::Validate.populated_string?(docstring)
27
+ valid_summary = AbideDevUtils::Validate.populated_string?(find_tag_name('summary')&.text)
28
+ return if valid_desc || valid_summary
29
+
30
+ new_finding(
31
+ :error,
32
+ :no_description_or_summary,
33
+ finding_data(valid_description: valid_desc, valid_summary: valid_summary),
34
+ )
35
+ end
36
+
37
+ # Checks if the class has parameters and if they are documented
38
+ def check_params
39
+ return if parameters.nil? || parameters.empty? # No params
40
+
41
+ param_tags = select_tag_name('param')
42
+ if param_tags.empty?
43
+ new_finding(:error, :no_parameter_documentation, finding_data(class_parameters: parameters))
44
+ return
45
+ end
46
+
47
+ parameters.each do |param|
48
+ param_name, def_val = param
49
+ check_param(param_name, def_val, param_tags)
50
+ end
51
+ end
52
+
53
+ # Checks if a parameter is documented properly and if it has a correct default value
54
+ def check_param(param_name, def_val = nil, param_tags = select_tag_name('param'))
55
+ param_tag = param_tags.find { |t| t.name == param_name }
56
+ return unless param_documented?(param_name, param_tag)
57
+
58
+ valid_param_description?(param_tag)
59
+ valid_param_types?(param_tag)
60
+ valid_param_default?(param_tag, def_val)
61
+ end
62
+
63
+ # Checks if a parameter is documented
64
+ def param_documented?(param_name, param_tag)
65
+ return true if param_tag
66
+
67
+ new_finding(:error, :param_not_documented, finding_data(param: param_name))
68
+ false
69
+ end
70
+
71
+ # Checks if a parameter has a description
72
+ def valid_param_description?(param)
73
+ return true if AbideDevUtils::Validate.populated_string?(param.text)
74
+
75
+ new_finding(:error, :param_missing_description, finding_data(param: param.name))
76
+ false
77
+ end
78
+
79
+ # Checks if a parameter is typed
80
+ def valid_param_types?(param)
81
+ unless param.types&.any?
82
+ new_finding(:error, :param_missing_types, finding_data(param: param.name))
83
+ return false
84
+ end
85
+ true
86
+ end
87
+
88
+ # Checks if a parameter has a default value and if it is correct for the type
89
+ def valid_param_default?(param, def_val)
90
+ return true if def_val.nil?
91
+
92
+ if param.types.first.start_with?('Optional[') && def_val != 'undef'
93
+ new_finding(:error, :param_optional_without_undef_default, param: param.name, default_value: def_val, name: name, file: file)
94
+ return false
95
+ end
96
+ true
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'puppet_class_validator'
4
+
5
+ module AbideDevUtils
6
+ module CEM
7
+ module Validate
8
+ module Strings
9
+ # Validates Puppet Defined Type strings objects
10
+ class PuppetDefinedTypeValidator < PuppetClassValidator
11
+ def validate_puppet_defined_type
12
+ validate_puppet_class
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AbideDevUtils
4
+ module CEM
5
+ module Validate
6
+ module Strings
7
+ # Represents a validation finding (warning or error)
8
+ class ValidationFinding
9
+ attr_reader :type, :title, :data
10
+
11
+ def initialize(type, title, data)
12
+ raise ArgumentError, 'type must be :error or :warning' unless %i[error warning].include?(type)
13
+
14
+ @type = type.to_sym
15
+ @title = title.to_sym
16
+ @data = data
17
+ end
18
+
19
+ def to_s
20
+ "#{@type}: #{@title}: #{@data}"
21
+ end
22
+
23
+ def to_hash
24
+ { type: @type, title: @title, data: @data }
25
+ end
26
+ alias to_h to_hash
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../ppt/strings'
4
+ require_relative 'strings/puppet_class_validator'
5
+ require_relative 'strings/puppet_defined_type_validator'
6
+
7
+ module AbideDevUtils
8
+ module CEM
9
+ module Validate
10
+ # Validation objects and methods for Puppet Strings
11
+ module Strings
12
+ # Convenience method to validate Puppet Strings of current module
13
+ def self.validate(**opts)
14
+ output = Validator.new(nil, **opts).validate
15
+ output.transform_values do |results|
16
+ results.select { |r| r[:errors].any? || r[:warnings].any? }
17
+ end
18
+ end
19
+
20
+ # Holds various validation methods for a AbideDevUtils::Ppt::Strings object
21
+ class Validator
22
+ def initialize(puppet_strings = nil, **opts)
23
+ unless puppet_strings.nil? || puppet_strings.is_a?(AbideDevUtils::Ppt::Strings)
24
+ raise ArgumentError, 'If puppet_strings is supplied, it must be a AbideDevUtils::Ppt::Strings object'
25
+ end
26
+
27
+ puppet_strings = AbideDevUtils::Ppt::Strings.new(**opts) if puppet_strings.nil?
28
+ @puppet_strings = puppet_strings
29
+ end
30
+
31
+ # Associate validators with each Puppet Strings object and calls #validate on each
32
+ # @return [Hash] Hash of validation results
33
+ def validate
34
+ AbideDevUtils::Ppt::Strings::REGISTRY_TYPES.each_with_object({}) do |rtype, hsh|
35
+ next unless rtype.to_s.start_with?('puppet_') && @puppet_strings.respond_to?(rtype)
36
+
37
+ hsh[rtype] = @puppet_strings.send(rtype).map do |item|
38
+ item.validator = validator_for(item)
39
+ item.validate
40
+ validation_output(item)
41
+ end
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ # Returns the appropriate validator for a given Puppet Strings object
48
+ def validator_for(item)
49
+ case item.type
50
+ when :puppet_class
51
+ PuppetClassValidator.new(item)
52
+ when :puppet_defined_type
53
+ PuppetDefinedTypeValidator.new(item)
54
+ else
55
+ BaseValidator.new(item)
56
+ end
57
+ end
58
+
59
+ def validation_output(item)
60
+ {
61
+ name: item.name,
62
+ file: item.file,
63
+ line: item.line,
64
+ errors: item.errors,
65
+ warnings: item.warnings,
66
+ }
67
+ end
68
+
69
+ # Validate Puppet Class strings hashes.
70
+ # @return [Hash] Hash of class names and errors
71
+ def validate_classes!
72
+ @puppet_strings.puppet_classes.map! do |klass|
73
+ klass.validator = PuppetClassValidator.new(klass)
74
+ klass.validate
75
+ klass
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'abide_dev_utils/cem/validate/resource_data'
3
+ require_relative 'validate/resource_data'
4
+ require_relative 'validate/strings'
4
5
 
5
6
  module AbideDevUtils
6
7
  module CEM
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'abide_dev_utils/xccdf'
4
4
  require 'abide_dev_utils/cem/generate'
5
+ require 'abide_dev_utils/cem/validate'
5
6
 
6
7
  module AbideDevUtils
7
8
  # Methods for working with Compliance Enforcement Modules (CEM)
@@ -17,6 +17,7 @@ module Abide
17
17
  super(CMD_NAME, CMD_SHORT, CMD_LONG, takes_commands: true)
18
18
  add_command(CemGenerate.new)
19
19
  add_command(CemUpdateConfig.new)
20
+ add_command(CemValidate.new)
20
21
  end
21
22
  end
22
23
 
@@ -69,11 +70,11 @@ module Abide
69
70
  quiet = @data.fetch(:quiet, false)
70
71
  console = @data.fetch(:verbose, false) && !quiet
71
72
  generate_opts = {
72
- benchmark: @data.fetch(:benchmark),
73
- profile: @data.fetch(:profile),
74
- level: @data.fetch(:level),
73
+ benchmark: @data[:benchmark],
74
+ profile: @data[:profile],
75
+ level: @data[:level],
75
76
  ignore_benchmark_errors: @data.fetch(:ignore_all, false),
76
- xccdf_dir: @data.fetch(:xccdf_dir),
77
+ xccdf_dir: @data[:xccdf_dir],
77
78
  }
78
79
  AbideDevUtils::Output.simple('Generating coverage report...') unless quiet
79
80
  coverage = AbideDevUtils::CEM::Generate::CoverageReport.generate(format_func: :to_h, opts: generate_opts)
@@ -104,11 +105,14 @@ module Abide
104
105
  @data[:format] = f
105
106
  end
106
107
  options.on('-v', '--verbose', 'Verbose output') do
107
- @data[:verbose] = true
108
+ @data[:debug] = true
108
109
  end
109
110
  options.on('-q', '--quiet', 'Quiet output') do
110
111
  @data[:quiet] = true
111
112
  end
113
+ options.on('-s', '--strict', 'Fails if there are any errors') do
114
+ @data[:strict] = true
115
+ end
112
116
  end
113
117
 
114
118
  def execute
@@ -167,5 +171,59 @@ module Abide
167
171
  AbideDevUtils::Output.simple(change_report) unless @data[:quiet]
168
172
  end
169
173
  end
174
+
175
+ class CemValidate < AbideCommand
176
+ CMD_NAME = 'validate'
177
+ CMD_SHORT = 'Validation commands for CEM modules'
178
+ CMD_LONG = 'Validation commands for CEM modules'
179
+ def initialize
180
+ super(CMD_NAME, CMD_SHORT, CMD_LONG, takes_commands: true)
181
+ add_command(CemValidatePuppetStrings.new)
182
+ end
183
+ end
184
+
185
+ class CemValidatePuppetStrings < AbideCommand
186
+ CMD_NAME = 'puppet-strings'
187
+ CMD_SHORT = 'Validates the Puppet Strings documentation'
188
+ CMD_LONG = 'Validates the Puppet Strings documentation'
189
+ def initialize
190
+ super(CMD_NAME, CMD_SHORT, CMD_LONG, takes_commands: false)
191
+ options.on('-v', '--verbose', 'Verbose output') do
192
+ @data[:verbose] = true
193
+ end
194
+ options.on('-q', '--quiet', 'Quiet output') do
195
+ @data[:quiet] = true
196
+ end
197
+ options.on('-f [FORMAT]', '--format [FORMAT]', 'Format for output (text, json, yaml)') do |f|
198
+ @data[:format] = f
199
+ end
200
+ options.on('-o [FILE]', '--out-file [FILE]', 'Path to save the updated config file') do |o|
201
+ @data[:out_file] = o
202
+ end
203
+ options.on('-s', '--strict', 'Exits with exit code 1 if there are any warnings') do
204
+ @data[:strict] = true
205
+ end
206
+ end
207
+
208
+ def execute
209
+ @data[:format] ||= 'text'
210
+ AbideDevUtils::Validate.puppet_module_directory
211
+ output = AbideDevUtils::CEM::Validate::Strings.validate(**@data)
212
+ has_errors = false
213
+ has_warnings = false
214
+ output.each do |_, i|
215
+ has_errors = true if i.any? { |j| j[:errors].any? }
216
+ has_warnings = true if i.any? { |j| j[:warnings].any? }
217
+ end
218
+ AbideDevUtils::Output.send(
219
+ @data[:format].to_sym,
220
+ output,
221
+ console: !@data[:quiet],
222
+ file: @data[:out_file],
223
+ stringify: true,
224
+ )
225
+ exit 1 if has_errors || (has_warnings && @data[:strict])
226
+ end
227
+ end
170
228
  end
171
229
  end
@@ -49,6 +49,10 @@ module AbideDevUtils
49
49
  "#### #{text}\n"
50
50
  end
51
51
 
52
+ def paragraph(text)
53
+ "#{text}\n"
54
+ end
55
+
52
56
  def ul(text, indent: 0)
53
57
  indented_text = []
54
58
  indent.times { indented_text << ' ' } if indent.positive?
@@ -10,30 +10,44 @@ require 'abide_dev_utils/files'
10
10
  module AbideDevUtils
11
11
  module Output
12
12
  FWRITER = AbideDevUtils::Files::Writer.new
13
- def self.simple(msg, stream: $stdout)
14
- stream.puts msg
13
+ def self.simple(msg, stream: $stdout, **_)
14
+ case msg
15
+ when Hash
16
+ stream.puts JSON.pretty_generate(msg)
17
+ else
18
+ stream.puts msg
19
+ end
15
20
  end
16
21
 
17
- def self.json(in_obj, console: false, file: nil, pretty: true)
22
+ def self.text(msg, console: false, file: nil, **_)
23
+ simple(msg) if console
24
+ FWRITER.write_text(msg, file: file) unless file.nil?
25
+ end
26
+
27
+ def self.json(in_obj, console: false, file: nil, pretty: true, **_)
18
28
  AbideDevUtils::Validate.hashable(in_obj)
19
29
  json_out = pretty ? JSON.pretty_generate(in_obj) : JSON.generate(in_obj)
20
30
  simple(json_out) if console
21
31
  FWRITER.write_json(json_out, file: file) unless file.nil?
22
32
  end
23
33
 
24
- def self.yaml(in_obj, console: false, file: nil)
34
+ def self.yaml(in_obj, console: false, file: nil, stringify: false, **_)
25
35
  yaml_out = if in_obj.is_a? String
26
36
  in_obj
27
37
  else
28
38
  AbideDevUtils::Validate.hashable(in_obj)
29
- # Use object's #to_yaml method if it exists, convert to hash if not
30
- in_obj.respond_to?(:to_yaml) ? in_obj.to_yaml : in_obj.to_h.to_yaml
39
+ if stringify
40
+ JSON.parse(JSON.generate(in_obj)).to_yaml
41
+ else
42
+ # Use object's #to_yaml method if it exists, convert to hash if not
43
+ in_obj.respond_to?(:to_yaml) ? in_obj.to_yaml : in_obj.to_h.to_yaml
44
+ end
31
45
  end
32
46
  simple(yaml_out) if console
33
47
  FWRITER.write_yaml(yaml_out, file: file) unless file.nil?
34
48
  end
35
49
 
36
- def self.yml(in_obj, console: false, file: nil)
50
+ def self.yml(in_obj, console: false, file: nil, **_)
37
51
  AbideDevUtils::Validate.hashable(in_obj)
38
52
  # Use object's #to_yaml method if it exists, convert to hash if not
39
53
  yml_out = in_obj.respond_to?(:to_yaml) ? in_obj.to_yaml : in_obj.to_h.to_yaml
@@ -41,7 +55,7 @@ module AbideDevUtils
41
55
  FWRITER.write_yml(yml_out, file: file) unless file.nil?
42
56
  end
43
57
 
44
- def self.progress(title: 'Progress', start: 0, total: 100, format: nil)
58
+ def self.progress(title: 'Progress', start: 0, total: 100, format: nil, **_)
45
59
  ProgressBar.create(title: title, starting_at: start, total: total, format: format)
46
60
  end
47
61
  end
@@ -10,17 +10,30 @@ module AbideDevUtils
10
10
  attr_reader :manifest_file
11
11
 
12
12
  def initialize(manifest_file)
13
+ @compiler = Puppet::Pal::Compiler.new(nil)
13
14
  @manifest_file = File.expand_path(manifest_file)
14
15
  raise ArgumentError, "File #{@manifest_file} is not a file" unless File.file?(@manifest_file)
15
16
  end
16
17
 
17
18
  def ast
18
- @ast ||= Puppet::Pal::Compiler.new(nil).parse_file(manifest_file)
19
+ @ast ||= non_validating_parse_file(manifest_file)
19
20
  end
20
21
 
21
22
  def declaration
22
23
  @declaration ||= Declaration.new(ast)
23
24
  end
25
+
26
+ private
27
+
28
+ # This method gets around the normal validation performed by the regular
29
+ # Puppet::Pal::Compiler#parse_file method. This is necessary because, with
30
+ # validation enabled, the parser will raise errors during parsing if the
31
+ # file contains any calls to Facter. This is due to facter being disallowed
32
+ # in Puppet when evaluating the code in a scripting context instead of catalog
33
+ # compilation, which is what we are doing here.
34
+ def non_validating_parse_file(file)
35
+ @compiler.send(:internal_evaluator).parser.parse_file(file)&.model
36
+ end
24
37
  end
25
38
 
26
39
  class Declaration