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
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