inspec 1.0.0.beta2 → 1.0.0.beta3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (95) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +41 -2
  3. data/Gemfile +4 -0
  4. data/Rakefile +2 -1
  5. data/docs/.gitignore +2 -0
  6. data/docs/README.md +21 -1
  7. data/docs/resources/apache_conf.md.erb +75 -0
  8. data/docs/resources/apt.md.erb +84 -0
  9. data/docs/resources/audit_policy.md.erb +61 -0
  10. data/docs/resources/auditd_conf.md.erb +79 -0
  11. data/docs/resources/auditd_rules.md.erb +132 -0
  12. data/docs/resources/bash.md.erb +84 -0
  13. data/docs/resources/bond.md.erb +97 -0
  14. data/docs/resources/bridge.md.erb +67 -0
  15. data/docs/resources/bsd_service.md.erb +76 -0
  16. data/docs/resources/command.md.erb +151 -0
  17. data/docs/resources/csv.md.erb +62 -0
  18. data/docs/resources/directory.md.erb +43 -0
  19. data/docs/resources/etc_group.md.erb +116 -0
  20. data/docs/resources/etc_passwd.md.erb +155 -0
  21. data/docs/resources/etc_shadow.md.erb +149 -0
  22. data/docs/resources/file.md.erb +460 -0
  23. data/docs/resources/gem.md.erb +73 -0
  24. data/docs/resources/group.md.erb +74 -0
  25. data/docs/resources/grub_conf.md.erb +115 -0
  26. data/docs/resources/host.md.erb +85 -0
  27. data/docs/resources/iis_site.md.erb +142 -0
  28. data/docs/resources/inetd_conf.md.erb +99 -0
  29. data/docs/resources/ini.md.erb +69 -0
  30. data/docs/resources/interface.md.erb +66 -0
  31. data/docs/resources/iptables.md.erb +70 -0
  32. data/docs/resources/json.md.erb +76 -0
  33. data/docs/resources/kernel_module.md.erb +60 -0
  34. data/docs/resources/kernel_parameter.md.erb +72 -0
  35. data/docs/resources/launchd_service.md.erb +76 -0
  36. data/docs/resources/limits_conf.md.erb +80 -0
  37. data/docs/resources/login_def.md.erb +77 -0
  38. data/docs/resources/mount.md.erb +83 -0
  39. data/docs/resources/mysql_conf.md.erb +102 -0
  40. data/docs/resources/mysql_session.md.erb +63 -0
  41. data/docs/resources/npm.md.erb +75 -0
  42. data/docs/resources/ntp_conf.md.erb +76 -0
  43. data/docs/resources/oneget.md.erb +67 -0
  44. data/docs/resources/os.md.erb +154 -0
  45. data/docs/resources/os_env.md.erb +98 -0
  46. data/docs/resources/package.md.erb +115 -0
  47. data/docs/resources/parse_config.md.erb +122 -0
  48. data/docs/resources/parse_config_file.md.erb +143 -0
  49. data/docs/resources/pip.md.erb +74 -0
  50. data/docs/resources/port.md.erb +150 -0
  51. data/docs/resources/postgres_conf.md.erb +90 -0
  52. data/docs/resources/postgres_session.md.erb +75 -0
  53. data/docs/resources/powershell.md.erb +116 -0
  54. data/docs/resources/process.md.erb +73 -0
  55. data/docs/resources/registry_key.md.erb +149 -0
  56. data/docs/resources/runit_service.md.erb +76 -0
  57. data/docs/resources/security_policy.md.erb +61 -0
  58. data/docs/resources/service.md.erb +135 -0
  59. data/docs/resources/ssh_config.md.erb +94 -0
  60. data/docs/resources/sshd_config.md.erb +97 -0
  61. data/docs/resources/ssl.md.erb +133 -0
  62. data/docs/resources/sys_info.md.erb +55 -0
  63. data/docs/resources/systemd_service.md.erb +76 -0
  64. data/docs/resources/sysv_service.md.erb +76 -0
  65. data/docs/resources/upstart_service.md.erb +76 -0
  66. data/docs/resources/user.md.erb +154 -0
  67. data/docs/resources/users.md.erb +140 -0
  68. data/docs/resources/vbscript.md.erb +69 -0
  69. data/docs/resources/windows_feature.md.erb +61 -0
  70. data/docs/resources/wmi.md.erb +95 -0
  71. data/docs/resources/xinetd_conf.md.erb +170 -0
  72. data/docs/resources/yaml.md.erb +69 -0
  73. data/docs/resources/yum.md.erb +103 -0
  74. data/docs/ruby_usage.md +154 -0
  75. data/docs/shared/matcher_be.md.erb +1 -0
  76. data/docs/shared/matcher_cmp.md.erb +45 -0
  77. data/docs/shared/matcher_eq.md.erb +3 -0
  78. data/docs/shared/matcher_include.md.erb +1 -0
  79. data/docs/shared/matcher_match.md.erb +1 -0
  80. data/lib/fetchers/url.rb +27 -29
  81. data/lib/inspec/cached_fetcher.rb +67 -0
  82. data/lib/inspec/dependencies/requirement.rb +6 -7
  83. data/lib/inspec/objects/each_loop.rb +5 -2
  84. data/lib/inspec/plugins/fetcher.rb +2 -0
  85. data/lib/inspec/profile.rb +9 -41
  86. data/lib/inspec/resource.rb +1 -1
  87. data/lib/inspec/rspec_json_formatter.rb +11 -5
  88. data/lib/inspec/version.rb +1 -1
  89. data/lib/resources/groups.rb +190 -0
  90. data/lib/resources/users.rb +3 -2
  91. metadata +79 -6
  92. data/docs/cli.rst +0 -448
  93. data/docs/resources.rst +0 -4836
  94. data/docs/ruby_usage.rst +0 -145
  95. data/lib/resources/group.rb +0 -137
