inspec-core 2.2.61 → 2.2.64

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -15
  3. data/README.md +0 -1
  4. data/docs/dev/plugins.md +321 -0
  5. data/docs/profiles.md +20 -18
  6. data/lib/bundles/inspec-artifact/cli.rb +1 -0
  7. data/lib/bundles/inspec-compliance/cli.rb +1 -0
  8. data/lib/bundles/inspec-habitat/cli.rb +1 -0
  9. data/lib/bundles/inspec-init/cli.rb +1 -0
  10. data/lib/bundles/inspec-supermarket/cli.rb +1 -0
  11. data/lib/inspec.rb +4 -2
  12. data/lib/inspec/base_cli.rb +1 -0
  13. data/lib/inspec/cli.rb +35 -16
  14. data/lib/inspec/control_eval_context.rb +7 -6
  15. data/lib/inspec/dependencies/requirement.rb +0 -1
  16. data/lib/inspec/fetcher.rb +1 -2
  17. data/lib/inspec/library_eval_context.rb +1 -1
  18. data/lib/inspec/plugin/v1.rb +2 -0
  19. data/lib/inspec/{plugins → plugin/v1/plugin_types}/cli.rb +2 -0
  20. data/lib/inspec/{plugins → plugin/v1/plugin_types}/fetcher.rb +1 -1
  21. data/lib/inspec/{plugins → plugin/v1/plugin_types}/resource.rb +0 -0
  22. data/lib/inspec/{plugins → plugin/v1/plugin_types}/secret.rb +1 -1
  23. data/lib/inspec/{plugins → plugin/v1/plugin_types}/source_reader.rb +1 -1
  24. data/lib/inspec/{plugins.rb → plugin/v1/plugins.rb} +7 -5
  25. data/lib/{utils/plugin_registry.rb → inspec/plugin/v1/registry.rb} +0 -0
  26. data/lib/inspec/plugin/v2.rb +30 -0
  27. data/lib/inspec/plugin/v2/activator.rb +16 -0
  28. data/lib/inspec/plugin/v2/loader.rb +204 -0
  29. data/lib/inspec/plugin/v2/plugin_base.rb +98 -0
  30. data/lib/inspec/plugin/v2/plugin_types/cli.rb +27 -0
  31. data/lib/inspec/plugin/v2/plugin_types/mock.rb +12 -0
  32. data/lib/inspec/plugin/v2/registry.rb +76 -0
  33. data/lib/inspec/plugin/v2/status.rb +29 -0
  34. data/lib/inspec/reporters.rb +5 -1
  35. data/lib/inspec/reporters/automate.rb +1 -1
  36. data/lib/inspec/reporters/{json_merged.rb → json_automate.rb} +1 -1
  37. data/lib/inspec/resource.rb +1 -1
  38. data/lib/inspec/rule.rb +14 -8
  39. data/lib/inspec/secrets.rb +1 -2
  40. data/lib/inspec/source_reader.rb +1 -2
  41. data/lib/inspec/version.rb +1 -1
  42. data/lib/resources/apache_conf.rb +1 -1
  43. metadata +20 -10
@@ -6,6 +6,7 @@ require 'pathname'
6
6
  require 'set'
7
7
  require 'tempfile'
8
8
  require 'yaml'
9
+ require 'inspec/base_cli'
9
10
 
10
11
  # Notes:
11
12
  #
@@ -4,6 +4,7 @@
4
4
 
5
5
  require 'thor'
6
6
  require 'erb'
7
+ require 'inspec/base_cli'
7
8
 
8
9
  module Compliance
9
10
  class ComplianceCLI < Inspec::BaseCLI
@@ -2,6 +2,7 @@
2
2
  # author: Adam Leff
3
3
 
4
4
  require 'thor'
5
+ require 'inspec/base_cli'
5
6
 
6
7
  module Habitat
7
8
  class HabitatProfileCLI < Thor
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'pathname'
4
4
  require_relative 'renderer'
5
+ require 'inspec/base_cli'
5
6
 
6
7
  module Init
7
8
  class CLI < Inspec::BaseCLI
@@ -1,6 +1,7 @@
1
1
  # encoding: utf-8
2
2
  # author: Christoph Hartmann
3
3
  # author: Dominik Richter
4
+ require 'inspec/base_cli'
4
5
 
