Merge pull request #1149 from chef/chris-rock/win-user-improve

replace wmi win32_useraccount with adsi users
This commit is contained in:
Christoph Hartmann 2016-09-26 01:40:45 +02:00 committed by GitHub
commit cd5905c2cf
5 changed files with 141 additions and 99 deletions

View file

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

View file

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

View file

@ -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"
}]
}

View file

@ -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
}
]

View file

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