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
data/lib/inspec/dsl.rb CHANGED
@@ -6,12 +6,12 @@
6
6
 
7
7
  module Inspec::DSL
8
8
  def require_controls(id, &block)
9
- opts = { profile_id: id, include_all: false, backend: @backend, conf: @conf }
9
+ opts = { profile_id: id, include_all: false, backend: @backend, conf: @conf, dependencies: @dependencies }
10
10
  ::Inspec::DSL.load_spec_files_for_profile(self, opts, &block)
11
11
  end
12
12
 
13
13
  def include_controls(id, &block)
14
- opts = { profile_id: id, include_all: true, backend: @backend, conf: @conf }
14
+ opts = { profile_id: id, include_all: true, backend: @backend, conf: @conf, dependencies: @dependencies }
15
15
  ::Inspec::DSL.load_spec_files_for_profile(self, opts, &block)
16
16
  end
17
17
 
@@ -33,8 +33,9 @@ module Inspec::DSL
33
33
 
34
34
  def self.load_spec_files_for_profile(bind_context, opts, &block)
35
35
  # get all spec files
36
- target = get_reference_profile(opts[:profile_id], opts[:conf])
37
- profile = Inspec::Profile.for_target(target, opts)
36
+ target = opts[:dependencies].list[opts[:profile_id]] ||
37
+ fail("Can't find profile #{opts[:profile_id].inspect}, please add it as a dependency.")
38
+ profile = Inspec::Profile.for_target(target.path, opts)
38
39
  context = load_profile_context(opts[:profile_id], profile, opts)
39
40
 
40
41
  # if we don't want all the rules, then just make 1 pass to get all rule_IDs
@@ -62,22 +63,6 @@ module Inspec::DSL
62
63
  end
63
64
  end
64
65
 
65
- def self.get_reference_profile(id, opts)
66
- profiles_path = opts['profiles_path'] ||
67
- fail('You must supply a --profiles-path to inherit from other profiles.')
68
- abs_path = File.expand_path(profiles_path.to_s)
69
- unless File.directory? abs_path
70
- fail("Cannot find profiles path #{abs_path}")
71
- end
72
-
73
- id_path = File.join(abs_path, id)
74
- unless File.directory? id_path
75
- fail("Cannot find referenced profile #{id} in #{id_path}")
76
- end
77
-
78
- id_path
79
- end
80
-
81
66
  def self.load_profile_context(id, profile, opts)
82
67
  ctx = Inspec::ProfileContext.new(id, opts[:backend], opts[:conf])
83
68
  profile.libraries.each do |path, content|
