inspec 0.34.1 → 1.0.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 (146) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +323 -6
  3. data/Gemfile +10 -0
  4. data/README.md +1 -1
  5. data/Rakefile +4 -55
  6. data/docs/.gitignore +2 -0
  7. data/docs/README.md +40 -0
  8. data/docs/cli.md +342 -0
  9. data/docs/dsl_inspec.md +245 -0
  10. data/docs/dsl_resource.md +93 -0
  11. data/docs/inspec_and_friends.md +105 -0
  12. data/docs/matchers.md +136 -0
  13. data/docs/plugin_kitchen_inspec.html.md +55 -0
  14. data/docs/profiles.md +285 -0
  15. data/docs/resources/apache_conf.md.erb +75 -0
  16. data/docs/resources/apt.md.erb +84 -0
  17. data/docs/resources/audit_policy.md.erb +61 -0
  18. data/docs/resources/auditd_conf.md.erb +79 -0
  19. data/docs/resources/auditd_rules.md.erb +132 -0
  20. data/docs/resources/bash.md.erb +84 -0
  21. data/docs/resources/bond.md.erb +97 -0
  22. data/docs/resources/bridge.md.erb +67 -0
  23. data/docs/resources/bsd_service.md.erb +76 -0
  24. data/docs/resources/command.md.erb +151 -0
  25. data/docs/resources/csv.md.erb +62 -0
  26. data/docs/resources/directory.md.erb +43 -0
  27. data/docs/resources/etc_group.md.erb +116 -0
  28. data/docs/resources/etc_passwd.md.erb +155 -0
  29. data/docs/resources/etc_shadow.md.erb +149 -0
  30. data/docs/resources/file.md.erb +460 -0
  31. data/docs/resources/gem.md.erb +73 -0
  32. data/docs/resources/group.md.erb +74 -0
  33. data/docs/resources/grub_conf.md.erb +115 -0
  34. data/docs/resources/host.md.erb +85 -0
  35. data/docs/resources/iis_site.md.erb +142 -0
  36. data/docs/resources/inetd_conf.md.erb +99 -0
  37. data/docs/resources/ini.md.erb +69 -0
  38. data/docs/resources/interface.md.erb +66 -0
  39. data/docs/resources/iptables.md.erb +70 -0
  40. data/docs/resources/json.md.erb +76 -0
  41. data/docs/resources/kernel_module.md.erb +60 -0
  42. data/docs/resources/kernel_parameter.md.erb +72 -0
  43. data/docs/resources/launchd_service.md.erb +76 -0
  44. data/docs/resources/limits_conf.md.erb +80 -0
  45. data/docs/resources/login_def.md.erb +77 -0
  46. data/docs/resources/mount.md.erb +83 -0
  47. data/docs/resources/mysql_conf.md.erb +102 -0
  48. data/docs/resources/mysql_session.md.erb +63 -0
  49. data/docs/resources/npm.md.erb +75 -0
  50. data/docs/resources/ntp_conf.md.erb +76 -0
  51. data/docs/resources/oneget.md.erb +67 -0
  52. data/docs/resources/os.md.erb +154 -0
  53. data/docs/resources/os_env.md.erb +98 -0
  54. data/docs/resources/package.md.erb +115 -0
  55. data/docs/resources/parse_config.md.erb +122 -0
  56. data/docs/resources/parse_config_file.md.erb +143 -0
  57. data/docs/resources/pip.md.erb +74 -0
  58. data/docs/resources/port.md.erb +150 -0
  59. data/docs/resources/postgres_conf.md.erb +90 -0
  60. data/docs/resources/postgres_session.md.erb +75 -0
  61. data/docs/resources/powershell.md.erb +116 -0
  62. data/docs/resources/process.md.erb +73 -0
  63. data/docs/resources/registry_key.md.erb +149 -0
  64. data/docs/resources/runit_service.md.erb +76 -0
  65. data/docs/resources/security_policy.md.erb +61 -0
  66. data/docs/resources/service.md.erb +135 -0
  67. data/docs/resources/ssh_config.md.erb +94 -0
  68. data/docs/resources/sshd_config.md.erb +97 -0
  69. data/docs/resources/ssl.md.erb +133 -0
  70. data/docs/resources/sys_info.md.erb +55 -0
  71. data/docs/resources/systemd_service.md.erb +76 -0
  72. data/docs/resources/sysv_service.md.erb +76 -0
  73. data/docs/resources/upstart_service.md.erb +76 -0
  74. data/docs/resources/user.md.erb +154 -0
  75. data/docs/resources/users.md.erb +140 -0
  76. data/docs/resources/vbscript.md.erb +69 -0
  77. data/docs/resources/windows_feature.md.erb +61 -0
  78. data/docs/resources/wmi.md.erb +95 -0
  79. data/docs/resources/xinetd_conf.md.erb +170 -0
  80. data/docs/resources/yaml.md.erb +69 -0
  81. data/docs/resources/yum.md.erb +103 -0
  82. data/docs/ruby_usage.md +154 -0
  83. data/docs/shared/matcher_be.md.erb +1 -0
  84. data/docs/shared/matcher_cmp.md.erb +45 -0
  85. data/docs/shared/matcher_eq.md.erb +3 -0
  86. data/docs/shared/matcher_include.md.erb +1 -0
  87. data/docs/shared/matcher_match.md.erb +1 -0
  88. data/docs/shell.md +150 -0
  89. data/examples/meta-profile/inspec.yml +2 -4
  90. data/inspec.gemspec +1 -1
  91. data/lib/bundles/inspec-compliance/api.rb +28 -18
  92. data/lib/bundles/inspec-compliance/cli.rb +19 -27
  93. data/lib/bundles/inspec-compliance/target.rb +15 -2
  94. data/lib/fetchers/git.rb +4 -0
  95. data/lib/fetchers/local.rb +16 -1
  96. data/lib/fetchers/mock.rb +4 -0
  97. data/lib/fetchers/url.rb +46 -20
  98. data/lib/inspec/base_cli.rb +14 -0
  99. data/lib/inspec/cached_fetcher.rb +67 -0
  100. data/lib/inspec/cli.rb +22 -8
  101. data/lib/inspec/control_eval_context.rb +12 -2
  102. data/lib/inspec/dependencies/{vendor_index.rb → cache.rb} +5 -4
  103. data/lib/inspec/dependencies/dependency_set.rb +8 -14
  104. data/lib/inspec/dependencies/lockfile.rb +5 -13
  105. data/lib/inspec/dependencies/requirement.rb +31 -41
  106. data/lib/inspec/dependencies/resolver.rb +3 -3
  107. data/lib/inspec/dsl.rb +16 -4
  108. data/lib/inspec/errors.rb +1 -0
  109. data/lib/inspec/fetcher.rb +25 -1
  110. data/lib/inspec/objects/each_loop.rb +5 -2
  111. data/lib/inspec/objects/test.rb +8 -2
  112. data/lib/inspec/plugins/fetcher.rb +13 -12
  113. data/lib/inspec/plugins/resource.rb +3 -0
  114. data/lib/inspec/profile.rb +62 -26
  115. data/lib/inspec/profile_context.rb +44 -8
  116. data/lib/inspec/resource.rb +18 -3
  117. data/lib/inspec/rspec_json_formatter.rb +91 -40
  118. data/lib/inspec/rule.rb +45 -2
  119. data/lib/inspec/runner.rb +51 -39
  120. data/lib/inspec/shell.rb +5 -16
  121. data/lib/inspec/version.rb +1 -1
  122. data/lib/resources/apache_conf.rb +1 -1
  123. data/lib/resources/gem.rb +1 -0
  124. data/lib/resources/groups.rb +190 -0
  125. data/lib/resources/oneget.rb +1 -0
  126. data/lib/resources/os.rb +1 -1
  127. data/lib/resources/package.rb +3 -1
  128. data/lib/resources/parse_config.rb +18 -1
  129. data/lib/resources/pip.rb +1 -1
  130. data/lib/resources/security_policy.rb +54 -0
  131. data/lib/resources/ssl.rb +9 -11
  132. data/lib/resources/sys_info.rb +1 -1
  133. data/lib/resources/users.rb +81 -64
  134. metadata +91 -17
  135. data/docs/cli.rst +0 -442
  136. data/docs/dsl_inspec.rst +0 -259
  137. data/docs/dsl_resource.rst +0 -90
  138. data/docs/inspec_and_friends.rst +0 -85
  139. data/docs/matchers.rst +0 -137
  140. data/docs/profiles.rst +0 -169
  141. data/docs/readme.rst +0 -105
  142. data/docs/resources.rst +0 -4836
  143. data/docs/ruby_usage.rst +0 -145
  144. data/docs/shell.rst +0 -130
  145. data/docs/template.rst +0 -51
  146. data/lib/resources/group.rb +0 -137
