inspec 2.2.112 → 2.3.4

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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +8 -2
  3. data/CHANGELOG.md +42 -19
  4. data/README.md +1 -1
  5. data/Rakefile +16 -3
  6. data/docs/dev/integration-testing.md +31 -0
  7. data/docs/dev/plugins.md +4 -2
  8. data/docs/dsl_inspec.md +104 -4
  9. data/docs/plugins.md +57 -0
  10. data/docs/resources/aws_ebs_volume.md.erb +76 -0
  11. data/docs/resources/aws_ebs_volumes.md.erb +86 -0
  12. data/docs/style.md +178 -0
  13. data/examples/plugins/inspec-resource-lister/Gemfile +12 -0
  14. data/examples/plugins/inspec-resource-lister/LICENSE +13 -0
  15. data/examples/plugins/inspec-resource-lister/README.md +62 -0
  16. data/examples/plugins/inspec-resource-lister/Rakefile +40 -0
  17. data/examples/plugins/inspec-resource-lister/inspec-resource-lister.gemspec +45 -0
  18. data/examples/plugins/inspec-resource-lister/lib/inspec-resource-lister.rb +16 -0
  19. data/examples/plugins/inspec-resource-lister/lib/inspec-resource-lister/cli_command.rb +70 -0
  20. data/examples/plugins/inspec-resource-lister/lib/inspec-resource-lister/plugin.rb +55 -0
  21. data/examples/plugins/inspec-resource-lister/lib/inspec-resource-lister/version.rb +10 -0
  22. data/examples/plugins/inspec-resource-lister/test/fixtures/README.md +24 -0
  23. data/examples/plugins/inspec-resource-lister/test/functional/README.md +18 -0
  24. data/examples/plugins/inspec-resource-lister/test/functional/inspec_resource_lister_test.rb +110 -0
  25. data/examples/plugins/inspec-resource-lister/test/helper.rb +26 -0
  26. data/examples/plugins/inspec-resource-lister/test/unit/README.md +17 -0
  27. data/examples/plugins/inspec-resource-lister/test/unit/cli_args_test.rb +64 -0
  28. data/examples/plugins/inspec-resource-lister/test/unit/plugin_def_test.rb +51 -0
  29. data/examples/profile/controls/example.rb +9 -8
  30. data/inspec.gemspec +2 -1
  31. data/lib/inspec/attribute_registry.rb +1 -1
  32. data/lib/inspec/globals.rb +4 -0
  33. data/lib/inspec/objects/control.rb +18 -3
  34. data/lib/inspec/plugin/v2.rb +14 -3
  35. data/lib/inspec/plugin/v2/activator.rb +7 -2
  36. data/lib/inspec/plugin/v2/installer.rb +426 -0
  37. data/lib/inspec/plugin/v2/loader.rb +137 -30
  38. data/lib/inspec/plugin/v2/registry.rb +13 -4
  39. data/lib/inspec/profile.rb +2 -1
  40. data/lib/inspec/reporters/json.rb +11 -1
  41. data/lib/inspec/resource.rb +6 -15
  42. data/lib/inspec/rule.rb +18 -9
  43. data/lib/inspec/runner_rspec.rb +1 -1
  44. data/lib/inspec/schema.rb +1 -0
  45. data/lib/inspec/version.rb +1 -1
  46. data/lib/plugins/inspec-plugin-manager-cli/README.md +6 -0
  47. data/lib/plugins/inspec-plugin-manager-cli/lib/inspec-plugin-manager-cli.rb +18 -0
  48. data/lib/plugins/inspec-plugin-manager-cli/lib/inspec-plugin-manager-cli/cli_command.rb +420 -0
  49. data/lib/plugins/inspec-plugin-manager-cli/lib/inspec-plugin-manager-cli/plugin.rb +12 -0
  50. data/lib/plugins/inspec-plugin-manager-cli/test/fixtures/config_dirs/empty/.gitkeep +0 -0
  51. data/lib/plugins/inspec-plugin-manager-cli/test/fixtures/plugins/inspec-egg-white-omelette/lib/inspec-egg-white-omelette.rb +2 -0
  52. data/lib/plugins/inspec-plugin-manager-cli/test/fixtures/plugins/inspec-egg-white-omelette/lib/inspec-egg-white-omelette/.gitkeep +0 -0
  53. data/lib/plugins/inspec-plugin-manager-cli/test/fixtures/plugins/inspec-wrong-structure/.gitkeep +0 -0
  54. data/lib/plugins/inspec-plugin-manager-cli/test/fixtures/plugins/wrong-name/lib/wrong-name.rb +1 -0
  55. data/lib/plugins/inspec-plugin-manager-cli/test/fixtures/plugins/wrong-name/lib/wrong-name/.gitkeep +0 -0
  56. data/lib/plugins/inspec-plugin-manager-cli/test/functional/inspec-plugin_test.rb +651 -0
  57. data/lib/plugins/inspec-plugin-manager-cli/test/unit/cli_args_test.rb +71 -0
  58. data/lib/plugins/inspec-plugin-manager-cli/test/unit/plugin_def_test.rb +20 -0
  59. data/lib/plugins/shared/core_plugin_test_helper.rb +101 -2
  60. data/lib/plugins/things-for-train-integration.rb +14 -0
  61. data/lib/resource_support/aws.rb +2 -0
  62. data/lib/resources/aws/aws_ebs_volume.rb +122 -0
  63. data/lib/resources/aws/aws_ebs_volumes.rb +63 -0
  64. data/lib/resources/port.rb +10 -6
  65. metadata +56 -11
  66. data/docs/ruby_usage.md +0 -204
