SysAdminTools.psm1

<#
.SYNOPSIS
    Adds a user that is currently in AD to a computer's "Remote Desktop Users" group.
.DESCRIPTION
    Adds a user that is currently in AD to a computer's "Remote Desktop Users" group.
.EXAMPLE
    PS C:\WINDOWS\system32> Add-RemoteDesktopUser -ComputerName pancake-3 -SamAccountName mrpig
 
    ComputerName SamAccountName UserAdded
    ------------ -------------- ---------
    pancake-3 mrpig True
 
    This examples adds the user "mrpig" to the computer pancake-3.
.INPUTS
    [String] ComputerName
    [String] SamAccountName
.OUTPUTS
    [PSCUSTOMOBJECT]
.NOTES
    Requires admin.
#>

function Add-RemoteDesktopUser{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory,Position=0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [string]$ComputerName,
        [Parameter(Mandatory,Position=1, ValueFromPipelineByPropertyName)]
        [string]$SamAccountName,
        [string]$Domain = $ENV:USERDOMAIN
    )
    try{
        if (Test-Connection -ComputerName $ComputerName -Count 1 -Quiet){
            #need to change error action perference to make any errors terminating since we are not using native powershell functions
            $ErrorActionPreference = "Stop"
            [ADSI]$Account = "WinNT://$Domain/$SamAccountName,User"
            [ADSI]$Group = "WinNT://$ComputerName/Remote Desktop Users,Group"
            $Group.Add($Account.Path)
            [PSCustomObject]@{
                ComputerName = $ComputerName
                SamAccountName = $SamAccountName
                UserDomain = $Domain
                UserAdded = $true
            }
            $ErrorActionPreference = "Continue"
        }
        else{
            Write-Error -Message "$ComputerName is offline or unreachable."
        }
    }
    catch{
        $ErrorActionPreference = "Continue"
        $PSCmdlet.WriteError($_)
    }
}
<#
.SYNOPSIS
    This functions get the current state of both internal and external (UPS) batteries.
.DESCRIPTION
        This functions get the current state of both internal and external (UPS) batteries. Including their charging state, charge remaining, estimated run time, etc.
.EXAMPLE
    PS C:\> Get-BatteryStatus
 
    ComputerName Name Charge (%) Run Time Battery Status Status
    ------------ ---- ---------- -------- -------------- ------
    DESKTOP-RFR3S01 GX1500U [XXXXXXXXXXXXXXXXXXXX] 100% 55 Connected_To_AC OK
 
    This example gets the local computer's battery status. The view built for this function shows the charge as bar graph (colored in the console).
.INPUTS
    System.String
        -ComputerName
.OUTPUTS
    SysAdminTools.BatteryStatus
.NOTES
    This function uses WsMan by default and Dcom protocol if that fails.
#>

function Get-BatteryStatus{
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline,ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [Alias("CN","Name","IPAddress")]
        [string[]]$ComputerName = $ENV:COMPUTERNAME
    )
    Begin{
        enum Availability {
            Other = 1
            Unknown = 2
            Running_FullPower = 3
            Warning = 4
            In_Test = 5
            Not_Applicable = 6
            Power_Off = 7
            Offline = 8
            Off_Duty = 9
            Degraded = 10
            Not_Installed = 11
            Install_Error = 12
            Power_Save_Unknown = 13
            Power_Save_Low_Power_Mode = 14
            Power_Save_StandBy = 15
            Power_Cycle = 16
            Power_Save_Warning = 17
            Paused = 18
            Not_Ready = 19
            Not_Configured = 20
            Quiesced = 21
        }

        enum BatteryStatus {
            Other_Discharging = 1
            Connected_To_AC = 2
            Fully_Charged = 3
            Low = 4
            Critical = 5
            Charging = 6
            Charing_High = 7
            Charging_Low = 8
            Charging_Critical = 9
            Partially_Charged = 11
        }

        enum Chemistry {
            Other = 1
            Unknown = 2
            Lead_Acid = 3
            Nickel_Cadmium = 4
            Nickel_Metal_Hydride = 5
            Lithium_ion = 6
            Zinc_air = 7
            Lithium_Polymer = 8
        }

        enum PowerManagementCapabilities {
            Unknown = 0
            Not_Supported = 1
            Disabled = 2
            Enabled = 3
            Power_Saving_Modes_Entered_Automatically = 4
            Power_State_Settable = 5
            Power_Cycling_Supported = 6
            Timed_Power_On_Supported = 7

        }
    } #begin

    Process{
        foreach ($computer in $ComputerName){
            if (Test-Connection -ComputerName $computer -Count 1 -Quiet){
                Try{
                    $CimSession = New-CimSession -ComputerName $computer -OperationTimeoutSec 1 -ErrorAction Stop
                }
                catch{
                    try{
                        Write-Information "Unable to connect to $computer with Wsman, using DCOM protocl instead" -Tags 'Process'
                        $CimSession = New-CimSession -ComputerName $computer -SessionOption (New-CimSessionOption -Protocol Dcom) -OperationTimeoutSec 1 -ErrorAction Stop
                    }
                    catch{
                        Write-Error "Unable to connect to $computer with Wsman or Dcom protocols"
                        continue
                    }   
                }
                try{
                    $Batteries = Get-CimInstance -CimSession $CimSession -ClassName Win32_Battery 
                    foreach ($battery in $Batteries){
                        [PSCustomObject]@{
                            PSTypeName = "SysAdminTools.BatteryStatus"
                            ComputerName = $computer
                            Name = $battery.Name
                            DesignVoltage = $battery.DesignVoltage
                            EstimatedChargeRemaining = $battery.EstimatedChargeRemaining
                            EstimatedRunTime = $battery.EstimatedRunTime
                            Availability = [Availability]($battery.Availability)
                            BatteryStatus = [BatteryStatus]($battery.BatteryStatus)
                            Chemistry = [Chemistry]($battery.Chemistry)
                            Status = $battery.Status
                            DeviceID = $battery.DeviceID
                        }
                    }
                    $CimSession | Remove-CimSession
                }
                catch{
                    if ($CimSession){
                        $CimSession | Remove-CimSession
                    }
                    $PSCmdlet.WriteError($_)
                } #catch
            } #try
        } #foreach
    } #process
}
<#
.SYNOPSIS
    Gets the default printer of a remote machine.
.DESCRIPTION
    Gets the default printer of a remote machine of a specfifc user, including Shared Server Printers.
.PARAMETER ComputerName
    Use this paramter to specify the computer(s) you want to run the command aganist using its name or IPAddress.
.PARAMETER SamAccountName
    This paramter allows you to only grab the default printers of the specifed user(s). This value is evaluated against the leaf of the localpath from Win32_UserProfile class
.PARAMETER Quiet
    This parameter prevents errors being generated if the computer is unreachable
.EXAMPLE
    PS C:\> Get-DefaultPrinter -ComputerName Client01v
 
    PrintServer PrinterName UserName ComputerName
    ----------- ----------- -------- ------------
    Local OneNote pwsh.cc Client01v
 
    This example gets the default printer of the only logged in user on Client01v.
.EXAMPLE
    PS C:\>Get-DefaultPrinter -ComputerName Client01v,dc01v
 
    PrintServer PrinterName UserName ComputerName
    ----------- ----------- -------- ------------
    Local OneNote pwsh.cc Client01v
    Local Microsoft Print to PDF Administrator dc01v
 
 
    PS C:\> Get-DefaultPrinter -ComputerName Client01v,dc01v -SamAccountName pwsh.cc
 
    PrintServer PrinterName UserName ComputerName
    ----------- ----------- -------- ------------
    Local OneNote pwsh.cc Client01v
 
    In this example it shows how the SamAccountName parameter works by only grabbing default users specified. Only pwsh.cc was returned by the second command even though the current user Administrator had a default printer on dc01v.
.EXAMPLE
    PS C:\>Test-Connection -ComputerName client01v -Count 1 -Quiet
    False
    PS C:\Users\Administrator> Get-DefaultPrinter -ComputerName Client01v,dc01v -Quiet
 
    PrintServer PrinterName UserName ComputerName
    ----------- ----------- -------- ------------
    Local Microsoft Print to PDF Administrator dc01v
 
    In this example we can see how the quiet paramter works, by omitting errors of computers that are unreachable such as Client01v.
.INPUTS
    String
.OUTPUTS
    PsCustomObject
        SysAdminTool.DefaultPrinter
.NOTES
    Uses WsMan Protocol as default and fallsback to DCOM. Grabs default printer from registry using StdRegProv wmi class in the root\default namespace
#>

