Class: Cabriolet::PluginValidator

Inherits:
Object
  • Object
show all
Defined in:
lib/cabriolet/plugin_validator.rb

Overview

Validates plugin classes and configurations

The PluginValidator provides comprehensive validation for plugins including inheritance checks, metadata validation, version compatibility, and safety scanning.

Examples:

Validate a plugin class

result = PluginValidator.validate(MyPlugin)
if result[:valid]
  puts "Plugin is valid"
else
  puts "Errors: #{result[:errors].join(', ')}"
end

Constant Summary collapse

REQUIRED_METADATA =

Required metadata fields

i[name version author description
cabriolet_version].freeze
DANGEROUS_METHODS =

Dangerous method names to check for

%w[
  system exec spawn ` fork eval instance_eval class_eval
  module_eval binding const_set remove_const send __send__
  method_missing respond_to_missing?
].freeze

Class Method Summary collapse

Class Method Details

.check_safety(plugin_class) ⇒ Array<String>

Check plugin for potentially dangerous code

Scans the plugin’s source code for dangerous method calls that might pose security risks.

Examples:

Safe plugin

PluginValidator.check_safety(MySafePlugin)
#=> []

Potentially dangerous plugin

PluginValidator.check_safety(MyDangerousPlugin)
#=> ["Uses system call in setup method"]

Parameters:

  • plugin_class (Class)

    Plugin class to check

Returns:

  • (Array<String>)

    List of safety warnings



311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
# File 'lib/cabriolet/plugin_validator.rb', line 311

def check_safety(plugin_class)
  warnings = []

  # Get source location
  begin
    methods_to_check = i[setup activate ]

    methods_to_check.each do |method_name|
      next unless plugin_class.method_defined?(method_name, false)

      method_obj = plugin_class.instance_method(method_name)
      source_location = method_obj.source_location

      if source_location && File.exist?(source_location[0])
        source = File.read(source_location[0])

        DANGEROUS_METHODS.each do |dangerous|
          pattern = /\b#{Regexp.escape(dangerous)}\b/
          if source&.match?(pattern)
            warnings << "Plugin uses potentially dangerous method " \
                        "'#{dangerous}' " \
                        "in #{source_location[0]}"
          end
        end
      end
    end
  rescue StandardError => e
    warnings << "Could not perform safety check: #{e.message}"
  end

  warnings
end

.validate(plugin_class) ⇒ Hash

Validate a plugin class

Performs comprehensive validation including inheritance, metadata, version compatibility, and safety checks.

Examples:

Validate a plugin

result = PluginValidator.validate(MyPlugin)
result[:valid] #=> true
result[:errors] #=> []
result[:warnings] #=> ["Uses eval in setup method"]

Parameters:

  • plugin_class (Class)

    Plugin class to validate

Returns:

  • (Hash)

    Validation result with:

    • :valid [Boolean] True if all checks pass

    • :errors [Array<String>] List of validation errors (empty if valid)

    • :warnings [Array<String>] List of warnings (non-fatal issues)



48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/cabriolet/plugin_validator.rb', line 48

def validate(plugin_class)
  errors = []
  warnings = []

  # Check inheritance
  inherit_errors = validate_inheritance(plugin_class)
  errors.concat(inherit_errors)

  # If inheritance fails, stop here
  return { valid: false, errors: errors, warnings: warnings } unless
    inherit_errors.empty?

  # Create instance to check metadata
  begin
    instance = plugin_class.new(nil)
     = instance.

    # Validate metadata
    meta_errors = ()
    errors.concat(meta_errors)

    # Check version compatibility
    if [:cabriolet_version]
      version_errors = validate_version_compatibility(
        [:cabriolet_version],
        Cabriolet::VERSION,
      )
      errors.concat(version_errors)
    end

    # Validate dependencies
    if [:dependencies]
      dep_warnings = validate_dependencies([:dependencies])
      warnings.concat(dep_warnings)
    end
  rescue NotImplementedError => e
    errors << "Plugin does not implement required method: " \
              "#{e.message}"
  rescue StandardError => e
    errors << "Failed to instantiate plugin: #{e.message}"
  end

  # Safety checks
  safety_warnings = check_safety(plugin_class)
  warnings.concat(safety_warnings)

  {
    valid: errors.empty?,
    errors: errors,
    warnings: warnings,
  }
end

.validate_dependencies(dependencies) ⇒ Array<String>

Validate plugin dependencies

Checks if dependency specifications are valid. This performs format validation only; actual dependency resolution happens at load time.

Examples:

Valid dependencies

deps = ["other-plugin >= 1.0"]
PluginValidator.validate_dependencies(deps)
#=> []

Parameters:

  • dependencies (Array<String>)

    Dependency specifications

Returns:

  • (Array<String>)

    List of validation warnings



270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
# File 'lib/cabriolet/plugin_validator.rb', line 270

def validate_dependencies(dependencies)
  warnings = []

  unless dependencies.is_a?(Array)
    warnings << "Dependencies must be an array"
    return warnings
  end

  dependencies.each do |dep|
    unless dep.is_a?(String)
      warnings << "Each dependency must be a string"
      next
    end

    parts = dep.split
    if parts.empty?
      warnings << "Empty dependency specification"
    elsif !/^[a-z0-9_-]+$/.match?(parts[0])
      warnings << "Invalid dependency name: #{parts[0]}"
    end
  end

  warnings
end

.validate_inheritance(plugin_class) ⇒ Array<String>

Validate plugin inheritance

Checks that the plugin class properly inherits from Cabriolet::Plugin.

Examples:

Valid inheritance

PluginValidator.validate_inheritance(MyPlugin)
#=> []

Invalid inheritance

PluginValidator.validate_inheritance(Object)
#=> ["Plugin must inherit from Cabriolet::Plugin"]

Parameters:

  • plugin_class (Class)

    Plugin class to validate

Returns:

  • (Array<String>)

    List of inheritance errors (empty if valid)



117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/cabriolet/plugin_validator.rb', line 117

def validate_inheritance(plugin_class)
  errors = []

  unless plugin_class.is_a?(Class)
    errors << "Plugin must be a class, got #{plugin_class.class}"
    return errors
  end

  unless plugin_class < Plugin
    errors << "Plugin must inherit from Cabriolet::Plugin"
  end

  errors
end

.validate_metadata(metadata) ⇒ Array<String>

Validate plugin metadata

Checks that all required metadata fields are present and valid.

Examples:

Valid metadata

meta = { name: "test", version: "1.0", ... }
PluginValidator.(meta)
#=> []

Missing fields

PluginValidator.({})
#=> ["Missing required metadata: name, version, ..."]

Parameters:

  • metadata (Hash)

    Plugin metadata to validate

Returns:

  • (Array<String>)

    List of metadata errors (empty if valid)



148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/cabriolet/plugin_validator.rb', line 148

def ()
  errors = []

  unless .is_a?(Hash)
    errors << "Metadata must be a Hash"
    return errors
  end

  # Check required fields
  missing =  - .keys
  unless missing.empty?
    errors << "Missing required metadata: #{missing.join(', ')}"
  end

  # Validate field types and formats
  if [:name]
    unless [:name].is_a?(String) &&
        ![:name].empty?
      errors << "Plugin name must be a non-empty string"
    end

    if [:name].is_a?(String) && [:name] =~ /^[a-z0-9_-]+$/
      # Valid format - do nothing
    elsif [:name].is_a?(String)
      errors << "Plugin name must contain only lowercase letters, " \
                "numbers, hyphens, and underscores"
    end
  end

  if [:version] && !valid_version?([:version])
    errors << "Plugin version must be a valid semantic version " \
              "(e.g., '1.0.0')"
  end

  if [:author] && !([:author].is_a?(String) &&
           ![:author].empty?)
    errors << "Plugin author must be a non-empty string"
  end

  if [:description] && !([:description].is_a?(String) &&
           ![:description].empty?)
    errors << "Plugin description must be a non-empty string"
  end

  # Optional fields validation
  if [:homepage] && ![:homepage].empty? && !valid_url?([:homepage])
    errors << "Plugin homepage must be a valid URL"
  end

  if [:dependencies] && ![:dependencies].is_a?(Array)
    errors << "Plugin dependencies must be an array"
  end

  if [:tags] && ![:tags].is_a?(Array)
    errors << "Plugin tags must be an array"
  end

  errors
end

.validate_version_compatibility(plugin_version, cabriolet_version) ⇒ Array<String>

Validate version compatibility

Checks if the plugin’s required Cabriolet version matches the current version.

Examples:

Compatible version

PluginValidator.validate_version_compatibility("~> 0.1", "0.1.0")
#=> []

Incompatible version

PluginValidator.validate_version_compatibility(">= 2.0", "0.1.0")
#=> ["Plugin requires Cabriolet version >= 2.0, ..."]

Parameters:

  • plugin_version (String)

    Required Cabriolet version

  • cabriolet_version (String)

    Current Cabriolet version

Returns:

  • (Array<String>)

    List of version errors (empty if compatible)



226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'lib/cabriolet/plugin_validator.rb', line 226

def validate_version_compatibility(plugin_version, cabriolet_version)
  errors = []

  # Parse version requirement
  if plugin_version.start_with?("~>")
    # Pessimistic version constraint
    required = plugin_version.sub("~>", "").strip
    unless version_compatible?(cabriolet_version, required, :pessimistic)
      errors << "Plugin requires Cabriolet version ~> #{required}, " \
                "but #{cabriolet_version} is installed"
    end
  elsif plugin_version.start_with?(">=")
    # Minimum version
    required = plugin_version.sub(">=", "").strip
    unless version_compatible?(cabriolet_version, required, :gte)
      errors << "Plugin requires Cabriolet version >= #{required}, " \
                "but #{cabriolet_version} is installed"
    end
  elsif plugin_version.start_with?("=")
    # Exact version
    required = plugin_version.sub("=", "").strip
    unless cabriolet_version == required
      errors << "Plugin requires exact Cabriolet version #{required}, " \
                "but #{cabriolet_version} is installed"
    end
  end

  errors
end