@@ -4,15 +4,16 @@
4
4
  title '/tmp profile'
5
5
 
6
6
  # you add controls here
7
- control "tmp-1.0" do # A unique ID for this control
8
- impact 0.7 # The criticality, if this control fails.
9
- title "Create /tmp directory" # A human-readable title
10
- desc "An optional description..." # Describe why this is needed
11
- tag data: "temp data" # A tag allows you to associate key information
12
- tag "security" # to the test
13
- ref "Document A-12", url: 'http://...' # Additional references
7
+ control "tmp-1.0" do # A unique ID for this control
8
+ impact 0.7 # The criticality, if this control fails.
9
+ title "Create /tmp directory" # A human-readable title
10
+ desc "An optional description..." # Describe why this is needed
11
+ desc "label", "An optional description with a label" # Pair a part of the description with a label
12
+ tag data: "temp data" # A tag allows you to associate key information
13
+ tag "security" # to the test
14
+ ref "Document A-12", url: 'http://...' # Additional references
14
15
 
15
- describe file('/tmp') do # The actual test
16
+ describe file('/tmp') do # The actual test
16
17
  it { should be_directory }
17
18
  end
18
19
  end
data/inspec.gemspec CHANGED
@@ -26,7 +26,7 @@ Gem::Specification.new do |spec|
26
26
 
27
27
  spec.required_ruby_version = '>= 2.3'
28
28
 
29
- spec.add_dependency 'train', '~> 1.4', '>= 1.4.37'
29
+ spec.add_dependency 'train', '~> 1.5'
30
30
  spec.add_dependency 'thor', '~> 0.20'
31
31
  spec.add_dependency 'json', '>= 1.8', '< 3.0'
32
32
  spec.add_dependency 'method_source', '~> 0.8'
@@ -47,4 +47,5 @@ Gem::Specification.new do |spec|
47
47
  spec.add_dependency 'semverse'
48
48
  spec.add_dependency 'htmlentities'
49
49
  spec.add_dependency 'multipart-post'
50
+ spec.add_dependency 'term-ansicolor'
50
51
  end
@@ -51,7 +51,7 @@ module Inspec
51
51
  error = Inspec::AttributeRegistry::AttributeError.new
52
52
  error.attribute_name = name
53
53
  error.profile_name = profile
54
- raise error, "Profile '#{error.profile_name}' does not have a attribute with name '#{error.attribute_name}'"
54
+ raise error, "Profile '#{error.profile_name}' does not have an attribute with name '#{error.attribute_name}'"
55
55
  end
56
56
  list[profile][name]
57
57
  end
@@ -2,4 +2,8 @@ module Inspec
2
2
  def self.config_dir