function Get-DefaultPrinter{
    [cmdletbinding()]
    param(
        [Parameter(ValueFromPipeline,ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [Alias("CN","Name","MachineName","IPAddress")]
        [string[]]$ComputerName = $ENV:COMPUTERNAME,

        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [Alias("UserName")]
        [string[]]$SamAccountName,

        [switch]$Quiet
    )

    Begin{
        #Keys to use to reference each regsitry hive, only need HKEY_Users, the other are left for future reference
        #$HKEY_CLASSES_ROOT = 2147483648
        #$HKEY_CURRENT_USER = 2147483649
        #$HKEY_LOCAL_MACHINE = 2147483650
        $HKEY_USERS = 2147483651
        #$HKEY_CURRENT_CONFIG = [Convert]::ToUInt32(2147483653)
    }

    Process{
        foreach ($computer in $ComputerName){
            if (Test-Connection -ComputerName $Computer -Quiet -Count 1){
                try{
                    Write-Information "Test connection to computer $computer successful, creating Cim Session and grabbing win32_UserProfile" -Tags "Process"
                    try{
                        $session = New-CimSession -ComputerName $computer -OperationTimeoutSec 1 -ErrorAction Stop
                    }
                    catch{
                        Write-Information "Unable to create Cim Session using WsMan, creating fallback session using DCOM"
                        $CimSessionOption = New-CimSessionOption -Protocol Dcom
                        $session = New-CimSession -ComputerName $computer -SessionOption $CimSessionOption -OperationTimeoutSec 1
                    }
                    $AllUserProfiles = Get-CimInstance -ClassName Win32_UserProfile -CimSession $session -Filter "SPECIAL=$false"
                    $RemoteRegistry = Get-CimClass -Namespace "root\default" -ClassName StdRegProv -CimSession $session

                    $currentUsersReg = ($RemoteRegistry | Invoke-CimMethod -Name "EnumKey" -Arguments @{hDefKey=$HKEY_USERS;sSubKeyName = ""} -CimSession $session).sNames

                    $currentUsers = $AllUserProfiles | where SID -in $currentUsersReg 

                    foreach ($user in $currentUsers){
                        $SID = $user.SID
                        $UserName = Split-Path $user.LocalPath -Leaf
                        if ($PSBoundParameters.ContainsKey("SamAccountName") -and ($UserName -notin $SamAccountName)){
                            Write-Information "SamAccountName parameter was used, and the user $UserName was not found as a current user on $computer" -Tags "Process"
                            continue
                        }

                        Write-Information "Attempting to grab default printer for user $UserName" -Tags "Process"
                        $Printer = ($RemoteRegistry | Invoke-CimMethod -MethodName GetStringValue -Arguments @{hDefKey=$HKEY_USERS;sSubKeyName = "$SID\Software\Microsoft\Windows NT\CurrentVersion\Windows";sValueName = "Device"} -CimSession $session).sValue

                        if ($Printer){
                            Write-Information "A default printer was found for user $UserName, creating output object" -Tags "Process"
                            if ($Printer.StartsWith('\\')){
                                $PrinterPath = $printer.Split(",")[0]
                                $ServerandPrinter = $PrinterPath.Split('\',[System.StringSplitOptions]::RemoveEmptyEntries)
                                [PSCustomObject]@{
                                    PSTypeName = "SysAdminTools.DefaultPrinter"
                                    PrintServer = $ServerandPrinter[0]
                                    PrinterName = $ServerandPrinter[1]
                                    UserName = $UserName
                                    ComputerName = $computer
                                }
                            }
                            else{
                                $PrinterPath = $printer.Split(",")[0]
                                [PSCustomObject]@{
                                    PSTypeName = "SysAdminTools.DefaultPrinter"
                                    PrintServer = "Local"
                                    PrinterName = $PrinterPath
                                    UserName = $UserName
                                    ComputerName = $computer
                                }
                            }
                        } #if
                        else{
                            Write-Information "No default printer was found for user $Username" -Tags "Process"
                        }
                    } #foreach user
                    $session | Remove-CimSession
                } #try
                catch [System.Runtime.InteropServices.COMException]{
                    Write-Warning "WMI query failed on $computer. Ensure 'Windows Management Instrumentation (WMI-In)' firewall rule is enabled."
                    $PSCmdlet.WriteError($_)
                }
                catch{
                    Write-Warning "An uncaught execption has occurred please open an issue at https://github.com/MrPig91/SysAdminTools/issues"
                    $PSCmdlet.WriteError($_)
                }
            } #if connection
            else{
                if ($Quiet){
                    Write-Information "The quiet switch was used, skipping the connection fail error for $computer" -Tags "Process"
                }
                else{
                    $ErrorRecord = [System.Management.Automation.ErrorRecord]::new(
                        [System.Net.NetworkInformation.PingException]::new("$computer is unreachable"),
                        'TestConnectionException',
                        [System.Management.Automation.ErrorCategory]::ConnectionError,
                        $computer
                    )
                    $PSCmdlet.WriteError($ErrorRecord)
                }
            }
        } #foreach computer
    } #Process
}
<#
.SYNOPSIS
    This function gets the current user sesions on a remote or local computer.
.DESCRIPTION
    This function uses quser.exe to get the current user sessions from a remote or local computer.
.PARAMETER ComputerName
    Use this paramter to specify the computer you want to run the command aganist using its name or IPAddress.
 
.EXAMPLE
    PS C:\> Get-LoggedInUser
 
    ComputerName UserName ID SessionType State ScreenLocked IdleTime
    ------------ -------- -- ----------- ----- ------------ --------
    DESKTOP-D7FU4K5 pwsh.cc 1 DirectLogon Active False 0
 
    This examples gets the logged in users of the local computer.
.EXAMPLE
    Get-LoggedInUser -ComputerName $env:COMPUTERNAME,dc01v
 
    ComputerName UserName ID SessionType State ScreenLocked IdleTime
    ------------ -------- -- ----------- ----- ------------ --------
    DESKTOP-D7FU4K5 pwsh.cc 1 DirectLogon Active False 0
    dc01v administrator 1 DirectLogon Active False 0
 
    This example gets the currently logged on users for the local computer and a remote computer called dc01v.
.INPUTS
    System.String
        You can pipe a string that contains the computer name.
.OUTPUTS
    AdminTools.LoggedInuser
        Outputs a custom powershell object
.NOTES
    Requires Admin
#>

Function Get-LoggedInUser () {
    [CmdletBinding()]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, ValueFromPipeline)]
        [Alias("CN","Name","MachineName")]
        [string[]]$ComputerName = $ENV:ComputerName
    )

    PROCESS {
        foreach ($computer in $ComputerName){
            try{
                Write-Information "Testing connection to $computer" -Tags 'Process'
                if (Test-Connection -ComputerName $computer -Count 1 -Quiet){
                    $Users = quser.exe /server:$computer 2>$null | select -Skip 1

                    if (!$?){
                        Write-Information "Error with quser.exe" -Tags 'Process'
                        if ($Global:Error[0].Exception.Message -eq ""){
                            throw $Global:Error[1]
                        }
                        elseif ($Global:Error[0].Exception.Message -like "No User exists*"){
                            Write-Warning "No users logged into $computer"
                        }
                        else{
                            throw $Global:Error[0]
                        }
                    }
    
                    $LoggedOnUsers = foreach ($user in $users){
                        [PSCustomObject]@{
                            PSTypeName = "AdminTools.LoggedInUser"
                            ComputerName = $computer
                            UserName = (-join $user[1 .. 20]).Trim()
                            SessionName = (-join $user[23 .. 37]).Trim()
                            SessionId = [int](-join $user[38 .. 44])
                            State = (-join $user[46 .. 53]).Trim()
                            IdleTime = (-join $user[54 .. 63]).Trim()
                            LogonTime = [datetime](-join $user[65 .. ($user.Length - 1)])
                            LockScreenPresent = $false
                            LockScreenTimer = (New-TimeSpan)
                            SessionType = "TBD"
                        }
                    }
                    try {
                        Write-Information "Using WinRM and CIM to grab LogonUI process" -Tags 'Process'
                        $LogonUI = Get-CimInstance -ClassName win32_process -Filter "Name = 'LogonUI.exe'" -ComputerName $Computer -Property SessionId,Name,CreationDate -OperationTimeoutSec 1 -ErrorAction Stop
                    }
                    catch{
                        Write-Information "WinRM is not configured for $computer, using Dcom and WMI to grab LogonUI process" -Tags 'Process'
                        $LogonUI = Get-WmiObject -Class win32_process -ComputerName $computer -Filter "Name = 'LogonUI.exe'" -Property SessionId,Name,CreationDate -ErrorAction Stop |
                        select name,SessionId,@{n="Time";e={[DateTime]::Now - $_.ConvertToDateTime($_.CreationDate)}}
                    }
    
                    foreach ($user in $LoggedOnUsers){
                        if ($LogonUI.SessionId -contains $user.SessionId){
                            $user.LockScreenPresent = $True
                            $user.LockScreenTimer = ($LogonUI | where SessionId -eq $user.SessionId).Time
                        }
                        if ($user.State -eq "Disc"){
                            $user.State = "Disconnected"
                        }
                        $user.SessionType = switch -wildcard ($user.SessionName){
                            "Console" {"DirectLogon"; Break}
                            "" {"Unkown"; Break}
                            "rdp*" {"RDP"; Break}
                            default {""}
                        }
                        if ($user.IdleTime -ne "None" -and $user.IdleTime -ne "."){
                            if ($user.IdleTime -Like "*+*"){
                                $user.IdleTime = New-TimeSpan -Days $user.IdleTime.Split('+')[0] -Hours $user.IdleTime.Split('+')[1].split(":")[0] -Minutes $user.IdleTime.Split('+')[1].split(":")[1]
                            }
                            elseif($user.IdleTime -like "*:*"){
                                $user.idleTime = New-TimeSpan -Hours $user.IdleTime.Split(":")[0] -Minutes $user.IdleTime.Split(":")[1]
                            }
                            else{
                                $user.idleTime = New-TimeSpan -Minutes $user.IdleTime
                            }
                        }
                        else{
                            $user.idleTime = New-TimeSpan
                        }
    
                        $user | Add-Member -Name LogOffUser -Value {logoff $this.SessionId /server:$($this.ComputerName)} -MemberType ScriptMethod
                        $user | Add-Member -MemberType AliasProperty -Name ScreenLocked -Value LockScreenPresent

                        Write-Information "Outputting user object $($user.UserName)" -Tags 'Process'
                        $user
                    } #foreach
                } #if ping
                else{
                    $ErrorRecord = [System.Management.Automation.ErrorRecord]::new(
                        [System.Net.NetworkInformation.PingException]::new("$computer is unreachable"),
                        'TestConnectionException',
                        [System.Management.Automation.ErrorCategory]::ConnectionError,
                        $computer
                    )
                    $PSCmdlet.WriteError($ErrorRecord)
                }
            } #try
            catch [System.Management.Automation.RemoteException]{
                if ($_.Exception.Message -like "*The RPC server is unavailable*"){
                    Write-Warning "quser.exe failed on $comptuer, Ensure 'Netlogon Service (NP-In)' firewall rule is enabled"
                    $PSCmdlet.WriteError($_)
                }
                else{
                    $PSCmdlet.WriteError($_)
                }
            }
            catch [System.Runtime.InteropServices.COMException]{
                Write-Warning "WMI query failed on $computer. Ensure 'Windows Management Instrumentation (WMI-In)' firewall rule is enabled."
                $PSCmdlet.WriteError($_)
            }
            catch{
                Write-Information "Unexpected error occurred with $computer"
                $PSCmdlet.WriteError($_)
            }
        } #foreach
    } #process
}
<#
.SYNOPSIS
    This will grab logon events (both failed and/or successful) on a local or remote computers.
.DESCRIPTION
    This will grab logon events (both failed and/or successful) on a local or remote computers.
    This will display information around this local attepmt as well, inclduing LogonType, UserName, Failure Reason, etc.
.EXAMPLE
    PS C:\> Get-LoginEvent -UserName mrpig -Status Failed
 
    ComputerName UserName DomainName LogonType IPAddress RemoteComputer Success TimeCreated FailureReason
    ------------ -------- ---------- --------- --------- -------------- ------- ----------- -------------
    test-desktop mrpig PIGLAND Interactive 127.0.0.1 test-desktop False 8/27/2021 11:08:26 AM Unknown user name or bad password.
    test-desktop mrpig PIGLAND Interactive 127.0.0.1 test-desktop False 8/27/2021 11:08:20 AM Unknown user name or bad password.
     
    This grabs all failed login attempts from the user mrpig on the local computer.
 
.EXAMPLE
    Get-LoginEvent -LogonType Interactive -ComputerName test-desktop | group username | where {$_.name -notlike "UMFD*" -and $_.name -notlike "DWM*"} | select count,Name
 
    Count Name
    ----- ----
        9 mrpig
        8 mrspig
 
    This example grabs all users who log into the computer test-desktop interactively, then filters out the windows logins UMFD and DWM. Finally it groups the users to show how many times each one logged in.
 
.EXAMPLE
    PS C:\> Get-LoginEvent -ComputerName server02v -LogonType RemoteInteractive -StartTime (get-date).AddDays(-2) -OutVariable Logins
 
    ComputerName UserName DomainName LogonType IPAddress RemoteComputer Success TimeCreated FailureReason
    ------------ -------- ---------- --------- --------- -------------- ------- ----------- -------------
    server02v admincs PIGLAND RemoteInteractive 10.99.120.85 server02v True 8/31/2021 4:39:30 PM
    server02v admincs PIGLAND RemoteInteractive 10.99.120.85 server02v True 8/31/2021 4:39:30 PM
 
    This grabs all logon events on the server server02v that are of type RemoteInteractive (RDP) in the last 2 days. It also store the output in the variable Logins since the query can take some time.
.INPUTS
    string
        ComputerName
.OUTPUTS
    SysAdminTools.LoginEvent
.NOTES
    This query can take some time on remote computers. Requires admin for remote computers. Requires Powershell version 6 or higher when using the UserName parameter.
#>

function Get-LoginEvent {
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline,ValueFromPipelineByPropertyName)]
        [string[]]$ComputerName = $env:COMPUTERNAME,
        
        [Parameter(ValueFromPipelineByPropertyName)]
        [Alias("SamAccountName")]
        [string]$UserName,

        [datetime]$StartTime,
        [datetime]$EndTime,

        [ValidateSet("Interactive","Network","Batch","Service","Unlock","NetworkClearText","NewCredentials","RemoteInteractive","CachedInteractive")]
        [string]$LogonType,

        [ValidateSet("Failed","Successful")]
        [ValidateNotNullOrEmpty()]
        [string[]]$Status = @("Failed","Successful"),

        [int]$MaxEvents
    )

    Begin {
        $Ids = [System.Collections.Generic.List[int]]::new()
        if ($Status.Contains("Failed")){
            [void]$Ids.Add(4625)
        }
        if ($Status.Contains("Successful")){
            [void]$Ids.Add(4624)
        }

        $filterHashTable = @{
            LogName = "Security"
            ID = $Ids.ToArray()
        }

        enum LogonType {
            Unknown = 0
            Interactive = 2
            Network = 3
            Batch = 4
            Service = 5
            Unlock = 7
            NetworkClearText = 8
            NewCredentials = 9
            RemoteInteractive = 10
            CachedInteractive = 11
        }

        if ($PSBoundParameters.ContainsKey("LogonType")){
            $filterHashTable["LogonType"] = [LogonType]$LogonType -as [int]
        }
        if ($PSBoundParameters.ContainsKey("StartTime")){
            $filterHashTable["StartTime"] = $StartTime
        }
        if ($PSBoundParameters.ContainsKey("EndTime")){
            $filterHashTable["EndTime"] = $EndTime
        }

        $Parameters = @{
            FilterHashtable = $filterHashTable
        }
        if ($PSBoundParameters.ContainsKey("MaxEvents")){
            $Parameters["MaxEvents"] = $MaxEvents
        }
    }

    Process {
        if ($PSBoundParameters.ContainsKey("UserName")){
            if (-not($PSVersionTable.PSVersion -ge [Version]::new(6,0))){
                throw "Requires Powershell Version 6 or higher in order to use the UserName parameter"
            }
            $filterHashTable["TargetUserName"] = $UserName
        }

        foreach ($computer in $ComputerName){
            $Parameters["ComputerName"] = $computer
            if (Test-Connection -ComputerName $computer -Quiet -Count 1){
                try{
                    Get-WinEvent @Parameters -ErrorAction Stop | ForEach-Object {
                        $Success = switch ($_.Id){
                            4624 {$true}
                            4625 {$false}
                        }
                        $LoginEvent = [PSCustomObject]@{
                            PSTypeName = "SysAdminTools.LoginEvent"
                            TimeCreated = $_.TimeCreated
                            ComputerName = $computer
                            Id = $_.Id
                            Success = $Success
                        }
                        $XML = [xml]$_.ToXml()
                        $XML.event.eventdata.data | ForEach-Object {
                            $LoginEvent | Add-Member -MemberType NoteProperty -Name $_.Name -Value $_.'#text'
                        }
                        $FailureReason = Get-LogonFailureReason -EventRecord $LoginEvent
                        $LoginEvent | Add-Member -MemberType AliasProperty -Name Username -Value TargetUserName
                        $LoginEvent | Add-Member -MemberType AliasProperty -Name DomainName -Value TargetDomainName
                        $LoginEvent | Add-Member -MemberType AliasProperty -Name SID -Value TargetUserSid
                        $LoginEvent | Add-Member -MemberType AliasProperty -Name RemoteComputer -Value WorkStationName
                        $LoginEvent | Add-Member -NotePropertyMembers @{
                            StatusString = $FailureReason.Status
                            SubStatusString = $FailureReason.SubStatus
                            FailureReasonString = $FailureReason.Reason
                        }
                        $LoginEvent.LogonType = [LogonType]($LoginEvent.LogonType)
                        $LoginEvent
                    }
                }
                catch{
                    $PSCmdlet.WriteError($_)
                }
            }
            else{
                Write-Error "Computer [$computer] is unreachable"
            }
        } #FOREACH
    } #PROCESS
}
<#
.SYNOPSIS
    This will grab the serial number, monitor name, and year of manufacture of all monitors connected to a computer.
