Public/Workstation_Reporting/Get-VBLoggedOnUser.ps1

#Requires -Version 5.1
# ============================================================
# FUNCTION : Get-VBLoggedOnUser
# MODULE : VB.WorkstationReport
# VERSION : 1.0.0
# AUTHOR : Vibhu Bhatnagar
# PURPOSE : Returns interactive logon sessions with quser session detail
# ENCODING : UTF-8 with BOM
# ============================================================

<#
.SYNOPSIS
    Returns interactive logon sessions on local or remote computers.
 
.DESCRIPTION
    Combines Win32_LogonSession (LogonType 2 and 10) with quser session data
    to return one object per interactive session with user, domain, SID,
    logon type, session name, session ID, state, idle time, and logon time.
 
    quser enrichment is best-effort -- if quser returns no match for a session,
    session-specific fields are $null rather than failing the entire result.
 
    Supports local and remote execution. Credentials are only passed to
    CIM and Invoke-Command when explicitly supplied.
 
.PARAMETER ComputerName
    Target computer(s). Defaults to local machine. Accepts pipeline input.
    Aliases: CN, MachineName, PSComputerName
 
.PARAMETER Credential
    Alternate credentials for remote execution.
 
.EXAMPLE
    Get-VBLoggedOnUser
    Returns all interactive sessions on the local computer.
 
.EXAMPLE
    Get-VBLoggedOnUser -ComputerName WS001, WS002
    Returns interactive sessions from two remote workstations.
 
.EXAMPLE
    'WS001','WS002' | Get-VBLoggedOnUser -Credential $cred
    Pipeline input with alternate credentials.
 
.OUTPUTS
    PSCustomObject with: ComputerName, Status, Name, Domain, SID,
    LogonType, LogonId, Session, SessionId, State, IdleTime, LogonTime,
    Error, CollectionTime
 
.NOTES
    Version : 1.0.0
    Author : Vibhu Bhatnagar
    Category : User Management
    Requirements :
      - PowerShell 5.1 or higher
      - quser.exe available on target (standard on all Windows desktop/server SKUs)
      - PowerShell Remoting enabled for remote targets
 
REMOTING PREREQUISITES:
  Target: WinRM running, Enable-PSRemoting -Force, port 5985 open
  Domain: current token used if -Credential omitted
  Workgroup: requires -Credential or TrustedHosts configured
  Production: use HTTPS (port 5986) with valid certificate
#>

