inspec 2.2.112 → 2.3.4

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