.PARAMETER ComputerName
    Use this paramter to specify the computer(s) you want to run the command aganist using its name or IPAddress.
.DESCRIPTION
    This functions grabs the serial number, monitor name, and year of manufacture of all monitors
    connected to a computer.
.EXAMPLE
    PS C:\> Get-MonitorInfo
 
    ComputerName MonitorName SerialNumber YearOfManufacture
    ------------ ----------- ------------ -----------------
    DESKTOP-RFR3S01 Acer K272HUL T0SAA0014200 2014
    DESKTOP-RFR3S01 VX2457 UG01842A1649 2018
 
    This example grabs the monitors connected to the local computer.
.EXAMPLE
    PS C:\> Get-ComputerMonitor Client01v,Client02v
 
    ComputerName MonitorName SerialNumber YearOfManufacture
    ------------ ----------- ------------ -----------------
    Client01v HP HC240 XXXXXXXXXX 2017
    Client01v HP HC240 XXXXXXXXXX 2017
    Client02v HP E243i XXXXXXXXXX 2018
    Client02v HP E243i XXXXXXXXXX 2018
 
    This example uses the ComputerName parameter, but it does so positionally which is why it
    is not written out. It grabs the info for all monitors connected to Client01v and Client02v.
.INPUTS
    None
.OUTPUTS
    PsCustomObject
.NOTES
    Does not grab built-in monitor info.
#>

function Get-MonitorInfo{
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline,ValueFromPipelineByPropertyName)]
        [Alias("CN","Name","IPAddress")]
        [ValidateNotNullOrEmpty()]
        [string[]]$ComputerName = $Env:COMPUTERNAME,

        [Parameter()]
        [ValidateSet("Wsman","Dcom")]
        $Protocol = "Wsman"
    )

    Begin{
        $options = New-CimSessionOption -Protocol $Protocol
    }

    Process{
        foreach ($computer in $ComputerName){
            try{
                Write-Information -MessageData "Creating new cim session for $computer with a $protocol connection" -Tags "Process"
                $Session = New-CimSession -ComputerName $computer -OperationTimeoutSec 1 -SessionOption $options -ErrorAction Stop
            
                Write-Information -MessageData "Calling WMIMonitorID Class to grab monitor info for computer $computer" -Tags "Process"
                $monitors = Get-CimInstance -ClassName WmiMonitorID -Namespace root\wmi -CimSession $Session | Where-Object UserFriendlyNameLength -NE 0
            
                foreach ($monitor in $monitors){
                    $SerialNumber = ($monitor.SerialNumberID -ne 0 | ForEach-Object{[char]$_}) -join ""
                    $MonitorName = ($monitor.UserFriendlyName -ne 0 | ForEach-Object{[char]$_}) -join ""

                    $Object = [PSCustomObject]@{
                        PSTypeName = "SysAdminTools.Monitor"
                        ComputerName = $computer.ToUpper()
                        MonitorName = $MonitorName
                        SerialNumber = $SerialNumber
                        YearOfManufacture = $monitor.YearOfManufacture
                    }
                    
                    $Object
                    Write-Information -MessageData "Created object for monitor $($object.MonitorName)" -Tags "Process"
                } #foreach

                Write-Information -MessageData "Removing $computer cim session" -Tags "Process"
                Get-CimSession | where computername -eq $computer | Remove-CimSession
            } 
            catch{
                Write-Warning "Unable to grab monitor info for $computer"
            }
        } #foreach computer
    } #Process
}
<#
.SYNOPSIS
    This function searches for any operations that awaiting a system reboot.
.DESCRIPTION
    This function searches the Windows registry for any operations that require a system reboot, most importantly a Windows update.
.EXAMPLE
    PS C:\> Get-PendingRebootStatus
 
    ComputerName PendingReboot PendingRebootReasons
    ------------ ------------- --------------------
    DESKTOP-RFR3S01 True {FileRename}
 
    This example grabs the local computers pending reboot status. Currently it is only waiting on a file rename which is urgent.
.INPUTS
    [string[]] ComputerName
.OUTPUTS
    PSCustomObject
.NOTES
    Uses CimClass StdRegProv to grab registry information.
#>

function Get-PendingRebootStatus {
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipelineByPropertyName,ValueFromPipeline)]
        [Alias("CN","Name","MachineName")]
        [string[]]$Computername = $env:COMPUTERNAME
    )

    Begin{
        #Keys to use to reference each regsitry hive
        $HKEY_LOCAL_MACHINE = 2147483650

        #return codes
        <#
            RC = 0 for success
            RC = 1 for key read with no default value
            RC = 2 for key not found
            RC = 6 for invalid hive
        #>


        #Registry Paths to check
        $Updates = "SOFTWARE\Microsoft\Updates" #value UpdateExeVolatile is anything other than 0
        $FileRename = "SYSTEM\CurrentControlSet\Control\Session Manager" #value PendingFileRenameOperations, PendingFileRenameOperations2 exists
        $RebootRequired = "SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update" #key RebootRequired or PostRebootReporting exists
        $Pending = "SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Services\Pending" #GUID subkeys exists
        $RunOnce = "SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce" #value DVDRebootSignal exist
        $ComponentBS = "Software\Microsoft\Windows\CurrentVersion\Component Based Servicing" #keys RebootPending,RebootInProgress,PackagesPending exits
        $CBSValues = @("RebootPending","RebootInProgress","PackagesPending")
        $CurrentRebootAttempts = "SOFTWARE\Microsoft\ServerManager" #key CurrentRebootAttempts exists
        $NetLogin = "SYSTEM\CurrentControlSet\Services\Netlogon" #values JoinDomain, AvoidSpnSet exits
        $NetLoginValues = @("JoinDomain","AvoidSpnSet")
        $ActiveComputerName = "SYSTEM\CurrentControlSet\Control\ComputerName\ActiveComputerName" #value ComputerName is different than `
        #Value ComputerName in HKLM:\SYSTEM\CurrentControlSet\Control\ComputerName\ComputerName is different
        $FutureName = "SYSTEM\CurrentControlSet\Control\ComputerName\ComputerName"

    } #Begin

    Process{
        foreach ($computer in $Computername){
            if (Test-Connection -ComputerName $computer -Quiet -Count 1){
                $PendingReboot = $false
                $PendingRebootReasons = [System.Collections.Generic.List[string]]::New()
                Write-Information "[$Computer] is reachable" -Tags "Process"
                try{
                    $CimSession = New-CimConnection -ComputerName $computer -ErrorAction Stop
                    $RemoteRegistry = Get-CimClass -Namespace "root\default" -ClassName StdRegProv -CimSession $CimSession -ErrorAction Stop
                    Write-Information "[$computer]: started a new cim session and connected to remote registry"
                    
                    $UpdatesResults = $RemoteRegistry | Invoke-CimMethod -Name "GetDWORDValue" -Arguments @{hDefKey=$HKEY_LOCAL_MACHINE;sSubKeyName=$Updates;sValueName="UpdateExeVolatile"} -CimSession $CimSession
                    if ($UpdatesResults.ReturnValue -eq 0 -and $UpdatesResults.uValue -ne 0){
                        Write-Information "UpdateExeVolatile value does not equal 0" -Tags "Process"
                        $PendingReboot = $true
                        $PendingRebootReasons.Add("MSUpdates")
                    }

                    $FileRenameResults = $RemoteRegistry | Invoke-CimMethod -Name "EnumValues" -Arguments @{hDefKey=$HKEY_LOCAL_MACHINE;sSubKeyName=$FileRename} -CimSession $CimSession
                    $ContainsFileNameValues = ($FileRenameResults.sNames -Contains "PendingFileRenameOperations" -or $FileRenameResults.sNames -Contains "PendingFileRenameOperations2")
                    if ($FileRenameResults.ReturnValue -eq 0 -and $ContainsFileNameValues){
                        Write-Information "FileNameOpertions values exists" -Tags "Process"
                        $PendingReboot = $true
                        $PendingRebootReasons.Add("FileRename")
                    }

                    $RebootRequiredResults = $RemoteRegistry | Invoke-CimMethod -Name "EnumKey" -Arguments @{hDefKey=$HKEY_LOCAL_MACHINE;sSubKeyName=$RebootRequired} -CimSession $CimSession
                    $ContainsRebootValues = ($RebootRequiredResults.sNames -Contains "RebootRequired" -or $RebootRequiredResults.sNames -Contains "PostRebootReporting")
                    if ($RebootRequiredResults.ReturnValue -eq 0 -and $ContainsRebootValues){
                        Write-Information "RebootRequired value exists" -Tags "Process"
                        $PendingReboot = $true
                        $PendingRebootReasons.Add("WindowsUpdates")
                    }

                    $PendingResults = $RemoteRegistry | Invoke-CimMethod -Name "EnumKey" -Arguments @{hDefKey=$HKEY_LOCAL_MACHINE;sSubKeyName=$Pending} -CimSession $CimSession
                    if ($PendingResults.ReturnValue -eq 0 -and $null -ne $PendingResults.sNames){
                        Write-Information "GUID keys exists under Pending key exits" -Tags "Process"
                        $PendingReboot = $true
                        $PendingRebootReasons.Add("ServicesPending")
                    }

                    $RunOnceResults = $RemoteRegistry | Invoke-CimMethod -Name "EnumValues" -Arguments @{hDefKey=$HKEY_LOCAL_MACHINE;sSubKeyName=$RunOnce} -CimSession $CimSession
                    if ($RunOnceResults.ReturnValue -eq 0 -and $RunOnceResults.sNames -contains "DVDRebootSignal"){
                        Write-Information "DVDRebootSignal value exits" -Tags "Process"
                        $PendingReboot = $true
                        $PendingRebootReasons.Add("RunOnce")
                    }

                    $ComponentBSResults = $RemoteRegistry | Invoke-CimMethod -Name "EnumKey" -Arguments @{hDefKey=$HKEY_LOCAL_MACHINE;sSubKeyname=$ComponentBS} -CimSession $CimSession
                    if ($ComponentBSResults.ReturnValue -eq 0){
                        $ComponentBSResults.sNames | where {$_ -in $CBSValues} | ForEach-Object -Process {
                            $PendingReboot = $true
                            Write-Information "$_ key exits" -Tags "Process"
                            $PendingRebootReasons.Add($_)
                        }
                    }

                    $CurrentRebootAttemptsResults = $RemoteRegistry | Invoke-CimMethod -Name "EnumKey" -Arguments @{hDefKey=$HKEY_LOCAL_MACHINE;sSubKeyname=$CurrentRebootAttempts} -CimSession $CimSession
                    if ($CurrentRebootAttemptsResults.ReturnValue -eq 0 -and $CurrentRebootAttemptsResults.sNames -contains "CurrentRebootAttempts"){
                        Write-Information "CurrentRebootAttempt key exits" -Tags "Process"
                        $PendingReboot = $true
                        $PendingRebootReasons.Add("CurrentRebootAttempts")
                    }

                    $NetLoginResults = $RemoteRegistry | Invoke-CimMethod -Name "EnumValues" -Arguments @{hDefKey=$HKEY_LOCAL_MACHINE;sSubKeyName=$NetLogin} -CimSession $CimSession
                    if ($NetLoginResults.ReturnValue -eq 0){
                        $NetLoginResults.sNames | where {$_ -in $NetLoginValues} | ForEach-Object -Process {
                            $PendingReboot = $true
                            Write-Information "$_ value exits" -Tags "Process"
                            $PendingRebootReasons.Add($_)
                        }
                    }

                    $ActiveComputerNameResults = $RemoteRegistry | Invoke-CimMethod -Name "GetStringValue" -Arguments @{hDefKey=$HKEY_LOCAL_MACHINE;sSubKeyName=$ActiveComputerName;sValueName="ComputerName"} -CimSession $CimSession
                    $FutureNameResults =  $RemoteRegistry | Invoke-CimMethod -Name "GetStringValue" -Arguments @{hDefKey=$HKEY_LOCAL_MACHINE;sSubKeyName=$FutureName;sValueName="ComputerName"} -CimSession $CimSession
                    if ($ActiveComputerName.ReturnValue -eq 0 -and $FutureNameResults.ReturnValue -eq 0){
                        if ($ActiveComputerNameResults.sValue -eq $FutureNameResults.sValue){
                            Write-Information "Pending computer name change" -Tags "Process"
                            $PendingReboot = $true
                            $PendingRebootReasons.Add("PendingNameChange")
                        }
                    }

                    [PSCustomObject]@{
                        ComputerName = $computer
                        PendingReboot = $PendingReboot
                        PendingRebootReasons = $PendingRebootReasons
                    }
                    $CimSession | Remove-CimSession
                }
                catch{
                    $PSCmdlet.WriteError($_)
                }
            }
            else{
                Write-Error "[$computer] is unreachable"
            }
        } #foreach computer
    } #process
}
<#
.SYNOPSIS
    This function gets power settings for local or remote computer(s).