function Get-VBLoggedOnUser {
    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('CN', 'MachineName', 'PSComputerName')]
        [string[]]$ComputerName = $env:COMPUTERNAME,

        [Parameter()]
        [PSCredential]$Credential
    )

    process {
        foreach ($computer in $ComputerName) {
            try {
                Write-Verbose "[$computer] Collecting interactive logon sessions"

                $credSplat = if ($Credential) { @{ Credential = $Credential } } else { @{} }

                # Step 1 -- Collect quser and CIM data: local or remote
                if ($computer -eq $env:COMPUTERNAME) {

                    # Step 1a -- Parse quser locally
                    $QuserSessions = quser 2>$null |
                        Select-Object -Skip 1 |
                        ForEach-Object {
                            if ($_ -match '^\s*>?(?<User>\S+)\s+(?<Session>\S+)\s+(?<Id>\d+)\s+(?<State>\w+)\s+(?<Idle>\S+)\s+(?<LogonTime>.+)$') {
                                [PSCustomObject]@{
                                    UserName  = $Matches.User
                                    Session   = $Matches.Session
                                    SessionId = [int]$Matches.Id
                                    State     = $Matches.State
                                    IdleTime  = $Matches.Idle
                                    LogonTime = $Matches.LogonTime.Trim()
                                }
                            }
                        }

                    # Step 1b -- Query CIM locally
                    $LogonSessions = Get-CimInstance -ClassName Win32_LogonSession -ErrorAction Stop |
                        Where-Object { $_.LogonType -in 2, 10 }

                    $Results = foreach ($session in $LogonSessions) {
                        Get-CimAssociatedInstance -InputObject $session -Association Win32_LoggedOnUser |
                            ForEach-Object {
                                $user  = $_
                                $match = $QuserSessions |
                                    Where-Object { $_.UserName -eq $user.Name } |
                                    Select-Object -First 1

                                [PSCustomObject]@{
                                    Name      = $user.Name
                                    Domain    = $user.Domain
                                    SID       = $user.SID
                                    LogonType = $session.LogonType
                                    LogonId   = $session.LogonId
                                    Session   = $match.Session
                                    SessionId = $match.SessionId
                                    State     = $match.State
                                    IdleTime  = $match.IdleTime
                                    LogonTime = $match.LogonTime
                                }
                            }
                    }

                } else {

                    # Step 1c -- Run quser and CIM remotely
                    $Results = Invoke-Command -ComputerName $computer @credSplat -ScriptBlock {

                        $QuserSessions = quser 2>$null |
                            Select-Object -Skip 1 |
                            ForEach-Object {
                                if ($_ -match '^\s*>?(?<User>\S+)\s+(?<Session>\S+)\s+(?<Id>\d+)\s+(?<State>\w+)\s+(?<Idle>\S+)\s+(?<LogonTime>.+)$') {
                                    [PSCustomObject]@{
                                        UserName  = $Matches.User
                                        Session   = $Matches.Session
                                        SessionId = [int]$Matches.Id
                                        State     = $Matches.State
                                        IdleTime  = $Matches.Idle
                                        LogonTime = $Matches.LogonTime.Trim()
                                    }
                                }
                            }

                        $LogonSessions = Get-CimInstance -ClassName Win32_LogonSession -ErrorAction Stop |
                            Where-Object { $_.LogonType -in 2, 10 }

                        foreach ($session in $LogonSessions) {
                            Get-CimAssociatedInstance -InputObject $session -Association Win32_LoggedOnUser |
                                ForEach-Object {
                                    $user  = $_
                                    $match = $QuserSessions |
                                        Where-Object { $_.UserName -eq $user.Name } |
                                        Select-Object -First 1

                                    [PSCustomObject]@{
                                        Name      = $user.Name
                                        Domain    = $user.Domain
                                        SID       = $user.SID
                                        LogonType = $session.LogonType
                                        LogonId   = $session.LogonId
                                        Session   = $match.Session
                                        SessionId = $match.SessionId
                                        State     = $match.State
                                        IdleTime  = $match.IdleTime
                                        LogonTime = $match.LogonTime
                                    }
                                }
                        }
                    }
                }

                # Step 2 -- Deduplicate and emit result objects
                $CollectionTime = (Get-Date).ToString('dd-MM-yyyy HH:mm:ss')

                $Results |
                    Sort-Object Domain, Name -Unique |
                    ForEach-Object {
                        [PSCustomObject]@{
                            ComputerName = $computer
                            Status       = 'Success'
                            Name         = $_.Name
                            Domain       = $_.Domain
                            SID          = $_.SID
                            LogonType    = $_.LogonType
                            LogonId      = $_.LogonId
                            Session      = $_.Session
                            SessionId    = $_.SessionId
                            State        = $_.State
                            IdleTime     = $_.IdleTime
                            LogonTime    = $_.LogonTime
                            Error        = $null
                            CollectionTime = $CollectionTime
                        }
                    }
            }
            catch {
                Write-Warning "[$computer] $($_.Exception.Message)"
                [PSCustomObject]@{
                    ComputerName   = $computer
                    Status         = 'Failed'
                    Name           = $null
                    Domain         = $null
                    SID            = $null
                    LogonType      = $null
                    LogonId        = $null
                    Session        = $null
                    SessionId      = $null
                    State          = $null
                    IdleTime       = $null
                    LogonTime      = $null
                    Error          = $_.Exception.Message
                    CollectionTime = (Get-Date).ToString('dd-MM-yyyy HH:mm:ss')
                }
            }
        }
    }
}