@@ -0,0 +1 @@
1
+ Use the `be` matcher to use a comparison operator---`=` (equal to), `>` (greater than), `<` (less than), `>=` (greater than or equal to), and `<=` (less than or equal to)---to compare two values: `its('value') { should be >= value }`, `its('value') { should be < value }`, and so on.
@@ -0,0 +1,45 @@
1
+ Use the `cmp` matcher compare two values, such as comparing strings to numbers, comparing a single value to an array of values, comparing an array of strings to a regular expression, improving the printing of octal values, and comparing while ignoring case sensitivity.
2
+
3
+ Compare a single value to an array:
4
+
5
+ describe some_resource do
6
+ its('users') { should cmp 'root' }
7
+ its('users') { should cmp ['root'] }
8
+ end
9
+
10
+ Compare strings and regular expressions:
11
+
12
+ describe some_resource do
13
+ its('setting') { should cmp /raw/i }
14
+ end
15
+
16
+ Compare strings and numbers:
17
+
18
+ describe some_resource do
19
+ its('setting') { should eq '2' }
20
+ end
21
+
22
+ vs:
23
+
24
+ describe some_resource do
25
+ its('setting') { should cmp '2' }
26
+ its('setting') { should cmp 2 }
27
+ end
28
+
29
+ Ignoring case sensitivity:
30
+
31
+ .. code-block:: ruby
32
+
33
+ describe some_resource do
34
+ its('setting') { should cmp 'raw' }
35
+ its('setting') { should cmp 'RAW' }
36
+ end
37
+
38
+ Printing octal values:
39
+
40
+ describe some_resource('/proc/cpuinfo') do
41
+ its('mode') { should cmp '0345' }
42
+ end
43
+
44
+ expected: 0345
45
+ got: 0444
@@ -0,0 +1,3 @@
1
+ Use the `eq` matcher to test the equality of two values: `its('Port') { should eq '22' }`.
2
+
3
+ Using `its('Port') { should eq 22 }` will fail because `22` is not a string value! Use the `cmp` matcher for less restrictive value comparisons.
@@ -0,0 +1 @@
1
+ Use the `include` matcher to verify that a string value is included in a list: `its('list') { should include 'string' }`.
@@ -0,0 +1 @@
1
+ Use the `match` matcher to check if a string matches a regular expression: `its('string') { should_not match /regex/ }`.
data/lib/fetchers/url.rb CHANGED
@@ -85,21 +85,12 @@ module Fetchers
85
85
  @archive_path ||= download_archive(path)