5
6
  module Supermarket
6
7
  class SupermarketCLI < Inspec::BaseCLI
data/lib/inspec.rb CHANGED
@@ -16,9 +16,11 @@ require 'inspec/shell'
16
16
  require 'inspec/formatters'
17
17
  require 'inspec/reporters'
18
18
 
19
- # all utils that may be required by plugins
19
+ require 'inspec/plugin/v2'
20
+ require 'inspec/plugin/v1'
21
+
22
+ # all utils that may be required by legacy plugins
20
23
  require 'inspec/base_cli'
21
24
  require 'inspec/fetcher'
22
25
  require 'inspec/source_reader'
23
26
  require 'inspec/resource'
24
- require 'inspec/plugins'
@@ -168,6 +168,7 @@ module Inspec
168
168
  'documentation',
169
169
  'html',
170
170
  'json',
171
+ 'json-automate',
171
172
  'json-min',
172
173
  'json-rspec',
173
174
  'junit',
data/lib/inspec/cli.rb CHANGED
@@ -10,7 +10,8 @@ require 'pp'
10
10
  require 'utils/json_log'
11
11
  require 'utils/latest_version'
12
12
  require 'inspec/base_cli'
13
- require 'inspec/plugins'
13
+ require 'inspec/plugin/v1'
14
+ require 'inspec/plugin/v2'
14
15
  require 'inspec/runner_mock'
15
16
  require 'inspec/env_printer'
16
17
  require 'inspec/schema'
@@ -20,7 +21,7 @@ class Inspec::InspecCLI < Inspec::BaseCLI
20
21
  desc: 'Set the log level: info (default), debug, warn, error'
21
22
 
22
23
  class_option :log_location, type: :string,
23
- desc: 'Location to send diagnostic log messages to. (default: STDOUT or STDERR)'
24
+ desc: 'Location to send diagnostic log messages to. (default: STDOUT or Inspec::Log.error)'
24
25
 
25
26
  class_option :diagnose, type: :boolean,
26
27
  desc: 'Show diagnostics (versions, configurations)'
@@ -282,17 +283,35 @@ class Inspec::InspecCLI < Inspec::BaseCLI
282
283
  end
283
284
  end
284
285
 
285
- # Load all plugins on startup
286
- ctl = Inspec::PluginCtl.new
287
- ctl.list.each { |x| ctl.load(x) }
288
-
289
- # load CLI plugins before the Inspec CLI has been started
290
- Inspec::Plugins::CLI.subcommands.each { |_subcommand, params|
291
- Inspec::InspecCLI.register(
292
- params[:klass],
293
- params[:subcommand_name],
294
- params[:usage],
295
- params[:description],
296
- params[:options],
297
- )
298
- }
286
+ begin
287
+ # Load v2 plugins
288
+ v2_loader = Inspec::Plugin::V2::Loader.new
289
+ v2_loader.load_all
290
+ v2_loader.exit_on_load_error
291
+ v2_loader.activate_mentioned_cli_plugins
292
+
293
+ # Load v1 plugins on startup
294
+ ctl = Inspec::PluginCtl.new
295
+ ctl.list.each { |x| ctl.load(x) }
296
+
297
+ # load v1 CLI plugins before the Inspec CLI has been started
298
+ Inspec::Plugins::CLI.subcommands.each { |_subcommand, params|
299
+ Inspec::InspecCLI.register(
300
+ params[:klass],
301
+ params[:subcommand_name],
302
+ params[:usage],
303
+ params[:description],
304
+ params[:options],
305
+ )
306
+ }
307
+ rescue Inspec::Plugin::V2::Exception => v2ex
308
+ Inspec::Log.error v2ex.message
309
+
310
+ if ARGV.include?('--debug')
311
+ Inspec::Log.error v2ex.class.name
312
+ Inspec::Log.error v2ex.backtrace.join("\n")
313
+ else
314
+ Inspec::Log.error 'Run again with --debug for a stacktrace.'
315
+ end
316
+ exit 2
317
+ end
@@ -52,6 +52,7 @@ module Inspec
52
52
  @conf = conf
53
53
  @dependencies = dependencies
54
54
  @require_loader = require_loader
55
+ @skip_file_message = nil
55
56
  @skip_file = false
