PR: Local User/Administrator - Add DomainJoined param

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)