3
3
  ENV['INSPEC_CONFIG_DIR'] ? ENV['INSPEC_CONFIG_DIR'] : File.join(Dir.home, '.inspec')
4
4
  end
5
+
6
+ def self.src_root
7
+ File.expand_path(File.join(__FILE__, '..', '..', '..'))
8
+ end
5
9
  end
@@ -2,11 +2,12 @@
2
2
 
3
3
  module Inspec
4
4
  class Control
5
- attr_accessor :id, :title, :desc, :impact, :tests, :tags, :refs
5
+ attr_accessor :id, :title, :descriptions, :impact, :tests, :tags, :refs
6
6
  def initialize
7
7
  @tests = []
8
8
  @tags = []
9
9
  @refs = []
10
+ @descriptions = {}
10
11
  end
11
12
 
12
13
  def add_test(t)
@@ -18,13 +19,27 @@ module Inspec
18
19
  end
19
20
 
20
21
  def to_hash
21
- { id: id, title: title, desc: desc, impact: impact, tests: tests.map(&:to_hash), tags: tags.map(&:to_hash) }
22
+ {
23
+ id: id,
24
+ title: title,
25
+ descriptions: descriptions,
26
+ impact: impact,
27
+ tests: tests.map(&:to_hash),
28
+ tags: tags.map(&:to_hash),
29
+ }
22
30
  end
23
31
 
24
32
  def to_ruby # rubocop:disable Metrics/AbcSize
25
33
  res = ["control #{id.inspect} do"]
26
34
  res.push " title #{title.inspect}" unless title.to_s.empty?
27
- res.push " desc #{prettyprint_text(desc, 2)}" unless desc.to_s.empty?
35
+ descriptions.each do |label, text|
36
+ if label == :default
37
+ next if text.nil? or text == '' # don't render empty/nil desc
38
+ res.push " desc #{prettyprint_text(text, 2)}"
39
+ else
40
+ res.push " desc #{label.to_s.inspect}, #{prettyprint_text(text, 2)}"
41
+ end
42
+ end
28
43
  res.push " impact #{impact}" unless impact.nil?
29
44
  tags.each { |t| res.push(indent(t.to_ruby, 2)) }
30
45
  refs.each { |t| res.push(" ref #{print_ref(t)}") }
@@ -6,13 +6,24 @@ module Inspec
6
6
  class Exception < Inspec::Error; end
7
7
  class ConfigError < Inspec::Plugin::V2::Exception; end
8
8
  class LoadError < Inspec::Plugin::V2::Exception; end
9
+ class GemActionError < Inspec::Plugin::V2::Exception
10
+ attr_accessor :plugin_name
11
+ attr_accessor :version
12
+ end
13
+ class InstallError < Inspec::Plugin::V2::GemActionError; end
14
+ class UpdateError < Inspec::Plugin::V2::GemActionError
15
+ attr_accessor :from_version, :to_version
16
+ end
17
+ class UnInstallError < Inspec::Plugin::V2::GemActionError; end
18
+ class SearchError < Inspec::Plugin::V2::GemActionError; end
9
19
  end
10
20
  end
11
21
  end
12
22
 
13
- require_relative 'v2/registry'
14
- require_relative 'v2/loader'
15
- require_relative 'v2/plugin_base'
23
+ require 'inspec/globals'
24
+ require 'inspec/plugin/v2/registry'
25
+ require 'inspec/plugin/v2/loader'
26
+ require 'inspec/plugin/v2/plugin_base'
16
27
 
17
28
  # Load all plugin type base classes
18
29
  Dir.glob(File.join(__dir__, 'v2', 'plugin_types', '*.rb')).each { |file| require file }
@@ -3,14 +3,19 @@ module Inspec::Plugin::V2
3
3
  :plugin_name,
4
4
  :plugin_type,
5
5
  :activator_name,
6
- :activated,
6
+ :'activated?',
7
7
  :exception,
8
8
  :activation_proc,
9
9
  :implementation_class,
10
10
  ) do
11
11
  def initialize(*)
12
12
  super
