inspec-core 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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +42 -19
  3. data/README.md +1 -1
  4. data/docs/dev/integration-testing.md +31 -0
  5. data/docs/dev/plugins.md +4 -2
  6. data/docs/dsl_inspec.md +104 -4
  7. data/docs/plugins.md +57 -0
  8. data/docs/style.md +178 -0
  9. data/examples/plugins/inspec-resource-lister/Gemfile +12 -0
  10. data/examples/plugins/inspec-resource-lister/LICENSE +13 -0
  11. data/examples/plugins/inspec-resource-lister/README.md +62 -0
  12. data/examples/plugins/inspec-resource-lister/Rakefile +40 -0
  13. data/examples/plugins/inspec-resource-lister/inspec-resource-lister.gemspec +45 -0
  14. data/examples/plugins/inspec-resource-lister/lib/inspec-resource-lister.rb +16 -0
  15. data/examples/plugins/inspec-resource-lister/lib/inspec-resource-lister/cli_command.rb +70 -0
  16. data/examples/plugins/inspec-resource-lister/lib/inspec-resource-lister/plugin.rb +55 -0
  17. data/examples/plugins/inspec-resource-lister/lib/inspec-resource-lister/version.rb +10 -0
  18. data/examples/plugins/inspec-resource-lister/test/fixtures/README.md +24 -0
  19. data/examples/plugins/inspec-resource-lister/test/functional/README.md +18 -0
  20. data/examples/plugins/inspec-resource-lister/test/functional/inspec_resource_lister_test.rb +110 -0
  21. data/examples/plugins/inspec-resource-lister/test/helper.rb +26 -0
  22. data/examples/plugins/inspec-resource-lister/test/unit/README.md +17 -0
  23. data/examples/plugins/inspec-resource-lister/test/unit/cli_args_test.rb +64 -0
  24. data/examples/plugins/inspec-resource-lister/test/unit/plugin_def_test.rb +51 -0
  25. data/examples/profile/controls/example.rb +9 -8
  26. data/inspec-core.gemspec +1 -1
  27. data/lib/inspec/attribute_registry.rb +1 -1
  28. data/lib/inspec/globals.rb +4 -0
  29. data/lib/inspec/objects/control.rb +18 -3
  30. data/lib/inspec/plugin/v2.rb +14 -3
  31. data/lib/inspec/plugin/v2/activator.rb +7 -2
  32. data/lib/inspec/plugin/v2/installer.rb +426 -0
  33. data/lib/inspec/plugin/v2/loader.rb +137 -30
  34. data/lib/inspec/plugin/v2/registry.rb +13 -4
  35. data/lib/inspec/profile.rb +2 -1
  36. data/lib/inspec/reporters/json.rb +11 -1
  37. data/lib/inspec/resource.rb +6 -15
  38. data/lib/inspec/rule.rb +18 -9
  39. data/lib/inspec/runner_rspec.rb +1 -1
  40. data/lib/inspec/schema.rb +1 -0
  41. data/lib/inspec/version.rb +1 -1
  42. data/lib/plugins/inspec-plugin-manager-cli/README.md +6 -0
  43. data/lib/plugins/inspec-plugin-manager-cli/lib/inspec-plugin-manager-cli.rb +18 -0
  44. data/lib/plugins/inspec-plugin-manager-cli/lib/inspec-plugin-manager-cli/cli_command.rb +420 -0
  45. data/lib/plugins/inspec-plugin-manager-cli/lib/inspec-plugin-manager-cli/plugin.rb +12 -0
  46. data/lib/plugins/inspec-plugin-manager-cli/test/fixtures/config_dirs/empty/.gitkeep +0 -0
  47. data/lib/plugins/inspec-plugin-manager-cli/test/fixtures/plugins/inspec-egg-white-omelette/lib/inspec-egg-white-omelette.rb +2 -0
  48. data/lib/plugins/inspec-plugin-manager-cli/test/fixtures/plugins/inspec-egg-white-omelette/lib/inspec-egg-white-omelette/.gitkeep +0 -0
  49. data/lib/plugins/inspec-plugin-manager-cli/test/fixtures/plugins/inspec-wrong-structure/.gitkeep +0 -0
  50. data/lib/plugins/inspec-plugin-manager-cli/test/fixtures/plugins/wrong-name/lib/wrong-name.rb +1 -0
  51. data/lib/plugins/inspec-plugin-manager-cli/test/fixtures/plugins/wrong-name/lib/wrong-name/.gitkeep +0 -0
  52. data/lib/plugins/inspec-plugin-manager-cli/test/functional/inspec-plugin_test.rb +651 -0
  53. data/lib/plugins/inspec-plugin-manager-cli/test/unit/cli_args_test.rb +71 -0
  54. data/lib/plugins/inspec-plugin-manager-cli/test/unit/plugin_def_test.rb +20 -0
  55. data/lib/plugins/shared/core_plugin_test_helper.rb +101 -2
  56. data/lib/plugins/things-for-train-integration.rb +14 -0
  57. data/lib/resources/port.rb +10 -6
  58. metadata +38 -11
  59. data/docs/ruby_usage.md +0 -204