.DESCRIPTION
    This function gets power setting for local or remote computer(s).
    You can filter the results using the parameters SettingName, PlanName, ActivePlanOnly, and SettingType.
.PARAMETER ComputerName
    This parameter specifies the ComputerName for the computer you want to get power setting from. Default value is $ENV:COMPUTERNAME.
.PARAMETER PlanName
    This parameter specifies the plan name of the plan you want to get power settings for. Common plan names include "HP Optmized" and "Balanced".
    The function will grab all settings for all plans if no plan name is give. User inputted wildcards are not permitted, but the value provided will have wildcards on both sides added to it.
.PARAMETER ActivePlanName
    This switch parameter will filter the power settings so that only the setting for currently active power plan are returned.
.PARAMETER SettingName
    This parameter specifies the name of the power setting you want to grab. Wildcards are accepted. By default, all settings are returned.
.PARAMETER SettingType
    This parameter specifies the power type for the setting, options include DC (on battery) or AC (wall power) power.
.EXAMPLE
    PS C:\> Get-PowerSetting -ActivePlanOnly -SettingName *timeout* -SettingType AC
 
    Plan Name: HP Optimized (recommended)
 
    ComputerName Active Type SettingName SettingValue Range
    ------------ ------ ---- ----------- ------------ -----
    TEST-MACHINE1-D True AC Secondary NVMe Idle Timeout 2000 milliseconds {0, 60000}
    TEST-MACHINE1-D True AC Primary NVMe Idle Timeout 200 milliseconds {0, 60000}
    TEST-MACHINE1-D True AC System unattended sleep timeout 3600 Seconds {0, 4294967295}
    TEST-MACHINE1-D True AC Hub Selective Suspend Timeout 50 Millisecond {0, 100000}
    TEST-MACHINE1-D True AC Execution Required power request timeout 4294967295 Seconds {0, 4294967295}
    TEST-MACHINE1-D True AC IO coalescing timeout 0 Milliseconds {0, 4294967295}
    TEST-MACHINE1-D True AC Console lock display off timeout 1800 Seconds {0, 4294967295}
    TEST-MACHINE1-D True AC Non-sensor Input Presence Timeout 240 Seconds {0, 4294967295}
 
    In this example we get all power settings that are on the Active power plan that have "timeout" in their name and associated with AC power type.
.EXAMPLE
    PS C:\> Get-PowerSetting -ComputerName TEST-LAPTOP02-L,TEST-MACHINE1-D -ActivePlanOnly -SettingType AC -SettingName "Console lock display off timeout"
 
   Plan Name: Balanced
 
    ComputerName Active Type SettingName SettingValue Range
    ------------ ------ ---- ----------- ------------ -----
    TEST-LAPTOP02-L True AC Console lock display off timeout 60 Seconds {0, 4294967295}
 
 
    Plan Name: HP Optimized (recommended)
 
    ComputerName Active Type SettingName SettingValue Range
    ------------ ------ ---- ----------- ------------ -----
    TEST-MACHINE1-D True AC Console lock display off timeout 1800 Seconds {0, 4294967295}
 
    In this example we get the power setting "Console lock display off timeout" from two computers for the active power plan on AC power.
    Here we can see TEST-MACHINE1-D has a 1800 seconds (30 minutes) time out for this second where TEST-LAPTOP02-L has 60 second timeout.
    This setting controls how fast the ocmputer goest to sleep after a user locks there computer. The default of 60 seconds is usually too low.
.EXAMPLE
    PS C:\> Get-PowerSetting -ComputerName TEST-LAPTOP02-L -ActivePlanOnly -SettingType AC -SettingName "Console lock display off timeout"
 
        Plan Name: Balanced
 
    ComputerName Active Type SettingName SettingValue Range
    ------------ ------ ---- ----------- ------------ -----
    TEST-LAPTOP02-L True AC Console lock display off timeout 60 Seconds {0, 4294967295}
 
 
    PS C:\> Get-PowerSetting -ComputerName TEST-LAPTOP02-L -ActivePlanOnly -SettingType AC -SettingName "Console lock display off timeout" | Set-PowerSetting -IndexValue 1800
    PS C:\> Get-PowerSetting -ComputerName TEST-LAPTOP02-L -ActivePlanOnly -SettingType AC -SettingName "Console lock display off timeout"
 
 
        Plan Name: Balanced
 
    ComputerName Active Type SettingName SettingValue Range
    ------------ ------ ---- ----------- ------------ -----
    TEST-LAPTOP02-L True AC Console lock display off timeout 1800 Seconds {0, 4294967295}
 
    The first command gets the "Console lock display off timeout" power setting for TEST-LAPTOP02-L and outputs it to the console. We se it is set to 60 seconds.
    The second command runs the same command but then pipes it to Set-PowerSetting and provides the value 1800 to the parameter IndexValue. This will set that setting to 1800 seconds (30 minutes).
    The last command run the first command again to confirm that the setting was udpated to the new vale, and here we can see that it was.
 
    This particular setting's default value of 60 seconds is pretty low. It will put the computer asleep after 60 seconds a user locking their computer.
    If the user is just getting up to for a minutes it can frustrating to come back and have to wake the computer.
.INPUTS
    [string]
        ComputerName
.OUTPUTS
    [spz.Utility.PowerSetting]
.NOTES
    The default grouping of the output is by plan name. All parameters have built-in tab completion, besides ComputerName.
    The following website was a big help in creating this function.
    https://www.dhb-scripting.com/Forums/posts/t44-Line-Up-Your-Windows-Power-and-Sleep-Settings-with-PowerShell-and-WMI
#>

function Get-PowerSetting {
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline,ValueFromPipelineByPropertyName)]
        [string[]]$ComputerName = $env:COMPUTERNAME,
        [Alias("PlanId")]
        [ArgumentCompleter({
            param ($commandName,$parameterName,$wordToComplete,$commandAst,$fakeBoundParameters)
                $param = @{
                    Query = "select ElementName,IsActive from Win32_PowerPlan WHERE ElementName LIKE '%$wordToComplete%'"
                    ComputerName = $fakeBoundParameters.ComputerName
                    Namespace = "root\cimv2\power"
                }
                Get-CimInstance @param  | Foreach-Object {
                    $Active = "InActive"
                    if ($_.IsActive){
                        $Active = "Active"
                    }
                    [System.Management.Automation.CompletionResult]::new("`"$($_.ElementName)`"","$($_.ElementName) - $Active","ParameterValue","$($_.ElementName)")
                }
        })]
        [string]$PlanName = "",
        [switch]$ActivePlanOnly,
        [ArgumentCompleter({
            param ($commandName,$parameterName,$wordToComplete,$commandAst,$fakeBoundParameters)
                $param = @{
                    Query = "select ElementName from Win32_PowerSetting WHERE ElementName LIKE '%$wordToComplete%'"
                    ComputerName = $fakeBoundParameters.ComputerName
                    Namespace = "root\cimv2\power"
                }
                Get-CimInstance @param  | Foreach-Object {
                    [System.Management.Automation.CompletionResult]::new("`"$($_.ElementName)`"","$($_.ElementName)","ParameterValue","$($_.ElementName)")
                }
        })]
        [string]$SettingName,
        [ValidateSet("AC","DC")]
        [string]$SettingType
    )

    Process{
        foreach ($computer in $ComputerName){
            try{
                $Session = New-CimConnection -ComputerName $computer -ErrorAction Stop
                $CimParameters = @{
                    CimSession = $Session
                    Namespace = "root\cimv2\power"
                    ErrorAction = "Stop"
                }
                $PlanQuery = "select ElementName,InstanceID,IsActive from Win32_Powerplan WHERE (ElementName LIKE '%$PlanName%' OR InstanceId LIKE '%$PlanName%')"
                if ($ActivePlanOnly){
                    $PlanQuery += " AND IsActive=True"
                }
           
                # Get the Active Power Plan
                Write-Information "Querying win32_PowerPlan - $PlanQuery"
                $CimParameters["Query"] = $PlanQuery
                $PowerPlans = Get-CimInstance @CimParameters

                foreach ($plan in $PowerPlans){
                    # Get Setting Names
                    $SettingNames_Hash = @{}
                    $CimParameters["Query"] = "select ElementName,InstanceID from Win32_PowerSetting"
                    Get-CimInstance @CimParameters | ForEach-Object {
                        $SettingId = $_.InstanceId.Split('\')[-1]
                        $SettingNames_Hash.Add($SettingId,$_)
                    }
                
                    # Change Microsoft:PowerPlan\{381b4222-f694-41f0-9685-ff5bb260df2e}
                    # to 381b4222-f694-41f0-9685-ff5bb260df2e for the query
                    $PlanId = $plan.InstanceId.Split("\")[-1]
                    $SettingQuery = "select InstanceId,SettingIndexValue from Win32_PowerSettingDataIndex"
                    if ($PSBoundParameters.ContainsKey("SettingType")){
                        $SettingQuery += " WHERE InstanceId LIKE '%$PlanId\\$SettingType\\%'"
                    }
                    else{
                        $SettingQuery += " WHERE InstanceId LIKE '%$PlanId%'"
                    }
                    $CimParameters["Query"] = $SettingQuery
                    $Settings = Get-CimInstance @CimParameters

                    $PossibleValues_Hash = @{}
                    $CimParameters["Query"] = "select * from Win32_PowerSettingDefinitionPossibleValue"
                    Get-CimInstance @CimParameters | Group-Object {$_.InstanceId.split('\')[-2]} | Foreach-Object {
                        $PossibleValues_Hash.Add($_.Name,$_.Group)
                    }

                    $RangeValues_Hash = @{}
                    $CimParameters["Query"] = "select * from Win32_PowerSettingDefinitionRangeData"
                    Get-CimInstance @CimParameters | Group-Object {$_.InstanceId.split('\')[-1]} | Foreach-Object {
                        $RangeValues_Hash.Add($_.Name,$_.Group)
                    }
                
                    foreach ($setting in $Settings) {
                        $settingGUID = Split-Path $setting.InstanceId -Leaf
                        $currentsettingName = $SettingNames_Hash["$SettingGUID"].ElementName
                        if ($PSBoundParameters.ContainsKey("SettingName") -and ($currentsettingName -notlike "$SettingName")){
                            continue
                        }
                        $SettingObj = [PSCustomObject]@{
                            PSTypeName = "SysAdminTools.PowerSetting"
                            ComputerName = $computer.ToUpper()
                            PlanName = $plan.ElementName
                            PlanId = $PlanId
                            Active = $plan.IsActive
                            SettingId = $setting.InstanceId.Split("\")[-1]
                            SettingType = $setting.InstanceId.Split('\')[-2]
                            SettingName = $currentsettingName
                            SettingIndex = $setting.SettingIndexValue
                            SettingBinaryValue = $null
                            SettingValue = $null
                            SettingValueFriendlyName = $null
                            SettingValueDescripter = $null
                            SettingValueType = $null
                            SettingValueRange = [System.Collections.Generic.List[Object]]::new()
                            SettingValueRangeObjects = [System.Collections.Generic.List[Object]]::new()
                        }
                        $possiblevalue = $PossibleValues_Hash["$SettingGUID"]
                        if ($null -ne $possiblevalue){
                            $value = $possiblevalue | Where-Object SettingIndex -eq $setting.SettingIndexValue
                            $SettingObj.SettingValueFriendlyName = $value.ElementName
                            switch ($value){
                                {$null -ne $value.BinaryValue} {$SettingObj.SettingBinaryValue = $value.BinaryValue; $SettingObj.SettingValueDescripter = "Binary"; Break;}
                                {$null -ne $value.UInt32Value} {$SettingObj.SettingValue = $value.UInt32Value; $SettingObj.SettingValueDescripter = "INT"}
                            }
                            $SettingObj.SettingValueRange.AddRange($possiblevalue.ElementName)
                            $SettingObj.SettingValueRangeObjects.AddRange($possiblevalue)
                            $SettingObj.SettingValueType = "Discrete"
                        }
                        else{
                            $rangevalue = $RangeValues_Hash["$SettingGUID"]
                            $min = $rangevalue | Where-Object ElementName -eq "ValueMin"
                            $max = $rangevalue | Where-Object ElementName -eq "ValueMax"
                            $SettingObj.SettingValueDescripter = $rangevalue | Select-Object -First 1 -ExpandProperty Description
                            $SettingObj.SettingValue = $setting.SettingIndexValue
                            $SettingObj.SettingValueFriendlyName = "$($SettingObj.SettingValue) $($SettingObj.SettingValueDescripter)"
                            $SettingObj.SettingValueRange.Add($min.SettingValue)
                            $SettingObj.SettingValueRange.Add($max.SettingValue)
                            $SettingObj.SettingValueType = "Range"
                        }
                        $SettingObj
                    } #foreach Setting
                } #foreach power plan
                $Session | Remove-CimSession -ErrorAction SilentlyContinue
            }
            catch{
                $Session | Remove-CimSession -ErrorAction SilentlyContinue
                if ($_.Exception.Message -like "*HRESULT 0x80070668*"){
                    Write-Error -Message "Access Denied or Unknown Error: HRESULT 0x80070668"
                }
                else{
                    $PSCmdlet.WriteError($_)
                }
            }
        } # foreach
    } #Process
}
function Get-RemoteDesktopUser{
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline,ValueFromPipelineByPropertyName)]
        [string[]]$ComputerName,
        [Parameter(ValueFromPipelineByPropertyName)]
        [string[]]$UserName
    )

    Begin{

    } #Begin

    Process{
        foreach ($computer in $ComputerName){
            Get-sysLocalGroupMember -ComputerName $computer -GroupName "Remote Desktop Users"
        } #foreach
    } #process
}
<#
.SYNOPSIS
    This function grabs local groups along with their members (if any) from local or remote computers.
.DESCRIPTION
    This function uses Win32_Group and Win32_GroupUser to grab local groups and their members form local and remote computers.
.EXAMPLE
    PS C:\> Get-sysLocalGroup -GroupName "Remote Desktop Users","RDS Remote Access Servers"
 
    GroupName Member ComputerName
    --------- ------ ------------
    Remote Desktop Users {Everyone, mrpig} DC01
    RDS Remote Access Servers DC01
 
    This examples grabs the group and members of 2 groups specified with the GroupName parameter from the local computer.
 
.EXAMPLE
    PS C:\>Get-sysLocalGroup -IncludeGroupsWithMembersOnly
 
    GroupName Member
    --------- ------
    Pre-Windows 2000 Compatible Access {Authenticated Users}
    Windows Authorization Access Group {ENTERPRISE DOMAIN CONTROLLERS}
    Administrators {Administrator, Enterprise Admins, Domain Admins}
    Users {INTERACTIVE, Authenticated Users, Domain Users}
    Guests {Guest, Domain Guests}
    Remote Desktop Users {Everyone, mrpig}
    IIS_IUSRS {IUSR}
    Denied RODC Password Replication Group {krbtgt, Domain Controllers, Schema Admins, Enterprise Admins, Cert Publisher...
 
    This example grabs all groups from the local computer if they have any members and ignores the one's with no members.
.EXAMPLE
    PS C:\>Get-sysLocalGroup -ComputerName $ENV:COMPUTERNAME,pancake-3 -Protocol Dcom -GroupName "Remote Desktop Users" -OutVariable groups
 
    GroupName Member ComputerName
    --------- ------ ------------
    Remote Desktop Users {Everyone, mrpig} DC01
    Remote Desktop Users {mrpig, mrpig} pancake-3
 
    PS C:\>$groups[1].Member
 
    Name Domain MemberType
    ---- ------ ----------
    mrpig CLEVELAND UserAccount
    mrpig PANCAKE-3 UserAccount
 
    This example grabs members of the "Remote Desktop Users" group from both dc01 and pancake-3 and uses the Dcom protocol since pancake-3 does not have WsMan enabled.
    It then stores the results into the groups variable. The second command expands the Member property of pancake-3 LocalGroup object to get more info about each group member.
.INPUTS
    Inputs (if any)
.OUTPUTS
    [SysAdminTools.LocalGroupMember]
.NOTES
    GroupName has tab completetion, but it only grabs the local groups from the local computer and not remote ones, but generally they should be the same.
#>

function Get-sysLocalGroup {
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipelineByPropertyName,ValueFromPipeline)]
        [string[]]$ComputerName = $ENV:COMPUTERNAME,

        [ArgumentCompleter({
            param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
            Get-CimInstance -ClassName Win32_Group -Filter "LocalAccount=True AND Name LIKE `"%$WordToComplete%`"" |
            where Name -notin $fakeBoundParameter.GroupName |
            foreach {
                $ToolTip = $_ | Format-List Name,Description,Caption | Out-String
                [System.Management.Automation.CompletionResult]::new("`"$($_.Name)`"","$($_.Name)","ParameterValue",$ToolTip)
            }
        })]
        [Parameter()]
        [string[]]$GroupName,
        [switch]$IncludeGroupsWithMembersOnly,
        [Parameter()]
        [ValidateSet("WsMan", "Dcom")]
        [string]$Protocol = "WsMan"
    )

    Begin{
        if ($PSBoundParameters.ContainsKey("GroupName")){
            $GroupNameFilter = "AND (Name=`"$($GroupName[0])`""
            foreach ($name in ($GroupName | select -Skip 1)){
                $GroupNameFilter += " OR Name=`"$name`""
            }
            $GroupNameFilter += ")"
        }
    } #Begin

    Process{
        foreach ($computer in $ComputerName){
            try{
                $Session = New-CimConnection -ComputerName $computer -Protocol $Protocol -ErrorAction Stop
                $Groups = Get-CimInstance -CimSession $Session -ClassName Win32_Group -Filter "LocalAccount=True $GroupNameFilter"
                
                foreach ($group in $Groups){
                    $GroupComponent = Get-CimInstance -CimSession $Session -ClassName Win32_GroupUser -Filter "GroupComponent=""Win32_Group.Domain='$computer',Name='$($group.Name)'"""
                    if ($IncludeGroupsWithMembersOnly){
                        if ($null -eq $GroupComponent){
                            continue
                        }
                    }
                    $Members = foreach ($member in $GroupComponent){
                        [PSCustomObject]@{
                            PSTypeName = "SysAdminTools.LocalGroupMember"
                            Name = $member.PartComponent.Name
                            Domain = $member.PartComponent.Domain
                            MemberType = $member.PartComponent.cimclass.cimclassname.split('_')[1]
                        }
                    }
                    [PSCustomObject]@{
                        PSTypeName = "SysAdminTools.LocalGroup"
                        ComputerName = $computer
                        GroupName = $group.Name
                        Member = $Members
                        Description = $group.Description
                        Domain = $group.Domain
                        Caption = $group.Caption
                        SID = $group.SID
                        SIDType = $group.SIDType
                        LocalAccount = $group.LocalAccount
                        Status = $group.Status
                    }
                }
                $Session | Remove-CimSession
            }
            catch{
                if ($Session){
                    $Session | Remove-CimSession
                }
                $PSCmdlet.WriteError($_)
            }
        } #foreach computer
    } #Process
}
<#
.SYNOPSIS
    This will grab all user profiles found on a local or remote computer (by default it ignores special profiles).
