Get-LoggedUser.psm1

function Get-LoggedUser {
<#
.SYNOPSIS
    Get logged users from computers

.DESCRIPTION
    Get logged users from computers converting result from quser.exe
    Get-LoggedUser get user information direct from ComputerName.

.LINK
    .Linkedin: https://www.linkedin.com/in/erickvtorres/
    .GitHub: https://github.com/erickvtorres

.NOTES
    .Creator: Erick Torres do Vale
    .Contact: ericktorres@hotmail.com.br
    .Date: 2023-04-25
    .LastUpdate: 2026-04-07
    .Version: 0.0.3

    CHANGE NOTES
    0.0.3
    Added ThrottleLimit parameter
    Added parallel execution support for PowerShell 7+

    0.0.2
    Added ComputerName object

.PARAMETER ComputerName
    Name of VMs or Computers to recover logged users, like computer01,computer02

.PARAMETER ThrottleLimit
    Maximum number of concurrent threads when running on PowerShell 7+. Default is 10.

.EXAMPLE
    Get-LoggedUser
    Get-LoggedUser -ComputerName VMACHINE01,VMACHINE02,VMACHINE03
    Get-LoggedUser -ComputerName VMACHINE01,VMACHINE02,VMACHINE03 -ThrottleLimit 5
#>

    [CmdletBinding(DefaultParameterSetName = 'ComputerName')]
    param (
        [Parameter(
            Mandatory = $false,
            ParameterSetName = 'ComputerName'
        )]
        [array]$ComputerName,

        [Parameter(Mandatory = $false)]
        [int]$ThrottleLimit = 10
    )
    
    begin {
        $defaultDisplaySet         = 'ID', 'Username', 'SessionName', 'State', 'ComputerName'
        $defaultDisplayPropertySet = New-Object System.Management.Automation.PSPropertySet('DefaultDisplayPropertySet',[string[]]$defaultDisplaySet)
        $PSStandardMembers         = [System.Management.Automation.PSMemberInfo[]]@($defaultDisplayPropertySet)
        $DateRegex                 = '[0-9]{2}\W{1}[0-9]{2}\W{1}[0-9]{4}\s{1}[0-9]{2}\W[0-9]{2}'

        if (-Not $ComputerName){$ComputerName = $env:COMPUTERNAME}
    }
    
    process {
        if ($PSVersionTable.PSVersion.Major -ge 7) {
            $ComputerName | ForEach-Object -Parallel {
                $Server    = $_
                $DateRegex = $using:DateRegex

                $defaultDisplaySet         = 'ID', 'Username', 'SessionName', 'State', 'ComputerName'
                $defaultDisplayPropertySet = New-Object System.Management.Automation.PSPropertySet('DefaultDisplayPropertySet',[string[]]$defaultDisplaySet)
                $PSStandardMembers         = [System.Management.Automation.PSMemberInfo[]]@($defaultDisplayPropertySet)

                $Users = quser /server:$Server 2>&1
                if ($Users[0] -like 'Error*') { return }

                $Regex = $Users | Select-Object -Skip 1 | ForEach-Object -Process { $_ -replace '\s{2,}', ',' }
                $Regex | ForEach-Object {
                    $Object = $_.Split(',')
                    $Result = [PSCustomObject]@{
                        Username     = $Object[0] -replace '>' -replace ' '
                        SessionName  = if ($Object[1] -eq 'console' -or $Object[1] -like 'rdp-tcp*'){$Object[1]} else {'no-session'}
                        ID           = if ($Object[2] -match '[0-9]'){$Object[2]} else {$Object[1]}
                        State        = if ($Object[3] -match '[A-Za-z]'){$Object[3]} else {$Object[2]}
                        IdleTime     = if ($Object[4] -notmatch $DateRegex){$Object[4]} else {$Object[3]}
                        LogonTime    = if ($Object[5]) {$Object[5]} else {$Object[4]}
                        ComputerName = $Server
                    }
                    $Result.PSObject.TypeNames.Insert(0,'User.Information')
                    $Result | Add-Member MemberSet PSStandardMembers $PSStandardMembers
                    $Result
                }
            } -ThrottleLimit $ThrottleLimit
        } else {
            $ComputerName | ForEach-Object {
                $Server = $_
                $Users  = quser /server:$Server 2>&1
                if ($Users[0] -like 'Error*') { return }

                $Regex = $Users | Select-Object -Skip 1 | ForEach-Object -Process { $_ -replace '\s{2,}', ',' }
                $Regex | ForEach-Object {
                    $Object = $_.Split(',')
                    $Result = [PSCustomObject]@{
                        Username     = $Object[0] -replace '>' -replace ' '
                        SessionName  = if ($Object[1] -eq 'console' -or $Object[1] -like 'rdp-tcp*'){$Object[1]} else {'no-session'}
                        ID           = if ($Object[2] -match '[0-9]'){$Object[2]} else {$Object[1]}
                        State        = if ($Object[3] -match '[A-Za-z]'){$Object[3]} else {$Object[2]}
                        IdleTime     = if ($Object[4] -notmatch $DateRegex){$Object[4]} else {$Object[3]}
                        LogonTime    = if ($Object[5]) {$Object[5]} else {$Object[4]}
                        ComputerName = $Server
                    }
                    $Result.PSObject.TypeNames.Insert(0,'User.Information')
                    $Result | Add-Member MemberSet PSStandardMembers $PSStandardMembers
                    $Result
                }
            }
        }
    }

    end {
        $List > $null
    }
}

