inspec 1.0.0.beta2 → 1.0.0.beta3

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