.DESCRIPTION
    This will grab all user profiles found on a local or remote computer (by default it ignores special profiles). It will resolve the SID to find the
    user account associated with the profile (whether local account or domain account). You can use StaleUsersOnly to only grab accounts that are no longer part of the domain.
.EXAMPLE
    PS C:\> Get-UserProfile
 
    ComputerName ProfileName AccountName DomainName Special Loaded LastUseTime
    ------------ ----------- ----------- ---------- ------- ------ -----------
    DESKTOP-RFR3S01 Syrius Cleveland Syrius Cleveland DESKTOP-RFR3S01 False True 9/23/2021 10:18:40 PM
 
 
    This grabs all user profiles on the local computer and displays the most important information in a table.
.EXAMPLE
    Get-UserProfile -ComputerName Test01v -StaleUsersOnly
 
    ComputerName ProfileName AccountName DomainName Special Loaded LastUseTime
    ------------ ----------- ----------- ---------- ------- ------ -----------
    Test01v testuser False False 8/20/2021 11:23:48 AM
 
 
    This grabs only the user profiles that have AccountName and DomainName that equal an empty string on the computer Test01v
.EXAMPLE
    Get-UserProfile -ComputerName Test01v -StaleUsersOnly | Remove-UserProfile
    User profile [C:\Users\testuser] has been successfully removed from computer [Test01v]
 
 
    This grabs only the user profiles that have AccountName and DomainName that equal an empty string on the computer
    Test01v and pipes the results to Remove-UserProfile command which then deletes this user's profile (folder and hive registry).
.INPUTS
    [string[]]$ComputerName
.OUTPUTS
    spz.Utility.UserProfile
.NOTES
    Requires Admin if ran aganist a remote computer
#>

function Get-UserProfile {
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline,ValueFromPipelineByPropertyName)]
        [Alias("CN","MachineName")]
        [string[]]$ComputerName = $env:COMPUTERNAME,
        [switch]$IncludeSpecialAccounts,
        [switch]$StaleUsersOnly
    )

    Begin {
        $cimParamerters = @{
            ClassName = "Win32_UserProfile"
        }
        if (-not$IncludeSpecialAccounts.IsPresent){
            $cimParamerters["Filter"] = "Special=false"
        }
    }

    Process{
        foreach ($computer in $ComputerName){
            try{
                Write-Information "Creating Cim Session for computer [$computer]"
                $cimSession = New-CimConnection -ComputerName $computer -ErrorAction Stop
                $cimParamerters["CimSession"] = $cimSession
                $UserProfiles = Get-CimInstance @cimParamerters
                foreach ($profile in $UserProfiles){
                    try{
                        $ResolvedSID = Resolve-SId -SID $profile.SID -ComputerName $computer -ErrorAction Stop
                    }
                    catch{
                        $ResolvedSID = $null
                        Write-Warning "Unable to resolve SID [$($profle.SID)] on computer [$computer]"
                    }
                    $userProfile = [PSCustomObject]@{
                        PSTypeName = "SysAdminTools.UserProfile"
                        ComputerName = $computer
                        LocalPath = $profile.LocalPath
                        ProfileName = ($profile.LocalPath -split '\\' | Select-Object -Last 1)
                        SID = $profile.SID
                        Loaded = $profile.loaded
                        Special = $profile.special
                        LastUseTime = $profile.LastUseTime
                        AccountName = $ResolvedSID.AccountName
                        DomainName = $ResolvedSID.ReferencedDomainName
                    }
                    if ($StaleUsersOnly){
                        if ($UserProfile.AccountName -eq "" -and $UserProfile.DomainName -eq ""){
                            $userProfile
                        }
                    }
                    else{
                        $userProfile
                    }
                }
                $cimSession | Remove-CimSession
            }
            catch{
                $PSCmdlet.WriteError($_)
            } #try/catch
        } #foreach
    } #process
}
<#
.SYNOPSIS
    This function will remove a user from the "Remote Desktop Users" group from a remote machine.
.DESCRIPTION
    This function will remove a user from the "Remote Desktop Users" group from a remote machine.
.EXAMPLE
    PS C:\> Remove-RemoteDesktopUser -ComputerName mrpig -SamAccountName mrpig
 
    ComputerName SamAccountName UserRemoved
    ------------ -------------- -----------
    pancake-3 mrpig True
 
    In this example the user account mrpig is removed from the "Remote Desktop Users" group on the computer mrpig.
.EXAMPLE
    PS C:\>Get-sysLocalGroupMember -ComputerName pancake-3 -GroupName "Remote Desktop Users" -Protocol Dcom
 
    GroupName Member ComputerName
    --------- ------ ------------
    Remote Desktop Users {mrpig, mrpig} pancake-3
 
    PS C:\> Remove-RemoteDesktopUser -ComputerName pancake-3 -SamAccountName mrpig -Protocol Dcom
 
    ComputerName SamAccountName UserDomain UserRemoved
    ------------ -------------- ---------- -----------
    pancake-3 mrpig CLEVELAND True
 
    Remove-RemoteDesktopUser -ComputerName pancake-3 -SamAccountName mrpig -Protocol Dcom -Domain pancake-3
 
    ComputerName SamAccountName UserDomain UserRemoved
    ------------ -------------- ---------- -----------
    pancake-3 mrpig pancake-3 True
 
    This example should show you full functionality of the command. The first command grabs the current users of the "Remote Desktop Users" group on pancake-3.
    We can see that there are 2 mrpig accounts in that group, we could expand the Member property to see one is domain mrpig account and the other is local mrpig account.
    The second command removes the domain mrpig account, it does this because the default value of the domain paramter is the current user's domain.
    The third command specifies the pancake-3 as the domain to target the pancake-3\mrpig account to remove.
     
.INPUTS
    [String]
.OUTPUTS
    [PSCustomObject]
.NOTES
    Requires Admin.
#>

