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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +42 -19
- data/README.md +1 -1
- data/docs/dev/integration-testing.md +31 -0
- data/docs/dev/plugins.md +4 -2
- data/docs/dsl_inspec.md +104 -4
- data/docs/plugins.md +57 -0
- data/docs/style.md +178 -0
- data/examples/plugins/inspec-resource-lister/Gemfile +12 -0
- data/examples/plugins/inspec-resource-lister/LICENSE +13 -0
- data/examples/plugins/inspec-resource-lister/README.md +62 -0
- data/examples/plugins/inspec-resource-lister/Rakefile +40 -0
- data/examples/plugins/inspec-resource-lister/inspec-resource-lister.gemspec +45 -0
- data/examples/plugins/inspec-resource-lister/lib/inspec-resource-lister.rb +16 -0
- data/examples/plugins/inspec-resource-lister/lib/inspec-resource-lister/cli_command.rb +70 -0
- data/examples/plugins/inspec-resource-lister/lib/inspec-resource-lister/plugin.rb +55 -0
- data/examples/plugins/inspec-resource-lister/lib/inspec-resource-lister/version.rb +10 -0
- data/examples/plugins/inspec-resource-lister/test/fixtures/README.md +24 -0
- data/examples/plugins/inspec-resource-lister/test/functional/README.md +18 -0
- data/examples/plugins/inspec-resource-lister/test/functional/inspec_resource_lister_test.rb +110 -0
- data/examples/plugins/inspec-resource-lister/test/helper.rb +26 -0
- data/examples/plugins/inspec-resource-lister/test/unit/README.md +17 -0
- data/examples/plugins/inspec-resource-lister/test/unit/cli_args_test.rb +64 -0
- data/examples/plugins/inspec-resource-lister/test/unit/plugin_def_test.rb +51 -0
- data/examples/profile/controls/example.rb +9 -8
- data/inspec-core.gemspec +1 -1
- data/lib/inspec/attribute_registry.rb +1 -1
- data/lib/inspec/globals.rb +4 -0
- data/lib/inspec/objects/control.rb +18 -3
- data/lib/inspec/plugin/v2.rb +14 -3
- data/lib/inspec/plugin/v2/activator.rb +7 -2
- data/lib/inspec/plugin/v2/installer.rb +426 -0
- data/lib/inspec/plugin/v2/loader.rb +137 -30
- data/lib/inspec/plugin/v2/registry.rb +13 -4
- data/lib/inspec/profile.rb +2 -1
- data/lib/inspec/reporters/json.rb +11 -1
- data/lib/inspec/resource.rb +6 -15
- data/lib/inspec/rule.rb +18 -9
- data/lib/inspec/runner_rspec.rb +1 -1
- data/lib/inspec/schema.rb +1 -0
- data/lib/inspec/version.rb +1 -1
- data/lib/plugins/inspec-plugin-manager-cli/README.md +6 -0
- data/lib/plugins/inspec-plugin-manager-cli/lib/inspec-plugin-manager-cli.rb +18 -0
- data/lib/plugins/inspec-plugin-manager-cli/lib/inspec-plugin-manager-cli/cli_command.rb +420 -0
- data/lib/plugins/inspec-plugin-manager-cli/lib/inspec-plugin-manager-cli/plugin.rb +12 -0
- data/lib/plugins/inspec-plugin-manager-cli/test/fixtures/config_dirs/empty/.gitkeep +0 -0
- data/lib/plugins/inspec-plugin-manager-cli/test/fixtures/plugins/inspec-egg-white-omelette/lib/inspec-egg-white-omelette.rb +2 -0
- data/lib/plugins/inspec-plugin-manager-cli/test/fixtures/plugins/inspec-egg-white-omelette/lib/inspec-egg-white-omelette/.gitkeep +0 -0
- data/lib/plugins/inspec-plugin-manager-cli/test/fixtures/plugins/inspec-wrong-structure/.gitkeep +0 -0
- data/lib/plugins/inspec-plugin-manager-cli/test/fixtures/plugins/wrong-name/lib/wrong-name.rb +1 -0
- data/lib/plugins/inspec-plugin-manager-cli/test/fixtures/plugins/wrong-name/lib/wrong-name/.gitkeep +0 -0
- data/lib/plugins/inspec-plugin-manager-cli/test/functional/inspec-plugin_test.rb +651 -0
- data/lib/plugins/inspec-plugin-manager-cli/test/unit/cli_args_test.rb +71 -0
- data/lib/plugins/inspec-plugin-manager-cli/test/unit/plugin_def_test.rb +20 -0
- data/lib/plugins/shared/core_plugin_test_helper.rb +101 -2
- data/lib/plugins/things-for-train-integration.rb +14 -0
- data/lib/resources/port.rb +10 -6
- metadata +38 -11
- 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
|
8
|
-
impact 0.7
|
9
|
-
title "Create /tmp directory"
|
10
|
-
desc "An optional description..."
|
11
|
-
|
12
|
-
tag "
|
13
|
-
|
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
|
16
|
+
describe file('/tmp') do # The actual test
|
16
17
|
it { should be_directory }
|
17
18
|
end
|
18
19
|
end
|
data/inspec-core.gemspec
CHANGED
@@ -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.
|
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
|
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
|
data/lib/inspec/globals.rb
CHANGED
@@ -2,11 +2,12 @@
|
|
2
2
|
|
3
3
|
module Inspec
|
4
4
|
class Control
|
5
|
-
attr_accessor :id, :title, :
|
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
|
-
{
|
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
|
-
|
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)}") }
|
data/lib/inspec/plugin/v2.rb
CHANGED
@@ -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
|
-
|
14
|
-
|
15
|
-
|
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
|