86
86
  end
87
87
 
88
- def sha256
89
- c = if @archive_path
90
- File.read(@archive_path)
91
- else
92
- content
93
- end
94
- Digest::SHA256.hexdigest c
95
- end
96
-
97
88
  def resolved_source
98
89
  @resolved_source ||= { url: @target, sha256: sha256 }
99
90
  end
100
91
 
101
92
  def cache_key
102
- sha256
93
+ @archive_shasum ||= sha256
103
94
  end
104
95
 
105
96
  def to_s
@@ -108,16 +99,9 @@ module Fetchers
108
99
 
109
100
  private
110
101
 
111
- def open_target
112
- Inspec::Log.debug("Fetching URL: #{@target}")
113
- http_opts = {}
114
- http_opts['ssl_verify_mode'.to_sym] = OpenSSL::SSL::VERIFY_NONE if @insecure
115
- http_opts['Authorization'] = "Bearer #{@token}" if @token
116
- open(@target, http_opts)
117
- end
118
-
119
- def content
120
- open_target.read
102
+ def sha256
103
+ file = @archive_path || temp_archive_path
104
+ Digest::SHA256.hexdigest File.read(file)
121
105
  end
122
106
 
123
107
  def file_type_from_remote(remote)
@@ -132,20 +116,34 @@ module Fetchers
132
116
  file_type
133
117
  end
134
118
 
135
- # download url into archive using opts,
136
- # returns File object and content-type from HTTP headers
137
- def download_archive(path)
138
- remote = open_target
139
- file_type = file_type_from_remote(remote)
140
- final_path = "#{path}#{file_type}"
141
- # download content
142
- archive = Tempfile.new(['inspec-dl-', file_type])
119
+ def temp_archive_path
120
+ @temp_archive_path ||= download_archive_to_temp
121
+ end
122
+
123
+ # Downloads archive to temporary file with side effect :( of setting @archive_type
124
+ def download_archive_to_temp
125
+ return @temp_archive_path if ! @temp_archive_path.nil?
126
+ Inspec::Log.debug("Fetching URL: #{@target}")
127
+ http_opts = {}
128
+ http_opts['ssl_verify_mode'.to_sym] = OpenSSL::SSL::VERIFY_NONE if @insecure
129
+ http_opts['Authorization'] = "Bearer #{@token}" if @token
130
+ remote = open(@target, http_opts)
131
+ @archive_type = file_type_from_remote(remote) # side effect :(
132
+ archive = Tempfile.new(['inspec-dl-', @archive_type])
143
133
  archive.binmode
144
134
  archive.write(remote.read)
145
135
  archive.rewind
146
136
  archive.close
147
- FileUtils.mv(archive.path, final_path)
137
+ Inspec::Log.debug("Archive stored at temporary location: #{archive.path}")
138
+ @temp_archive_path = archive.path
139
+ end
140
+
141
+ def download_archive(path)
142
+ download_archive_to_temp
143
+ final_path = "#{path}#{@archive_type}"
144
+ FileUtils.mv(temp_archive_path, final_path)
148
145
  Inspec::Log.debug("Fetched archive moved to: #{final_path}")
146
+ @temp_archive_path = nil
149
147
  final_path
150
148
  end
151
149
  end
