abide_dev_utils 0.12.2 → 0.14.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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