inspec 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/.rubocop.yml +8 -2
- data/CHANGELOG.md +42 -19
- data/README.md +1 -1
- data/Rakefile +16 -3
- 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/resources/aws_ebs_volume.md.erb +76 -0
- data/docs/resources/aws_ebs_volumes.md.erb +86 -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.gemspec +2 -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/resource_support/aws.rb +2 -0
- data/lib/resources/aws/aws_ebs_volume.rb +122 -0
- data/lib/resources/aws/aws_ebs_volumes.rb +63 -0
- data/lib/resources/port.rb +10 -6
- metadata +56 -11
- 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
|
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.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.
|
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
|
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
|