@@ -0,0 +1,149 @@
1
+ # encoding: utf-8
2
+ require 'inspec/shell_detector'
3
+ require 'erb'
4
+ require 'shellwords'
5
+
6
+ module Inspec
7
+ class EnvPrinter
8
+ def initialize(command_class, shell = nil)
9
+ if !shell
10
+ @detected = true
11
+ @shell = Inspec::ShellDetector.new.shell
12
+ else
13
+ @shell = shell
14
+ end
15
+ @command_class = command_class
16
+ end
17
+
18
+ def print_and_exit!
19
+ exit_no_shell if !have_shell?
20
+ exit_no_completion if !have_shell_completion?
21
+
22
+ print_completion_for_shell
23
+ print_detection_warning($stdout) if @detected
24
+ print_usage_guidance
25
+ exit 0
26
+ end
27
+
28
+ private
29
+
30
+ def print_completion_for_shell
31
+ erb = ERB.new(File.read(completion_template_path), nil, '-')
32
+ puts erb.result(TemplateContext.new(@command_class).get_bindings)
33
+ end
34
+
35
+ def have_shell?
36
+ ! (@shell.nil? || @shell.empty?)
37
+ end
38
+
39
+ def have_shell_completion?
40
+ File.exist?(completion_template_path)
41
+ end
42
+
43
+ def completion_dir
44
+ File.join(File.dirname(__FILE__), 'completions')
45
+ end
46
+
47
+ def completion_template_path
48
+ File.join(completion_dir, "#{@shell}.sh.erb")
49
+ end
50
+
51
+ def shells_with_completions
52
+ Dir.glob("#{completion_dir}/*.sh.erb").map { |f| File.basename(f, '.sh.erb') }
53
+ end
54
+
55
+ def print_usage_guidance
56
+ puts <<EOF
57
+ # To use this, eval it in your shell
58
+ #
59
+ # eval "$(inspec env #{@shell})"
60
+ #
61
+ #
62
+ EOF
63
+ end
64
+
65
+ def print_detection_warning(device)
66
+ device.puts <<EOF
67
+ #
68
+ # The shell #{@shell} was auto-detected. If this is incorrect, please
69
+ # specify a shell explicitly by running:
70
+ #
71
+ # inspec env SHELLNAME
72
+ #
73
+ # Currently supported shells are: #{shells_with_completions.join(', ')}
74
+ #
75
+ EOF
76
+ end
77
+
78
+ def exit_no_completion
79
+ $stderr.puts "# No completion script for #{@shell}!"
80
+ print_detection_warning($stderr) if @detected
81
+ exit 1
82
+ end
83
+
84
+ def exit_no_shell
85
+ if @detected
86
+ $stderr.puts '# Unable to automatically detect shell and no shell was provided.'
87
+ end
88
+ $stderr.puts <<EOF
89
+ #
90
+ # Please provide the name of your shell via the command line:
91
+ #
92
+ # inspec env SHELLNAME
93
+ #
94
+ # Currently supported shells are: #{shells_with_completions.join(', ')}
95
+ EOF
96
+ exit 1
97
+ end
98
+
99
+ class TemplateContext
100
+ def initialize(command_class)
101
+ @command_class = command_class
102
+ end
103
+
104
+ def get_bindings # rubocop:disable Style/AccessorMethodName
105
+ binding
106
+ end
107
+
108
+ #
109
+ # The following functions all assume that @command_class
110
+ # is something that provides a Thor-like API
111
+ #
112
+ def top_level_commands
113
+ commands_for_thor_class(@command_class)
114
+ end
115
+
116
+ def top_level_commands_with_descriptions
117
+ descript_lines_for_class(@command_class)
118
+ end
119
+
120
+ def subcommands_with_commands
121
+ ret = {}
122
+ @command_class.subcommand_classes.each do |k, v|
123
+ ret[k] = commands_for_thor_class(v)
124
+ end
125
+ ret
126
+ end
127
+
128
+ def subcommands_with_commands_and_descriptions
129
+ ret = {}
130
+ @command_class.subcommand_classes.each do |k, v|
131
+ ret[k] = descript_lines_for_class(v)
132
+ end
133
+ ret
134
+ end
135
+
136
+ def commands_for_thor_class(thor_class)
137
+ thor_class.all_commands.values.map { |c| c.usage.split.first }
138
+ end
139
+
140
+ def descript_lines_for_class(thor_class)
141
+ thor_class.all_commands.values.map { |c| descript_line_for_command(c) }
142
+ end
143
+
144
+ def descript_line_for_command(c)
145
+ "#{c.usage.split.first}:#{Shellwords.escape(c.description)}"
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,17 @@
1
+ # encoding: utf-8
2
+ # author: Dominik Richter
3
+ # author: Christoph Hartmann
4
+
5
+ module Inspec
6
+ class Error < StandardError; end
7
+
8
+ # dependency resolution
9
+ class CyclicDependencyError < Error; end
10
+ class VersionConflict < Error
11
+ attr_reader :conflicts
12
+ def initialize(conflicts, msg = nil)
13
+ super(msg)
14
+ @conflicts = conflicts
15
+ end
16
+ end
17
+ end
@@ -36,6 +36,10 @@ module Inspec
36
36
  end
37
37
  end
38
38
 
39
+ def dependencies
40
+ params[:depends] || []
41
+ end
42
+
39
43
  def supports(sth, version = nil)
40
44
  # Ignore supports with metadata.rb. This file is legacy and the way it
41
45
  # it handles `supports` deprecated. A deprecation warning will be printed
@@ -8,6 +8,7 @@ require 'inspec/polyfill'
8
8
  require 'inspec/fetcher'
