diff --git a/lib/resources/users.rb b/lib/resources/users.rb index 8b7c43bc8..16d9dd133 100644 --- a/lib/resources/users.rb +++ b/lib/resources/users.rb @@ -547,21 +547,12 @@ module Inspec::Resources end end - # For now, we stick with WMI Win32_UserAccount + # This optimization was inspired by + # @see https://mcpmag.com/articles/2015/04/15/reporting-on-local-accounts.aspx + # Alternative solutions are WMI Win32_UserAccount # @see https://msdn.microsoft.com/en-us/library/aa394507(v=vs.85).aspx # @see https://msdn.microsoft.com/en-us/library/aa394153(v=vs.85).aspx - # - # using Get-AdUser would be the best command for domain machines, but it will not be installed - # on client machines by default - # @see https://technet.microsoft.com/en-us/library/ee617241.aspx - # @see https://technet.microsoft.com/en-us/library/hh509016(v=WS.10).aspx - # @see http://woshub.com/get-aduser-getting-active-directory-users-data-via-powershell/ - # @see http://stackoverflow.com/questions/17548523/the-term-get-aduser-is-not-recognized-as-the-name-of-a-cmdlet - # - # Just for reference, we could also use ADSI (Active Directory Service Interfaces) - # @see https://mcpmag.com/articles/2015/04/15/reporting-on-local-accounts.aspx class WindowsUser < UserInfo - # parse windows account name def parse_windows_account(username) account = username.split('\\') name = account.pop @@ -570,67 +561,92 @@ module Inspec::Resources end def identity(username) - # extract domain/user information - account, domain = parse_windows_account(username) + # TODO: we look for local users only at this point + name, _domain = parse_windows_account(username) + return if collect_user_details.nil? + res = collect_user_details.select { |user| user[:username] == name } + res[0] if res.length > 0 + end - # TODO: escape content - if !domain.nil? - filter = "Name = '#{account}' and Domain = '#{domain}'" - else - filter = "Name = '#{account}' and LocalAccount = true" - end + def list_users + collect_user_details.map { |user| user[:username] } + end + # https://msdn.microsoft.com/en-us/library/aa746340(v=vs.85).aspx + def collect_user_details # rubocop:disable Metrics/MethodLength + return @users_cache if defined?(@users_cache) script = <<-EOH - # find user - $user = Get-WmiObject Win32_UserAccount -filter "#{filter}" - # get related groups - $groups = $user.GetRelated('Win32_Group') | Select-Object -Property Caption, Domain, Name, LocalAccount, SID, SIDType, Status - # filter user information - $user = $user | Select-Object -Property Caption, Description, Domain, Name, LocalAccount, Lockout, PasswordChangeable, PasswordExpires, PasswordRequired, SID, SIDType, Status, Disabled - # build response object - New-Object -Type PSObject | ` - Add-Member -MemberType NoteProperty -Name User -Value ($user) -PassThru | ` - Add-Member -MemberType NoteProperty -Name Groups -Value ($groups) -PassThru | ` - ConvertTo-Json +Function ConvertTo-SID { Param([byte[]]$BinarySID) + (New-Object System.Security.Principal.SecurityIdentifier($BinarySID,0)).Value +} + +Function Convert-UserFlag { Param ($UserFlag) + $List = @() + Switch ($UserFlag) { + ($UserFlag -BOR 0x0001) { $List += 'SCRIPT' } + ($UserFlag -BOR 0x0002) { $List += 'ACCOUNTDISABLE' } + ($UserFlag -BOR 0x0008) { $List += 'HOMEDIR_REQUIRED' } + ($UserFlag -BOR 0x0010) { $List += 'LOCKOUT' } + ($UserFlag -BOR 0x0020) { $List += 'PASSWD_NOTREQD' } + ($UserFlag -BOR 0x0040) { $List += 'PASSWD_CANT_CHANGE' } + ($UserFlag -BOR 0x0080) { $List += 'ENCRYPTED_TEXT_PWD_ALLOWED' } + ($UserFlag -BOR 0x0100) { $List += 'TEMP_DUPLICATE_ACCOUNT' } + ($UserFlag -BOR 0x0200) { $List += 'NORMAL_ACCOUNT' } + ($UserFlag -BOR 0x0800) { $List += 'INTERDOMAIN_TRUST_ACCOUNT' } + ($UserFlag -BOR 0x1000) { $List += 'WORKSTATION_TRUST_ACCOUNT' } + ($UserFlag -BOR 0x2000) { $List += 'SERVER_TRUST_ACCOUNT' } + ($UserFlag -BOR 0x10000) { $List += 'DONT_EXPIRE_PASSWORD' } + ($UserFlag -BOR 0x20000) { $List += 'MNS_LOGON_ACCOUNT' } + ($UserFlag -BOR 0x40000) { $List += 'SMARTCARD_REQUIRED' } + ($UserFlag -BOR 0x80000) { $List += 'TRUSTED_FOR_DELEGATION' } + ($UserFlag -BOR 0x100000) { $List += 'NOT_DELEGATED' } + ($UserFlag -BOR 0x200000) { $List += 'USE_DES_KEY_ONLY' } + ($UserFlag -BOR 0x400000) { $List += 'DONT_REQ_PREAUTH' } + ($UserFlag -BOR 0x800000) { $List += 'PASSWORD_EXPIRED' } + ($UserFlag -BOR 0x1000000) { $List += 'TRUSTED_TO_AUTH_FOR_DELEGATION' } + ($UserFlag -BOR 0x04000000) { $List += 'PARTIAL_SECRETS_ACCOUNT' } + } + $List +} + +$Computername = $Env:Computername +$adsi = [ADSI]"WinNT://$Computername" +$adsi.Children | where {$_.SchemaClassName -eq 'user'} | ForEach { + New-Object PSObject -property @{ + uid = ConvertTo-SID -BinarySID $_.ObjectSID[0] + username = $_.Name[0] + description = $_.Description[0] + disabled = $_.AccountDisabled[0] + userflags = Convert-UserFlag -UserFlag $_.UserFlags[0] + passwordage = [math]::Round($_.PasswordAge[0]/86400) + minpasswordlength = $_.MinPasswordLength[0] + mindays = [math]::Round($_.MinPasswordAge[0]/86400) + maxdays = [math]::Round($_.MaxPasswordAge[0]/86400) + warndays = $null + badpasswordattempts = $_.BadPasswordAttempts[0] + maxbadpasswords = $_.MaxBadPasswordsAllowed[0] + gid = $null + group = $null + groups = $null + home = $_.HomeDirectory[0] + shell = $null + domain = $Computername + } +} | ConvertTo-Json EOH - cmd = inspec.powershell(script) - # cannot rely on exit code for now, successful command returns exit code 1 # return nil if cmd.exit_status != 0, try to parse json begin - params = JSON.parse(cmd.stdout) + users = JSON.parse(cmd.stdout) rescue JSON::ParserError => _e return nil end - user_hash = params['User'] || {} - group_hashes = params['Groups'] || [] - # if groups is no array, generate one - group_hashes = [group_hashes] unless group_hashes.is_a?(Array) - group_names = group_hashes.map { |grp| grp['Caption'] } - { - uid: user_hash['SID'], - username: user_hash['Caption'], - gid: nil, - group: nil, - groups: group_names, - disabled: user_hash['Disabled'], - } - end - - # not implemented yet - def meta_info(_username) - { - home: nil, - shell: nil, - } - end - - def list_users - script = 'Get-WmiObject Win32_UserAccount | Select-Object -ExpandProperty Caption' - cmd = inspec.powershell(script) - cmd.stdout.chomp.lines + # ensure we have an array of groups + users = [users] if !users.is_a?(Array) + # convert keys to symbols + @users_cache = users.map { |user| user.each_with_object({}) { |(k, v), h| h[k.to_sym] = v } } end end end diff --git a/test/helper.rb b/test/helper.rb index adadbe4ac..62c94c9db 100644 --- a/test/helper.rb +++ b/test/helper.rb @@ -209,7 +209,7 @@ class MockLoader # user info for freebsd 'pw usershow root -7' => cmd.call('pw-usershow-root-7'), # user info for windows (winrm 1.6.0, 1.6.1) - '942eeec2b290bda610229d4bd29981ee945ed27b0f4ce7cca099aabe38af6386' => cmd.call('GetUserAccount'), + '21c8fabaade05b84ec979759a30814f04353722f173424921bddedc7b65cacbf' => cmd.call('adsiusers'), # group info for windows 'd8d5b3e3355650399e23857a526ee100b4e49e5c2404a0a5dbb7d85d7f4de5cc' => cmd.call('adsigroups'), # network interface diff --git a/test/unit/mock/cmd/GetUserAccount b/test/unit/mock/cmd/GetUserAccount deleted file mode 100644 index 2b9cc5a7f..000000000 --- a/test/unit/mock/cmd/GetUserAccount +++ /dev/null @@ -1,34 +0,0 @@ -{ - "User": { - "Caption": "EXAMPLE\\Administrator", - "Description": "Built-in account for administering the computer/domain", - "Domain": "EXAMPLE", - "Name": "Administrator", - "LocalAccount": false, - "Lockout": false, - "PasswordChangeable": true, - "PasswordExpires": true, - "PasswordRequired": true, - "SID": "S-1-5-21-725088257-906184668-2367214287-500", - "SIDType": 1, - "Status": "OK", - "Disabled": false - }, - "Groups": [{ - "Caption": "WIN-K0AKLED332V\\Administrators", - "Domain": "WIN-K0AKLED332V", - "Name": "Administrators", - "LocalAccount": true, - "SID": "S-1-5-32-544", - "SIDType": 4, - "Status": "OK" - }, { - "Caption": "EXAMPLE\\Domain Admins", - "Domain": "EXAMPLE", - "Name": "Domain Admins", - "LocalAccount": false, - "SID": "S-1-5-21-725088257-906184668-2367214287-512", - "SIDType": 2, - "Status": "OK" - }] -} diff --git a/test/unit/mock/cmd/adsiusers b/test/unit/mock/cmd/adsiusers new file mode 100644 index 000000000..d02a9345e --- /dev/null +++ b/test/unit/mock/cmd/adsiusers @@ -0,0 +1,53 @@ +[ + { + "warndays": null, + "mindays": 0, + "minpasswordlength": 0, + "badpasswordattempts": 0, + "description": "Built-in account for administering the computer/domain", + "username": "Administrator", + "disabled": false, + "passwordage": 355, + "maxbadpasswords": 0, + "home": "", + "uid": "S-1-5-21-1759981009-4135989804-1844563890-500", + "domain": "WIN-CIV7VMLVHLD", + "group": null, + "userflags": [ + "SCRIPT", + "NORMAL_ACCOUNT", + "PASSWORD_EXPIRED" + ], + "groups": null, + "gid": null, + "maxdays": 42, + "shell": null + }, + { + "warndays": null, + "mindays": 0, + "minpasswordlength": 0, + "badpasswordattempts": 0, + "description": "Built-in account for guest access to the computer/domain", + "username": "Guest", + "disabled": true, + "passwordage": 0, + "maxbadpasswords": 0, + "home": "", + "uid": "S-1-5-21-1759981009-4135989804-1844563890-501", + "domain": "WIN-CIV7VMLVHLD", + "group": null, + "userflags": [ + "SCRIPT", + "ACCOUNTDISABLE", + "PASSWD_NOTREQD", + "PASSWD_CANT_CHANGE", + "NORMAL_ACCOUNT", + "DONT_EXPIRE_PASSWORD" + ], + "groups": null, + "gid": null, + "maxdays": 42, + "shell": null + } +] diff --git a/test/unit/resources/user_test.rb b/test/unit/resources/user_test.rb index 6d22d2f01..b5ca93ccf 100644 --- a/test/unit/resources/user_test.rb +++ b/test/unit/resources/user_test.rb @@ -99,11 +99,11 @@ describe 'Inspec::Resources::User' do end it 'read user on Windows' do - resource = MockLoader.new(:windows).load_resource('user', 'example/Administrator') + resource = MockLoader.new(:windows).load_resource('user', 'Administrator') _(resource.uid).wont_be_nil _(resource.exists?).must_equal true _(resource.group).must_equal nil - _(resource.groups).must_equal ['WIN-K0AKLED332V\\Administrators', 'EXAMPLE\\Domain Admins'] + _(resource.groups).must_equal nil _(resource.home).must_equal nil _(resource.shell).must_equal nil _(resource.mindays).must_equal nil @@ -112,8 +112,15 @@ describe 'Inspec::Resources::User' do _(resource.disabled?).must_equal false end + it 'read disabled user on Windows' do + resource = MockLoader.new(:windows).load_resource('user', 'Guest') + _(resource.uid).wont_be_nil + _(resource.exists?).must_equal true + _(resource.disabled?).must_equal true + end + it 'read user on undefined os' do - resource = MockLoader.new(:undefined).load_resource('user', 'example/Administrator') + resource = MockLoader.new(:undefined).load_resource('user', 'root') _(resource.exists?).must_equal false _(resource.group).must_equal nil _(resource.groups).must_equal nil