inspec 0.29.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -2
  3. data/Rakefile +53 -0
  4. data/docs/cli.rst +442 -0
  5. data/examples/inheritance/inspec.yml +3 -0
  6. data/inspec.gemspec +1 -0
  7. data/lib/inspec/cli.rb +10 -1
  8. data/lib/inspec/completions/bash.sh.erb +45 -0
  9. data/lib/inspec/completions/zsh.sh.erb +61 -0
  10. data/lib/inspec/dependencies.rb +307 -0
  11. data/lib/inspec/dsl.rb +5 -20
  12. data/lib/inspec/env_printer.rb +149 -0
  13. data/lib/inspec/errors.rb +17 -0
  14. data/lib/inspec/metadata.rb +4 -0
  15. data/lib/inspec/profile.rb +12 -0
  16. data/lib/inspec/profile_context.rb +5 -2
  17. data/lib/inspec/shell.rb +7 -2
  18. data/lib/inspec/shell_detector.rb +90 -0
  19. data/lib/inspec/version.rb +1 -1
  20. data/lib/resources/postgres.rb +94 -12
  21. data/lib/resources/registry_key.rb +106 -27
  22. data/lib/utils/hash_map.rb +37 -0
  23. data/test/bench/startup.flat.txt +998 -0
  24. data/test/bench/startup.graph.html +71420 -0
  25. data/test/bench/startup.grind.dat +103554 -0
  26. data/test/bench/startup.stack.html +25015 -0
  27. data/test/bench/startup/startup.flat.txt +1005 -0
  28. data/test/bench/startup/startup.graph.html +71958 -0
  29. data/test/bench/startup/startup.grind.dat +101602 -0
  30. data/test/bench/startup/startup.stack.html +24516 -0
  31. data/test/cookbooks/os_prepare/metadata.rb +1 -0
  32. data/test/cookbooks/os_prepare/recipes/file.rb +5 -0
  33. data/test/cookbooks/os_prepare/recipes/registry_key.rb +13 -0
  34. data/test/docker_run.rb +3 -1
  35. data/test/functional/inheritance_test.rb +26 -13
  36. data/test/helper.rb +2 -2
  37. data/test/integration/default/file_spec.rb +16 -0
  38. data/test/integration/default/powershell_spec.rb +4 -1
  39. data/test/integration/default/registry_key_spec.rb +47 -4
  40. data/test/integration/default/secpol_spec.rb +4 -1
  41. data/test/integration/default/wmi_spec.rb +4 -1
  42. data/test/unit/mock/profiles/resource-tiny/inspec.yml +10 -0
  43. data/test/unit/mock/profiles/resource-tiny/libraries/resource.rb +3 -0
  44. data/test/unit/shell_detector_test.rb +78 -0
  45. metadata +47 -4
  46. data/docs/ctl_inspec.rst +0 -247
@@ -8,3 +8,6 @@ summary: Demonstrates the use of InSpec profile inheritance
8
8
  version: 1.0.0
9
9
  supports:
10
10
  - os-family: unix
11
+ depends:
12
+ - name: profile
13
+ path: ../profile
data/inspec.gemspec CHANGED
@@ -34,6 +34,7 @@ Gem::Specification.new do |spec|
34
34
  spec.add_dependency 'rspec-its', '~> 1.2'
35
35
  spec.add_dependency 'pry', '~> 0'
36
36
  spec.add_dependency 'hashie', '~> 3.4'
37
+ spec.add_dependency 'molinillo', '~> 0.5'
37
38
 
38
39
  spec.add_development_dependency 'mocha', '~> 1.1'
39
40
  end
data/lib/inspec/cli.rb CHANGED
@@ -4,12 +4,15 @@
4
4
  # author: Dominik Richter
5
5
  # author: Christoph Hartmann
6
6
 
7
+ require 'logger'
7
8
  require 'thor'
8
9
  require 'json'
9
10
  require 'pp'
10
11
  require 'utils/json_log'
11
12
  require 'inspec/base_cli'
13
+ require 'inspec/plugins'
12
14
  require 'inspec/runner_mock'
15
+ require 'inspec/env_printer'
13
16
 
14
17
  class Inspec::InspecCLI < Inspec::BaseCLI # rubocop:disable Metrics/ClassLength
15
18
  class_option :diagnose, type: :boolean,
