Public/Get-UserEverything.ps1

function Get-UserEverything {
    <#
    .SYNOPSIS
        Universal user lookup - returns everything about a user from AD, M365, Intune, and sign-in logs.
    .DESCRIPTION
        The main entry point for Admin-UserLookup. Takes any form of user identifier,
        resolves it, then queries Active Directory, Microsoft 365 (Graph), Intune, and
        optionally sign-in logs to build a complete user profile. If any subsystem is
        unavailable, it continues with warnings rather than failing entirely.
 
        Optionally generates a dark-themed HTML dashboard with all collected data.
    .PARAMETER Identity
        User identifier: SAMAccountName, UPN, email address, or display name.
    .PARAMETER OutputPath
        Optional file path for an HTML dashboard report. Parent directory will be
        created if it does not exist.
    .PARAMETER IncludeSignInHistory
        Include sign-in history and risk detections. Off by default because the
        audit log queries can be slower and require Entra ID P1/P2.
    .OUTPUTS
        PSCustomObject with sections: Identity, ADAccount, M365, Devices, SignIns,
        Metadata (query timestamps and errors).
    .EXAMPLE
        Get-UserEverything -Identity "jsmith"
    .EXAMPLE
        Get-UserEverything -Identity "john.smith@contoso.com" -OutputPath "C:\Reports\jsmith.html" -IncludeSignInHistory
    .EXAMPLE
        # Pipe to Format-List for quick console review
        Get-UserEverything -Identity "John Smith" | Format-List
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, Position = 0, ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [string]$Identity,

        [Parameter()]
        [string]$OutputPath,

        [Parameter()]
        [switch]$IncludeSignInHistory
    )

    process {
        $stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
        $errors    = @()

        Write-Verbose "=== Get-UserEverything: Starting lookup for '$Identity' ==="

        # ------------------------------------------------------------------
        # Step 1: Resolve identity
        # ------------------------------------------------------------------
        $resolvedUser = $null
        try {
            $resolvedUser = Resolve-UserIdentity -Identity $Identity
            Write-Verbose "Resolved to: $($resolvedUser.SAMAccountName) / $($resolvedUser.UPN)"
        }
        catch {
            $errors += "Identity resolution failed: $($_.Exception.Message)"
            Write-Warning "Could not resolve identity '$Identity' in Active Directory: $($_.Exception.Message)"

            # If AD is unavailable, try using the Identity directly as UPN for cloud-only queries
            Write-Verbose "Attempting cloud-only lookup with '$Identity' as UPN"
            $resolvedUser = [PSCustomObject]@{
                SAMAccountName = $null
                UPN            = $Identity
                Mail           = $Identity
                ObjectId       = $null
                DisplayName    = $Identity
                Enabled        = $null
            }
        }

        # ------------------------------------------------------------------
        # Step 2: AD details
        # ------------------------------------------------------------------
        $adDetails = $null
        if ($resolvedUser.SAMAccountName) {
            try {
                Write-Verbose "Fetching AD details..."
                $adDetails = Get-UserADDetails -Identity $resolvedUser.SAMAccountName
            }
            catch {
                $errMsg = "AD lookup failed: $($_.Exception.Message)"
                $errors += $errMsg
                Write-Warning $errMsg
            }
        }
        else {
            $errors += "AD lookup skipped: SAMAccountName not available (cloud-only user or AD unavailable)"
            Write-Warning "Skipping AD lookup - no SAMAccountName resolved."
        }

        # ------------------------------------------------------------------
        # Step 3: M365 details
        # ------------------------------------------------------------------
        $m365Details = $null
        $upnForCloud = if ($resolvedUser.UPN) { $resolvedUser.UPN } elseif ($resolvedUser.Mail) { $resolvedUser.Mail } else { $Identity }

        try {
            Write-Verbose "Fetching M365 details for $upnForCloud..."
            $m365Details = Get-UserM365Details -Identity $upnForCloud
        }
        catch {
            $errMsg = "M365 lookup failed: $($_.Exception.Message)"
            $errors += $errMsg
            Write-Warning $errMsg
        }

        # ------------------------------------------------------------------
        # Step 4: Devices
        # ------------------------------------------------------------------
        $deviceDetails = $null
        try {
            Write-Verbose "Fetching device details for $upnForCloud..."
            $deviceDetails = Get-UserDevices -Identity $upnForCloud
        }
        catch {
            $errMsg = "Device lookup failed: $($_.Exception.Message)"
            $errors += $errMsg
            Write-Warning $errMsg
        }

        # ------------------------------------------------------------------
        # Step 5: Sign-in history (optional)
        # ------------------------------------------------------------------
        $signInDetails = $null
        if ($IncludeSignInHistory) {
            try {
                Write-Verbose "Fetching sign-in history for $upnForCloud..."
                $signInDetails = Get-UserSignInHistory -Identity $upnForCloud
            }
            catch {
                $errMsg = "Sign-in history lookup failed: $($_.Exception.Message)"
                $errors += $errMsg
                Write-Warning $errMsg
            }
        }
        else {
            Write-Verbose "Sign-in history skipped (use -IncludeSignInHistory to include)"
        }

        # ------------------------------------------------------------------
        # Build consolidated result
        # ------------------------------------------------------------------
        $stopwatch.Stop()

        $result = [PSCustomObject]@{
            Identity  = $resolvedUser
            ADAccount = $adDetails
            M365      = $m365Details
            Devices   = $deviceDetails
            SignIns   = $signInDetails
            Metadata  = [PSCustomObject]@{
                QueryTimestamp = Get-Date
                ElapsedSeconds = [math]::Round($stopwatch.Elapsed.TotalSeconds, 2)
                Errors         = $errors
                SectionsLoaded = @(
                    if ($adDetails)     { 'ADAccount' }
                    if ($m365Details)   { 'M365' }
                    if ($deviceDetails) { 'Devices' }
                    if ($signInDetails) { 'SignIns' }
                )
            }
        }

        # Add custom type name for formatting
        $result.PSObject.TypeNames.Insert(0, 'AdminUserLookup.UserEverything')

        # ------------------------------------------------------------------
        # Generate HTML report if requested
        # ------------------------------------------------------------------
        if ($OutputPath) {
            try {
                Write-Verbose "Generating HTML dashboard at: $OutputPath"
                $reportFile = New-HtmlDashboard -UserData $result -OutputPath $OutputPath
                Write-Host "HTML report saved to: $($reportFile.FullName)" -ForegroundColor Green
            }
            catch {
                $errMsg = "HTML report generation failed: $($_.Exception.Message)"
                $errors += $errMsg
                Write-Warning $errMsg
            }
        }

        # ------------------------------------------------------------------
        # Summary output
        # ------------------------------------------------------------------
        $sectionCount = $result.Metadata.SectionsLoaded.Count
        $totalSections = if ($IncludeSignInHistory) { 4 } else { 3 }
        if ($errors.Count -gt 0) {
            Write-Host "`nCompleted with $($errors.Count) warning(s). Loaded $sectionCount/$totalSections data sections in $($result.Metadata.ElapsedSeconds)s." -ForegroundColor Yellow
        }
        else {
            Write-Host "`nAll $sectionCount data sections loaded successfully in $($result.Metadata.ElapsedSeconds)s." -ForegroundColor Green
        }

        $result
    }
}