13
- self[:activated] = false
13
+ self[:'activated?'] = false
14
+ end
15
+
16
+ def activated?(new_value = nil)
17
+ return self[:'activated?'] if new_value.nil?
18
+ self[:'activated?'] = new_value
14
19
  end
15
20
  end
16
21
  end
@@ -0,0 +1,426 @@
1
+ # This file is not required by default.
2
+
3
+ require 'singleton'
4
+ require 'forwardable'
5
+ require 'fileutils'
6
+
7
+ # Gem extensions for doing unusual things - not loaded by Gem default
8
+ require 'rubygems/package'
9
+ require 'rubygems/name_tuple'
10
+ require 'rubygems/uninstaller'
11
+
12
+ module Inspec::Plugin::V2
13
+ # Handles all actions modifying the user's plugin set:
14
+ # * Modifying the plugins.json file
15
+ # * Installing, updating, and removing gem-based plugins
16
+ # Loading plugins is handled by Loader.
17
+ # Listing plugins is handled by Loader.
18
+ # Searching for plugins is handled by ???
19
+ class Installer
20
+ include Singleton
21
+ extend Forwardable
22
+
23
+ Gem.configuration['verbose'] = false
24
+
25
+ attr_reader :loader, :registry
26
+ def_delegator :loader, :plugin_gem_path, :gem_path
27
+ def_delegator :loader, :plugin_conf_file_path
28
+ def_delegator :loader, :list_managed_gems
29
+ def_delegator :loader, :list_installed_plugin_gems
30
+
31
+ def initialize
32
+ @loader = Inspec::Plugin::V2::Loader.new
33
+ @registry = Inspec::Plugin::V2::Registry.instance
34
+ end
35
+
36
+ def plugin_installed?(name)
37
+ list_installed_plugin_gems.detect { |spec| spec.name == name }
38
+ end
39
+
40
+ def plugin_version_installed?(name, version)
41
+ list_installed_plugin_gems.detect { |spec| spec.name == name && spec.version == Gem::Version.new(version) }
42
+ end
43
+
44
+ # Installs a plugin. Defaults to assuming the plugin provided is a gem, and will try to install
45
+ # from whatever gemsources `rubygems` thinks it should use.
46
+ # If it's a gem, installs it and its dependencies to the `gem_path`. The gem is not activated.
47
+ # If it's a path, leaves it in place.
48
+ # Finally, updates the plugins.json file with the new information.
49
+ # No attempt is made to load the plugin.
50
+ #
51
+ # @param [String] plugin_name
52
+ # @param [Hash] opts The installation options
53
+ # @option opts [String] :gem_file Path to a local gem file to install from
54
+ # @option opts [String] :path Path to a file to be used as the entry point for a path-based plugin
55
+ # @option opts [String] :version Version constraint for remote gem installs
56
+ def install(plugin_name, opts = {})
57
+ # TODO: - check plugins.json for validity before trying anything that needs to modify it.
58
+ validate_installation_opts(plugin_name, opts)
59
+
60
+ if opts[:path]
61
+ install_from_path(plugin_name, opts)
62
+ elsif opts[:gem_file]
63
+ install_from_gem_file(plugin_name, opts)
64
+ else
65
+ install_from_remote_gems(plugin_name, opts)
66
+ end
67
+
68
+ update_plugin_config_file(plugin_name, opts.merge({ action: :install }))
69
+ end
70
+
71
+ # Updates a plugin. Most options same as install, but will not handle path installs.
72
+ # If no :version is provided, updates to the latest.
73
+ # If a version is provided, the plugin becomes pinned at that specified version.
74
+ #
75
+ # @param [String] plugin_name
76
+ # @param [Hash] opts The installation options
77
+ # @option opts [String] :gem_file Reserved for future use. No effect.
78
+ # @option opts [String] :version Version constraint for remote gem updates
79
+ def update(plugin_name, opts = {})
80
+ # TODO: - check plugins.json for validity before trying anything that needs to modify it.
81
+ validate_update_opts(plugin_name, opts)
82
+ opts[:update_mode] = true
83
+
84
+ # TODO: Handle installing from a local file
85
+ # TODO: Perform dependency checks to make sure the new solution is valid
86
+ install_from_remote_gems(plugin_name, opts)
87
+
88
+ update_plugin_config_file(plugin_name, opts.merge({ action: :update }))
89
+ end
90
+
91
+ # Uninstalls (removes) a plugin. Refers to plugin.json to determine if it
92
+ # was a gem-based or path-based install.
93
+ # If it's a gem, uninstalls it, and all other unused plugins.
94
+ # If it's a path, removes the reference from the plugins.json, but does not
95
+ # tamper with the plugin source tree.
96
+ # Either way, the plugins.json file is updated with the new information.
97
+ #
98
+ # @param [String] plugin_name
99
+ # @param [Hash] opts The uninstallation options. Currently unused.
100
+ def uninstall(plugin_name, opts = {})
101
+ # TODO: - check plugins.json for validity before trying anything that needs to modify it.
102
+ validate_uninstall_opts(plugin_name, opts)
103
+
104
+ if registry.path_based_plugin?(plugin_name)
105
+ uninstall_via_path(plugin_name, opts)
106
+ else
107
+ uninstall_via_gem(plugin_name, opts)
108
+ end
109
+
110
+ update_plugin_config_file(plugin_name, opts.merge({ action: :uninstall }))
111
+ end
112
+
113
+ # Search rubygems.org for a plugin gem.
114
+ #
115
+ # @param [String] plugin_seach_term
116
+ # @param [Hash] opts Search options
117
+ # @option opts [TrueClass, FalseClass] :exact If true, use plugin_search_term exactly. If false (default), append a wildcard.
118
+ # @option opts [Symbol] :scope Which versions to search for. :released (default) - all released versions. :prerelease - Also include versioned marked prerelease. :latest - only return one version, the latest one.
119
+ # @return [Hash of Arrays] - Keys are String names of gems, arrays contain String versions.
120
+ def search(plugin_query, opts = {})
121
+ validate_search_opts(plugin_query, opts)
122
+
123
+ fetcher = Gem::SpecFetcher.fetcher
124
+ matched_tuples = []
125
+ if opts[:exact]
126
+ matched_tuples = fetcher.detect(opts[:scope]) { |tuple| tuple.name == plugin_query }
127
+ else
128
+ regex = Regexp.new('^' + plugin_query + '.*')
129
+ matched_tuples = fetcher.detect(opts[:scope]) do |tuple|
130
+ tuple.name != 'inspec-core' && tuple.name =~ regex
131
+ end
132
+ end
133
+
134
+ gem_info = {}
135
+ matched_tuples.each do |tuple|
136
+ gem_info[tuple.first.name] ||= []
137
+ gem_info[tuple.first.name] << tuple.first.version.to_s
138
+ end
139
+ gem_info
140
+ end
141
+
142
+ # Testing API. Performs a hard reset on the installer and registry, and reloads the loader.
143
+ # Not for public use.
144
+ # TODO: bad timing coupling in tests
145
+ def __reset
146
+ registry.__reset
147
+ end
148
+
149
+ def __reset_loader
150
+ @loader = Loader.new
151
+ end
152
+
153
+ private
154
+
155
+ #===================================================================#
156
+ # Validation Methods #
157
+ #===================================================================#
158
+
159
+ # rubocop: disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize
160
+ # rationale for rubocop exemption: While there are many conditionals, they are all of the same form;
161
+ # its goal is to check for several subtle combinations of params, and raise an error if needed. It's
162
+ # straightforward to understand, but has to handle many cases.
163
+ def validate_installation_opts(plugin_name, opts)
164
+ unless plugin_name =~ /^(inspec|train)-/
165
+ raise InstallError, "All inspec plugins must begin with either 'inspec-' or 'train-' - refusing to install #{plugin_name}"
166
+ end
167
+
168
+ if opts.key?(:gem_file) && opts.key?(:path)
169
+ raise InstallError, 'May not specify both gem_file and a path (for installing from source)'
170
+ end
171
+
172
+ if opts.key?(:version) && (opts.key?(:gem_file) || opts.key?(:path))
173
+ raise InstallError, 'May not specify a version when installing from a gem file or source path'
174
+ end
175
+
176
+ if opts.key?(:gem_file)
177
+ unless opts[:gem_file].end_with?('.gem')
178
+ raise InstallError, "When installing from a local gem file, gem file must have '.gem' extension - saw #{opts[:gem_file]}"
179
+ end
180
+ unless File.exist?(opts[:gem_file])
181
+ raise InstallError, "Could not find local gem file to install - #{opts[:gem_file]}"
182
+ end
183
+ elsif opts.key?(:path)
184
+ unless File.exist?(opts[:path])
185
+ raise InstallError, "Could not find path for install from source path - #{opts[:path]}"
186
+ end
187
+ end
188
+
189
+ if plugin_installed?(plugin_name)
190
+ if opts.key?(:version) && plugin_version_installed?(plugin_name, opts[:version])
191
+ raise InstallError, "#{plugin_name} version #{opts[:version]} is already installed."
192
+ else
193
+ raise InstallError, "#{plugin_name} is already installed. Use 'inspec plugin update' to change version."
194
+ end
195
+ end
196
+ end
197
+ # rubocop: enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize
198
+
199
+ def validate_update_opts(plugin_name, opts)
200
+ # Only update plugins we know about
201
+ unless plugin_name =~ /^(inspec|train)-/
202
+ raise UpdateError, "All inspec plugins must begin with either 'inspec-' or 'train-' - refusing to update #{plugin_name}"
203
+ end
204
+ unless registry.known_plugin?(plugin_name.to_sym)
205
+ raise UpdateError, "'#{plugin_name}' is not installed - use 'inspec plugin install' to install it"
206
+ end
207
+
208
+ # No local path support for update
209
+ if registry[plugin_name.to_sym].installation_type == :path
210
+ raise UpdateError, "'inspec plugin update' will not handle path-based plugins like '#{plugin_name}'. Use 'inspec plugin uninstall' to remove the reference, then install as a gem."
211
+ end
212
+ if opts.key?(:path)
213
+ raise UpdateError, "'inspec plugin update' will not install from a path."
214
+ end
215
+
216
+ if opts.key?(:version) && plugin_version_installed?(plugin_name, opts[:version])
217
+ raise UpdateError, "#{plugin_name} version #{opts[:version]} is already installed."
218
+ end
219
+ end
220
+
221
+ def validate_uninstall_opts(plugin_name, _opts)
222
+ # Only uninstall plugins we know about
223
+ unless plugin_name =~ /^(inspec|train)-/
224
+ raise UnInstallError, "All inspec plugins must begin with either 'inspec-' or 'train-' - refusing to uninstall #{plugin_name}"
225
+ end
226
+ unless registry.known_plugin?(plugin_name.to_sym)
227
+ raise UnInstallError, "'#{plugin_name}' is not installed, refusing to uninstall."
228
+ end
229
+ end
230
+
231
+ def validate_search_opts(search_term, opts)
232
+ unless search_term =~ /^(inspec|train)-/
233
+ raise SearchError, "All inspec plugins must begin with either 'inspec-' or 'train-'."
234
+ end
235
+
236
+ opts[:scope] ||= :released
237
+ unless [:prerelease, :released, :latest].include?(opts[:scope])
238
+ raise SearchError, 'Search scope for listing versons must be :prerelease, :released, or :latest.'
239
+ end
240
+ end
241
+
242
+ #===================================================================#
243
+ # Install / Upgrade Methods #
244
+ #===================================================================#
245
+
246
+ def install_from_path(requested_plugin_name, opts)
247
+ # Nothing to do here; we will later update the plugins file with the path.
248
+ end
249
+
250
+ def install_from_gem_file(requested_plugin_name, opts)
251
+ plugin_dependency = Gem::Dependency.new(requested_plugin_name)
252
+
253
+ # Make Set that encompasses just the gemfile that was provided
254
+ plugin_local_source = Gem::Source::SpecificFile.new(opts[:gem_file])
255
+ requested_local_gem_set = Gem::Resolver::InstallerSet.new(:both) # :both means local and remote; allow satisfying our gemfile's deps from rubygems.org
256
+ requested_local_gem_set.add_local(plugin_dependency.name, plugin_local_source.spec, plugin_local_source)
257
+
258
+ install_gem_to_plugins_dir(plugin_dependency, [requested_local_gem_set])
259
+ end
260
+
261
+ def install_from_remote_gems(requested_plugin_name, opts)
262
+ plugin_dependency = Gem::Dependency.new(requested_plugin_name, opts[:version] || '> 0')
263
+ # BestSet is rubygems.org API + indexing
264
+ install_gem_to_plugins_dir(plugin_dependency, [Gem::Resolver::BestSet.new], opts[:update_mode])
265
+ end
266
+
267
+ def install_gem_to_plugins_dir(new_plugin_dependency, extra_request_sets = [], update_mode = false)
268
+ # Get a list of all the gems available to us.
269
+ gem_to_force_update = update_mode ? new_plugin_dependency.name : nil
270
+ set_available_for_resolution = build_gem_request_universe(extra_request_sets, gem_to_force_update)
271
+
272
+ # Solve the dependency (that is, find a way to install the new plugin and anything it needs)
273
+ request_set = Gem::RequestSet.new(new_plugin_dependency)
274
+ begin
275
+ request_set.resolve(set_available_for_resolution)
276
+ rescue Gem::UnsatisfiableDependencyError => gem_ex
277
+ # TODO: use search facility to determine if the requested gem exists at all, vs if the constraints are impossible
278
+ ex = Inspec::Plugin::V2::InstallError.new(gem_ex.message)
279
+ ex.plugin_name = new_plugin_dependency.name
280
+ raise ex
281
+ end
282
+
283
+ # OK, perform the installation.
284
+ # Ignore deps here, because any needed deps should already be baked into new_plugin_dependency
285
+ request_set.install_into(gem_path, true, ignore_dependencies: true)
286
+
287
+ # Painful aspect of rubygems: the VendorSet request set type needs to be able to find a gemspec
288
+ # file within the source of the gem (and not all gems include it in their source tree; they are
289
+ # not obliged to during packaging.)
290
+ # So, after each install, run a scan for all gem(specs) we manage, and copy in their gemspec file
291
+ # into the exploded gem source area if absent.
292
+ loader.list_managed_gems.each do |spec|
293
+ path_inside_source = File.join(spec.gem_dir, "#{spec.name}.gemspec")
294
+ unless File.exist?(path_inside_source)
295
+ File.write(path_inside_source, spec.to_ruby)
296
+ end
297
+ end
298
+ end
299
+
300
+ #===================================================================#
301
+ # UnInstall Methods #
302
+ #===================================================================#
303
+
304
+ def uninstall_via_path(requested_plugin_name, opts)
305
+ # Nothing to do here; we will later update the plugins file to remove the plugin entry.
306
+ end
307
+
308
+ def uninstall_via_gem(plugin_name_to_be_removed, _opts)
309
+ # Strategy: excluding the plugin we want to uninstall, determine a gem install solution
310
+ # based on gems we already have, then remove anything not needed. This removes 3 kinds
311
+ # of cruft:
312
+ # 1. All versions of the unwanted plugin gem
313
+ # 2. All dependencies of the unwanted plugin gem (that aren't needed by something else)
314
+ # 3. All other gems installed under the ~/.inspec/gems area that are not needed
315
+ # by a plugin gem. TODO: ideally this would be a separate 'clean' operation.
316
+
317
+ # Create a list of plugins dependencies, including any version constraints,
318
+ # excluding any that are path-or-core-based, excluding the gem to be removed
319
+ plugin_deps_we_still_must_satisfy = registry.plugin_statuses
320
+ plugin_deps_we_still_must_satisfy = plugin_deps_we_still_must_satisfy.select do |status|
321
+ status.installation_type == :gem && status.name != plugin_name_to_be_removed.to_sym
322
+ end
323
+ plugin_deps_we_still_must_satisfy = plugin_deps_we_still_must_satisfy.map do |status|
324
+ constraint = status.version || '> 0'
325
+ Gem::Dependency.new(status.name.to_s, constraint)
326
+ end
327
+
328
+ # Make a Request Set representing the still-needed deps
329
+ request_set_we_still_must_satisfy = Gem::RequestSet.new(*plugin_deps_we_still_must_satisfy)
330
+ request_set_we_still_must_satisfy.remote = false
331
+
332
+ # Find out which gems we still actually need...
333
+ names_of_gems_we_actually_need = \
334
+ request_set_we_still_must_satisfy.resolve(build_gem_request_universe)
335
+ .map(&:full_spec).map(&:full_name)
336
+
337
+ # ... vs what we currently have, which should have some cruft
338
+ cruft_gem_specs = loader.list_managed_gems.reject do |spec|
339
+ names_of_gems_we_actually_need.include?(spec.full_name)
340
+ end
341
+
342
+ # Ok, delete the unneeded gems
343
+ cruft_gem_specs.each do |cruft_spec|
344
+ Gem::Uninstaller.new(
345
+ cruft_spec.name,
346
+ version: cruft_spec.version,
347
+ install_dir: gem_path,
348
+ # Docs on this class are poor. Next 4 are reasonable, but cargo-culted.
349
+ all: true,
350
+ executables: true,
351
+ force: true,
352
+ ignore: true,
353
+ ).uninstall_gem(cruft_spec)
354
+ end
355
+ end
356
+
357
+ #===================================================================#
358
+ # Utilities
359
+ #===================================================================#
360
+
361
+ # Provides a RequestSet (a set of gems representing the gems that are available to
362
+ # solve a dependency request) that represents a combination of:
363
+ # * the gems included in the system
364
+ # * the gems included in the inspec install
365
+ # * the currently installed gems in the ~/.inspec/gems directory
366
+ # * any other sets you provide
367
+ def build_gem_request_universe(extra_request_sets = [], gem_to_force_update = nil)
368
+ installed_plugins_gem_set = Gem::Resolver::VendorSet.new
369
+ loader.list_managed_gems.each do |spec|
370
+ next if spec.name == gem_to_force_update
371
+ installed_plugins_gem_set.add_vendor_gem(spec.name, spec.gem_dir)
372
+ end
373
+
374
+ # Combine the Sets, so the resolver has one composite place to look
375
+ Gem::Resolver.compose_sets(
376
+ installed_plugins_gem_set, # The gems that are in the plugin gem path directory tree
377
+ Gem::Resolver::CurrentSet.new, # The gems that are already included either with Ruby or with the InSpec install
378
+ *extra_request_sets, # Anything else our caller wanted to include
379
+ )
380
+ end
381
+
382
+ #===================================================================#
383
+ # plugins.json Maintenance Methods #
384
+ #===================================================================#
385
+
386
+ # TODO: refactor the plugin.json file to have its own class, which Installer consumes
387
+ def update_plugin_config_file(plugin_name, opts)
388
+ config = update_plugin_config_data(plugin_name, opts)
389
+ FileUtils.mkdir_p(Inspec.config_dir)
390
+ File.write(plugin_conf_file_path, JSON.pretty_generate(config))
391
+ end
392
+
393
+ # TODO: refactor the plugin.json file to have its own class, which Installer consumes
394
+ def update_plugin_config_data(plugin_name, opts)
395
+ config = read_or_init_config_data
396
+ config['plugins'].delete_if { |entry| entry['name'] == plugin_name }
397
+ return config if opts[:action] == :uninstall
398
+
399
+ entry = { 'name' => plugin_name }
400
+
401
+ # Parsing by Requirement handles lot of awkward formattoes
402
+ entry['version'] = Gem::Requirement.new(opts[:version]).to_s if opts.key?(:version)
403
+
404
+ if opts.key?(:path)
405
+ entry['installation_type'] = 'path'
406
+ entry['installation_path'] = opts[:path]
407
+ end
408
+
409
+ config['plugins'] << entry
410
+ config
411
+ end
412
+
413
+ # TODO: check for validity
414
+ # TODO: refactor the plugin.json file to have its own class, which Installer consumes
415
+ def read_or_init_config_data
416
+ if File.exist?(plugin_conf_file_path)
417
+ JSON.parse(File.read(plugin_conf_file_path))
418
+ else
419
+ {
420
+ 'plugins_config_version' => '1.0.0',
421
+ 'plugins' => [],
422
+ }
423
+ end
424
+ end
425
+ end
426
+ end