mirror of
https://github.com/inspec/inspec
synced 2024-11-23 21:23:29 +00:00
Merge pull request #1149 from chef/chris-rock/win-user-improve
replace wmi win32_useraccount with adsi users
This commit is contained in:
commit
cd5905c2cf
5 changed files with 141 additions and 99 deletions
|
@ -547,21 +547,12 @@ module Inspec::Resources
|
||||||
end
|
end
|
||||||
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/aa394507(v=vs.85).aspx
|
||||||
# @see https://msdn.microsoft.com/en-us/library/aa394153(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
|
class WindowsUser < UserInfo
|
||||||
# parse windows account name
|
|
||||||
def parse_windows_account(username)
|
def parse_windows_account(username)
|
||||||
account = username.split('\\')
|
account = username.split('\\')
|
||||||
name = account.pop
|
name = account.pop
|
||||||
|
@ -570,67 +561,92 @@ module Inspec::Resources
|
||||||
end
|
end
|
||||||
|
|
||||||
def identity(username)
|
def identity(username)
|
||||||
# extract domain/user information
|
# TODO: we look for local users only at this point
|
||||||
account, domain = parse_windows_account(username)
|
name, _domain = parse_windows_account(username)
|
||||||
|
return if collect_user_details.nil?
|
||||||
# TODO: escape content
|
res = collect_user_details.select { |user| user[:username] == name }
|
||||||
if !domain.nil?
|
res[0] if res.length > 0
|
||||||
filter = "Name = '#{account}' and Domain = '#{domain}'"
|
|
||||||
else
|
|
||||||
filter = "Name = '#{account}' and LocalAccount = true"
|
|
||||||
end
|
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
|
script = <<-EOH
|
||||||
# find user
|
Function ConvertTo-SID { Param([byte[]]$BinarySID)
|
||||||
$user = Get-WmiObject Win32_UserAccount -filter "#{filter}"
|
(New-Object System.Security.Principal.SecurityIdentifier($BinarySID,0)).Value
|
||||||
# get related groups
|
}
|
||||||
$groups = $user.GetRelated('Win32_Group') | Select-Object -Property Caption, Domain, Name, LocalAccount, SID, SIDType, Status
|
|
||||||
# filter user information
|
Function Convert-UserFlag { Param ($UserFlag)
|
||||||
$user = $user | Select-Object -Property Caption, Description, Domain, Name, LocalAccount, Lockout, PasswordChangeable, PasswordExpires, PasswordRequired, SID, SIDType, Status, Disabled
|
$List = @()
|
||||||
# build response object
|
Switch ($UserFlag) {
|
||||||
New-Object -Type PSObject | `
|
($UserFlag -BOR 0x0001) { $List += 'SCRIPT' }
|
||||||
Add-Member -MemberType NoteProperty -Name User -Value ($user) -PassThru | `
|
($UserFlag -BOR 0x0002) { $List += 'ACCOUNTDISABLE' }
|
||||||
Add-Member -MemberType NoteProperty -Name Groups -Value ($groups) -PassThru | `
|
($UserFlag -BOR 0x0008) { $List += 'HOMEDIR_REQUIRED' }
|
||||||
ConvertTo-Json
|
($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
|
EOH
|
||||||
|
|
||||||
cmd = inspec.powershell(script)
|
cmd = inspec.powershell(script)
|
||||||
|
|
||||||
# cannot rely on exit code for now, successful command returns exit code 1
|
# cannot rely on exit code for now, successful command returns exit code 1
|
||||||
# return nil if cmd.exit_status != 0, try to parse json
|
# return nil if cmd.exit_status != 0, try to parse json
|
||||||
begin
|
begin
|
||||||
params = JSON.parse(cmd.stdout)
|
users = JSON.parse(cmd.stdout)
|
||||||
rescue JSON::ParserError => _e
|
rescue JSON::ParserError => _e
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
user_hash = params['User'] || {}
|
# ensure we have an array of groups
|
||||||
group_hashes = params['Groups'] || []
|
users = [users] if !users.is_a?(Array)
|
||||||
# if groups is no array, generate one
|
# convert keys to symbols
|
||||||
group_hashes = [group_hashes] unless group_hashes.is_a?(Array)
|
@users_cache = users.map { |user| user.each_with_object({}) { |(k, v), h| h[k.to_sym] = v } }
|
||||||
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
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -209,7 +209,7 @@ class MockLoader
|
||||||
# user info for freebsd
|
# user info for freebsd
|
||||||
'pw usershow root -7' => cmd.call('pw-usershow-root-7'),
|
'pw usershow root -7' => cmd.call('pw-usershow-root-7'),
|
||||||
# user info for windows (winrm 1.6.0, 1.6.1)
|
# user info for windows (winrm 1.6.0, 1.6.1)
|
||||||
'942eeec2b290bda610229d4bd29981ee945ed27b0f4ce7cca099aabe38af6386' => cmd.call('GetUserAccount'),
|
'21c8fabaade05b84ec979759a30814f04353722f173424921bddedc7b65cacbf' => cmd.call('adsiusers'),
|
||||||
# group info for windows
|
# group info for windows
|
||||||
'd8d5b3e3355650399e23857a526ee100b4e49e5c2404a0a5dbb7d85d7f4de5cc' => cmd.call('adsigroups'),
|
'd8d5b3e3355650399e23857a526ee100b4e49e5c2404a0a5dbb7d85d7f4de5cc' => cmd.call('adsigroups'),
|
||||||
# network interface
|
# network interface
|
||||||
|
|
|
@ -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"
|
|
||||||
}]
|
|
||||||
}
|
|
53
test/unit/mock/cmd/adsiusers
Normal file
53
test/unit/mock/cmd/adsiusers
Normal 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
|
||||||
|
}
|
||||||
|
]
|
|
@ -99,11 +99,11 @@ describe 'Inspec::Resources::User' do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'read user on Windows' do
|
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.uid).wont_be_nil
|
||||||
_(resource.exists?).must_equal true
|
_(resource.exists?).must_equal true
|
||||||
_(resource.group).must_equal nil
|
_(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.home).must_equal nil
|
||||||
_(resource.shell).must_equal nil
|
_(resource.shell).must_equal nil
|
||||||
_(resource.mindays).must_equal nil
|
_(resource.mindays).must_equal nil
|
||||||
|
@ -112,8 +112,15 @@ describe 'Inspec::Resources::User' do
|
||||||
_(resource.disabled?).must_equal false
|
_(resource.disabled?).must_equal false
|
||||||
end
|
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
|
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.exists?).must_equal false
|
||||||
_(resource.group).must_equal nil
|
_(resource.group).must_equal nil
|
||||||
_(resource.groups).must_equal nil
|
_(resource.groups).must_equal nil
|
||||||
|
|
Loading…
Reference in a new issue