function Remove-RemoteDesktopUser{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$ComputerName,
        [Parameter(Mandatory)]
        [string]$SamAccountName,
        [string]$Domain = $ENV:USERDOMAIN,
        [ValidateSet("WsMan","Dcom")]
        [string]$Protocol
    )
    try{
        if (Test-Connection -ComputerName $ComputerName -Count 1 -Quiet){
            $Options = New-CimSessionOption -Protocol Dcom
            $Sesssion = New-CimSession -ComputerName $ComputerName -OperationTimeoutSec 1 -SessionOption $options -ErrorAction Stop
            $Users = Get-sysLocalGroupMember -ComputerName $ComputerName -GroupName "Remote Desktop Users" -Protocol $Protocol
            $UserFound = $Users | where {$_.Member.Name -eq $SamAccountName -and $_.Member.Domain -eq $Domain}
            if ($UserFound){
                $ErrorActionPreference = "Stop"
                [ADSI]$Account = "WinNT://$Domain/$SamAccountName,User"
                [ADSI]$Group = "WinNT://$ComputerName/Remote Desktop Users,Group"
                $Group.Remove($Account.Path)
                [PSCustomObject]@{
                    ComputerName = $ComputerName
                    SamAccountName = $SamAccountName
                    UserDomain = $Domain
                    UserRemoved = $true
                }
                $ErrorActionPreference = "Continue"
                
            }
            else{
                Write-Error -Message "$SamAccountName is not a member of the Remote Desktop Users group on $ComputerName. Try using 'Get-sysLocalGroupMember -ComputerName $ComputerName -GroupName `"Remote Desktop Users`"' to find the current members of that group."`
                 -ErrorAction Stop
            }

            $Sesssion | Remove-CimSession
        }
        else{
            Write-Error -Message "$ComputerName is offline or unreachable. Possibly try grabbing its IP address by using Get-ComputerIP -ComputerName $ComputerName"
        }
    }
    catch{
        if ($Sesssion){
            $Sesssion | Remove-CimSession
        }
        $ErrorActionPreference = "Continue"
        $PSCmdlet.WriteError($_)
    }
}
function Resolve-SID {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$SID,
        
        [Parameter(ValueFromPipelineByPropertyName,ValueFromPipeline)]
        [string]$ComputerName = $env:COMPUTERNAME
    )

    Process {
        $params=@{
            ErrorAction="Stop"
            ResourceURI="wmicimv2/win32_SID"
            SelectorSet=@{SID="$SID"}
            Computername=$Computername
        }
        try {
            Get-WSManInstance @params
        }
        catch{
            try{
                Write-Information "Failed to resolve SID using WSMan, switching to WMI"
                [wmi]"\\$ComputerName\root\cimv2:win32_sid.sid='$SID'"
            }
            catch{
                Write-Information "Failed to resolve SID using WSMan and WMI, throwing an error"
                throw $_
            }
        }
    }
}
<#
.SYNOPSIS
    This function sets a power settings for local or remote computer(s) to a new value.
.DESCRIPTION
    This function sets a power settings for local or remote computer(s) to a new value, it always activates the power plan to ensure this setting takes effect.
    All settings values are Index Values that map to human readable values or an integer value that indicate seconds, charge, etc.
    In order to best know what values are availabe for a specific setting I strongly recommend using CTRL+Space to see all possible Index values and their human readable names.
.PARAMETER ComputerName
    This parameter specifies the ComputerName for the computer you want to set power setting for. Default value is $ENV:COMPUTERNAME.
.PARAMETER PlanName
    This parameter specifies the plan name of the plan you want to set a power settings for, use tab completion or pipe a power setting object for ease of use.
.PARAMETER SettingName
    This parameter specifies the name of the power setting you want to set, tab completion is available.
.PARAMETER SettingType
    This parameter specifies the power type for the setting, options include DC (on battery) or AC (wall power) power.
.PARAMETER IndexValue
    This is the value you want to set the power setting to. I highly recommend using CTRL+Space to see all availabe options and to see friendly names for index values.
.EXAMPLE
    PS C:\> Get-PowerSetting -ComputerName TEST-LAPTOP02-L -ActivePlanOnly -SettingType AC -SettingName "Console lock display off timeout"
 
        Plan Name: Balanced
 
    ComputerName Active Type SettingName SettingValue Range
    ------------ ------ ---- ----------- ------------ -----
    TEST-LAPTOP02-L True AC Console lock display off timeout 60 Seconds {0, 4294967295}
 
 
    PS C:\> Get-PowerSetting -ComputerName TEST-LAPTOP02-L -ActivePlanOnly -SettingType AC -SettingName "Console lock display off timeout" | Set-PowerSetting -IndexValue 1800
    PS C:\> Get-PowerSetting -ComputerName TEST-LAPTOP02-L -ActivePlanOnly -SettingType AC -SettingName "Console lock display off timeout"
 
 
        Plan Name: Balanced
 
    ComputerName Active Type SettingName SettingValue Range
    ------------ ------ ---- ----------- ------------ -----
    TEST-LAPTOP02-L True AC Console lock display off timeout 1800 Seconds {0, 4294967295}
 
    The first command gets the "Console lock display off timeout" power setting for TEST-LAPTOP02-L and outputs it to the console. We se it is set to 60 seconds.
    The second command runs the same command but then pipes it to Set-PowerSetting and provides the value 1800 to the parameter IndexValue. This will set that setting to 1800 seconds (30 minutes).
    The last command run the first command again to confirm that the setting was udpated to the new vale, and here we can see that it was.
 
    This particular setting's default value of 60 seconds is pretty low. It will put the computer asleep after 60 seconds a user locking their computer.
    If the user is just getting up to for a minutes it can frustrating to come back and have to wake the computer.
.EXAMPLE
    PS C:\> Set-PowerSetting -SettingName "Lid close action" -SettingType AC -IndexValue 0 -PlanName "HP Optimized (recommended)"
    PS C:\> Get-PowerSetting -SettingName "Lid close action" -SettingType AC -PlanName "HP Optimized (recommended)"
 
 
    Plan Name: HP Optimized (recommended)
 
    ComputerName Active Type SettingName SettingValue Range
    ------------ ------ ---- ----------- ------------ -----
    Test-COMPUTER-L True AC Lid close action Do nothing {Do nothing, Sleep, Hibernate, Shut down}
 
    The first command set the "Lid close action" setting to the IndexValue of 0 (do nothing). We used ctrl+space tab complettion to see all the options for index values.
    The second command confirms the setting was changed to what we want and we can see that it was.
.INPUTS
    Inputs (if any)
.OUTPUTS
    None
.NOTES
    Requires Admin
#>

function Set-PowerSetting {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [Alias("PlanId")]
        [ArgumentCompleter({
            param ($commandName,$parameterName,$wordToComplete,$commandAst,$fakeBoundParameters)
                $param = @{
                    Query = "select ElementName,IsActive from Win32_PowerPlan WHERE ElementName LIKE '%$wordToComplete%'"
                    ComputerName = $fakeBoundParameters.ComputerName
                    Namespace = "root\cimv2\power"
                }
                Get-CimInstance @param  | Foreach-Object {
                    $Active = "InActive"
                    if ($_.IsActive){
                        $Active = "Active"
                    }
                    [System.Management.Automation.CompletionResult]::new("`"$($_.ElementName)`"","$($_.ElementName) - $Active","ParameterValue","$($_.ElementName)")
                }
        })]
        [string]$PlanName,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [Alias("SettingId")]
        [ArgumentCompleter({
            param ($commandName,$parameterName,$wordToComplete,$commandAst,$fakeBoundParameters)
                $param = @{
                    Query = "select ElementName from Win32_PowerSetting WHERE ElementName LIKE '%$wordToComplete%'"
                    ComputerName = $fakeBoundParameters.ComputerName
                    Namespace = "root\cimv2\power"
                }
                Get-CimInstance @param  | Foreach-Object {
                    [System.Management.Automation.CompletionResult]::new("`"$($_.ElementName)`"","$($_.ElementName)","ParameterValue","$($_.ElementName)")
                }
        })]
        [string]$SettingName,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateSet("AC","DC")]
        [string]$SettingType,
        
        [Parameter()]
        [ArgumentCompleter({
            param ($commandName,$parameterName,$wordToComplete,$commandAst,$fakeBoundParameters)
                $param = @{
                    PlanName =  $fakeBoundParameters.PlanName
                    SettingName = $fakeBoundParameters.SettingName
                    SettingType = $fakeBoundParameters.SettingType
                }
                Get-PowerSetting @param  | Foreach-Object {
                    if ($_.SettingValueType -eq "Range"){
                        $ToolTip = "The value can be any number between $($_.SettingValueRange -join " - ")"
                        $_.SettingValueRange | foreach {
                            [System.Management.Automation.CompletionResult]::new("`"$($_)`"","$_","ParameterValue",$ToolTip)
                        }
                    }
                    else{
                        $_.SettingValueRangeObjects | foreach {
                            $ToolTip =  "Name: $($_.ElementName)`nIndexValue: $($_.SettingIndex)"
                            [System.Management.Automation.CompletionResult]::new("$($_.SettingIndex)","$($_.ElementName)","ParameterValue",$ToolTip)
                        }
                    }
                }
        })]
        $IndexValue,

        [Parameter(ValueFromPipelineByPropertyName)]
        [string]$ComputerName = $env:COMPUTERNAME
    )

    Process {
        try{
            $Session = New-CimConnection -ComputerName $ComputerName
                $CimParameters = @{
                    CimSession = $Session
                    Namespace = "root\cimv2\power"
                    ErrorAction = "Stop"
                }
                $PlanQuery = "select InstanceID,ElementName from Win32_Powerplan WHERE ElementName='$PlanName' OR InstanceID LIKE '%$PlanName'"
                # Get the Active Power Plan
                Write-Information "Querying win32_PowerPlan - $PlanQuery"
                $CimParameters["Query"] = $PlanQuery
                $PowerPlan = Get-CimInstance @CimParameters
                $PowerPlanId = $PowerPlan | Select-Object -ExpandProperty InstanceId | Split-Path -Leaf
                if ($null -eq $PowerPlanId){
                    throw "Power Plan [$PlanName] was not found on computer [$ComputerName]!"
                }
                Write-Information "Found power plan [$PlanName] on computer [$ComputerName]"

                #Get Setting Name / ID
                $CimParameters["Query"] = "select ElementName,InstanceID from Win32_PowerSetting WHERE ElementName = '$SettingName' OR InstanceId LIKE '%$SettingName'"
                $SettingId = Get-CimInstance @CimParameters | Select-Object -ExpandProperty InstanceId | Split-Path -Leaf
                if ([string]::IsNullOrEmpty($SettingId)){
                    throw "Setting [$SettingName] was not found on computer [$ComputerName]!"
                }
                Write-Information "Matched setting name [$SettingName] with setting ID [$SettingId]"

                $CimParameters["Query"] = "select * from Win32_PowerSettingDataIndex WHERE InstanceID LIKE '%$PowerPlanId\\$SettingType\\$SettingId'"
                $Setting = Get-CimInstance @CimParameters
                if ($null -eq $Setting){
                    throw "Unable to find setting with supplied parameters on computer [$ComputerName]!"
                }

                #$Setting
                Invoke-Command -ComputerName $ComputerName -ScriptBlock {
                    Set-CimInstance -Namespace "root\cimv2\power" -Query $args[0] -Property @{SettingIndexValue = $args[1]}
                    powercfg /setactive $args[2].Replace('{',"").Replace('}',"")
                } -ArgumentList $CimParameters["Query"],$IndexValue,$PowerPlanId

                $Session | Remove-CimSession -ErrorAction SilentlyContinue
        }
        catch{
            $Session | Remove-CimSession -ErrorAction SilentlyContinue
            $PSCmdlet.WriteError($_)
        } #try / catch
    } #Process
}
<#
.SYNOPSIS
    This function will set the Powershell prompt with some predefined functions like the CPU/Mem, Random Command, etc.
.DESCRIPTION
    This function uses the "function Prompt {}" function to set the powershell prompt with a new function. You can add a prefix to your main
     prompt display, change the color of the foreground or background. Save this command to your powershell profile to have your custom prompt ready to go every time you open up powershell.
.PARAMETER Name
    This paramter sets the prompt to a predefined funtion like showing the last commands execution time or a random fact.
.PARAMETER Prefix
    This paramter prefixes the prompt with a prefined function that is enclosed with [] and can be added together. The order you place them when you run the command is the order they will appear on the prompt.
.PARAMETER ForegroundColor
    This is the color the foreground text will be. You can set this color by calling $prompt_FGColor at anytime after calling the function for the first time.
.PARAMETER BackgroundColor
    This is the color that the background text will be. You can set this color by calling $prompt_BGColor at anytime after calling the function for the first time.
.PARAMETER Scriptblock
    This paramter allows you to set your custom scriptblock to run every time the prompt function is called.
.EXAMPLE
    PS C:\> Set-Prompt -Name CPU_Memory -Prefix Admin -ForegroundColor Red -BackGroundColor Blue
    [Non-Admin]CPU: 100% | Mem: 37%:\>
     
    Tthis function sets the prompt to display the current CPU and memory usage. Also lets you know if the user is an admin or not. Also changed the color of the prompt.
.EXAMPLE
    PS C:\>Set-Prompt -Name Measure_Command -Prefix Error_Count,Admin -ForegroundColor Red -BackGroundColor Green
    [7][Non-Admin]0 milliseconds:\> 1.. 100 | foreach {Get-CimInstance -ClassName Win32_Process | where name -eq "Explorer.exe"} | Out-Null
    [7][Non-Admin]4.18 seconds:\> 1.. 100 | foreach {Get-CimInstance -ClassName Win32_Process -Filter "Name='Explorer.exe'"} | Out-Null
    [7][Non-Admin]3.05 seconds:\>
 
    This example sets the prompt to show the amount of time it took the last command to run and displays the number of errors as a prefix. Here you can see
    that filtering with the Filter paramter of Get-CimInstance is faster than filtering on the left. Always filter left when possible. This particular prompt is useful
    when creating new functions and trying to optmize them.
