Public/Get-VBUserProfile.ps1

# ============================================================
# FUNCTION : Get-VBUserProfile
# MODULE : VB.WorkstationReport
# VERSION : 1.0.0
# CHANGED : 16-04-2026 -- Initial release
# AUTHOR : Vibhu Bhatnagar
# PURPOSE : Enumerates all non-system user profiles on local or remote computers
# ENCODING : UTF-8 with BOM
# ============================================================
function Get-VBUserProfile {
    <#
    .SYNOPSIS
        Enumerates all non-system user profiles on local or remote computers.
 
    .DESCRIPTION
        Get-VBUserProfile queries Win32_UserProfile to return every non-special user
        profile on the target computer. For each profile it resolves the SID to a
        domain and username via NTAccount translation.
 
        Entra ID / Azure AD accounts (S-1-12-1-* SIDs) cannot be resolved via
        NTAccount translation. These are detected by SID prefix and returned with
        Domain = 'AzureAD' and Username derived from the profile folder name.
 
        Supports local and remote execution. Credentials are only passed to
        Get-CimInstance when explicitly supplied.
 
    .PARAMETER ComputerName
        Computer names to query. Accepts pipeline input. Defaults to the local computer.
 
    .PARAMETER Credential
        Credentials for remote computer access. Not required for local execution.
 
    .EXAMPLE
        Get-VBUserProfile
        Returns all user profiles on the local computer.
 
    .EXAMPLE
        Get-VBUserProfile -ComputerName 'WS001','WS002' -Credential (Get-Credential)
        Returns all user profiles on two remote workstations.
 
    .EXAMPLE
        'WS001','WS002' | Get-VBUserProfile -Credential $cred | Where-Object Loaded
        Returns only profiles that are currently loaded (user is logged in).
 
    .EXAMPLE
        Get-VBUserProfile | Select-Object Username, Domain, ProfilePath, LastUseTime
        Returns a summary of all local profiles with last login time.
 
    .OUTPUTS
        PSCustomObject
        Returns one object per user profile with:
          - ComputerName : Target computer
          - SID : User SID
          - Domain : Domain or machine name ('AzureAD' for Entra ID accounts)
          - Username : Resolved username (profile folder name for Entra ID)
          - ProfilePath : Local profile path
          - LastUseTime : Last time the profile was used
          - Loaded : True if the profile hive is currently loaded in HKEY_USERS
          - CollectionTime : Timestamp of data collection
          - Status : 'Success' or 'Failed'
          - Error : Error message (only present on failure)
 
    .NOTES
        Version : 1.0.0
        Author : Vibhu Bhatnagar
        Category : User Profile Management
        Requirements :
          - PowerShell 5.1 or higher
          - PowerShell Remoting enabled for remote targets
    #>

    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [Alias('Name', 'Server')]
        [string[]]$ComputerName = $env:COMPUTERNAME,

        [PSCredential]$Credential
    )

    process {
        foreach ($computer in $ComputerName) {
            try {
                # Only pass ComputerName and Credential for remote queries
                $cimParams = @{
                    ClassName   = 'Win32_UserProfile'
                    Filter      = "Special = 'False'"
                    ErrorAction = 'Stop'
                }
                if ($computer -ne $env:COMPUTERNAME) {
                    $cimParams['ComputerName'] = $computer
                    if ($Credential) { $cimParams['Credential'] = $Credential }
                }

                $profiles       = Get-CimInstance @cimParams
                $collectionTime = (Get-Date).ToString('dd-MM-yyyy HH:mm:ss')

                foreach ($profile in $profiles) {
                    $domain   = $null
                    $username = $null

                    try {
                        # Translate SID to NTAccount (DOMAIN\Username)
                        # Fails for Entra ID (S-1-12-1-*) SIDs -- caught below
                        $fullAccount = (New-Object System.Security.Principal.SecurityIdentifier($profile.SID)).
                                           Translate([System.Security.Principal.NTAccount]).Value

                        if ($fullAccount -match '\\') {
                            $parts    = $fullAccount -split '\\'
                            $domain   = $parts[0]
                            $username = $parts[1]
                        }
                        else {
                            $domain   = $null
                            $username = $fullAccount
                        }
                    }
                    catch {
                        # S-1-12-1-* = Entra ID / Azure AD joined device account.
                        # These SIDs have no NTAccount mapping -- use profile path for username.
                        if ($profile.SID -match '^S-1-12-1-') {
                            $domain   = 'AzureAD'
                            $username = Split-Path $profile.LocalPath -Leaf
                        }
                        else {
                            $domain   = 'Unknown'
                            $username = Split-Path $profile.LocalPath -Leaf
                        }
                    }

                    [PSCustomObject]@{
                        ComputerName   = $computer
                        SID            = $profile.SID
                        Domain         = $domain
                        Username       = $username
                        ProfilePath    = $profile.LocalPath
                        LastUseTime    = $profile.LastUseTime
                        Loaded         = $profile.Loaded
                        CollectionTime = $collectionTime
                        Status         = 'Success'
                    }
                }
            }
            catch {
                [PSCustomObject]@{
                    ComputerName   = $computer
                    Error          = $_.Exception.Message
                    CollectionTime = (Get-Date).ToString('dd-MM-yyyy HH:mm:ss')
                    Status         = 'Failed'
                }
            }
        }
    }
}