@@ -0,0 +1,67 @@
1
+ # encoding: utf-8
2
+ require 'inspec/fetcher'
3
+ require 'forwardable'
4
+
5
+ module Inspec
6
+ class CachedFetcher
7
+ extend Forwardable
8
+
9
+ attr_reader :cache, :target, :fetcher
10
+ def initialize(target, cache)
11
+ @target = target
12
+ @fetcher = Inspec::Fetcher.resolve(target)
13
+
14
+ if @fetcher.nil?
15
+ fail("Could not fetch inspec profile in #{target.inspect}.")
16
+ end
17
+
18
+ @cache = cache
19
+ end
20
+
21
+ def resolved_source
22
+ fetch
23
+ @fetcher.resolved_source
24
+ end
25
+
26
+ def cache_key
27
+ k = if target.is_a?(Hash)
28
+ target[:sha256] || target[:ref]
29
+ end
30
+
31
+ if k.nil?
32
+ fetcher.cache_key
33
+ else
34
+ k
35
+ end
36
+ end
37
+
38
+ def fetch
39
+ if cache.exists?(cache_key)
40
+ Inspec::Log.debug "Using cached dependency for #{target}"
41
+ [cache.prefered_entry_for(cache_key), false]
42
+ else
43
+ Inspec::Log.debug "Dependency does not exist in the cache #{target}"
44
+ fetcher.fetch(cache.base_path_for(fetcher.cache_key))
45
+ assert_cache_sanity!
46
+ [fetcher.archive_path, fetcher.writable?]
47
+ end
48
+ end
49
+
50
+ def assert_cache_sanity!
51
+ if target.respond_to?(:key?) && target.key?(:sha256)
52
+ if fetcher.resolved_source[:sha256] != target[:sha256]
53
+ fail <<EOF
54
+ The remote source #{fetcher} no longer has the requested content:
55
+
56
+ Request Content Hash: #{target[:sha256]}
57
+ Actual Content Hash: #{fetcher.resolved_source[:sha256]}
58
+
59
+ For URL, supermarket, compliance, and other sources that do not
60
+ provide versioned artifacts, this likely means that the remote source
61
+ has changed since your lockfile was generated.
62
+ EOF
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -1,5 +1,5 @@
1
1
  # encoding: utf-8
2
- require 'inspec/fetcher'
2
+ require 'inspec/cached_fetcher'
3
3
  require 'inspec/dependencies/dependency_set'
4
4
  require 'digest'
5
5
 
@@ -82,9 +82,7 @@ module Inspec
82
82
  end
83
83
 
84
84
  def fetcher
85
- @fetcher ||= Inspec::Fetcher.resolve(opts)
86
- fail "No fetcher for #{name} (options: #{opts})" if @fetcher.nil?
87
- @fetcher
85
+ @fetcher ||= Inspec::CachedFetcher.new(opts, @cache)
88
86
  end
89
87
 
90
88
  def dependencies
@@ -94,17 +92,18 @@ module Inspec
94
92
  end
95
93
 
96
94
  def to_s
97
- "#{name ? name : '<unfetched>'} (#{resolved_source})"
95
+ name
98
96
  end
99
97
 
100
98
  def profile
99
+ return @profile if ! @profile.nil?
100
+
101
101
  opts = @opts.dup
102
- opts[:cache] = @cache
103
102
  opts[:backend] = @backend
104
103
  if !@dependencies.nil?
105
104
  opts[:dependencies] = Inspec::DependencySet.from_array(@dependencies, @cwd, @cache, @backend)
106
105
  end
107
- @profile ||= Inspec::Profile.for_target(opts, opts)
106
+ @profile = Inspec::Profile.for_fetcher(fetcher, opts)
108
107
  end
109
108
  end
110
109
  end
@@ -2,10 +2,11 @@
2
2
 
3
3
  module Inspec
4
4
  class EachLoop < List
5
- attr_reader :tests
5
+ attr_reader :tests, :variables
6
6
  def initialize
7
7
  super
8
8
  @tests = []
9
+ @variables = []
9
10
  end
10
11
 
11
12
  def add_test(t = nil)
@@ -24,9 +25,11 @@ module Inspec
24
25
  end
25
26
 
26
27
  def to_ruby
28
+ vars = variables.map(&:to_ruby).join("\n")
29
+ vars += "\n" unless vars.empty?
27
30
  obj = super
28
31
  all_tests = @tests.map(&:to_ruby).join("\n").gsub("\n", "\n ")
29
- format("%s.each do |entry|\n %s\nend", obj, all_tests)
32
+ format("%s%s.each do |entry|\n %s\nend", vars, obj, all_tests)
30
33
  end
31
34
  end
32
35
  end
@@ -24,6 +24,8 @@ module Inspec
24
24
  Inspec::Fetcher