9
9
  require 'inspec/source_reader'
10
10
  require 'inspec/metadata'
11
+ require 'inspec/dependencies'
11
12
 
12
13
  module Inspec
13
14
  class Profile # rubocop:disable Metrics/ClassLength
@@ -206,6 +207,10 @@ module Inspec
206
207
  true
207
208
  end
208
209
 
210
+ def locked_dependencies
211
+ @locked_dependencies ||= load_dependencies
212
+ end
213
+
209
214
  private
210
215
 
211
216
  # Create an archive name for this profile and an additional options
@@ -290,5 +295,12 @@ module Inspec
290
295
  }
291
296
  groups[file][:controls].push(id)
292
297
  end
298
+
299
+ def load_dependencies
300
+ cwd = File.directory?(@target) ? @target : nil
301
+ res = Inspec::Dependencies.new(cwd, nil)
302
+ res.vendor(metadata.dependencies)
303
+ res
304
+ end
293
305
  end
294
306
  end
@@ -22,6 +22,8 @@ module Inspec
22
22
  @backend = backend
23
23
  @conf = conf.dup
24
24
  @rules = {}
25
+ @dependencies = {}
26
+ @dependencies = conf['profile'].locked_dependencies unless conf['profile'].nil?
25
27
  @require_loader = ::Inspec::RequireLoader.new
26
28
  @attributes = []
27
29
  reload_dsl
@@ -30,7 +32,7 @@ module Inspec
30
32
  def reload_dsl
31
33
  resources_dsl = Inspec::Resource.create_dsl(@backend)
32
34
  ctx = create_context(resources_dsl, rule_context(resources_dsl))
33
- @profile_context = ctx.new(@backend, @conf, @require_loader)
35
+ @profile_context = ctx.new(@backend, @conf, @dependencies, @require_loader)
34
36
  end
35
37
 
36
38
  def load_libraries(libs)
@@ -136,9 +138,10 @@ module Inspec
136
138
  include Inspec::DSL
137
139
  include resources_dsl
138
140
 
139
- def initialize(backend, conf, require_loader) # rubocop:disable Lint/NestedMethodDefinition, Lint/DuplicateMethods
141
+ def initialize(backend, conf, dependencies, require_loader) # rubocop:disable Lint/NestedMethodDefinition, Lint/DuplicateMethods
140
142
  @backend = backend
141
143
  @conf = conf
144
+ @dependencies = dependencies
142
145
  @require_loader = require_loader
143
146
  @skip_profile = false
144
147
  end
data/lib/inspec/shell.rb CHANGED
@@ -32,7 +32,7 @@ module Inspec
32
32
 
33
33
  # configure pry shell prompt
34
34
  Pry.config.prompt_name = 'inspec'
