SysAdminTools.psm1

<#
.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 ($Error[0].Exception.Message -eq ""){
                            throw $Error[1]
                        }
                        else{
                            throw $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'
                        return $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
                    )
                    throw $ErrorRecord
                }
            } #try
            catch [System.Management.Automation.RemoteException]{
                if ($_.Exception.Message -like "No User exists for *"){
                    Write-Warning "No users logged into $computer"
                }
                elseif ($_.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($_)
                }
            }
            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 the serial number, monitor name, and year of manufacture of all monitors connected to a computer.
.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")]
        [ValidateNotNullOrEmpty()]
        [string[]]$ComputerName = $Env:COMPUTERNAME,

        [Parameter()]
        [Microsoft.Management.Infrastructure.CimCmdlets.ProtocolType]$Protocol = [Microsoft.Management.Infrastructure.CimCmdlets.ProtocolType]::Dcom
    )

    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]@{
                        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
}
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
}
Export-ModuleMember -function Get-LoggedInUser, Get-MonitorInfo, Start-Shutdown, Stop-Shutdown, Test-Credential