@@ -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
@@ -16,6 +16,7 @@ module Inspec::Resources
16
16
  example "
17
17
  describe oneget('zoomit') do
18
18
  it { should be_installed }
19
+ its('version') { should eq '1.2.3' }
19
20
  end
20
21
  "
21
22
 
data/lib/resources/os.rb CHANGED
@@ -7,7 +7,7 @@ module Inspec::Resources
7
7
  name 'os'
8
8
  desc 'Use the os InSpec audit resource to test the platform on which the system is running.'
9
9
  example "
10
- describe os.family do
10
+ describe os[:family] do
11
11
  it { should eq 'redhat' }
12
12
  end
13
13
 
@@ -91,9 +91,11 @@ module Inspec::Resources
91
91
  assignment_re: /^\s*([^:]*?)\s*:\s*(.*?)\s*$/,
92
92
  multiple_values: false,
93
93
  ).params
94
+ # If the package is removed and not purged, Status is "deinstall ok config-files" with exit_status 0
95
+ # If the package is purged cmd fails with non-zero exit status
94
96
  {
95
97
  name: params['Package'],
96
- installed: true,
98
+ installed: params['Status'].split(' ').first == 'install',
97
99
  version: params['Version'],
98
100
  type: 'deb',
99
101
  }
@@ -19,10 +19,27 @@ module Inspec::Resources
19
19
  desc 'Use the parse_config InSpec audit resource to test arbitrary configuration files.'