.EXAMPLE
    PS C:\>Set-Prompt -ScriptBlock {"$((Get-NetIPAddress -AddressState Preferred -AddressFamily IPv4 -PrefixOrigin Dhcp).IPAddress):\>"}
    10.0.0.63:\>
 
    This example show you how to set your own prompt funtion by making the prompt your current IP Address of your prefferred DHCP connection.
.INPUTS
    None
.OUTPUTS
    None
.NOTES
    The CPU_Memory prompt does create a job that runs in the background to grab the most current reading of the CPU usage. This job is removed whenver a new prompt is set.
    Three global variables are created with the use of this function prompt_FGColor,prompt_BGColor, and Prompt_Prefixblock. These are necessary to be in the global scope
    so that the prompt function can read them.
#>

function Set-Prompt{
    [CmdletBinding(DefaultParameterSetName = "Name")]
    param(
        [Parameter(ParameterSetName = "Name")]
        [ValidateSet("Random_Cmdlet","Time-short","Time-long","Date-Time","Random_Fact","Measure_Command","CPU_Memory","System_Uptime")]
        [string]$Name,

        [Parameter(ParameterSetName = "Name")]
        [ValidateSet("Admin","Time","Error_Count","Debug")]
        [string[]]$Prefix,

        [ConsoleColor]$ForegroundColor = $host.UI.RawUI.ForegroundColor,

        [ConsoleColor]$BackGroundColor = $host.UI.RawUI.BackgroundColor,

        [Parameter(ParameterSetName = "Custom",Mandatory)]
        [scriptblock]$ScriptBlock
    )

    New-Variable -Name prompt_FGColor -Value $ForegroundColor -Scope Global -Force
    New-Variable -Name prompt_BGColor -Value $BackGroundColor -Scope Global -Force
    New-Variable -Name Prompt_Prefixblock -Value $null -Scope Global -Force

    if (Get-Job -Name CPU_Mem_Prompt -ErrorAction SilentlyContinue){
        Remove-Job -Name CPU_Mem_Prompt -Force
    }

    $Global:Prompt_Prefixblock = foreach ($Pre in $Prefix){
        switch ($Pre){
            "Admin" {{
                        $identity = [Security.Principal.WindowsIdentity]::GetCurrent()
                        $principal = [Security.Principal.WindowsPrincipal] $identity
                        $adminRole = [Security.Principal.WindowsBuiltInRole]::Administrator
            
                        if ($principal.IsInRole($adminRole)){
                           "[Admin]"
                        }
                        else{
                            "[Non-Admin]"
                    }
                }}
    
            "Time" {{
                "[$((Get-Date).ToShortTimeString())]"
            }}
    
            "Error_Count" {{
                "[$($Error.Count)]"
            }}

            "Debug" {{
                if (Test-Path variable:/PSDebugContext) {'[DBG]'}
            }}
        }
    }

    if ($PSBoundParameters.ContainsKey("Scriptblock")){
        New-Item -Path function:prompt -Value $ScriptBlock -Force | Out-Null
        return
    }

    switch ($Name) {
        "Random_Cmdlet" {
            function global:prompt {
                Write-Host "$(($Prompt_Prefixblock | foreach {&$_}) -join '')$((Get-Command -Verb ((Get-Verb).Verb | Get-Random) | Get-Random).name):\>" -ForegroundColor $prompt_FGColor -BackgroundColor $prompt_BGColor -NoNewline
                return " "
            }
        }
        "Time-short" {
            function global:prompt {
                Write-Host "$(($Prompt_Prefixblock | foreach {&$_}) -join '')$((Get-Date).ToShortTimeString()):\>" -ForegroundColor $prompt_FGColor -BackgroundColor $prompt_BGColor -NoNewline
                return " "
            }
        }
        "Time-long" {
            function global:prompt {
                Write-Host "$(($Prompt_Prefixblock | foreach {&$_}) -join '')$((Get-Date).ToLongTimeString()):\>" -ForegroundColor $prompt_FGColor -BackgroundColor $prompt_BGColor -NoNewline
                return " "
            }

        }
        "Date-Time"{
            function global:prompt {
                Write-Host "$(($Prompt_Prefixblock | foreach {&$_}) -join '')$((Get-Date).ToString()):\>" -ForegroundColor $prompt_FGColor -BackgroundColor $prompt_BGColor -NoNewline
                return " "
            }
        }
        "Random_Fact" {
            function global:prompt {
                Write-Host "$(($Prompt_Prefixblock | foreach {&$_}) -join '')$((Invoke-RestMethod -Method Get -Uri 'https://uselessfacts.jsph.pl/random.json?language=en').Text)`nPS :\>" -ForegroundColor $prompt_FGColor -BackgroundColor $prompt_BGColor -NoNewline
                return " "
            }
        }
        "Measure_Command" {
            function global:prompt {
                $lastcommand = Get-History | select -Last 1
                $timespan = New-TimeSpan -Start $lastcommand.StartExecutionTime -End $lastcommand.EndExecutionTime

                if ($timespan.Minutes -lt 1){
                    if ($timespan.Seconds -lt 1){
                        Write-Host "$(($Prompt_Prefixblock | foreach {&$_}) -join '')$([math]::round($timespan.TotalMilliseconds,2)) milliseconds:\>" -ForegroundColor $prompt_FGColor -BackgroundColor $prompt_BGColor -NoNewline
                        return " "
                    }
                    Write-Host "$(($Prompt_Prefixblock | foreach {&$_}) -join '')$([math]::round($timespan.TotalSeconds,2)) seconds:\>" -ForegroundColor $prompt_FGColor -BackgroundColor $prompt_BGColor -NoNewline
                    return " "
                }
                Write-Host "$(($Prompt_Prefixblock | foreach {&$_}) -join '')$([math]::round($timespan.TotalMinutes,2)) minutes:\>" -ForegroundColor $prompt_FGColor -BackgroundColor $prompt_BGColor -NoNewline
                return " "
            }
        }
        "CPU_Memory" {

            Start-Job -Name CPU_Mem_Prompt -ScriptBlock {
                get-counter -Counter "\processor(_total)\% processor time","\memory\% committed bytes in use" -Continuous |
                    foreach {"CPU: $([math]::Round($_.CounterSamples.cookedvalue[0]))% | Mem: $([math]::Round($_.CounterSamples.cookedvalue[1]))%:\>"}
            }  | Out-Null

            function global:prompt {
                $jobresults = Receive-Job CPU_Mem_Prompt | select -First 1
                if ($jobresults){
                    Write-Host "$(($Prompt_Prefixblock | foreach {&$_}) -join '')$jobresults" -ForegroundColor $prompt_FGColor -BackgroundColor $prompt_BGColor -NoNewline
                    return " "
                }
                $results = get-counter -Counter "\processor(_total)\% processor time","\memory\% committed bytes in use" |
                foreach {"$(($Prompt_Prefixblock | foreach {&$_}) -join '')CPU: $([math]::Round($_.CounterSamples.cookedvalue[0]))% | Mem: $([math]::Round($_.CounterSamples.cookedvalue[1]))%:\>"}

                Write-Host $results -ForegroundColor $prompt_FGColor -BackgroundColor $prompt_BGColor -NoNewline
                return " "
            }
        }

        "System_Uptime" {
            function global:prompt {
                $Lastboot = (Get-CimInstance -ClassName Win32_OperatingSystem).LastBootUpTime
                $timespan = New-TimeSpan -Start $Lastboot -End (Get-Date)

                if ($timespan.TotalDays -lt 0){
                    if ($timespan.TotalHours -lt 0){
                        Write-Host "$(($Prompt_Prefixblock | foreach {&$_}) -join '')$([math]::round($timespan.TotalMinutes,2)) minutes:\>" -ForegroundColor $prompt_FGColor -BackgroundColor $prompt_BGColor -NoNewline
                        return " "
                    }
                    Write-Host "$(($Prompt_Prefixblock | foreach {&$_}) -join '')$([math]::round($timespan.TotalHours,2)) hours:\>" -ForegroundColor $prompt_FGColor -BackgroundColor $prompt_BGColor -NoNewline
                    return " "
                }
                Write-Host "$(($Prompt_Prefixblock | foreach {&$_}) -join '')$([math]::round($timespan.TotalDays,2)) days:\>" -ForegroundColor $prompt_FGColor -BackgroundColor $prompt_BGColor -NoNewline
                return " "
            }
        }

        default {
            function global:prompt {
                Write-Host "$(($Prompt_Prefixblock | foreach {&$_}) -join '')PS $($executionContext.SessionState.Path.CurrentLocation)$('>' * ($nestedPromptLevel + 1)) " -ForegroundColor $prompt_FGColor -BackgroundColor $prompt_BGColor -NoNewline
                return " "
            }
        }
    }
}
function Start-Shutdown {
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline,ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [Alias("CN","Name","MachineName")]
        [string[]]$ComputerName = $ENV:COMPUTERNAME,

        [Parameter()]
        [ValidateSet("Shutdown","Reboot","PowerOff")]
        [string]$ShutdownType = "Reboot",

        [Parameter()]
        [int]$Delay = 0,

        [Parameter()]
        [ShutDown_MajorReason]$Major_ReasonCode = [ShutDown_MajorReason]::Other,

        [Parameter()]
        [ShutDown_MinorReason]$Minor_ReasonCode = [ShutDown_MinorReason]::Other,

        [Parameter()]
        [string]$Comment,

        [Parameter()]
        [switch]$Force,

        [Parameter()]
        [switch]$Unplanned
    )

    begin {
        if ($Force){
            $Flags = ([ShutDownType]$ShutdownType).value__ + 4
        }
        else{
            $Flags = ([ShutDownType]$ShutdownType).value__
        }
        $Planned_ReasonCode = (0x80000000) * -1
        if ($Unplanned){
            $ReasonCode = $Major_ReasonCode.value__ + $Minor_ReasonCode.value__
        }
        else{
            $ReasonCode = $Major_ReasonCode.value__ + $Minor_ReasonCode.value__ + $Planned_ReasonCode
        }

        
        if (!($PSBoundParameters.ContainsKey("Comment"))){
            $Comment = "$Type command sent from user $ENV:USERNAME on computer $ENV:COMPUTERNAME with a delay of $Delay seconds"
        }

        $ShutdownParamters = @{
            Flags = $Flags
            Comment = $Comment
            ReasonCode = $ReasonCode
            Timeout = $Delay
        }
    } #begin
    
    process {
        foreach ($computer in $ComputerName){
            if (Test-Connection -ComputerName $computer -Quiet -Count 1){
                Try{
                   $session = New-CimSession -ComputerName $computer -OperationTimeoutSec 1 -ErrorAction Stop
                }
                catch{
                    try{
                        Write-Information "Unable to connect to $computer with Wsman, using DCOM protocl instead" -Tags Process
                        $session = New-CimSession -ComputerName $computer -SessionOption (New-CimSessionOption -Protocol Dcom) -ErrorAction Stop
                    }
                    catch{
                        Write-Error "Unable to connect to $computer with Wsman or Dcom protocols"
                        continue
                    }   
                }
                $Win32_OperatingSystem = Get-CimInstance -ClassName Win32_OperatingSystem -CimSession $session
                $ReturnCode = (Invoke-CimMethod -CimInstance $Win32_OperatingSystem -MethodName Win32ShutdownTracker -Arguments $ShutdownParamters -CimSession $Session).ReturnValue
                $session | Remove-CimSession
                if ($ReturnCode -eq 0){
                    [PSCustomObject]@{
                        ComputerName = $computer
                        ShutdownType = $ShutdownType
                        ReasonCode = "$($Major_ReasonCode): $Minor_ReasonCode"
                        Delay = $Delay
                        CommandSuccessful = $true
                    }
                }
                elseif ($ReturnCode -eq 1191){
                    Write-Error "$ShutdownType action Failed for $($computer): The system shutdown cannot be initiated because there are other users logged on to the computer, use the -Force parameter to force a shutdown operation($Returncode)"
                }
                elseif ($ReturnCode -eq 1190){
                    Write-Error "$ShutdownType action failed for $($computer): A system shutdown has already been scheduled.($ReturnCode)"
                }
                else{
                    Write-Error "$ShutdownType action failed for $($computer): Reason code $ReturnValue"
                }
            } #if
            else{
                Write-Warning "$computer is unreachable"
            }
        } #foreach
    } #process
}
<#
.SYNOPSIS
    This function will abort a scheduled shutdown.
.DESCRIPTION
    This function uses the shutdown.exe utility to abort a scheduled shutdown. If no error was given then the abort action was successful.
.PARAMETER ComputerName
    Specifies the computers the scheduled shutdown (if any) will be stopped on. Type computer names or IP addresses.
.PARAMETER Passthru
    Returns the results of the command. Otherwise, this cmdlet does not generate any output.
.EXAMPLE
    PS C:\> Stop-Shutdown -ComputerName Client01v -Passthru
 
    ComputerName ShutdownAborted
    ------------ ---------------
    Client01v True
 
    This example aborts a scheduled shutdown on computer Client01v and uses the passthru parameter to output an object that tells you if the abort was successful or not.
.INPUTS
    System.String
        ComputerName - The name of the computer to abort the action
.OUTPUTS
    None
.NOTES
    Requires Admin for remote computer abort actions and shutdown.exe
#>