56
57
  @skip_only_if_eval = skip_only_if_eval
57
58
  end
@@ -118,18 +119,18 @@ module Inspec
118
119
 
119
120
  define_method :register_control do |control, &block|
120
121
  if @skip_file
121
- ::Inspec::Rule.set_skip_rule(control, true)
122
+ ::Inspec::Rule.set_skip_rule(control, true, @skip_file_message)
122
123
  end
123
124
 
124
125
  unless profile_context_owner.profile_supports_platform?
125
126
  platform = inspec.platform
126
127
  msg = "Profile #{profile_context_owner.profile_id} is not supported on platform #{platform.name}/#{platform.release}."
127
- ::Inspec::Rule.set_skip_rule(control, msg)
128
+ ::Inspec::Rule.set_skip_rule(control, true, msg)
128
129
  end
129
130
 
130
131
  unless profile_context_owner.profile_supports_inspec_version?
131
132
  msg = "Profile #{profile_context_owner.profile_id} is not supported on InSpec version (#{Inspec::VERSION})."
132
- ::Inspec::Rule.set_skip_rule(control, msg)
133
+ ::Inspec::Rule.set_skip_rule(control, true, msg)
133
134
  end
134
135
 
135
136
  profile_context_owner.register_rule(control, &block) unless control.nil?
@@ -144,19 +145,19 @@ module Inspec
144
145
  profile_context_owner.unregister_rule(id)
145
146
  end
146
147
 
147
- define_method :only_if do |&block|
148
+ define_method :only_if do |message = nil, &block|
148
149
  return unless block
149
150
  return if @skip_file == true
150
151
  return if @skip_only_if_eval == true
151
152
 
152
153
  return if block.yield == true
153
-
154
154
  # Apply `set_skip_rule` for other rules in the same file
155
155
  profile_context_owner.rules.values.each do |r|
156
156
  sources_match = r.source_file == block.source_location[0]
157
- Inspec::Rule.set_skip_rule(r, true) if sources_match
157
+ Inspec::Rule.set_skip_rule(r, true, message) if sources_match
158
158
  end
159
159
 
160
+ @skip_file_message = message
160
161
  @skip_file = true
161
162
  end
162
163
 
@@ -1,6 +1,5 @@
1
1
  # encoding: utf-8
2
2
  require 'inspec/cached_fetcher'
3
- require 'inspec/dependencies/dependency_set'
4
3
  require 'semverse'
5
4
 
6
5
  module Inspec
@@ -2,8 +2,7 @@
2
2
  # author: Dominik Richter
3
3
  # author: Christoph Hartmann
4
4
 
5
- require 'inspec/plugins'
6
- require 'utils/plugin_registry'
5
+ require 'inspec/plugin/v1'
7
6
 
8
7
  module Inspec
9
8
  class FetcherRegistry < PluginRegistry
@@ -1,7 +1,7 @@
1
1
  # encoding: utf-8
2
2
  # author: Steven Danna
3
3
  # author: Victoria Jeffrey
4
- require 'inspec/plugins/resource'
4
+ require 'inspec/plugin/v1/plugin_types/resource'
5
5
  require 'inspec/dsl_shared'
6
6
 
7
7
  module Inspec
@@ -0,0 +1,2 @@
1
+ require 'inspec/plugin/v1/plugins'
2
+ require 'inspec/plugin/v1/registry'
@@ -2,6 +2,8 @@
2
2
  # author: Christoph Hartmann
3
3
  # author: Dominik Richter
4
4
 
5
+ require 'inspec/plugin/v1/registry'
6
+
5
7
  module Inspec
6
8
  module Plugins
7
9
  # stores all CLI plugin, we expect those to the `Thor` subclasses
@@ -1,8 +1,8 @@
1
1
  # encoding: utf-8
2
2
  # author: Dominik Richter
3
3
  # author: Christoph Hartmann
4
- require 'utils/plugin_registry'
5
4
  require 'inspec/file_provider'
5
+ require 'inspec/plugin/v1/registry'
6
6
 
7
7
  module Inspec
8
8
  module Plugins
@@ -2,7 +2,7 @@
2
2
  # author: Dominik Richter
3
3
  # author: Christoph Hartmann
4
4
 
5
- require 'utils/plugin_registry'
5
+ require 'inspec/plugin/v1/registry'
6
6
 