@@ -152,7 +155,7 @@ class Inspec::InspecCLI < Inspec::BaseCLI # rubocop:disable Metrics/ClassLength
152
155
  desc 'shell', 'open an interactive debugging shell'
153
156
  target_options
154
157
  option :command, aliases: :c
155
- option :format, type: :string, default: Inspec::NoSummaryFormatter, hide: true
158
+ option :format, type: :string, default: nil, hide: true
156
159
  def shell_func
157
160
  diagnose
158
161
  o = opts.dup
@@ -170,6 +173,12 @@ class Inspec::InspecCLI < Inspec::BaseCLI # rubocop:disable Metrics/ClassLength
170
173
  $stderr.puts e.message
171
174
  end
172
175
 
176
+ desc 'env', 'Output shell-appropriate completion configuration'
177
+ def env(shell = nil)
178
+ p = Inspec::EnvPrinter.new(self.class, shell)
179
+ p.print_and_exit!
180
+ end
181
+
173
182
  desc 'version', 'prints the version of this tool'
174
183
  def version
175
184
  puts Inspec::VERSION
@@ -0,0 +1,45 @@
1
+ _inspec() {
2
+ local _inspec_top_level_commands="<%= top_level_commands.join(" ") %>"
3
+ <% subcommands_with_commands.each do |name, subcommands| -%>
4
+ local _inspec_<%= name %>_commands="<%= subcommands.join(" ") -%>"
5
+ <% end -%>
6
+
7
+ cur=${COMP_WORDS[COMP_CWORD]}
8
+ prev=${COMP_WORDS[COMP_CWORD-1]}
9
+
10
+ if [ "$COMP_CWORD" -eq 1 ]; then
11
+ case "$prev" in
12
+ inspec)
13
+ COMPREPLY=( $( compgen -W "$_inspec_top_level_commands" -- "$cur" ) )
14
+ ;;
15
+ esac
16
+ elif [ "$COMP_CWORD" -eq 2 ]; then
17
+ case "$prev" in
18
+ archive|check|exec|json)
19
+ COMPREPLY=( $( compgen -f -- "$cur" ) )
20
+ ;;
21
+ help)
22
+ COMPREPLY=( $( compgen -W "$_inspec_top_level_commands" -- "$cur" ) )
23
+ ;;
24
+ <% subcommands_with_commands.each do |name, subcommands| -%>
25
+ <%= name %>)
26
+ COMPREPLY=( $( compgen -W "$_inspec_<%= name %>_commands" -- "$cur" ) )
27
+ ;;
28
+ <% end -%>
29
+ esac
30
+ elif [ "$COMP_CWORD" -eq 3 ]; then
31
+ prev2=${COMP_WORDS[COMP_CWORD-2]}
32
+ case "$prev2-$prev" in
33
+ compliance-upload)
34
+ COMPREPLY=( $( compgen -f -- "$cur" ) )
35
+ ;;
36
+ <% subcommands_with_commands.each do |name, subcommands| -%>
37
+ <%= name %>-help)
38
+ COMPREPLY=( $( compgen -W "$_inspec_<%= name %>_commands" -- "$cur" ) )
39
+ ;;
40
+ <% end -%>
41
+ esac
42
+ fi
43
+ }
44
+
45
+ complete -F _inspec inspec
@@ -0,0 +1,61 @@
1
+ function _inspec() {
2
+ local curcontext="$curcontext" state line
3
+ typeset -A opt_args
4
+
5
+ local -a _top_level_commands <%= subcommands_with_commands_and_descriptions.keys.map {|i| "_#{i}_commands" }.join(' ') %>
6
+
7
+ _top_level_commands=(
8
+ <%= top_level_commands_with_descriptions.map {|i| " "*8 + "\"#{i}\"" }. join("\n") %>
9
+ )
10
+
11
+ <% subcommands_with_commands_and_descriptions.each do |name, entry| -%>
12
+ _<%= name %>_commands=(
13
+ <%= entry.map {|i| " "*8 + "\"#{i}\"" }.join("\n") %>
14
+ )
15
+
16
+ <% end -%>
17
+ _arguments '1:::->toplevel' && return 0
18
+ _arguments '2:::->subcommand' && return 0
19
+ _arguments '3:::->subsubcommand' && return 0
20
+
21
+ #
22
+ # Are you thinking? "Jeez, whoever wrote this really doesn't get
23
+ # zsh's completion system?" If so, you are correct. However, I
24
+ # have goodnews! Pull requests are accepted!
25
+ #
26
+ case $state in
27
+ toplevel)
28
+ _describe -t commands "InSpec subcommands" _top_level_commands
29
+ ;;
30
+ subcommand)
31
+ case "$words[2]" in
32
+ archive|check|exec|json)
33
+ _alternative 'files:filenames:_files'
34
+ ;;
35
+ help)
36
+ _describe -t commands "InSpec subcommands" _top_level_commands
37
+ ;;
38
+ <% subcommands_with_commands_and_descriptions.each do |name, entry| -%>
39
+ <%= name %>)
40
+ _describe -t <%= name %>_commands "InSpec <%= name -%> subcommands" _<%= name %>_commands
41
+ ;;
42
+ <% end -%>
43
+ esac
44
+ ;;
45
+ subsubcommand)
46
+ case "$words[2]-$words[3]" in
47
+ compliance-upload)
48
+ _alternative 'files:filenames:_files'
49
+ ;;
50
+ <% subcommands_with_commands_and_descriptions.each do |name, entry| -%>
51
+ <%= name %>-help)
52
+ _describe -t <%= name %>_commands "InSpec <%= name %> subcommands" _<%= name %>_commands
53
+ ;;
54
+ <% end -%>
55
+ esac
56
+
57
+ esac
58
+
59
+ }
60
+
61
+ compdef _inspec inspec
@@ -0,0 +1,307 @@
1
+ # encoding: utf-8
2
+ # author: Dominik Richter
3
+ # author: Christoph Hartmann
4
+
5
+ require 'logger'
6
+ require 'fileutils'
7
+ require 'molinillo'
8
+ require 'inspec/errors'
9
+
10
+ module Inspec
11
+ class Resolver
12
+ def self.resolve(requirements, vendor_index, cwd, opts = {})
13
+ reqs = requirements.map do |req|
14
+ Requirement.from_metadata(req, cwd: cwd) ||
15
+ fail("Cannot initialize dependency: #{req}")
16
+ end
17
+
18
+ new(vendor_index, opts).resolve(reqs)
19
+ end
20
+
21
+ def initialize(vendor_index, opts = {})
22
+ @logger = opts[:logger] || Logger.new(nil)
23
+ @debug_mode = false # TODO: hardcoded for now, grab from options
24
+
25
+ @vendor_index = vendor_index
26
+ @resolver = Molinillo::Resolver.new(self, self)
27
+ @search_cache = {}
28
+ end
29
+
30
+ # Resolve requirements.
31
+ #
32
+ # @param requirements [Array(Inspec::requirement)] Array of requirements
33
+ # @return [Array(String)] list of resolved dependency paths
34
+ def resolve(requirements)
35
+ requirements.each(&:pull)
36
+ @base_dep_graph = Molinillo::DependencyGraph.new
37
+ @dep_graph = @resolver.resolve(requirements, @base_dep_graph)
38
+ arr = @dep_graph.map(&:payload)
39
+ Hash[arr.map { |e| [e.name, e] }]
40
+ rescue Molinillo::VersionConflict => e
41
+ raise VersionConflict.new(e.conflicts.keys.uniq, e.message)
42
+ rescue Molinillo::CircularDependencyError => e
43
+ names = e.dependencies.sort_by(&:name).map { |d| "profile '#{d.name}'" }
44
+ raise CyclicDependencyError,
45
+ 'Your profile has requirements that depend on each other, creating '\
46
+ "an infinite loop. Please remove #{names.count > 1 ? 'either ' : ''} "\
47
+ "#{names.join(' or ')} and try again."
48
+ end
49
+
50
+ # --------------------------------------------------------------------------
51
+ # SpecificationProvider
52
+
53
+ # Search for the specifications that match the given dependency.
54
+ # The specifications in the returned array will be considered in reverse
55
+ # order, so the latest version ought to be last.
56
+ # @note This method should be 'pure', i.e. the return value should depend
57
+ # only on the `dependency` parameter.
58
+ #
59
+ # @param [Object] dependency
60
+ # @return [Array<Object>] the specifications that satisfy the given
61
+ # `dependency`.
62
+ def search_for(dep)
63
+ unless dep.is_a?(Inspec::Requirement)
64
+ fail 'Internal error: Dependency resolver requires an Inspec::Requirement object for #search_for(dependency)'
65
+ end
66
+ @search_cache[dep] ||= uncached_search_for(dep)
67
+ end
68
+
69
+ def uncached_search_for(dep)
70
+ # pre-cached and specified dependencies
71
+ return [dep] unless dep.profile.nil?
72
+
73
+ results = @vendor_index.find(dep)
74
+ return [] unless results.any?
75
+
76
+ # TODO: load dep from vendor index
77
+ # vertex = @dep_graph.vertex_named(dep.name)
78
+ # locked_requirement = vertex.payload.requirement if vertex
79
+ fail NotImplementedError, "load dependency #{dep} from vendor index"
80
+ end
81
+
82
+ # Returns the dependencies of `specification`.
83
+ # @note This method should be 'pure', i.e. the return value should depend
84
+ # only on the `specification` parameter.
85
+ #
86
+ # @param [Object] specification
87
+ # @return [Array<Object>] the dependencies that are required by the given
88
+ # `specification`.
89
+ def dependencies_for(specification)
90
+ specification.profile.metadata.dependencies
91
+ end
92
+
93
+ # Determines whether the given `requirement` is satisfied by the given
94
+ # `spec`, in the context of the current `activated` dependency graph.
95
+ #
96
+ # @param [Object] requirement
97
+ # @param [DependencyGraph] activated the current dependency graph in the
98
+ # resolution process.
99
+ # @param [Object] spec
100
+ # @return [Boolean] whether `requirement` is satisfied by `spec` in the
101
+ # context of the current `activated` dependency graph.
102
+ def requirement_satisfied_by?(requirement, _activated, spec)
103
+ requirement.matches_spec?(spec) || spec.is_a?(Inspec::Profile)
104
+ end
105
+
106
+ # Returns the name for the given `dependency`.
107
+ # @note This method should be 'pure', i.e. the return value should depend
108
+ # only on the `dependency` parameter.
109
+ #
110
+ # @param [Object] dependency
111
+ # @return [String] the name for the given `dependency`.
112
+ def name_for(dependency)
113
+ unless dependency.is_a?(Inspec::Requirement)
114
+ fail 'Internal error: Dependency resolver requires an Inspec::Requirement object for #name_for(dependency)'
115
+ end
116
+ dependency.name
117
+ end
118
+
119
+ # @return [String] the name of the source of explicit dependencies, i.e.
120
+ # those passed to {Resolver#resolve} directly.
121
+ def name_for_explicit_dependency_source
122
+ 'inspec.yml'
123
+ end
124
+
125
+ # @return [String] the name of the source of 'locked' dependencies, i.e.
126
+ # those passed to {Resolver#resolve} directly as the `base`
127
+ def name_for_locking_dependency_source
128
+ 'inspec.lock'
129
+ end
130
+
131
+ # Sort dependencies so that the ones that are easiest to resolve are first.
132
+ # Easiest to resolve is (usually) defined by:
133
+ # 1) Is this dependency already activated?
134
+ # 2) How relaxed are the requirements?
135
+ # 3) Are there any conflicts for this dependency?
136
+ # 4) How many possibilities are there to satisfy this dependency?
137
+ #
138
+ # @param [Array<Object>] dependencies
139
+ # @param [DependencyGraph] activated the current dependency graph in the
140
+ # resolution process.
141
+ # @param [{String => Array<Conflict>}] conflicts
142
+ # @return [Array<Object>] a sorted copy of `dependencies`.
143
+ def sort_dependencies(dependencies, activated, conflicts)
144
+ dependencies.sort_by do |dependency|
145
+ name = name_for(dependency)
146
+ [
147
+ activated.vertex_named(name).payload ? 0 : 1,
148
+ # amount_constrained(dependency), # TODO
149
+ conflicts[name] ? 0 : 1,
150
+ # activated.vertex_named(name).payload ? 0 : search_for(dependency).count, # TODO
151
+ ]
152
+ end
153
+ end
154
+
155
+ # Returns whether this dependency, which has no possible matching
156
+ # specifications, can safely be ignored.
157
+ #
158
+ # @param [Object] dependency
159
+ # @return [Boolean] whether this dependency can safely be skipped.
160
+ def allow_missing?(_dependency)
161
+ # TODO
162
+ false
163
+ end
164
+
165
+ # --------------------------------------------------------------------------
166
+ # UI
167
+
168
+ include Molinillo::UI
169
+
170
+ # The {IO} object that should be used to print output. `STDOUT`, by default.
171
+ #
172
+ # @return [IO]
173
+ def output
174
+ self
175
+ end
176
+
177
+ def print(what = '')
178
+ @logger.info(what)
179
+ end
180
+ alias puts print
181
+ end
182
+
183
+ class Package
184
+ def initialize(path, version)
185
+ @path = path
186
+ @version = version
187
+ end
188
+ end
189
+
190
+ class VendorIndex
191
+ attr_reader :list, :path
192
+ def initialize(path)
193
+ @path = path
194
+ FileUtils.mkdir_p(path) unless File.directory?(path)
195
+ @list = Dir[File.join(path, '*')].map { |x| load_path(x) }
196
+ end
197
+
198
+ def find(_dependency)
199
+ # TODO
200
+ fail NotImplementedError, '#find(dependency) on VendorIndex seeks implementation.'
201
+ end
202
+
203
+ private
204
+
205
+ def load_path(_path)
206
+ # TODO
207
+ fail NotImplementedError, '#load_path(path) on VendorIndex wants to be implemented.'
208
+ end
209
+ end
210
+
211
+ class Requirement
212
+ attr_reader :name, :dep, :cwd, :opts
213
+ def initialize(name, dep, cwd, opts)
214
+ @name = name
215
+ @dep = Gem::Dependency.new(name, Gem::Requirement.new(Array(dep)), :runtime)
216
+ @opts = opts
217
+ @cwd = cwd
218
+ end
219
+
220
+ def matches_spec?(spec)
221
+ params = spec.profile.metadata.params
222
+ @dep.match?(params[:name], params[:version])
223
+ end
224
+
225
+ def pull
226
+ case
227
+ when @opts[:path] then pull_path(@opts[:path])
228
+ else
229
+ # TODO: should default to supermarket
230
+ fail 'You must specify the source of the dependency (for now...)'
231
+ end
232
+ end
233
+
234
+ def path
235
+ @path || pull
236
+ end
237
+
238
+ def profile
239
+ return nil if path.nil?
240
+ @profile ||= Inspec::Profile.for_target(path, {})
241
+ end
242
+
243
+ def self.from_metadata(dep, opts)
244
+ fail 'Cannot load empty dependency.' if dep.nil? || dep.empty?
245
+ name = dep[:name] || fail('You must provide a name for all dependencies')
246
+ version = dep[:version]
247
+ new(name, version, opts[:cwd], dep)
248
+ end
249
+
250
+ def to_s
251
+ @dep.to_s
252
+ end
253
+
254
+ private
255
+
256
+ def pull_path(path)
257
+ abspath = File.absolute_path(path, @cwd)
258
+ fail "Dependency path doesn't exist: #{path}" unless File.exist?(abspath)
259
+ fail "Dependency path isn't a folder: #{path}" unless File.directory?(abspath)
260
+ @path = abspath
261
+ true
262
+ end
263
+ end
264
+
265
+ class SupermarketDependency
266
+ def initialize(url, requirement)
267
+ @url = url
268
+ @requirement = requirement
269
+ end
270
+
271
+ def self.load(dep)
272
+ return nil if dep.nil?
273
+ sname = dep[:supermarket]
274
+ return nil if sname.nil?
275
+ surl = dep[:supermarket_url] || 'default_url...'
276
+ requirement = dep[:version]
277
+ url = surl + '/' + sname
278
+ new(url, requirement)
279
+ end
280
+ end
281
+
282
+ class Dependencies
283
+ attr_reader :list, :vendor_path
284
+
285
+ # initialize
286
+ #
287
+ # @param cwd [String] current working directory for relative path includes
288
+ # @param vendor_path [String] path which contains vendored dependencies
289
+ # @return [dependencies] this
290
+ def initialize(cwd, vendor_path)
291
+ @cwd = cwd
292
+ @vendor_path = vendor_path || File.join(Dir.home, '.inspec', 'cache')
293
+ @list = nil
294
+ end
295
+
296
+ # 1. Get dependencies, pull things to a local cache if necessary
297
+ # 2. Resolve dependencies
298
+ #
299
+ # @param dependencies [Gem::Dependency] list of dependencies
300
+ # @return [nil]
301
+ def vendor(dependencies)
302
+ return if dependencies.nil? || dependencies.empty?
303
+ @vendor_index ||= VendorIndex.new(@vendor_path)
304
+ @list = Resolver.resolve(dependencies, @vendor_index, @cwd)
305
+ end
306
+ end
307
+ end