20
20
  example "
21
21
  output = command('some-command').stdout
22
-
23
22
  describe parse_config(output, { data_config_option: value } ) do
24
23
  its('setting') { should eq 1 }
25
24
  end
25
+
26
+ output2 = command('curl http://127.0.0.1/php_status').stdout
27
+ # php status is in format 'key : value', and we do not allow for multiple values
28
+ options2 = {
29
+ assignment_re: /^\s*([^:]*?)\s*:\s*(.*?)\s*$/,
30
+ multiple_values: false
31
+ }
32
+
33
+ describe parse_config(output2, options2) do
34
+ its('pool') { should eq 'www'}
35
+ its('process manager') { should eq process_manager }
36
+ end
37
+
38
+ # getting specific key from the output above, convert it to integer and then compare
39
+ # make sure 'listen queue' is below 100
40
+ describe parse_config(output2, options2 ).params['listen queue'].to_i do
41
+ it { should be < 100 }
42
+ end
26
43
  "
27
44
 
28
45
  attr_reader :content
data/lib/resources/pip.rb CHANGED
@@ -49,7 +49,7 @@ module Inspec::Resources
49
49
  end
50
50
 
51
51
  def to_s
52
- "Pip Package #{@package_name}"
52
+ "Pip Package #{@package_name}"
53
53
  end
54
54
 
55
55
  private
@@ -16,6 +16,57 @@
16
16
  require 'hashie'
17
17
 
18
18
  module Inspec::Resources