function Stop-Shutdown{
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline,ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [Alias("CN","Name","MachineName")]
        [string[]]$ComputerName = $ENV:COMPUTERNAME,
        [switch]$Passthru
    )


    Process{
        foreach ($computer in $ComputerName){
            Write-Information "Sending abort command to $computer" -Tags "Process"
            shutdown /a /m "\\$computer" 2> $null
            if (!$?){
                if ($Passthru){
                Write-Information "Passthru paramter was used, creating object for unsuccessful abort action for $computer" -Tags "Process"
                    [PSCustomObject]@{
                        ComputerName = $Computer
                        ShutdownAborted = $false
                    }
                }
                else{
                    $PSCmdlet.WriteError($Error[0])
                }
            }
            elseif ($Passthru){
                Write-Information "Passthru paramter was used, creating object for successful abort action for $computer" -Tags "Process"
                [PSCustomObject]@{
                    ComputerName = $Computer
                    ShutdownAborted = $true
                }
            }
        } #foreach
    } #process
}
<#
.SYNOPSIS
    Verifies that a given credential is valid or invalid.
.DESCRIPTION
    Will test a given username with a given password and return either true or false.
    True if the credentials provided are valid and false if they are not.
.PARAMETER UserName
    The username you want to test the credentials for. Accpets pipeline input.
.PARAMETER Password
    The password you want to test with the UserName that was provided. Requires a secure string to be inputted.
.EXAMPLE
    PS C:\> Test-Credential -Credential "MrPig"
    True
 
    This example shows you can enter in just a username and it will prompt for the password, it return "True" which indicates that the credentials are valid.
.EXAMPLE
    PS C:\> Test-Credential
    cmdlet Test-Credential at command pipeline position 1
    Supply values for the following parameters:
    Credential
    False
 
    If you do not enter in any parameters it will prompt for Credentials.
    Since credentials enter in this example were not valid it return a false boolean value.
.EXAMPLE
    PS C:\> Test-Credential -UserName syrius.cleveland -Password (Read-Host -AsSecureString)
    ***********
    False
 
    This example uses the Read-Host -AsSecureString command to provide the value for the password and filles our the UserName parameter beforehand.
    Since credentials enter in this example were not valid it return a false boolean value.
.INPUTS
    None
.OUTPUTS
    Boolean
.NOTES
    Requires secure string for password. I made the Output just a simple boolean value since the rest of the cmdlets that have test as the verb do the same.
#>

function Test-Credential{
    [Cmdletbinding(DefaultParameterSetName = "Credentials")]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory,ValueFromPipeline,ParameterSetName="Credentials")]
        [pscredential]$Credential,

        [Parameter(ParameterSetName="IsAdmin")]
        [Parameter(Mandatory,ValueFromPipeline,ValueFromPipelineByPropertyName,
        ParameterSetName="UserNameandPassword")]
        [String]$UserName = $ENV:USERNAME,

        [Parameter(Mandatory,ValueFromPipeline,ValueFromPipelineByPropertyName,
        ParameterSetName="UserNameandPassword")]
        [securestring]$Password,

        [Parameter(ParameterSetName="IsAdmin")]
        [switch]$IsAdmin
    )

    Begin{
        Write-Information "Adding System.DirectoryServices.AccountManagement assembly" -Tags "Begin"
        Add-Type -AssemblyName System.DirectoryServices.AccountManagement
        Write-Information "Checking to see if computer is part of a domain using Get-CimInstance" -Tags "Process"
        $PartofDomain = (Get-CimInstance -ClassName Win32_ComputerSystem).PartOfDomain

        if ($PartofDomain){
            $ContextType = [System.DirectoryServices.AccountManagement.ContextType]::Domain
        }
        else{
            $ContextType = [System.DirectoryServices.AccountManagement.ContextType]::Machine
        }
    }

    Process{
        try{
            $Previous = $ErrorActionPreference
            $ErrorActionPreference = "Stop"
            if ($IsAdmin){
                if ($PartofDomain){
                    $Identity = [System.Security.Principal.WindowsIdentity]::new($UserName)
                    $WinPrincipal = [Security.Principal.WindowsPrincipal]::new($Identity)
                    $Admin = $WinPrincipal.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)
                    Write-Information "Username $Username is admin: $Admin"
                    
                    return $Admin
                }
                else{
                    $Admingroupmember = (Get-LocalGroupMember -Name Administrators).Name | foreach {$_.Split('\',2)[1]}
                    $Admin = ($Admingroupmember -contains $UserName.Split('\',2)[0])
                    return $Admin
                }
            }

            if ($PSCmdlet.ParameterSetName -eq "UserNameAndPassword"){
                $Credential = [System.Management.Automation.PSCredential]::new($UserName,$Password)
            }
            
            $PrincipalContext = [System.DirectoryServices.AccountManagement.PrincipalContext]::new($ContextType)
    
            Write-Information "Validating Credentials" -Tags "Process"
            $ValidatedCreds = $PrincipalContext.ValidateCredentials($Credential.UserName,$Credential.GetNetworkCredential().Password)
            Write-Information "Username $($Credential.UserName) with provided password resulted in: $ValidatedCreds" -Tags "Process"
            $ErrorActionPreference = $Previous
            return $ValidatedCreds
        }
        catch{
            $ErrorActionPreference = $Previous
            $PSCmdlet.WriteError($_)
        }
    } #Process
}
function Get-LogonFailureReason {
    param($EventRecord)
    # modified this function from "https://www.powershellgallery.com/packages/PoShEvents/0.4.1/Content/Public%5CGet-LogonFailureEvent.ps1"
    $Reason = $null
    $Status = $null
    $SubStatus = $null
    switch ($EventRecord.FailureReason) {
        "%%2305" { $Reason = 'The specified user account has expired.' }
        "%%2309" { $Reason = "The specified account's password has expired." }
        "%%2310" { $Reason = 'Account currently disabled.' }
        "%%2311" { $Reason = 'Account logon time restriction violation.' }
        "%%2312" { $Reason = 'User not allowed to logon at this computer.' }
        "%%2313" { $Reason = 'Unknown user name or bad password.' }
        "%%2304" { $Reason = 'An Error occurred during Logon.' }
    }
    if ($EventRecord.Id -eq 4625) {
        switch ($EventRecord.Status) {
            "0xC0000234" { $Status = "Account locked out" }
            "0xC0000193" { $Status = "Account expired" }
            "0xC0000133" { $Status = "Clocks out of sync" }
            "0xC0000224" { $Status = "Password change required" }
            "0xc000015b" { $Status = "User does not have logon right" }
            "0xc000006d" { $Status = "Logon failure" }
            "0xc000006e" { $Status = "Account restriction" }
            "0xc00002ee" { $Status = "An error occurred during logon" }
            "0xC0000071" { $Status = "Password expired" }
            "0xC0000072" { $Status = "Account disabled" }
            "0xC0000413" { $Status = "Authentication firewall prohibits logon" }
            default { $Status = $Event.Status }
        }
        if ($EventRecord.Status -ne $EventRecord.SubStatus) {
            switch ($EventRecord.SubStatus) {
                "0xC0000234" { $SubStatus = "Account locked out" }
                "0xC0000193" { $SubStatus = "Account expired" }
                "0xC0000133" { $SubStatus = "Clocks out of sync" }
                "0xC0000224" { $SubStatus = "Password change required" }
                "0xc000015b" { $SubStatus = "User does not have logon right" }
                "0xc000006d" { $SubStatus = "Logon failure" }
                "0xc000006e" { $SubStatus = "Account restriction" }
                "0xc00002ee" { $SubStatus = "An error occurred during logon" }
                "0xC0000071" { $SubStatus = "Password expired" }
                "0xC0000072" { $SubStatus = "Account disabled" }
                "0xc000006a" { $SubStatus = "Incorrect password" }
                "0xc0000064" { $SubStatus = "Account does not exist" }
                "0xC0000413" { $SubStatus = "Authentication firewall prohibits logon" }
                default { $SubStatus = $EventRecord.SubStatus }
            }
        }
    } elseif ($EventRecord.Id -eq 4771)  {
        switch ($EventRecord.Status) {
            "0x1" { $Status = "Client's entry in database has expired" }
            "0x2" { $Status = "Server's entry in database has expired" }
            "0x3" { $Status = "Requested protocol version # not supported" }
            "0x4" { $Status = "Client's key encrypted in old master key" }
            "0x5" { $Status = "Server's key encrypted in old master key" }
            "0x6" { $Status = "Client not found in Kerberos database" }    #Bad user name, or new computer/user account has not replicated to DC yet
            "0x7" { $Status = "Server not found in Kerberos database" } # New computer account has not replicated yet or computer is pre-w2k
            "0x8" { $Status = "Multiple principal entries in database" }
            "0x9" { $Status = "The client or server has a null key" } # administrator should reset the password on the account
            "0xA" { $Status = "Ticket not eligible for postdating" }
            "0xB" { $Status = "Requested start time is later than end time" }
            "0xC" { $Status = "KDC policy rejects request" } # Workstation restriction
            "0xD" { $Status = "KDC cannot accommodate requested option" }
            "0xE" { $Status = "KDC has no support for encryption type" }
            "0xF" { $Status = "KDC has no support for checksum type" }
            "0x10" { $Status = "KDC has no support for padata type" }
            "0x11" { $Status = "KDC has no support for transited type" }
            "0x12" { $Status = "Clients credentials have been revoked" } # Account disabled, expired, locked out, logon hours.
            "0x13" { $Status = "Credentials for server have been revoked" }
            "0x14" { $Status = "TGT has been revoked" }
            "0x15" { $Status = "Client not yet valid - try again later" }
            "0x16" { $Status = "Server not yet valid - try again later" }
            "0x17" { $Status = "Password has expired" } # The user’s password has expired.
            "0x18" { $Status = "Pre-authentication information was invalid" } # Usually means bad password
            "0x19" { $Status = "Additional pre-authentication required*" }
            "0x1F" { $Status = "Integrity check on decrypted field failed" }
            "0x20" { $Status = "Ticket expired" } #Frequently logged by computer accounts
            "0x21" { $Status = "Ticket not yet valid" }
            "0x21" { $Status = "Ticket not yet valid" }
            "0x22" { $Status = "Request is a replay" }
            "0x23" { $Status = "The ticket isn't for us" }
            "0x24" { $Status = "Ticket and authenticator don't match" }
            "0x25" { $Status = "Clock skew too great" } # Workstation’s clock too far out of sync with the DC’s
            "0x26" { $Status = "Incorrect net address" } # IP address change?
            "0x27" { $Status = "Protocol version mismatch" }
            "0x28" { $Status = "Invalid msg type" }
            "0x29" { $Status = "Message stream modified" }
            "0x2A" { $Status = "Message out of order" }
            "0x2C" { $Status = "Specified version of key is not available" }
            "0x2D" { $Status = "Service key not available" }
            "0x2E" { $Status = "Mutual authentication failed" } # may be a memory allocation failure
            "0x2F" { $Status = "Incorrect message direction" }
            "0x30" { $Status = "Alternative authentication method required*" }
            "0x31" { $Status = "Incorrect sequence number in message" }
            "0x32" { $Status = "Inappropriate type of checksum in message" }
            "0x3C" { $Status = "Generic error (description in e-text)" }
            "0x3D" { $Status = "Field is too long for this implementation" }
            default { $Status = $EventRecord.Status }
        }
    }
    [PSCustomObject]@{
        Reason = $Reason
        Status = $Status
        SubStatus = $SubStatus
    }
}
function New-CimConnection {
    [CmdletBinding()]
    param(
        [string]$ComputerName,
        [Parameter()]
        [ValidateSet("WsMan", "Dcom")]
        [string]$Protocol = "WsMan"
    )

    $CimSessionOption = New-CimSessionOption -Protocol $Protocol

    Try{
        $CimSession = New-CimSession -ComputerName $ComputerName -SessionOption $CimSessionOption -OperationTimeoutSec 1 -ErrorAction Stop
    }
    catch{
        try{
            switch ($Protocol){
                "WsMan" {$CimSessionOption = New-CimSessionOption -Protocol "Dcom"; $Backup = "Dcom"}
                "Dcom" {$CimSessionOption = New-CimSessionOption -Protocol "WsMan"; $Backup = "WsMan"}
            }
            $CimSession = New-CimSession -ComputerName $ComputerName -SessionOption $CimSessionOption -OperationTimeoutSec 1 -ErrorAction Stop
            Write-Warning "Unable to connect to $ComputerName with $Protocol, using $Backup protocol instead! Try using setting the Protocol parameter to $Backup for faster execution time."
        }
        catch{
            $PSCmdlet.WriteError($_)
        }   
    }

    $CimSession
}
Export-ModuleMember -function Add-RemoteDesktopUser, Get-BatteryStatus, Get-DefaultPrinter, Get-LoggedInUser, Get-LoginEvent, Get-MonitorInfo, Get-PendingRebootStatus, Get-PowerSetting, Get-RemoteDesktopUser, Get-sysLocalGroup, Get-UserProfile, Remove-RemoteDesktopUser, Resolve-SID, Set-PowerSetting, Set-Prompt, Start-Shutdown, Stop-Shutdown, Test-Credential