@@ -0,0 +1,26 @@
1
+ # Test helper file for example plugins
2
+
3
+ # This file's job is to collect any libraries needed for testing, as well as provide
4
+ # any utilities to make testing a plugin easier.
5
+
6
+ # InSpec core provides a number of such libraries and facilities, in the file
7
+ # lib/pligins/shared/core_plugin_test_helper.rb . So, one job in this file is
8
+ # to locate and load that file.
9
+ require 'inspec/../plugins/shared/core_plugin_test_helper'
10
+
11
+ # Also load the InSpec plugin system. We need this so we can unit-test the plugin
12
+ # classes, which will rely on the plugin system.
13
+ require 'inspec/plugin/v2'
14
+
15
+ # Caution: loading all of InSpec (i.e. require 'inspec') may cause interference with
16
+ # minitest/spec; one symptom would be appearing to have no tests.
17
+ # See https://github.com/inspec/inspec/issues/3380
18
+
19
+ # You can select from a number of test harnesses. Since InSpec uses Spec-style controls
20
+ # in profile code, you will probably want to use something like minitest/spec, which provides
21
+ # Spec-style tests.
22
+ require 'minitest/spec'
23
+ require 'minitest/autorun'
24
+
25
+ # You might want to put some debugging tools here. We run tests to find bugs, after all.
26
+ require 'byebug'
@@ -0,0 +1,17 @@
1
+ # Unit Testing Area for Example Plugins
2
+
3
+ ## What Example Tests are Provided?
4
+
5
+ Here, since this is a CliCommand plugin, we provide two sets of unit tests:
6
+
7
+ * plugin_def_test.rb - Would be useful in any plugin. Verifies that the plugin is properly detected and registered.
8
+ * cli_args_test.rb - Verifies that the expected commands are present, and that they have the expected options and args.
9
+
10
+ ## What are Unit Tests?
11
+
12
+ Unit tests are tests that verify that the individual components of your plugin work as intended. To be picked up by the Rake tasks as tests, each test file should end in `_test.rb`.
13
+
14
+ ## Unit vs Functional Tests
15
+
16
+ A practical difference between unit tests and functional tests is that unit tests all run within one process, while functional tests might exercise a CLI plugin by shelling out to an inspec command in a subprocess, and examining the results.
17
+
@@ -0,0 +1,64 @@
1
+ # This unit test performs some tests to verify that the command line options for
2
+ # inspec-resource-lister are correct.
3
+
4
+ # Include our test harness
5
+ require_relative '../helper'
6
+
7
+ # Load the class under test, the CliCommand definition.
8
+ require 'inspec-resource-lister/cli_command'
9
+
10
+ # Because InSpec is a Spec-style test suite, we're going to use MiniTest::Spec
11
+ # here, for familiar look and feel. However, this isn't InSpec (or RSpec) code.
12
+ describe InspecPlugins::ResourceLister::CliCommand do
13
+
14
+ # When writing tests, you can use `let` to create variables that you
15
+ # can reference easily.
16
+
17
+ # This is the CLI Command implementation class.
18
+ # It is a subclass of Thor, which is a CLI framework.
19
+ # This unit test file is mostly about verifying the Thor settings.
20
+ let(:cli_class) { InspecPlugins::ResourceLister::CliCommand }
21
+
22
+ # This is a Hash of Structs that tells us details of options for the 'core' subcommand.
23
+ let(:core_options) { cli_class.all_commands['core'].options }
24
+
25
+ # To group tests together, you can nest 'describe' in minitest/spec
26
+ # (that is discouraged in InSpec control code.)
27
+ describe 'the core command' do
28
+
29
+ # Some tests through here use minitest Expectations, which attach to all
30
+ # Objects, and begin with 'must' (positive) or 'wont' (negative)
31
+ # See https://ruby-doc.org/stdlib-2.1.0/libdoc/minitest/rdoc/MiniTest/Expectations.html
32
+
33
+ # Option count OK?
34
+ it "should take one option" do
35
+ core_options.count.must_equal(1)
36
+ end
37
+
38
+ # Summary option
39
+ describe "the summary option" do
40
+ it "should be present" do
41
+ core_options.keys.must_include(:summary)
42
+ end
43
+ it "should have a description" do
44
+ core_options[:summary].description.wont_be_nil
45
+ end
46
+ it "should not be required" do
47
+ core_options[:summary].required.wont_equal(true)
48
+ end
49
+ it "should have a single-letter alias" do
50
+ core_options[:summary].aliases.must_include(:s)
51
+ end
52
+ end
53
+
54
+ # Argument count
55
+ # The 'core' command takes one optional argument. According to the
56
+ # metaprogramming rules of Ruby, the core() method should thus have an
57
+ # arity of -1. See http://ruby-doc.org/core-2.5.1/Method.html#method-i-arity
58
+ # for how that number is caclulated.
59
+ it "should take one optional argument" do
60
+ cli_class.instance_method(:core).arity.must_equal(-1)
61
+ end
62
+
63
+ end
64
+ end
@@ -0,0 +1,51 @@
1
+ # This unit test performs some tests to verify that
2
+ # the inspec-resource-lister plugin is configured correctly.
3
+
4
+ # Include our test harness
5
+ require_relative '../helper'
6
+
7
+ # Load the class under test, the Plugin definition.
8
+ require 'inspec-resource-lister/plugin'
9
+
10
+ # Because InSpec is a Spec-style test suite, we're going to use MiniTest::Spec
11
+ # here, for familiar look and feel. However, this isn't InSpec (or RSpec) code.
12
+
13
+ describe InspecPlugins::ResourceLister::Plugin do
14
+
15
+ # When writing tests, you can use `let` to create variables that you
16
+ # can reference easily.
17
+
18
+ # Internally, plugins are always known by a Symbol name. Convert here.
19
+ let(:plugin_name) { :'inspec-resource-lister' }
20
+
21
+ # The Registry knows about all plugins that ship with InSpec by
22
+ # default, as well as any that are installed by the user. When a
23
+ # plugin definition is loaded, it will also self-register.
24
+ let(:registry) { Inspec::Plugin::V2::Registry.instance }
25
+
26
+ # The plugin status record tells us what the Registry knows.
27
+ # Note that you can use previously-defined 'let's.
28
+ let(:status) { registry[plugin_name] }
29
+
30
+ # OK, actual tests!
31
+
32
+ # Does the Registry know about us at all?
33
+ it "should be registered" do
34
+ registry.known_plugin?(plugin_name)
35
+ end
36
+
37
+ # Some tests through here use minitest Expectations, which attach to all
38
+ # Objects, and begin with 'must' (positive) or 'wont' (negative)
39
+ # See https://ruby-doc.org/stdlib-2.1.0/libdoc/minitest/rdoc/MiniTest/Expectations.html
40
+
41
+ # The plugin system had an undocumented v1 API; this should be a v2 example.
42
+ it "should be an api-v2 plugin" do
43
+ status.api_generation.must_equal(2)
44
+ end
45
+
46
+ # Plugins can support several different activator hooks, each of which has a type.
47
+ # Since this is (primarily) a CliCommand plugin, we'd expect to see that among our types.
48
+ it "should include a cli_command activator hook" do
49
+ status.plugin_types.must_include(:cli_command)
50
+ end
51
+ end
@@ -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
@@ -22,7 +22,7 @@ Gem::Specification.new do |spec|
22
22
 
23
23
  spec.required_ruby_version = '>= 2.3'
24
24
 
25
- spec.add_dependency 'train-core', '~> 1.4', '>= 1.4.37'
25
+ spec.add_dependency 'train-core', '~> 1.5'
26
26
  spec.add_dependency 'thor', '~> 0.20'
27
27
  spec.add_dependency 'json', '>= 1.8', '< 3.0'
28
28
  spec.add_dependency 'method_source', '~> 0.8'
@@ -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