19
+ # known and supported MS privilege rights
20
+ # @see https://technet.microsoft.com/en-us/library/dd277311.aspx
21
+ # @see https://msdn.microsoft.com/en-us/library/windows/desktop/bb530716(v=vs.85).aspx
22
+ MS_PRIVILEGES_RIGHTS = [
23
+ 'SeNetworkLogonRight',
24
+ 'SeBackupPrivilege',
25
+ 'SeChangeNotifyPrivilege',
26
+ 'SeSystemtimePrivilege',
27
+ 'SeCreatePagefilePrivilege',
28
+ 'SeDebugPrivilege',
29
+ 'SeRemoteShutdownPrivilege',
30
+ 'SeAuditPrivilege',
31
+ 'SeIncreaseQuotaPrivilege',
32
+ 'SeIncreaseBasePriorityPrivilege',
33
+ 'SeLoadDriverPrivilege',
34
+ 'SeBatchLogonRight',
35
+ 'SeServiceLogonRight',
36
+ 'SeInteractiveLogonRight',
37
+ 'SeSecurityPrivilege',
38
+ 'SeSystemEnvironmentPrivilege',
39
+ 'SeProfileSingleProcessPrivilege',
40
+ 'SeSystemProfilePrivilege',
41
+ 'SeAssignPrimaryTokenPrivilege',
42
+ 'SeRestorePrivilege',
43
+ 'SeShutdownPrivilege',
44
+ 'SeTakeOwnershipPrivilege',
45
+ 'SeUndockPrivilege',
46
+ 'SeManageVolumePrivilege',
47
+ 'SeRemoteInteractiveLogonRight',
48
+ 'SeImpersonatePrivilege',
49
+ 'SeCreateGlobalPrivilege',
50
+ 'SeIncreaseWorking',
51
+ 'SeTimeZonePrivilege',
52
+ 'SeCreateSymbolicLinkPrivilege',
53
+ 'SeDenyNetworkLogonRight', # Deny access to this computer from the network
54
+ 'SeDenyInteractiveLogonRight', # Deny logon locally
55
+ 'SeDenyBatchLogonRight', # Deny logon as a batch job
56
+ 'SeDenyServiceLogonRight', # Deny logon as a service
57
+ 'SeTcbPrivilege',
58
+ 'SeMachineAccountPrivilege',
59
+ 'SeCreateTokenPrivilege',
60
+ 'SeCreatePermanentPrivilege',
61
+ 'SeEnableDelegationPrivilege',
62
+ 'SeLockMemoryPrivilege',
63
+ 'SeSyncAgentPrivilege',
64
+ 'SeUnsolicitedInputPrivilege',
65
+ 'SeTrustedCredManAccessPrivilege',
66
+ 'SeRelabelPrivilege', # the privilege to change a Windows integrity label (new to Windows Vista)
67
+ 'SeDenyRemoteInteractiveLogonRight', # Deny logon through Terminal Services
68
+ ].freeze
69
+
19
70
  class SecurityPolicy < Inspec.resource(1)
20
71
  name 'security_policy'
21
72
  desc 'Use the security_policy InSpec audit resource to test security policies on the Microsoft Windows platform.'
@@ -42,6 +93,9 @@ module Inspec::Resources
42
93
  # deep search for hash key
43
94
  params.extend Hashie::Extensions::DeepFind
44
95
  res = params.deep_find(name.to_s)
96
+
97
+ # return an empty array if configuration does not include rights configuration
98
+ return [] if res.nil? && MS_PRIVILEGES_RIGHTS.include?(name.to_s)
45
99
  res
46
100
  end
47
101
 
data/lib/resources/ssl.rb CHANGED
@@ -44,18 +44,16 @@ class SSL < Inspec.resource(1)
44
44
  attr_reader :host, :port
45
45
 
46
46
  def initialize(opts = {})