7
7
  module Inspec
8
8
  module Plugins
@@ -2,7 +2,7 @@
2
2
  # author: Dominik Richter
3
3
  # author: Christoph Hartmann
4
4
 
5
- require 'utils/plugin_registry'
5
+ require 'inspec/plugin/v1/registry'
6
6
 
7
7
  module Inspec
8
8
  module Plugins
@@ -6,12 +6,14 @@ require 'forwardable'
6
6
 
7
7
  module Inspec
8
8
  # Resource Plugins
9
+ # NOTE: the autoloading here is rendered moot by the fact that
10
+ # all core plugins are `require`'d by the base inspec.rb
9
11
  module Plugins
10
- autoload :Resource, 'inspec/plugins/resource'
11
- autoload :CLI, 'inspec/plugins/cli'
12
- autoload :Fetcher, 'inspec/plugins/fetcher'
13
- autoload :SourceReader, 'inspec/plugins/source_reader'
14
- autoload :Secret, 'inspec/plugins/secret'
12
+ autoload :Resource, 'inspec/plugin/v1/plugin_types/resource'
13
+ autoload :CLI, 'inspec/plugin/v1/plugin_types/cli'
14
+ autoload :Fetcher, 'inspec/plugin/v1/plugin_types/fetcher'
15
+ autoload :SourceReader, 'inspec/plugin/v1/plugin_types/source_reader'
16
+ autoload :Secret, 'inspec/plugin/v1/plugin_types/secret'
15
17
  end
16
18
 
17
19
  # PLEASE NOTE: The Plugin system is an internal mechanism for connecting