25
25
  end
26
26
 
27
+ attr_accessor :target
28
+
27
29
  def writable?
28
30
  false
29
31
  end
@@ -5,7 +5,7 @@
5
5
 
6
6
  require 'forwardable'
7
7
  require 'inspec/polyfill'
8
- require 'inspec/fetcher'
8
+ require 'inspec/cached_fetcher'
9
9
  require 'inspec/file_provider'
10
10
  require 'inspec/source_reader'
11
11
  require 'inspec/metadata'
@@ -21,45 +21,8 @@ module Inspec
21
21
  class Profile # rubocop:disable Metrics/ClassLength
22
22
  extend Forwardable
23
23
 
24
- #
25
- # TODO: This function is getting pretty gross.
26
- #
27
24
  def self.resolve_target(target, cache = nil)
28
- cache ||= Cache.new
29
- fetcher = Inspec::Fetcher.resolve(target)
30
-
31
- if fetcher.nil?
32
- fail("Could not fetch inspec profile in #{target.inspect}.")
33
- end
34
-
35
- cache_key = if target.is_a?(Hash)
36
- target[:sha256] || target[:ref] || fetcher.cache_key
37
- else
38
- fetcher.cache_key
39
- end
40
-
41
- if cache.exists?(cache_key)
42
- Inspec::Log.debug "Using cached dependency for #{target}"
43
- [cache.prefered_entry_for(cache_key), false]
44
- else
45
- fetcher.fetch(cache.base_path_for(fetcher.cache_key))
46
- if target.respond_to?(:key?) && target.key?(:sha256)
47
- if fetcher.resolved_source[:sha256] != target[:sha256]
48
- fail <<EOF
49
- The remote source #{fetcher} no longer has the requested content:
50
-
51
- Request Content Hash: #{target[:sha256]}
52
- Actual Content Hash: #{fetcher.resolved_source[:sha256]}
53
-
54
- For URL, supermarket, compliance, and other sources that do not
55
- provide versioned artifacts, this likely means that the remote source
56
- has changed since your lockfile was generated.
57
- EOF
58
- end
59
- end
60
-
61
- [fetcher.archive_path, fetcher.writable?]
62
- end
25
+ Inspec::CachedFetcher.new(target, cache || Cache.new)
63
26
  end
64
27
 
65
28
  def self.for_path(path, opts)
@@ -72,9 +35,14 @@ EOF
72
35
  new(reader, opts)
73
36
  end
74
37
 
38
+ def self.for_fetcher(fetcher, opts)
39
+ path, writable = fetcher.fetch
40
+ for_path(path, opts.merge(target: fetcher.target, writable: writable))
41
+ end
42
+
75
43
  def self.for_target(target, opts = {})
76
- path, writable = resolve_target(target, opts[:cache])
77
- for_path(path, opts.merge(target: target, writable: writable))
44
+ fetcher = resolve_target(target, opts[:cache])
45
+ for_fetcher(fetcher, opts)
78
46
  end
79
47
 
80
48
  attr_reader :source_reader, :backend, :runner_context
@@ -83,7 +83,7 @@ require 'resources/directory'
83
83
  require 'resources/etc_group'
84
84
  require 'resources/file'
85
85
  require 'resources/gem'
86
- require 'resources/group'
86
+ require 'resources/groups'
87
87
  require 'resources/grub_conf'
88
88
  require 'resources/host'
89
89
  require 'resources/iis_site'
@@ -63,10 +63,18 @@ class InspecRspecMiniJson < RSpec::Core::Formatters::JsonFormatter
63
63
  private
64
64
 
65
65
  def format_example(example)
66
+ if example.metadata[:description_args].length == 0
67
+ code_description = example.metadata[:full_description]
68
+ else
69
+ # For skipped profiles, rspec returns in full_description the skip_message as well. We don't want
70
+ # to mix the two, so we pick the full_description from the example.metadata[:example_group] hash.
71
+ code_description = example.metadata[:example_group][:description]
72
+ end
73
+
66
74
  res = {
67
75
  id: example.metadata[:id],
68
76
  status: example.execution_result.status.to_s,
69
- code_desc: example.full_description,
77
+ code_desc: code_description,
70
78
  }