35
- Pry.prompt = [proc { "\e[0;32m#{Pry.config.prompt_name}>\e[0m " }]
35
+ Pry.prompt = [proc { "#{readline_ignore("\e[0;32m")}#{Pry.config.prompt_name}> #{readline_ignore("\e[0m")}" }]
36
36
 
37
37
  # Add a help menu as the default intro
38
38
  Pry.hooks.add_hook(:before_session, :intro) do
@@ -40,8 +40,12 @@ module Inspec
40
40
  end
41
41
  end
42
42
 
43
+ def readline_ignore(code)
44
+ "\001#{code}\002"
45
+ end
46
+
43
47
  def mark(x)
44
- "\033[1m#{x}\033[0m"
48
+ "#{readline_ignore("\033[1m")}#{x}#{readline_ignore("\033[0m")}"
45
49
  end
46
50
 
47
51
  def print_example(example)
@@ -83,6 +87,7 @@ You can use resources in this environment to test the target machine. For exampl
83
87
 
84
88
  You are currently running on:
85
89
 
90
+ OS platform: #{mark ctx.os[:name] || 'unknown'}
86
91
  OS family: #{mark ctx.os[:family] || 'unknown'}
87
92
  OS release: #{mark ctx.os[:release] || 'unknown'}
88
93
 
@@ -0,0 +1,90 @@
1
+ # encoding: utf-8
2
+ require 'etc'
3
+ require 'rbconfig'
4
+
5
+ module Inspec
6
+ #
7
+ # ShellDetector attempts to detect the shell the invoking user is
8
+ # running by checking:
9
+ #
10
+ # - The command of our parent
11
+ # - The SHELL environment variable
12
+ # - The shell returned by getpwuid for our process UID
13
+ #
14
+ # Since none of these methods is fullproof, the detected shell is
15
+ # verified against a list of known shells before being returned to
16
+ # the caller.
17
+ #
18
+ class ShellDetector
19
+ NOT_DETECTED = Object.new.freeze
20
+ KNOWN_SHELLS = %w{bash zsh ksh csh sh fish}.freeze
21
+
22
+ def initialize
23
+ @shell = NOT_DETECTED
24
+ end
25
+
26
+ def shell
27
+ @shell = detect if !detected?(@shell)
28
+ @shell
29
+ end
30
+
31
+ def shell!
32
+ @shell = detect
33
+ end
34
+
35
+ private
36
+
37
+ def detect
38
+ # Most of our detection code assumes a unix-like environment
39
+ return nil if RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/
40
+
41
+ shellpath = detect_by_ppid
42
+
43
+ if shellpath.nil? || shellpath.empty? || !known_shell?(shellpath)
44
+ shellpath = detect_by_env
45
+ end
46
+
47
+ if shellpath.nil? || shellpath.empty? || !known_shell?(shellpath)
48
+ shellpath = detect_by_getpwuid
49
+ end
50
+
51
+ shellname(shellpath) if known_shell?(shellpath)
52
+ end
53
+
54
+ def detected?(arg)
55
+ arg != NOT_DETECTED
56
+ end
57
+
58
+ def detect_by_ppid
59
+ ppid = Process.ppid
60
+ if Dir.exist?('/proc')
61
+ File.readlink("/proc/#{ppid}/exe")
62
+ else
63
+ `ps -cp #{ppid} -o command=`.chomp
64
+ end
65
+ end
66
+
67
+ def detect_by_env
68
+ ENV['SHELL']
69
+ end
70
+
71
+ def detect_by_getpwuid
72
+ Etc.getpwuid(Process.uid).shell
73
+ end
74
+
75
+ #
76
+ # Strip any leading path elements
77
+ #
78
+ def shellname(shellpath)
79
+ shellpath.split('/').last
80
+ end
81
+
82
+ #
83
+ # Only return shells that we know about, just to be sure we never
84
+ # do anything very silly.
85
+ #
86
+ def known_shell?(shell)
87
+ KNOWN_SHELLS.include?(shellname(shell))
88
+ end
89
+ end
90
+ end
@@ -3,5 +3,5 @@
3
3
  # author: Christoph Hartmann
4
4
 
5
5
  module Inspec
6
- VERSION = '0.29.0'.freeze
6
+ VERSION = '0.30.0'.freeze
7
7
  end
@@ -12,29 +12,111 @@ module Inspec::Resources
12
12
  def initialize
13
13
  os = inspec.os
14
14
  if os.debian?
15
- @service = 'postgresql'
16
- @data_dir = '/var/lib/postgresql'
17
- @version = inspec.command('ls /etc/postgresql/').stdout.chomp
18
- @conf_dir = "/etc/postgresql/#{@version}/main"
15
+ #
16
+ # https://wiki.debian.org/PostgreSql
17
+ #
18
+ # Debian allows multiple versions of postgresql to be
19
+ # installed as well as multiple "clusters" to be configured.
20
+ #
21
+ version = version_from_dir('/etc/postgresql')
22
+ cluster = cluster_from_dir("/etc/postgresql/#{version}")
23
+ @conf_dir = "/etc/postgresql/#{version}/#{cluster}"
24
+ @data_dir = "/var/lib/postgresql/#{version}/#{cluster}"
19
25
  elsif os.redhat?
20
- @service = 'postgresql'
21
- @version = inspec.command('ls /var/lib/pgsql/').stdout.chomp
22
- @data_dir = "/var/lib/pgsql/#{@version}/data"
26
+ #
27
+ # /var/lib/pgsql/data is the default data directory on RHEL6
28
+ # and RHEL7. However, PR #824 explicitly added version-based
29
+ # directories. Thus, we call #version_from_dir unless it looks
30
+ # like we are using unversioned directories.
31
+ #
32
+ # TODO(ssd): This has the potential to be noisy because of the
33
+ # warning in version_from_dir. We should determine which case
34
+ # is more common and only warn in the less common case.
35
+ #
36
+ version = if inspec.directory('/var/lib/pgsql/data').exist?
37
+ warn 'Found /var/lib/pgsql/data. Assuming postgresql install uses un-versioned directories.'
38
+ nil
39
+ else
40
+ version_from_dir('/var/lib/pgsql/')
41
+ end
42
+
43
+ @data_dir = File.join('/var/lib/pgsql/', version.to_s, 'data')
23
44
  elsif os[:name] == 'arch'
24
- @service = 'postgresql'
45
+ #
46
+ # https://wiki.archlinux.org/index.php/PostgreSQL
47
+ #
48
+ # The archlinux wiki points to /var/lib/postgresql/data as the
49
+ # main data directory.
50
+ #
25
51
  @data_dir = '/var/lib/postgres/data'
26
- @conf_dir = '/var/lib/postgres/data'
27
52
  else
28
- @service = 'postgresql'
29
- @data_dir = '/var/lib/postgresql'
30
- @conf_dir = '/var/lib/pgsql/data'
53
+ #
54
+ # According to https://www.postgresql.org/docs/9.5/static/creating-cluster.html
55
+ #
56
+ # > There is no default, although locations such as
57
+ # > /usr/local/pgsql/data or /var/lib/pgsql/data are popular.
58
+ #
59
+ @data_dir = '/var/lib/pgsql/data'
31
60
  end
32
61
 
62
+ @service = 'postgresql'
63
+ @conf_dir ||= @data_dir
64
+ verify_dirs
33
65
  @conf_path = File.join @conf_dir, 'postgresql.conf'
34
66
  end
35
67
 
36
68
  def to_s
37
69
  'PostgreSQL'
38
70
  end
71
+
72
+ private
73
+
74
+ def verify_dirs
75
+ if !inspec.directory(@conf_dir).exist?
76
+ warn "Default postgresql configuration directory: #{@conf_dir} does not exist. Postgresql may not be installed or we've misidentified the configuration directory."
77
+ end
78
+
79
+ if !inspec.directory(@data_dir).exist?
80
+ warn "Default postgresql data directory: #{@data_dir} does not exist. Postgresql may not be installed or we've misidentified the data directory."
81
+ end
82
+ end
83
+
84
+ def version_from_dir(dir)
85
+ dirs = inspec.command("ls -d #{dir}/*/").stdout
86
+ entries = dirs.lines.count
87
+ case entries
88
+ when 0
89
+ warn "Could not determine version of installed postgresql by inspecting #{dir}"
90
+ nil
91
+ when 1
92
+ warn "Using #{dirs}: #{dir_to_version(dirs)}"
93
+ dir_to_version(dirs)
94
+ else
95
+ warn "Multiple versions of postgresql installed or incorrect base dir #{dir}"
96
+ first = dir_to_version(dirs.lines.first)
97
+ warn "Using the first version found: #{first}"
98
+ first
99
+ end
100
+ end
101
+
102
+ def dir_to_version(dir)
103
+ dir.chomp.split('/').last
104
+ end
105
+
106
+ def cluster_from_dir(dir)
107
+ # Main is the default cluster name on debian use it if it
108
+ # exists.
109
+ if inspec.directory("#{dir}/main").exist?
110
+ 'main'
111
+ else
112
+ dirs = inspec.command("ls -d #{dir}/*/").stdout.lines
113
+ first = dirs.first.chomp.split('/').last
114
+ if dirs.count > 1
115
+ warn "Multiple postgresql clusters configured or incorrect base dir #{dir}"
116
+ warn "Using the first directory found: #{first}"
117
+ end
118
+ first
119
+ end
120
+ end
39
121
  end
40
122
  end