Task: Local User/Administrator (official)
—Script: [Function] Configure-LocalUser.ps1
What:
- Add DomainJoined Boolean param
- No-op if DomainJoined param doesn’t match machine status
Why: Want to configure user account on machines Domain or AzureAD joined – if not joined, skip but report status in run. The Filter Script #2170-DomainJoined did not work for AzureAD, and while could add a combo helper filter script wanted local user account function to not be dependent on helper scripts/run and report on all machines.
- UX: It might sense for a “skipped” stage/status option for ImmyBot as a whole. The concept of only required tasks are pulled into sessions is sometimes confusing when reviewing logs for critical task – for some key tasks a checkbox that include them in all deployments and show as skipped and reason why would be instantly understandable UX, IMHO.
Diff: Configure-LocalUser.ps1<->Configure-LocalUser2.ps1
diff --git "1/.\\Configure-LocalUser.ps1" "2/.\\Configure-LocalUser2.ps1"
index 2a44dce..ec66f6d 100644
--- "1/.\\Configure-LocalUser.ps1"
+++ "2/.\\Configure-LocalUser2.ps1"
@@ -28,7 +28,10 @@ param(
[Boolean]$UserCannotChangePassword = $false,
[Parameter(Position = 8, Mandatory = $False, DontShow)]
- [Boolean]$AccountIsDisabled = $false
+ [Boolean]$AccountIsDisabled = $false,
+
+ [Parameter(Position = 9, Mandatory = $False, HelpMessage = "Run this only if the computer is joined to a domain/AzureAD.")]
+ [Boolean]$DomainJoinedOnly = $false
)
if (!$Computer -or $null -eq $Computer.Inventory.WindowsSystemInfo) {
@@ -47,6 +50,13 @@ if ($Computer.Inventory.WindowsSystemInfo.DomainRole -in 4, 5) {
throw "This computer is a domain controller, local accounts cannot be created or managed with this script."
}
+$IsDomainJoined = ($Computer.Inventory.WindowsSystemInfo.DSRegStatus.DeviceState.AzureADJoined -eq "YES") -or ` #Azure AD Check
+ ( ` # Check if domain joined (not AzureAD)
+ ($Computer.Inventory.WindowsSystemInfo.DomainRole -in 1) -and `
+ ($Computer.Inventory.WindowsSystemInfo.Domain -ne $null) -and `
+ ($Computer.Inventory.WindowsSystemInfo.Domain -ne "") `
+ )
+
function Test-LocalAdmin {
param(
[string]$Username
@@ -103,6 +113,16 @@ function Test-LocalUser {
if (!$TestResults) {
$TestResults = [Ordered]@{}
}
+
+ $TestResults.DomainJoined = ($DomainJoinedOnly -eq $IsDomainJoined)
+ if (-NOT $TestResults.DomainJoined) {
+ Write-Host "SKIPPING: DomainJoined status not matched (requested: $DomainJoinedOnly - actual: $IsDomainJoined), marking as compliant"
+ Write-Host "DomainRole: $($Computer.Inventory.WindowsSystemInfo.DomainRole)"
+ Write-Host "Domain: $($Computer.Inventory.WindowsSystemInfo.Domain)"
+ Write-Host "AzureADJoined: $($Computer.Inventory.WindowsSystemInfo.DSRegStatus.DeviceState.AzureADJoined)"
+ return $true
+ }
+
Write-Progress "Testing if user $Username exists"
$LocalUser = Invoke-ImmyCommand {(Get-LocalUser $using:Username -ErrorAction SilentlyContinue)}
$TestResults.UserExists = ($null -ne $LocalUser)
File: Configure-LocalUser2.ps1
[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "")]
[CmdletBinding()]
param(
[Parameter(Position = 0, Mandatory, HelpMessage = "A new user will be created if this user doesn't exist")]
[String]$Username,
[Parameter(Position = 1, Mandatory, HelpMessage = "The desired password.")]
[Password()]$Password,
[Parameter(Position = 2, Mandatory = $False, HelpMessage = "The Full Name of the user, if desired")]
[String]$FullName = "",
[Parameter(Position = 3, Mandatory = $False, HelpMessage = "This will hide the user from the logon screen (applicable when the machine is not joined to a domain)")]
[Alias('HideAccount')]
[Boolean]$Hidden = $false,
[Parameter(Position = 4, Mandatory = $False)]
[Alias('LocalAdmin')]
[Boolean]$LocalAdministrator = $true,
[Parameter(Position = 5, Mandatory = $False)]
[Boolean]$PasswordNeverExpires = $true,
[Parameter(Position = 6, Mandatory = $False, DontShow)]
[Boolean]$UserMustChangePasswordAtNextLogon = $false,
[Parameter(Position = 7, Mandatory = $False, DontShow)]
[Boolean]$UserCannotChangePassword = $false,
[Parameter(Position = 8, Mandatory = $False, DontShow)]
[Boolean]$AccountIsDisabled = $false,
[Parameter(Position = 9, Mandatory = $False, HelpMessage = "Run this only if the computer is joined to a domain/AzureAD.")]
[Boolean]$DomainJoinedOnly = $false
)
if (!$Computer -or $null -eq $Computer.Inventory.WindowsSystemInfo) {
$Computer = Get-ImmyComputer -InventoryKeys WindowsSystemInfo
}
##DR 20230809 - Commenting the following because: "Cannot invoke method. Method invocation is supported only on core types in this language mode."
# - Also, not sure why it even needs to be done? If $Hidden is null, why does it need to be set like this here?
#if(!$PSBoundParameters.ContainsKey('Hidden'))
#{
# $Hidden = $null
#}
#DR 20230815 - Some folks are attempting to use this against domain controllers. Domain controllers do not have local groups and won't work with this script. Will throw exceptions for Domain Controllers.
if ($Computer.Inventory.WindowsSystemInfo.DomainRole -in 4, 5) {
# 4,5 means Domain Controller or Primary Domain Controller
throw "This computer is a domain controller, local accounts cannot be created or managed with this script."
}
$IsDomainJoined = ($Computer.Inventory.WindowsSystemInfo.DSRegStatus.DeviceState.AzureADJoined -eq "YES") -or ` #Azure AD Check
( ` # Check if domain joined (not AzureAD)
($Computer.Inventory.WindowsSystemInfo.DomainRole -in 1) -and `
($Computer.Inventory.WindowsSystemInfo.Domain -ne $null) -and `
($Computer.Inventory.WindowsSystemInfo.Domain -ne "") `
)
function Test-LocalAdmin {
param(
[string]$Username
)
Invoke-ImmyCommand -Timeout 360 {
$UserName = $using:Username
Write-Progress "Querying WMI"
# Universal SID for the local "Administrators" group
$AdministratorsGroupSID = 'S-1-5-32-544'
# Now query using the resolved group name
$AdminGroupMembers = Get-LocalGroupMember -SID $AdministratorsGroupSID | ForEach-Object {
$type = switch ($_.ObjectClass) {
'User' { 'User'; break }
'Group' { 'Group'; break }
default { 'Unknown' }
}
if ($_.Name -match '^(?<Domain>[^\\]+)\\(?<Name>.+)$') {
$domainPrefix = $Matches['Domain']
$nameOnly = $Matches['Name']
} else {
$domainPrefix = [System.Environment]::MachineName
$nameOnly = $_.Name
}
$user = if ($type -eq 'User') { Get-LocalUser -Name $nameOnly -ErrorAction SilentlyContinue } else { $null }
[PSCustomObject]@{
Member = "$domainPrefix\$nameOnly"
Name = "$domainPrefix\$nameOnly"
Disabled = if ($user) { -not $user.Enabled } else { $null }
LocalAccount = if ($_.PrincipalSource -eq 'Local') { $true } else { $false }
Type = $type
}
}
Write-Progress "Queried WMI"
if($UserName -like "*\*"){
$AdminGroupMember = $AdminGroupMembers | Where-Object {$_.Name -like $Username}
}else{
$AdminGroupMember = $AdminGroupMembers | Where-Object {$_.Name -like "*\$Username"}
}
if (!$AdminGroupMember) {
return $false
}
return $true
}
}
function Test-LocalUser {
$Path = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\SpecialAccounts\UserList"
if (!$TestResults) {
$TestResults = [Ordered]@{}
}
$TestResults.DomainJoined = ($DomainJoinedOnly -eq $IsDomainJoined)
if (-NOT $TestResults.DomainJoined) {
Write-Host "SKIPPING: DomainJoined status not matched (requested: $DomainJoinedOnly - actual: $IsDomainJoined), marking as compliant"
Write-Host "DomainRole: $($Computer.Inventory.WindowsSystemInfo.DomainRole)"
Write-Host "Domain: $($Computer.Inventory.WindowsSystemInfo.Domain)"
Write-Host "AzureADJoined: $($Computer.Inventory.WindowsSystemInfo.DSRegStatus.DeviceState.AzureADJoined)"
return $true
}
Write-Progress "Testing if user $Username exists"
$LocalUser = Invoke-ImmyCommand {(Get-LocalUser $using:Username -ErrorAction SilentlyContinue)}
$TestResults.UserExists = ($null -ne $LocalUser)
Write-Progress "Testing if user $Username hidden/unhidden"
if ($Hidden -eq $false) {
$ShowUser = $null #Null will cause registry deletion
} else {
[int]$ShowUser = 0
}
$TestResults.UserHidden = Get-WindowsRegistryValue -Path $Path -Name $Username | RegistryShould-Be -Value $ShowUser -Type DWord
if ($TestResults.UserExists) {
Write-Progress "Testing if user $Username is Local Admin"
if ($null -ne $LocalAdministrator) {
switch ($LocalAdministrator) {
$true {
$TestResults.IsLocalAdmin = Test-LocalAdmin -Username $Username
if (!$TestResults.IsLocalAdmin) {
Write-Warning "User $Username is supposed to be local admin but is not"
}
}
$false {
$TestResults.IsNotLocalAdmin = !(Test-LocalAdmin -Username $Username)
if (!$TestResults.IsNotLocalAdmin) {
Write-Warning "User $Username is not supposed to be local admin but is"
}
}
}
}
if ($null -ne $Hidden -and $TestResults.UserHidden -eq $false) {
if ($Hidden -eq $false) {
Write-Warning "User $Username is not supposed to be hidden but is"
} elseif ($Hidden -eq $true) {
Write-Warning "User $Username is supposed to be hidden but is not"
}
}
Write-Progress "Testing credentials for user $Username"
$TestResults.CredentialsValid = Test-Credential -Username $Username -Password $Password
if (!$TestResults.CredentialsValid) {
Write-Warning "User $Username password does not match the specified password"
}
Write-Progress "Testing fullname for user $Username"
$TestResults.FullName = ($LocalUser.FullName -eq $FullName)
if (!$TestResults.FullName) {
Write-Warning "User $Username Fullname is not ""$FullName"""
}
Write-Progress "Testing for PasswordNeverExpires for user $Username"
$TestResults.PasswordNeverExpires = Invoke-ImmyCommand {
$User = Get-LocalUser -Name $Using:Username -ErrorAction SilentlyContinue
if ([bool]$User.PasswordExpires) {
return $($false -eq $Using:PasswordNeverExpires)
} else {
return $($True -eq $Using:PasswordNeverExpires)
}
return $null
}
}
return $TestResults
}
$TestResults = Test-LocalUser
switch ($method) {
'test' {
$TestResults | Test-All -Verbose
}
'set' {
$VerbosePreference = 'Continue'
Invoke-Immycommand {
$AccountName = $using:Username
$Password = $using:password
$FullName = $using:FullName
$Hidden = $using:Hidden
$LocalAdmin = $using:LocalAdministrator
$PasswordNeverExpires = $using:PasswordNeverExpires
$TestResults = $using:TestResults
$BuiltinAdministratorGroupSID = 'S-1-5-32-544'
$BuiltinUsersGroupSID = 'S-1-5-32-545'
$SecurePassword = ConvertTo-SecureString $Password -AsPlainText -Force
$LocalUserParams = @{
Name = $AccountName
AccountNeverExpires = $true
PasswordNeverExpires = $PasswordNeverExpires -eq $true
Password = $SecurePassword
FullName = $FullName
}
if ($null -ne $TestResults -and $TestResults.UserExists -eq $false) {
Write-Progress "Creating user $AccountName"
$LocalUserParams.UserMayNotChangePassword = $false
New-LocalUser @LocalUserParams
} else {
Write-Progress "Updating user $AccountName"
$LocalUserParams.UserMayChangePassword = $true
Set-LocalUser @LocalUserParams
}
Enable-LocalUser -Name $AccountName -Verbose
try {
([adsi]"WinNT://./$AccountName").SetPassword("$Password")
} catch {
Write-Warning "Unable to set password for account: $AccountName"
}
switch ($LocalAdmin) {
$true {
Write-Progress "Adding $AccountName to LocalAdmin Group"
Add-LocalGroupMember -SID $BuiltinAdministratorGroupSID -Member $AccountName -ErrorAction SilentlyContinue
}
$false {
Write-Progress "Removing $AccountName from LocalAdmin Group"
Remove-LocalGroupMember -SID $BuiltinAdministratorGroupSID -Member $AccountName -ErrorAction SilentlyContinue
Write-Progress "Adding $AccountName to Users Group"
Add-LocalGroupMember -SID $BuiltinUsersGroupSID -Member $AccountName -ErrorAction SilentlyContinue
}
}
# Hide account from logon screen
$UserListPath = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\SpecialAccounts\UserList"
switch ($Hidden) {
$true {
if (-not (Test-Path $UserListPath)) {
New-Item $UserListPath -Force | Out-Null
}
if (Get-ItemProperty -Path $UserListPath -Name $AccountName -ErrorAction SilentlyContinue) {
Set-ItemProperty -Path $UserListPath -Name $AccountName -Value 0
} else {
New-ItemProperty -Path $UserListPath -PropertyType DWORD -Name $AccountName -Value 0 | Out-Null
}
#Add-LocalGroupMember -SID $BuiltinUsersGroupSID -Member $AccountName -ErrorAction SilentlyContinue | Out-Null
}
$false {
Remove-ItemProperty -Path $UserListPath -Name $AccountName -ErrorAction SilentlyContinue
#Remove-LocalGroupMember -SID $BuiltinUsersGroupSID -Member $AccountName -ErrorAction SilentlyContinue | Out-Null
}
}
if (($TestResults.UserExists -eq $false -and $Hidden -eq $false) -or ($null -ne $TestResults.UserHidden -and $TestResults.UserHidden -eq $false)) {
try {
$process = Get-Process "logonui" -ErrorAction Stop
if ($null -ne $process) {
Stop-Process -Name "logonui" -Force -ErrorAction Stop
Write-Progress "The logonui process has been terminated successfully."
}
} catch {
Write-Warning $_
}
}
}
}
}
Example runs:
CC: @TerryW (or whomever else can handle this kind of PR)