71
79
 
72
80
  unless (pid = example.metadata[:profile_id]).nil?
@@ -374,8 +382,6 @@ class InspecRspecCli < InspecRspecJson # rubocop:disable Metrics/ClassLength
374
382
  if res.length == 1
375
383
  # Single test - be nice and just print the exception message if the test
376
384
  # failed. No need to say "1 failed".
377
- fails.clear
378
- skips.clear
379
385
  res[0][:message].to_s
380
386
  else
381
387
  [
@@ -425,7 +431,7 @@ class InspecRspecCli < InspecRspecJson # rubocop:disable Metrics/ClassLength
425
431
  end
426
432
  end
427
433
 
428
- def print_tests
434
+ def print_tests # rubocop:disable Metrics/AbcSize
429
435
  @anonymous_tests.each do |control|
430
436
  control_result = control[:results]
431
437
  title = control_result[0][:code_desc].split[0..1].join(' ')
@@ -438,7 +444,7 @@ class InspecRspecCli < InspecRspecJson # rubocop:disable Metrics/ClassLength
438
444
  test_result = test[:message]
439
445
  else
440
446
  # determine title
441
- test_result = test[:code_desc].split[2..-1].join(' ')
447
+ test_result = test[:skip_message] || test[:code_desc].split[2..-1].join(' ')
442
448
  # show error message
443
449
  test_result += "\n" + test[:message] unless test[:message].nil?
444
450
  end
@@ -4,5 +4,5 @@
4
4
  # author: Christoph Hartmann
5
5
 
6
6
  module Inspec
7
- VERSION = '1.0.0.beta2'.freeze
7
+ VERSION = '1.0.0.beta3'.freeze
8
8
  end
@@ -0,0 +1,190 @@
1
+ # encoding: utf-8
2
+ # author: Christoph Hartmann
3
+ # author: Dominik Richter
4
+
5
+ require 'utils/filter'
6
+
7
+ module Inspec::Resources
8
+ # This file contains two resources, the `group` and `groups` resource.
9
+ # The `group` resource is optimized for requests that verify specific groups
10
+ # that you know upfront for testing. If you need to query all groups or search
11
+ # specific groups with certain properties, use the `groups` resource.
12
+ module GroupManagementSelector
13
+ # select group provider based on the operating system
14
+ # returns nil, if no group manager was found for the operating system
15
+ def select_group_manager(os)
16
+ if os.unix?
17
+ @group_provider = UnixGroup.new(inspec)
18
+ elsif os.windows?
19
+ @group_provider = WindowsGroup.new(inspec)
20
+ end
21
+ end
22
+ end
23
+
24
+ class Groups < Inspec.resource(1)
25
+ include GroupManagementSelector
26
+
27
+ name 'groups'
28
+ desc 'Use the group InSpec audit resource to test groups on the system. Groups can be filtered.'
29
+ example "
30
+ describe groups.where { name == 'root'} do
31
+ its('names') { should eq ['root'] }
32
+ its('gids') { should eq [0] }
33
+ end
34
+
35
+ describe groups.where { name == 'Administrators'} do
36
+ its('names') { should eq ['Administrators'] }
37
+ its('gids') { should eq ['S-1-5-32-544'] }
38
+ end
39
+ "
40
+
41
+ def initialize
42
+ # select group manager
43
+ @group_provider = select_group_manager(inspec.os)
44
+ return skip_resource 'The `groups` resource is not supported on your OS yet.' if @group_provider.nil?
45
+ end
46
+
47
+ filter = FilterTable.create
48
+ filter.add_accessor(:where)
49
+ .add_accessor(:entries)
50
+ .add(:names, field: 'name')
51
+ .add(:gids, field: 'gid')
52
+ .add(:domains, field: 'domain')
53
+ .add(:exists?) { |x| !x.entries.empty? }
54
+ filter.connect(self, :collect_group_details)
55
+
56
+ def to_s
57
+ 'Groups'
58
+ end
59
+
60
+ private
61
+
62
+ # collects information about every group
63
+ def collect_group_details
64
+ return @groups_cache ||= @group_provider.groups unless @group_provider.nil?
65
+ []
66
+ end
67
+ end
68
+
69
+ # Usage:
70
+ # describe group('root') do
71
+ # it { should exist }
72
+ # its('gid') { should eq 0 }
73
+ # end
74
+ #
75
+ # deprecated has matcher
76
+ # describe group('root') do
77
+ # it { should have_gid 0 }
78
+ # end
79
+ class Group < Inspec.resource(1)
80
+ include GroupManagementSelector
81
+
82
+ name 'group'
83
+ desc 'Use the group InSpec audit resource to test groups on the system.'
84
+ example "
85
+ describe group('root') do
86
+ it { should exist }
87
+ its('gid') { should eq 0 }
88
+ end
89
+ "
90
+
91
+ def initialize(groupname)
92
+ @group = groupname
93
+ @group = @group.downcase unless inspec.os.windows?
94
+
95
+ # select group manager
96
+ @group_provider = select_group_manager(inspec.os)
97
+ return skip_resource 'The `group` resource is not supported on your OS yet.' if @group_provider.nil?
98
+ end
99
+
100
+ # verifies if a group exists
101
+ def exists?
102
+ group_info.entries.size > 0
103
+ end
104
+
105
+ def gid
106
+ gids = group_info.gids
107
+ if gids.size == 0
108
+ nil
109
+ # the default case should be one group
110
+ elsif gids.size == 1
111
+ gids.entries[0]
112
+ else
113
+ fail 'found more than one group with the same name, please use `groups` resource'
114
+ end
115
+ end
116
+
117
+ # implements rspec has matcher, to be compatible with serverspec
118
+ def has_gid?(compare_gid)
119
+ gid == compare_gid
120
+ end
121
+
122
+ def local
123
+ # at this point the implementation only returns local groups
124
+ true
125
+ end
126
+
127
+ def to_s
128
+ "Group #{@group}"
129
+ end
130
+
131
+ private
132
+
133
+ def group_info
134
+ # we need a local copy for the block
135
+ group = @group.dup
136
+ @groups_cache ||= inspec.groups.where { name == group }
137
+ end
138
+ end
139
+
140
+ class GroupInfo
141
+ attr_reader :inspec
142
+ def initialize(inspec)
143
+ @inspec = inspec
144
+ end
145
+
146
+ def groups
147
+ fail 'group provider must implement the `groups` method'
148
+ end
149
+ end
150
+
151
+ # implements generic unix groups via /etc/group
152
+ class UnixGroup < GroupInfo
153
+ def groups
154
+ inspec.etc_group.entries
155
+ end
156
+ end
157
+
158
+ class WindowsGroup < GroupInfo
159
+ # returns all local groups
160
+ def groups
161
+ script = <<-EOH
162
+ Function ConvertTo-SID { Param([byte[]]$BinarySID)
163
+ (New-Object System.Security.Principal.SecurityIdentifier($BinarySID,0)).Value
164
+ }
165
+
166
+ $Computername = $Env:Computername
167
+ $adsi = [ADSI]"WinNT://$Computername"
168
+ $groups = $adsi.Children | where {$_.SchemaClassName -eq 'group'} | ForEach {
169
+ $name = $_.Name[0]
170
+ $sid = ConvertTo-SID -BinarySID $_.ObjectSID[0]
171
+ $group =[ADSI]$_.Path
172
+ new-object psobject -property @{name = $group.Name[0]; gid = $sid; domain=$Computername}
173
+ }
174
+ $groups | ConvertTo-Json -Depth 3
175
+ EOH
176
+ cmd = inspec.powershell(script)
177
+ # cannot rely on exit code for now, successful command returns exit code 1
178
+ # return nil if cmd.exit_status != 0, try to parse json
179
+ begin
180
+ groups = JSON.parse(cmd.stdout)
181
+ rescue JSON::ParserError => _e
182
+ return []
183
+ end
184
+
185
+ # ensure we have an array of groups
186
+ groups = [groups] if !groups.is_a?(Array)
187
+ groups
188
+ end
189
+ end
190
+ end