abide_dev_utils 0.13.0 → 0.14.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'tempfile'
4
+ require 'fileutils'
5
+
3
6
  module AbideDevUtils
4
7
  # Formats text for output in markdown
5
8
  class Markdown
@@ -14,11 +17,17 @@ module AbideDevUtils
14
17
  def to_markdown
15
18
  toc = @toc.join("\n")
16
19
  body = @body.join("\n")
17
- "#{@title}\n#{toc}\n\n#{body}"
20
+ "#{@title}\n#{toc}\n\n#{body}".encode(universal_newline: true)
18
21
  end
22
+ alias to_s to_markdown
19
23
 
20
24
  def to_file
21
- File.write(@file, to_markdown)
25
+ this_markdown = to_markdown
26
+ Tempfile.create('markdown') do |f|
27
+ f.write(this_markdown)
28
+ check_file_content(f.path, this_markdown)
29
+ FileUtils.mv(f.path, @file)
30
+ end
22
31
  end
23
32
 
24
33
  def method_missing(name, *args, &block)
@@ -49,6 +58,10 @@ module AbideDevUtils
49
58
  "#### #{text}\n"
50
59
  end
51
60
 
61
+ def paragraph(text)
62
+ "#{text}\n"
63
+ end
64
+
52
65
  def ul(text, indent: 0)
53
66
  indented_text = []
54
67
  indent.times { indented_text << ' ' } if indent.positive?
@@ -84,6 +97,14 @@ module AbideDevUtils
84
97
 
85
98
  private
86
99
 
100
+ def check_file_content(file, content)
101
+ raise "File #{file} not found! Not saving to #{@file}" unless File.exist?(file)
102
+ raise "File #{file} is empty! Not saving to #{@file}" if File.zero?(file)
103
+ return if File.read(file).include?(content)
104
+
105
+ raise "File #{file} does not contain correct content! Not saving to #{@file}"
106
+ end
107
+
87
108
  def add(type, text, *args, **kwargs)
88
109
  @toc << ul(link(text, text, anchor: true), indent: 0) if @with_toc && type == :h1
89
110
 
@@ -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