function Get-ADLoggedUser {
<#
.SYNOPSIS
    Get logged users from computers using AD

.DESCRIPTION
    Get logged users from computers converting result from quser.exe
    Get-ADLoggedUser check if computers exists in AD, get all
    computer objects from OU or get all Domain Controllers.

    Get-ADLoggedUser requires ActiveDirectory module

.LINK
    .Linkedin: https://www.linkedin.com/in/erickvtorres/
    .GitHub: https://github.com/erickvtorres

.NOTES
    .Creator: Erick Torres do Vale
    .Contact: ericktorres@hotmail.com.br
    .Date: 2023-04-25
    .LastUpdate: 2026-04-07
    .Version: 0.0.3

    CHANGE NOTES
    0.0.3
    Added ThrottleLimit parameter
    Added parallel execution support for PowerShell 7+

    0.0.2
    Added ComputerName object

.PARAMETER ComputerName
    Name of VMs or Computers to recover logged users, like computer01,computer02

.PARAMETER OU
    DistinguishedName from OU with desired servers or computers

.PARAMETER DomainControllers
    Get all domain controllers from domain

.PARAMETER ThrottleLimit
    Maximum number of concurrent threads when running on PowerShell 7+. Default is 10.

.EXAMPLE
    Get-ADLoggedUser -OU 'OU=Servers,DC=vkr,DC=inc'
    Get-ADLoggedUser -DomainControllers
    Get-ADLoggedUser -DomainControllers -ThrottleLimit 5
#>

    [CmdletBinding(DefaultParameterSetName = 'ComputerName')]
    param (
        [Parameter(
            Mandatory = $false,
            ParameterSetName = 'ComputerName'
        )]
        [array]$ComputerName,

        [Parameter(
            Mandatory = $false,
            ParameterSetName = 'OU'
        )]
        [string]$OU,

        [Parameter(
            Mandatory = $false,
            ParameterSetName = 'DCs'
        )]
        [switch]$DomainControllers,

        [Parameter(Mandatory = $false)]
        [int]$ThrottleLimit = 10
    )
    
    begin {
        $defaultDisplaySet         = 'ID', 'Username', 'SessionName', 'State', 'ComputerName'
        $defaultDisplayPropertySet = New-Object System.Management.Automation.PSPropertySet('DefaultDisplayPropertySet',[string[]]$defaultDisplaySet)
        $PSStandardMembers         = [System.Management.Automation.PSMemberInfo[]]@($defaultDisplayPropertySet)

        $DateRegex = '[0-9]{2}\W{1}[0-9]{2}\W{1}[0-9]{4}\s{1}[0-9]{2}\W[0-9]{2}'
        $Computers = New-Object -TypeName System.Collections.ArrayList
        switch ($PSBoundParameters.Keys) {
            ComputerName {
                try {
                    $ComputerName | ForEach-Object {
                        Get-ADComputer -Identity $_ | ForEach-Object {
                            Write-Verbose "Computer: $($_.DistinguishedName)"
                            [void]$Computers.Add($_.Name)
                        }
                    }
                }
                catch {
                    Write-Warning $_.Exception.Message
                    break
                }
            }

            OU {
                try {
                    Get-ADComputer -Filter {Enabled -eq $true} -SearchBase $OU | ForEach-Object {
                        Write-Verbose "Computer: $($_.DistinguishedName)"
                        [void]$Computers.Add($_.Name)
                    }
                }
                catch {
                    Write-Warning $_.Exception.Message
                    break
                }
            }

            DomainControllers {
                try {
                    Get-ADDomainController -Filter * | ForEach-Object {
                        Write-Verbose "Computer: $($_.ComputerObjectDN)"
                        [void]$Computers.Add($_.Name)
                    }
                }
                catch {
                    Write-Warning $_.Exception.Message
                    break
                }
            }

            Default {
                break
            }
        }
    }
    
    process {
        if ($PSVersionTable.PSVersion.Major -ge 7) {
            $Computers | ForEach-Object -Parallel {
                $Server    = $_
                $DateRegex = $using:DateRegex

                $defaultDisplaySet         = 'ID', 'Username', 'SessionName', 'State', 'ComputerName'
                $defaultDisplayPropertySet = New-Object System.Management.Automation.PSPropertySet('DefaultDisplayPropertySet',[string[]]$defaultDisplaySet)
                $PSStandardMembers         = [System.Management.Automation.PSMemberInfo[]]@($defaultDisplayPropertySet)

                $Users = quser /server:$Server 2>&1
                if ($Users[0] -like 'Error*') { return }

                $Regex = $Users | Select-Object -Skip 1 | ForEach-Object -Process { $_ -replace '\s{2,}', ',' }
                $Regex | ForEach-Object {
                    $Object = $_.Split(',')
                    $Result = [PSCustomObject]@{
                        Username     = $Object[0] -replace '>' -replace ' '
                        SessionName  = if ($Object[1] -eq 'console' -or $Object[1] -like 'rdp-tcp*'){$Object[1]} else {'no-session'}
                        ID           = if ($Object[2] -match '[0-9]'){$Object[2]} else {$Object[1]}
                        State        = if ($Object[3] -match '[A-Za-z]'){$Object[3]} else {$Object[2]}
                        IdleTime     = if ($Object[4] -notmatch $DateRegex){$Object[4]} else {$Object[3]}
                        LogonTime    = if ($Object[5]) {$Object[5]} else {$Object[4]}
                        ComputerName = $Server
                    }
                    $Result.PSObject.TypeNames.Insert(0,'User.Information')
                    $Result | Add-Member MemberSet PSStandardMembers $PSStandardMembers
                    $Result
                }
            } -ThrottleLimit $ThrottleLimit
        } else {
            $Computers | ForEach-Object {
                $Server = $_
                $Users  = quser /server:$Server 2>&1
                if ($Users[0] -like 'Error*') { return }

                $Regex = $Users | Select-Object -Skip 1 | ForEach-Object -Process { $_ -replace '\s{2,}', ',' }
                $Regex | ForEach-Object {
                    $Object = $_.Split(',')
                    $Result = [PSCustomObject]@{
                        Username     = $Object[0] -replace '>' -replace ' '
                        SessionName  = if ($Object[1] -eq 'console' -or $Object[1] -like 'rdp-tcp*'){$Object[1]} else {'no-session'}
                        ID           = if ($Object[2] -match '[0-9]'){$Object[2]} else {$Object[1]}
                        State        = if ($Object[3] -match '[A-Za-z]'){$Object[3]} else {$Object[2]}
                        IdleTime     = if ($Object[4] -notmatch $DateRegex){$Object[4]} else {$Object[3]}
                        LogonTime    = if ($Object[5]) {$Object[5]} else {$Object[4]}
                        ComputerName = $Server
                    }
                    $Result.PSObject.TypeNames.Insert(0,'User.Information')
                    $Result | Add-Member MemberSet PSStandardMembers $PSStandardMembers
                    $Result
                }
            }
        }
    }

    end {
        $Computers > $null
    }
}