@@ -0,0 +1,30 @@
1
+ require 'inspec/errors'
2
+
3
+ module Inspec
4
+ module Plugin
5
+ module V2
6
+ class Exception < Inspec::Error; end
7
+ class ConfigError < Inspec::Plugin::V2::Exception; end
8
+ class LoadError < Inspec::Plugin::V2::Exception; end
9
+ end
10
+ end
11
+ end
12
+
13
+ require_relative 'v2/registry'
14
+ require_relative 'v2/loader'
15
+ require_relative 'v2/plugin_base'
16
+
17
+ # Load all plugin type base classes
18
+ Dir.glob(File.join(__dir__, 'v2', 'plugin_types', '*.rb')).each { |file| require file }
19
+
20
+ module Inspec
21
+ # Provides the base class that plugin implementors should use.
22
+ def self.plugin(version, plugin_type = nil)
23
+ unless version == 2
24
+ raise 'Only plugins version 2 is supported!'
25
+ end
26
+
27
+ return Inspec::Plugin::V2::PluginBase if plugin_type.nil?
28
+ Inspec::Plugin::V2::PluginBase.base_class_for_type(plugin_type)
29
+ end
30
+ end
@@ -0,0 +1,16 @@
1
+ module Inspec::Plugin::V2
2
+ Activator = Struct.new(
3
+ :plugin_name,
4
+ :plugin_type,
5
+ :activator_name,
6
+ :activated,
7
+ :exception,
8
+ :activation_proc,
9
+ :implementation_class,
10
+ ) do
11
+ def initialize(*)
12
+ super
13
+ self[:activated] = false
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,204 @@
1
+ require 'json'
2
+ require 'inspec/log'
3
+
4
+ # Add the current directory of the process to the load path
5
+ $LOAD_PATH.unshift('.') unless $LOAD_PATH.include?('.')
6
+ # Add the InSpec source root directory to the load path
7
+ folder = File.expand_path(File.join('..', '..', '..', '..'), __dir__)
8
+ $LOAD_PATH.unshift(folder) unless $LOAD_PATH.include?('folder')
9
+
10
+ module Inspec::Plugin::V2
11
+ class Loader
12
+ attr_reader :registry, :options
13
+
14
+ def initialize(options = {})
15
+ @options = options
16
+ @registry = Inspec::Plugin::V2::Registry.instance
17
+ determine_plugin_conf_file
18
+ read_conf_file
19
+ unpack_conf_file
20
+ detect_bundled_plugins unless options[:omit_bundles]
21
+ end
22
+
23
+ def load_all
24
+ registry.each do |plugin_name, plugin_details|
25
+ # We want to capture literally any possible exception here, since we are storing them.
26
+ # rubocop: disable Lint/RescueException
27
+ begin
28
+ # We could use require, but under testing, we need to repeatedly reload the same
29
+ # plugin.
30
+ if plugin_details.entry_point.include?('test/unit/mock/plugins')
31
+ load plugin_details.entry_point + '.rb'
32
+ else
33
+ require plugin_details.entry_point
34
+ end
35
+ plugin_details.loaded = true
36
+ annotate_status_after_loading(plugin_name)
37
+ rescue ::Exception => ex
38
+ plugin_details.load_exception = ex
39
+ Inspec::Log.error "Could not load plugin #{plugin_name}"
40
+ end
41
+ # rubocop: enable Lint/RescueException
42
+ end
43
+ end
44
+
45
+ # This should possibly be in either lib/inspec/cli.rb or Registry
46
+ def exit_on_load_error
47
+ if registry.any_load_failures?
48
+ Inspec::Log.error 'Errors were encountered while loading plugins...'
49
+ registry.plugin_statuses.select(&:load_exception).each do |plugin_status|
50
+ Inspec::Log.error 'Plugin name: ' + plugin_status.name.to_s
51
+ Inspec::Log.error 'Error: ' + plugin_status.load_exception.message
52
+ if ARGV.include?('--debug')
53
+ Inspec::Log.error 'Exception: ' + plugin_status.load_exception.class.name
54
+ Inspec::Log.error 'Trace: ' + plugin_status.load_exception.backtrace.join("\n")
55
+ end
56
+ end
57
+ Inspec::Log.error('Run again with --debug for a stacktrace.') unless ARGV.include?('--debug')
58
+ exit 2
59
+ end
60
+ end
61
+
62
+ def activate(plugin_type, hook_name)
63
+ activator = registry.find_activators(plugin_type: plugin_type, activation_name: hook_name).first
64
+ # We want to capture literally any possible exception here, since we are storing them.
65
+ # rubocop: disable Lint/RescueException
66
+ begin
67
+ impl_class = activator.activation_proc.call
68
+ activator.activated = true
69
+ activator.implementation_class = impl_class
70
+ rescue Exception => ex
71
+ activator.exception = ex
72
+ Inspec::Log.error "Could not activate #{activator.plugin_type} hook named '#{activator.activator_name}' for plugin #{plugin_name}"
73
+ end
74
+ # rubocop: enable Lint/RescueException
75
+ end
76
+
77
+ def activate_mentioned_cli_plugins(cli_args = ARGV)
78
+ # Get a list of CLI plugin activation hooks
79
+ registry.find_activators(plugin_type: :cli_command).each do |act|
80
+ next if act.activated
81
+ # If there is anything in the CLI args with the same name, activate it
82
+ # If the word 'help' appears in the first position, load all CLI plugins
83
+ if cli_args.include?(act.activator_name.to_s) || cli_args[0] == 'help'
84
+ activate(:cli_command, act.activator_name)
85
+ act.implementation_class.register_with_thor
86
+ end
87
+ end
88
+ end
89
+
90
+ private
91
+
92
+ def annotate_status_after_loading(plugin_name)
93
+ status = registry[plugin_name]
94
+ return if status.api_generation == 2 # Gen2 have self-annotating superclasses
95
+ case status.installation_type
96
+ when :bundle
97
+ annotate_bundle_plugin_status_after_load(plugin_name)
98
+ else
99
+ # TODO: are there any other cases? can this whole thing be eliminated?
100
+ raise "I only know how to annotate :bundle plugins when trying to load plugin #{plugin_name}" unless status.installation_type == :bundle
101
+ end
102
+ end
103
+
104
+ def annotate_bundle_plugin_status_after_load(plugin_name)
105
+ # HACK: we're relying on the fact that all bundles are gen0 and cli type
106
+ status = registry[plugin_name]
107
+ status.api_generation = 0
108
+ act = Activator.new
109
+ act.activated = true
110
+ act.plugin_type = :cli_command
111
+ act.plugin_name = plugin_name
112
+ act.activator_name = :default
113
+ status.activators = [act]
114
+
115
+ v0_subcommand_name = plugin_name.to_s.gsub('inspec-', '')
116
+ status.plugin_class = Inspec::Plugins::CLI.subcommands[v0_subcommand_name][:klass]
117
+ end
118
+
119
+ def detect_bundled_plugins
120
+ bundle_dir = File.expand_path(File.join(File.dirname(__FILE__), '..', '..', '..', 'bundles'))
121
+ globs = [
122
+ File.join(bundle_dir, 'inspec-*.rb'),
123
+ File.join(bundle_dir, 'train-*.rb'),
124
+ ]
125
+ Dir.glob(globs).each do |loader_file|
126
+ name = File.basename(loader_file, '.rb').gsub(/^(inspec|train)-/, '')
127
+ status = Inspec::Plugin::V2::Status.new
128
+ status.name = name
129
+ status.entry_point = loader_file
130
+ status.installation_type = :bundle
131
+ status.loaded = false
132
+ registry[name] = status
133
+ end
134
+ end
135
+
136
+ def determine_plugin_conf_file
137
+ @plugin_conf_file_path = ENV['INSPEC_CONFIG_DIR'] ? ENV['INSPEC_CONFIG_DIR'] : File.join(Dir.home, '.inspec')
138
+ @plugin_conf_file_path = File.join(@plugin_conf_file_path, 'plugins.json')
139
+ end
140
+
141
+ def read_conf_file
142
+ if File.exist?(@plugin_conf_file_path)
143
+ @plugin_file_contents = JSON.parse(File.read(@plugin_conf_file_path))
144
+ else
145
+ @plugin_file_contents = {
146
+ 'plugins_config_version' => '1.0.0',
147
+ 'plugins' => [],
148
+ }
149
+ end
150
+ rescue JSON::ParserError => e
151
+ raise Inspec::Plugin::V2::ConfigError, "Failed to load plugins JSON configuration from #{@plugin_conf_file_path}:\n#{e}"
152
+ end
153
+
154
+ def unpack_conf_file
155
+ validate_conf_file
156
+ @plugin_file_contents['plugins'].each do |plugin_json|
157
+ status = Inspec::Plugin::V2::Status.new
158
+ status.name = plugin_json['name'].to_sym
159
+ status.loaded = false
160
+ status.installation_type = plugin_json['installation_type'].to_sym || :gem
161
+ case status.installation_type
162
+ when :gem
163
+ status.entry_point = status.name
164
+ status.version = plugin_json['version']
165
+ when :path
166
+ status.entry_point = plugin_json['installation_path']
167
+ end
168
+
169
+ registry[status.name] = status
170
+ end
171
+ end
172
+
173
+ def validate_conf_file
174
+ unless @plugin_file_contents['plugins_config_version'] == '1.0.0'
175
+ raise Inspec::Plugin::V2::ConfigError, "Unsupported plugins.json file version #{@plugin_file_contents['plugins_config_version']} at #{@plugin_conf_file_path} - currently support versions: 1.0.0"
176
+ end
177
+
178
+ plugin_entries = @plugin_file_contents['plugins']
179
+ unless plugin_entries.is_a?(Array)
180
+ raise Inspec::Plugin::V2::ConfigError, "Malformed plugins.json file - should have a top-level key named 'plugins', whose value is an array"
181
+ end
182
+
183
+ plugin_entries.each do |plugin_entry|
184
+ unless plugin_entry.is_a? Hash
185
+ raise Inspec::Plugin::V2::ConfigError, "Malformed plugins.json file - each 'plugins' entry should be a Hash / JSON object"
186
+ end
187
+
188
+ unless plugin_entry.key? 'name'
189
+ raise Inspec::Plugin::V2::ConfigError, "Malformed plugins.json file - each 'plugins' entry must have a 'name' field"
190
+ end
191
+
192
+ next unless plugin_entry.key?('installation_type')
193
+ unless %w{gem path}.include? plugin_entry['installation_type']
194
+ raise Inspec::Plugin::V2::ConfigError, "Malformed plugins.json file - each 'installation_type' must be one of 'gem' or 'path'"
195
+ end
196
+
197
+ next unless plugin_entry['installation_type'] == 'path'
198
+ unless plugin_entry.key?('installation_path')
199
+ raise Inspec::Plugin::V2::ConfigError, "Malformed plugins.json file - each 'plugins' entry with a 'path' installation_type must provide an 'installation_path' field"
200
+ end
201
+ end
202
+ end
203
+ end
204
+ end