47
- @host = opts[:host] ||
48
- inspec.backend.instance_variable_get(:@hostname)
49
- # FIXME: This can be removed when/if @hostname is available as a property for 'Train::Transports::WinRM::Connection'
50
- # Train enhancement request for this here: https://github.com/chef/train/issues/128
51
- if @host.nil? && inspec.backend.class.to_s == 'Train::Transports::WinRM::Connection'
52
- @host = URI.parse(inspec.backend.instance_variable_get(:@options)[:endpoint]).hostname
53
- end
54
- if @host.nil? && inspec.backend.class.to_s == 'Train::Transports::Local::Connection'
55
- @host = 'localhost'
56
- end
47
+ @host = opts[:host]
57
48
  if @host.nil?
58
- fail 'Cannot determine host for SSL test. Please specify it or use a different target.'
49
+ # Transports like SSH and WinRM will provide a hostname
50
+ if inspec.backend.respond_to?('hostname')
51
+ @host = inspec.backend.hostname
52
+ elsif inspec.backend.class.to_s == 'Train::Transports::Local::Connection'
53
+ @host = 'localhost'
54
+ else
55
+ fail 'Cannot determine host for SSL test. Please specify it or use a different target.'
56
+ end
59
57
  end
60
58
  @port = opts[:port] || 443
61
59
  @timeout = opts[:timeout]
@@ -6,7 +6,7 @@ module Inspec::Resources
6
6
 
7
7
  desc 'Use the user InSpec system resource to test for operating system properties.'
8
8
  example "
9
- describe sysinfo do
9
+ describe sys_info do
10
10
  its('hostname') { should eq 'example.com' }
11
11
  end
12
12
  "
@@ -4,11 +4,12 @@
4
4
 
5
5
  require 'utils/parser'
6
6
  require 'utils/convert'
7
+ require 'utils/filter'
7
8
 
8
9
  module Inspec::Resources
9
10
  # This file contains two resources, the `user` and `users` resource.
10
11
  # The `user` resource is optimized for requests that verify specific users
11
- # that you know upfront for testing. If you need to query all users or find
12
+ # that you know upfront for testing. If you need to query all users or search
12
13
  # specific users with certain properties, use the `users` resource.
13
14
  module UserManagementSelector
14
15
  # select user provider based on the operating system
@@ -65,7 +66,7 @@ module Inspec::Resources
65
66
  def initialize
66
67
  # select user provider
67
68
  @user_provider = select_user_manager(inspec.os)
68
- return skip_resource 'The `user` resource is not supported on your OS yet.' if @user_provider.nil?
69
+ return skip_resource 'The `users` resource is not supported on your OS yet.' if @user_provider.nil?
69
70
  end
70
71
 
71
72
  filter = FilterTable.create
@@ -546,21 +547,12 @@ module Inspec::Resources
546
547
  end
547
548
  end
548
549
 
549
- # For now, we stick with WMI Win32_UserAccount
550
+ # This optimization was inspired by
551
+ # @see https://mcpmag.com/articles/2015/04/15/reporting-on-local-accounts.aspx
552
+ # Alternative solutions are WMI Win32_UserAccount
550
553
  # @see https://msdn.microsoft.com/en-us/library/aa394507(v=vs.85).aspx
551
554
  # @see https://msdn.microsoft.com/en-us/library/aa394153(v=vs.85).aspx
552
- #
553
- # using Get-AdUser would be the best command for domain machines, but it will not be installed
554
- # on client machines by default
555
- # @see https://technet.microsoft.com/en-us/library/ee617241.aspx
556
- # @see https://technet.microsoft.com/en-us/library/hh509016(v=WS.10).aspx
557
- # @see http://woshub.com/get-aduser-getting-active-directory-users-data-via-powershell/
558
- # @see http://stackoverflow.com/questions/17548523/the-term-get-aduser-is-not-recognized-as-the-name-of-a-cmdlet
559
- #
560
- # Just for reference, we could also use ADSI (Active Directory Service Interfaces)
561
- # @see https://mcpmag.com/articles/2015/04/15/reporting-on-local-accounts.aspx
562
555
  class WindowsUser < UserInfo
