inspec 0.29.0 → 0.30.0

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