abide_dev_utils 0.12.2 → 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,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'validation_finding'
4
+
5
+ module AbideDevUtils
6
+ module CEM
7
+ module Validate
8
+ module Strings
9
+ # Base class for validating Puppet Strings objects. This class can be used directly, but it is
10
+ # recommended to use a subclass of this class to provide more specific validation logic. Each
11
+ # subclass should implement a `validate_<type>` method that will be called by the `validate` method
12
+ # of this class. The `validate_<type>` method should contain the validation logic for the
13
+ # corresponding type of Puppet Strings object.
14
+ class BaseValidator
15
+ SAFE_OBJECT_METHODS = %i[
16
+ type
17
+ name
18
+ file
19
+ line
20
+ docstring
21
+ tags
22
+ parameters
23
+ source
24
+ ].freeze
25
+ PDK_SUMMARY_REGEX = %r{^A short summary of the purpose.*}.freeze
26
+ PDK_DESCRIPTION_REGEX = %r{^A description of what this.*}.freeze
27
+
28
+ attr_reader :findings
29
+
30
+ def initialize(strings_object)
31
+ @object = strings_object
32
+ @findings = []
33
+ # Define instance methods for each of the SAFE_OBJECT_METHODS
34
+ SAFE_OBJECT_METHODS.each do |method|
35
+ define_singleton_method(method) { safe_method_call(@object, method) }
36
+ end
37
+ end
38
+
39
+ def errors
40
+ @findings.select { |f| f.type == :error }
41
+ end
42
+
43
+ def warnings
44
+ @findings.select { |f| f.type == :warning }
45
+ end
46
+
47
+ def errors?
48
+ !errors.empty?
49
+ end
50
+
51
+ def warnings?
52
+ !warnings.empty?
53
+ end
54
+
55
+ def find_tag_name(tag_name)
56
+ tags&.find { |t| t.tag_name == tag_name }
57
+ end
58
+
59
+ def select_tag_name(tag_name)
60
+ return [] if tags.nil?
61
+
62
+ tags.select { |t| t.tag_name == tag_name }
63
+ end
64
+
65
+ def find_parameter(param_name)
66
+ parameters&.find { |p| p[0] == param_name }
67
+ end
68
+
69
+ def validate
70
+ license_tag?
71
+ non_generic_summary?
72
+ non_generic_description?
73
+ send("validate_#{type}".to_sym) if respond_to?("validate_#{type}".to_sym)
74
+ end
75
+
76
+ # Checks if the object has a license tag and if it is formatted correctly.
77
+ # Comparison is not case sensitive.
78
+ def license_tag?
79
+ see_tags = select_tag_name('see')
80
+ if see_tags.empty? || see_tags.none? { |t| t.name.casecmp('LICENSE.pdf') && t.text.casecmp('for license') }
81
+ new_finding(
82
+ :error,
83
+ :no_license_tag,
84
+ remediation: 'Add "@see LICENSE.pdf for license" to the class documentation'
85
+ )
86
+ return false
87
+ end
88
+ true
89
+ end
90
+
91
+ # Checks if the summary is not the default PDK summary.
92
+ def non_generic_summary?
93
+ summary = find_tag_name('summary')&.text
94
+ return true if summary.nil?
95
+
96
+ if summary.match?(PDK_SUMMARY_REGEX)
97
+ new_finding(:warning, :generic_summary, remediation: 'Add a more descriptive summary')
98
+ return false
99
+ end
100
+ true
101
+ end
102
+
103
+ # Checks if the description is not the default PDK description.
104
+ def non_generic_description?
105
+ description = docstring
106
+ return true if description.nil?
107
+
108
+ if description.match?(PDK_DESCRIPTION_REGEX)
109
+ new_finding(:warning, :generic_description, remediation: 'Add a more descriptive description')
110
+ return false
111
+ end
112
+ true
113
+ end
114
+
115
+ private
116
+
117
+ def safe_method_call(obj, method, *args)
118
+ obj.send(method, *args)
119
+ rescue NoMethodError
120
+ nil
121
+ end
122
+
123
+ def new_finding(type, title, **data)
124
+ @findings << ValidationFinding.new(type, title.to_sym, data)
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
@@ -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
@@ -123,8 +123,9 @@ module Abide
123
123
  super(CMD_NAME, takes_commands: false)
124
124
  short_desc(CMD_SHORT)
125
125
  long_desc(CMD_LONG)
126
- argument_desc(PATH: 'An XCCDF file from the abide puppet ticket coverage command', PROJECT: 'A Jira project')
126
+ argument_desc(PATH: 'An XCCDF file', PROJECT: 'A Jira project')
127
127
  options.on('-d', '--dry-run', 'Print to console instead of saving objects') { |_| @data[:dry_run] = true }
128
+ options.on('-e [EPIC]', '--epic [EPIC]', 'If given, tasks will be created and assigned to this epic. Takes form <PROJECT>-<NUM>') { |e| @data[:epic] = e }
128
129
  end
129
130
 
130
131
  def execute(path, project)
@@ -132,7 +133,7 @@ module Abide
132
133
  @data[:dry_run] = false if @data[:dry_run].nil?
133
134
  client = JIRA.client(options: {})
134
135
  proj = JIRA.project(client, project)
135
- JIRA.new_issues_from_xccdf(client, proj, path, dry_run: @data[:dry_run])
136
+ JIRA.new_issues_from_xccdf(client, proj, path, epic: @data[:epic], dry_run: @data[:dry_run])
136
137
  end
137
138
  end
138
139
  end
@@ -9,6 +9,10 @@ module AbideDevUtils
9
9
  @default = 'Failed to create Jira issue:'
10
10
  end
11
11
 
12
+ class CreateEpicError < GenericError
13
+ @default = 'Failed to create Jira epic:'
14
+ end
15
+
12
16
  class CreateSubtaskError < GenericError
13
17
  @default = 'Failed to create Jira subtask for issue:'
14
18
  end