563
- # parse windows account name
564
556
  def parse_windows_account(username)
565
557
  account = username.split('\\')
566
558
  name = account.pop
@@ -569,67 +561,92 @@ module Inspec::Resources
569
561
  end
570
562
 
571
563
  def identity(username)
572
- # extract domain/user information
573
- account, domain = parse_windows_account(username)
574
-
575
- # TODO: escape content
576
- if !domain.nil?
577
- filter = "Name = '#{account}' and Domain = '#{domain}'"
578
- else
579
- filter = "Name = '#{account}' and LocalAccount = true"
580
- end
564
+ # TODO: we look for local users only at this point
565
+ name, _domain = parse_windows_account(username)
566
+ return if collect_user_details.nil?
567
+ res = collect_user_details.select { |user| user[:username] == name }
568
+ res[0] if res.length > 0
569
+ end
570
+
571
+ def list_users
572
+ collect_user_details.map { |user| user[:username] }
573
+ end
581
574
 
575
+ # https://msdn.microsoft.com/en-us/library/aa746340(v=vs.85).aspx
576
+ def collect_user_details # rubocop:disable Metrics/MethodLength
577
+ return @users_cache if defined?(@users_cache)
582
578
  script = <<-EOH
583
- # find user
584
- $user = Get-WmiObject Win32_UserAccount -filter "#{filter}"
585
- # get related groups
586
- $groups = $user.GetRelated('Win32_Group') | Select-Object -Property Caption, Domain, Name, LocalAccount, SID, SIDType, Status
587
- # filter user information
588
- $user = $user | Select-Object -Property Caption, Description, Domain, Name, LocalAccount, Lockout, PasswordChangeable, PasswordExpires, PasswordRequired, SID, SIDType, Status, Disabled
589
- # build response object
590
- New-Object -Type PSObject | `
591
- Add-Member -MemberType NoteProperty -Name User -Value ($user) -PassThru | `
592
- Add-Member -MemberType NoteProperty -Name Groups -Value ($groups) -PassThru | `
593
- ConvertTo-Json
579
+ Function ConvertTo-SID { Param([byte[]]$BinarySID)
580
+ (New-Object System.Security.Principal.SecurityIdentifier($BinarySID,0)).Value
581
+ }
582
+
583
+ Function Convert-UserFlag { Param ($UserFlag)
584
+ $List = @()
585
+ Switch ($UserFlag) {
586
+ ($UserFlag -BOR 0x0001) { $List += 'SCRIPT' }
587
+ ($UserFlag -BOR 0x0002) { $List += 'ACCOUNTDISABLE' }
588
+ ($UserFlag -BOR 0x0008) { $List += 'HOMEDIR_REQUIRED' }
589
+ ($UserFlag -BOR 0x0010) { $List += 'LOCKOUT' }
590
+ ($UserFlag -BOR 0x0020) { $List += 'PASSWD_NOTREQD' }
591
+ ($UserFlag -BOR 0x0040) { $List += 'PASSWD_CANT_CHANGE' }
592
+ ($UserFlag -BOR 0x0080) { $List += 'ENCRYPTED_TEXT_PWD_ALLOWED' }
593
+ ($UserFlag -BOR 0x0100) { $List += 'TEMP_DUPLICATE_ACCOUNT' }
594
+ ($UserFlag -BOR 0x0200) { $List += 'NORMAL_ACCOUNT' }
595
+ ($UserFlag -BOR 0x0800) { $List += 'INTERDOMAIN_TRUST_ACCOUNT' }
596
+ ($UserFlag -BOR 0x1000) { $List += 'WORKSTATION_TRUST_ACCOUNT' }
597
+ ($UserFlag -BOR 0x2000) { $List += 'SERVER_TRUST_ACCOUNT' }
598
+ ($UserFlag -BOR 0x10000) { $List += 'DONT_EXPIRE_PASSWORD' }
599
+ ($UserFlag -BOR 0x20000) { $List += 'MNS_LOGON_ACCOUNT' }
600
+ ($UserFlag -BOR 0x40000) { $List += 'SMARTCARD_REQUIRED' }
601
+ ($UserFlag -BOR 0x80000) { $List += 'TRUSTED_FOR_DELEGATION' }
602
+ ($UserFlag -BOR 0x100000) { $List += 'NOT_DELEGATED' }
603
+ ($UserFlag -BOR 0x200000) { $List += 'USE_DES_KEY_ONLY' }
604
+ ($UserFlag -BOR 0x400000) { $List += 'DONT_REQ_PREAUTH' }
605
+ ($UserFlag -BOR 0x800000) { $List += 'PASSWORD_EXPIRED' }
606
+ ($UserFlag -BOR 0x1000000) { $List += 'TRUSTED_TO_AUTH_FOR_DELEGATION' }
607
+ ($UserFlag -BOR 0x04000000) { $List += 'PARTIAL_SECRETS_ACCOUNT' }
608
+ }
609
+ $List
610
+ }
611
+
612
+ $Computername = $Env:Computername
613
+ $adsi = [ADSI]"WinNT://$Computername"
614
+ $adsi.Children | where {$_.SchemaClassName -eq 'user'} | ForEach {
615
+ New-Object PSObject -property @{
616
+ uid = ConvertTo-SID -BinarySID $_.ObjectSID[0]
617
+ username = $_.Name[0]
618
+ description = $_.Description[0]
619
+ disabled = $_.AccountDisabled[0]
620
+ userflags = Convert-UserFlag -UserFlag $_.UserFlags[0]
621
+ passwordage = [math]::Round($_.PasswordAge[0]/86400)
622
+ minpasswordlength = $_.MinPasswordLength[0]
623
+ mindays = [math]::Round($_.MinPasswordAge[0]/86400)
624
+ maxdays = [math]::Round($_.MaxPasswordAge[0]/86400)
625
+ warndays = $null
626
+ badpasswordattempts = $_.BadPasswordAttempts[0]
627
+ maxbadpasswords = $_.MaxBadPasswordsAllowed[0]
628
+ gid = $null
629
+ group = $null
630
+ groups = $null
631
+ home = $_.HomeDirectory[0]
632
+ shell = $null
633
+ domain = $Computername
634
+ }
635
+ } | ConvertTo-Json
594
636
  EOH
595
-
596
637
  cmd = inspec.powershell(script)
597
-
598
638
  # cannot rely on exit code for now, successful command returns exit code 1
599
639
  # return nil if cmd.exit_status != 0, try to parse json
600
640
  begin
601
- params = JSON.parse(cmd.stdout)
641
+ users = JSON.parse(cmd.stdout)
602
642
  rescue JSON::ParserError => _e
603
643
  return nil
604
644
  end
605
645
 
606
- user_hash = params['User'] || {}
607
- group_hashes = params['Groups'] || []
608
- # if groups is no array, generate one
609
- group_hashes = [group_hashes] unless group_hashes.is_a?(Array)
610
- group_names = group_hashes.map { |grp| grp['Caption'] }
611
- {
612
- uid: user_hash['SID'],
613
- username: user_hash['Caption'],
614
- gid: nil,
615
- group: nil,
616
- groups: group_names,
617
- disabled: user_hash['Disabled'],
618
- }
619
- end
620
-
621
- # not implemented yet
622
- def meta_info(_username)
623
- {
624
- home: nil,
625
- shell: nil,
626
- }
627
- end
628
-
629
- def list_users
630
- script = 'Get-WmiObject Win32_UserAccount | Select-Object -ExpandProperty Caption'
631
- cmd = inspec.powershell(script)
632
- cmd.stdout.chomp.lines
646
+ # ensure we have an array of groups
647
+ users = [users] if !users.is_a?(Array)
648
+ # convert keys to symbols
649
+ @users_cache = users.map { |user| user.each_with_object({}) { |(k, v), h| h[k.to_sym] = v } }
633
650
  end
634
651
  end
635
652
  end