Public/Export-AzureADActiveUsers.ps1

function Export-AzureADActiveUsers {
    <#
    .SYNOPSIS
        Exports all active users from Azure AD to CSV files with comprehensive user information.
     
    .DESCRIPTION
        This function connects to Microsoft Graph and exports all active (enabled) users from Azure AD.
        It retrieves detailed user information including profile data, licenses, group memberships,
        and authentication methods. Results are exported to CSV files with automatic file opening.
     
    .PARAMETER ExportPath
        Path where CSV files will be saved. Defaults to C:\Temp.
     
    .PARAMETER IncludeDisabledUsers
        Include disabled users in the export (exports all users instead of just active ones).
     
    .PARAMETER IncludeLicenseDetails
        Include detailed license information for each user.
     
    .PARAMETER IncludeGroupMemberships
        Include group membership information for each user.
     
    .PARAMETER IncludeSignInActivity
        Include last sign-in activity and authentication details.
     
    .PARAMETER FilterByDomain
        Filter users by specific domain(s). Accepts array of domain names.
     
    .PARAMETER CreateSummaryReport
        Create additional summary reports with user statistics.
     
    .PARAMETER OpenResults
        Automatically open the export folder and summary file when complete.
     
    .PARAMETER BatchSize
        Number of users to process in each batch for large tenants. Default is 1000.
     
    .EXAMPLE
        Export-AzureADActiveUsers
         
        Exports all active users from Azure AD to C:\Temp with basic information.
     
    .EXAMPLE
        Export-AzureADActiveUsers -IncludeLicenseDetails -IncludeGroupMemberships
         
        Exports active users with detailed license and group membership information.
     
    .EXAMPLE
        Export-AzureADActiveUsers -FilterByDomain @("contoso.com", "subsidiary.com") -ExportPath "D:\Reports"
         
        Exports users from specific domains to a custom path.
     
    .EXAMPLE
        Export-AzureADActiveUsers -IncludeDisabledUsers -IncludeSignInActivity -CreateSummaryReport
         
        Exports all users (including disabled) with sign-in activity and creates summary reports.
     
    .NOTES
        Author: Forthencho Module
        Requires: Microsoft Graph PowerShell modules
        Required Graph Scopes: User.Read.All, UserAuthenticationMethod.Read.All, Group.Read.All, Directory.Read.All
        Version: 1.0
    #>

    
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]$ExportPath = "C:\Temp",
        
        [Parameter()]
        [switch]$IncludeDisabledUsers,
        
        [Parameter()]
        [switch]$IncludeLicenseDetails,
        
        [Parameter()]
        [switch]$IncludeGroupMemberships,
        
        [Parameter()]
        [switch]$IncludeSignInActivity,
        
        [Parameter()]
        [string[]]$FilterByDomain,
        
        [Parameter()]
        [switch]$CreateSummaryReport = $true,
        
        [Parameter()]
        [switch]$OpenResults = $true,
        
        [Parameter()]
        [int]$BatchSize = 1000
    )
    
    begin {
        Write-Host "🔍 Azure AD Active Users Export Utility" -ForegroundColor Cyan
        Write-Host "=" * 50 -ForegroundColor Gray
        Write-Host ""
        
        # Ensure export directory exists
        if (-not (Test-Path $ExportPath)) {
            try {
                New-Item -Path $ExportPath -ItemType Directory -Force | Out-Null
                Write-Host "✓ Created export directory: $ExportPath" -ForegroundColor Green
            }
            catch {
                Write-Error "Failed to create export directory: $($_.Exception.Message)"
                return
            }
        }
        
        # Initialize timestamp for file naming
        $timestamp = Get-Date -Format "yyyy-MM-dd_HH-mm-ss"
        $logFile = Join-Path $ExportPath "AzureADUserExport_$timestamp.log"
        
        # Function to write log messages
        function Write-LogMessage {
            param(
                [string]$Message,
                [string]$Level = "INFO",
                [string]$Color = "White"
            )
            
            $logTimestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
            $logEntry = "[$logTimestamp] [$Level] $Message"
            
            # Write to log file
            $logEntry | Out-File -FilePath $logFile -Append -Encoding UTF8
            
            # Write to console with color
            Write-Host $Message -ForegroundColor $Color
        }
        
        Write-LogMessage "Starting Azure AD active users export process" "INFO" "Cyan"
        Write-LogMessage "Export Path: $ExportPath" "INFO" "Gray"
        Write-LogMessage "Include Disabled Users: $IncludeDisabledUsers" "INFO" "Gray"
        Write-LogMessage "Include License Details: $IncludeLicenseDetails" "INFO" "Gray"
        Write-LogMessage "Include Group Memberships: $IncludeGroupMemberships" "INFO" "Gray"
        Write-LogMessage "Include Sign-in Activity: $IncludeSignInActivity" "INFO" "Gray"
        
        # Define required Microsoft Graph modules
        $requiredGraphModules = @(
            "Microsoft.Graph.Authentication",
            "Microsoft.Graph.Users",
            "Microsoft.Graph.Identity.DirectoryManagement"
        )
        
        # Add conditional modules based on parameters
        if ($IncludeGroupMemberships) {
            $requiredGraphModules += "Microsoft.Graph.Groups"
        }
        if ($IncludeSignInActivity) {
            $requiredGraphModules += "Microsoft.Graph.Reports"
        }
        
        # Install and import required modules
        Write-LogMessage "Setting up Microsoft Graph modules..." "INFO" "Cyan"
        
        foreach ($module in $requiredGraphModules) {
            if (-not (Get-Module -ListAvailable -Name $module)) {
                Write-LogMessage "Installing required module: $module" "INFO" "Yellow"
                try {
                    Install-Module -Name $module -Scope CurrentUser -Force -AllowClobber -Repository PSGallery
                    Write-LogMessage "Successfully installed: $module" "SUCCESS" "Green"
                }
                catch {
                    Write-LogMessage "Failed to install $module`: $($_.Exception.Message)" "ERROR" "Red"
                    throw "Required module installation failed"
                }
            }
            
            try {
                Import-Module $module -Force
                Write-LogMessage "Imported module: $module" "INFO" "Gray"
            }
            catch {
                Write-LogMessage "Failed to import $module`: $($_.Exception.Message)" "ERROR" "Red"
                throw "Module import failed"
            }
        }
        
        # Define required Graph scopes
        $requiredScopes = @(
            "User.Read.All",
            "Directory.Read.All"
        )
        
        if ($IncludeSignInActivity) {
            $requiredScopes += "AuditLog.Read.All"
            $requiredScopes += "UserAuthenticationMethod.Read.All"
        }
        
        if ($IncludeGroupMemberships) {
            $requiredScopes += "Group.Read.All"
            $requiredScopes += "GroupMember.Read.All"
        }
        
        # Connect to Microsoft Graph
        Write-LogMessage "Connecting to Microsoft Graph..." "INFO" "Cyan"
        try {
            $context = Get-MgContext -ErrorAction SilentlyContinue
            if (-not $context -or ($context.Scopes -notcontains "User.Read.All")) {
                Connect-MgGraph -Scopes $requiredScopes -NoWelcome
                Write-LogMessage "Successfully connected to Microsoft Graph" "SUCCESS" "Green"
                
                $context = Get-MgContext
                Write-LogMessage "Connected as: $($context.Account)" "INFO" "Gray"
                Write-LogMessage "Tenant ID: $($context.TenantId)" "INFO" "Gray"
            } else {
                Write-LogMessage "Already connected to Microsoft Graph as $($context.Account)" "INFO" "Green"
            }
        }
        catch {
            Write-LogMessage "Failed to connect to Microsoft Graph: $($_.Exception.Message)" "ERROR" "Red"
            throw "Graph connection failed"
        }
        
        # Initialize collections and counters
        $exportedFiles = @()
        $allUsers = @()
        $userCount = 0
        $processedCount = 0
        
        # Helper function to get user license details
        function Get-UserLicenseDetails {
            param([string]$UserId)
            
            try {
                $licenses = Get-MgUserLicenseDetail -UserId $UserId -ErrorAction SilentlyContinue
                if ($licenses) {
                    $licenseNames = $licenses | ForEach-Object { $_.SkuPartNumber } | Sort-Object -Unique
                    return ($licenseNames -join "; ")
                }
                return "No licenses assigned"
            }
            catch {
                return "License info unavailable"
            }
        }
        
        # Helper function to get user group memberships
        function Get-UserGroupMemberships {
            param([string]$UserId)
            
            try {
                $memberships = Get-MgUserMemberOf -UserId $UserId -All -ErrorAction SilentlyContinue | 
                              Where-Object { $_.AdditionalProperties.'@odata.type' -eq '#microsoft.graph.group' }
                
                if ($memberships) {
                    $groupNames = @()
                    foreach ($membership in $memberships) {
                        try {
                            $group = Get-MgGroup -GroupId $membership.Id -Property DisplayName -ErrorAction SilentlyContinue
                            if ($group) {
                                $groupNames += $group.DisplayName
                            }
                        }
                        catch {
                            $groupNames += "Unknown Group ($($membership.Id))"
                        }
                    }
                    return ($groupNames | Sort-Object -Unique) -join "; "
                }
                return "No group memberships"
            }
            catch {
                return "Group info unavailable"
            }
        }
        
        # Helper function to get sign-in activity
        function Get-UserSignInActivity {
            param([string]$UserId)
            
            try {
                $signIns = Get-MgAuditLogSignIn -Filter "userId eq '$UserId'" -Top 1 -OrderBy "createdDateTime desc" -ErrorAction SilentlyContinue
                if ($signIns) {
                    return @{
                        LastSignIn = $signIns[0].CreatedDateTime
                        LastSignInApp = $signIns[0].AppDisplayName
                        LastSignInLocation = "$($signIns[0].Location.City), $($signIns[0].Location.CountryOrRegion)"
                    }
                }
                return @{
                    LastSignIn = "Never signed in"
                    LastSignInApp = "N/A"
                    LastSignInLocation = "N/A"
                }
            }
            catch {
                return @{
                    LastSignIn = "Sign-in info unavailable"
                    LastSignInApp = "N/A"
                    LastSignInLocation = "N/A"
                }
            }
        }
    }
    
    process {
        try {
            # Build user filter
            $userFilter = if ($IncludeDisabledUsers) {
                $null  # No filter - get all users
            } else {
                "accountEnabled eq true"  # Only active users
            }
            
            # Add domain filter if specified
            if ($FilterByDomain -and $FilterByDomain.Count -gt 0) {
                $domainFilters = @()
                foreach ($domain in $FilterByDomain) {
                    $domainFilters += "endswith(userPrincipalName,'@$domain')"
                }
                $domainFilterString = "(" + ($domainFilters -join " or ") + ")"
                
                if ($userFilter) {
                    $userFilter = "$userFilter and $domainFilterString"
                } else {
                    $userFilter = $domainFilterString
                }
            }
            
            Write-LogMessage "Retrieving users from Azure AD..." "INFO" "Cyan"
            if ($userFilter) {
                Write-LogMessage "Filter: $userFilter" "INFO" "Gray"
            }
            
            # Define properties to retrieve
            $userProperties = @(
                "Id", "DisplayName", "UserPrincipalName", "Mail", "GivenName", "Surname",
                "JobTitle", "Department", "CompanyName", "OfficeLocation", "BusinessPhones",
                "MobilePhone", "AccountEnabled", "CreatedDateTime", "LastPasswordChangeDateTime",
                "UserType", "OnPremisesSyncEnabled", "ProxyAddresses"
            )
            
            # Get users in batches for better performance
            $allUsers = @()
            $skip = 0
            $hasMore = $true
            
            while ($hasMore) {
                try {
                    $userBatch = if ($userFilter) {
                        Get-MgUser -Filter $userFilter -Property $userProperties -Top $BatchSize -Skip $skip -All:$false
                    } else {
                        Get-MgUser -Property $userProperties -Top $BatchSize -Skip $skip -All:$false
                    }
                    
                    if ($userBatch -and $userBatch.Count -gt 0) {
                        $allUsers += $userBatch
                        $skip += $userBatch.Count
                        Write-LogMessage "Retrieved $($allUsers.Count) users so far..." "INFO" "Gray"
                        
                        # Check if we got fewer users than requested (indicates end of results)
                        if ($userBatch.Count -lt $BatchSize) {
                            $hasMore = $false
                        }
                    } else {
                        $hasMore = $false
                    }
                }
                catch {
                    Write-LogMessage "Error retrieving user batch: $($_.Exception.Message)" "ERROR" "Red"
                    $hasMore = $false
                }
            }
            
            $userCount = $allUsers.Count
            Write-LogMessage "Found $userCount users to process" "SUCCESS" "Green"
            
            if ($userCount -eq 0) {
                Write-LogMessage "No users found matching the specified criteria" "WARNING" "Yellow"
                return
            }
            
            # Process users and collect detailed information
            Write-LogMessage "Processing user details..." "INFO" "Cyan"
            $processedUsers = @()
            
            foreach ($user in $allUsers) {
                $processedCount++
                $percentComplete = [math]::Round(($processedCount / $userCount) * 100, 1)
                
                Write-Progress -Activity "Processing Users" -Status "Processing $($user.DisplayName) ($processedCount of $userCount)" -PercentComplete $percentComplete
                
                try {
                    # Build user object with basic information
                    $userObject = [PSCustomObject]@{
                        DisplayName = $user.DisplayName
                        UserPrincipalName = $user.UserPrincipalName
                        Email = $user.Mail
                        FirstName = $user.GivenName
                        LastName = $user.Surname
                        JobTitle = $user.JobTitle
                        Department = $user.Department
                        Company = $user.CompanyName
                        Office = $user.OfficeLocation
                        BusinessPhone = ($user.BusinessPhones -join "; ")
                        MobilePhone = $user.MobilePhone
                        AccountEnabled = $user.AccountEnabled
                        UserType = $user.UserType
                        OnPremisesSynced = $user.OnPremisesSyncEnabled
                        CreatedDate = $user.CreatedDateTime
                        LastPasswordChange = $user.LastPasswordChangeDateTime
                        ProxyAddresses = ($user.ProxyAddresses -join "; ")
                        ObjectId = $user.Id
                        ExportDate = Get-Date
                    }
                    
                    # Add license details if requested
                    if ($IncludeLicenseDetails) {
                        Write-LogMessage "Getting license details for $($user.DisplayName)" "INFO" "Gray"
                        $userObject | Add-Member -NotePropertyName "AssignedLicenses" -NotePropertyValue (Get-UserLicenseDetails -UserId $user.Id)
                    }
                    
                    # Add group memberships if requested
                    if ($IncludeGroupMemberships) {
                        Write-LogMessage "Getting group memberships for $($user.DisplayName)" "INFO" "Gray"
                        $userObject | Add-Member -NotePropertyName "GroupMemberships" -NotePropertyValue (Get-UserGroupMemberships -UserId $user.Id)
                    }
                    
                    # Add sign-in activity if requested
                    if ($IncludeSignInActivity) {
                        Write-LogMessage "Getting sign-in activity for $($user.DisplayName)" "INFO" "Gray"
                        $signInInfo = Get-UserSignInActivity -UserId $user.Id
                        $userObject | Add-Member -NotePropertyName "LastSignIn" -NotePropertyValue $signInInfo.LastSignIn
                        $userObject | Add-Member -NotePropertyName "LastSignInApp" -NotePropertyValue $signInInfo.LastSignInApp
                        $userObject | Add-Member -NotePropertyName "LastSignInLocation" -NotePropertyValue $signInInfo.LastSignInLocation
                    }
                    
                    $processedUsers += $userObject
                    
                    if ($processedCount % 100 -eq 0) {
                        Write-LogMessage "Processed $processedCount of $userCount users..." "INFO" "Gray"
                    }
                }
                catch {
                    Write-LogMessage "Failed to process user $($user.DisplayName): $($_.Exception.Message)" "WARNING" "Yellow"
                }
            }
            
            Write-Progress -Activity "Processing Users" -Completed
            Write-LogMessage "Successfully processed $($processedUsers.Count) users" "SUCCESS" "Green"
            
        }
        catch {
            Write-LogMessage "Critical error during user processing: $($_.Exception.Message)" "ERROR" "Red"
            throw
        }
    }
    
    end {
        try {
            # Export main user data
            if ($processedUsers.Count -gt 0) {
                Write-LogMessage "Exporting user data to CSV..." "INFO" "Cyan"
                
                $mainExportFile = Join-Path $ExportPath "AzureAD_ActiveUsers_$timestamp.csv"
                $processedUsers | Export-Csv -Path $mainExportFile -NoTypeInformation -Encoding UTF8
                $exportedFiles += $mainExportFile
                
                Write-LogMessage "✓ Main export completed: AzureAD_ActiveUsers_$timestamp.csv" "SUCCESS" "Green"
                Write-LogMessage " Records: $($processedUsers.Count)" "INFO" "Gray"
                Write-LogMessage " File size: $([math]::Round((Get-Item $mainExportFile).Length / 1KB, 2)) KB" "INFO" "Gray"
                
                # Create summary reports if requested
                if ($CreateSummaryReport) {
                    Write-LogMessage "Creating summary reports..." "INFO" "Cyan"
                    
                    # User statistics by department
                    $deptStats = $processedUsers | 
                                Group-Object Department | 
                                Select-Object @{Name='Department'; Expression={if($_.Name) {$_.Name} else {'Not Specified'}}},
                                            @{Name='UserCount'; Expression={$_.Count}},
                                            @{Name='EnabledUsers'; Expression={($_.Group | Where-Object {$_.AccountEnabled -eq $true}).Count}},
                                            @{Name='DisabledUsers'; Expression={($_.Group | Where-Object {$_.AccountEnabled -eq $false}).Count}} |
                                Sort-Object UserCount -Descending
                    
                    $deptStatsFile = Join-Path $ExportPath "AzureAD_UsersByDepartment_$timestamp.csv"
                    $deptStats | Export-Csv -Path $deptStatsFile -NoTypeInformation -Encoding UTF8
                    $exportedFiles += $deptStatsFile
                    
                    # User type statistics
                    $userTypeStats = $processedUsers | 
                                    Group-Object UserType | 
                                    Select-Object @{Name='UserType'; Expression={$_.Name}},
                                                @{Name='Count'; Expression={$_.Count}} |
                                    Sort-Object Count -Descending
                    
                    $userTypeStatsFile = Join-Path $ExportPath "AzureAD_UserTypes_$timestamp.csv"
                    $userTypeStats | Export-Csv -Path $userTypeStatsFile -NoTypeInformation -Encoding UTF8
                    $exportedFiles += $userTypeStatsFile
                    
                    # License statistics (if license details were included)
                    if ($IncludeLicenseDetails) {
                        $licenseStats = $processedUsers | 
                                      Where-Object { $_.AssignedLicenses -ne "No licenses assigned" } |
                                      ForEach-Object { $_.AssignedLicenses -split "; " } |
                                      Group-Object | 
                                      Select-Object @{Name='License'; Expression={$_.Name}},
                                                  @{Name='AssignedCount'; Expression={$_.Count}} |
                                      Sort-Object AssignedCount -Descending
                        
                        $licenseStatsFile = Join-Path $ExportPath "AzureAD_LicenseStatistics_$timestamp.csv"
                        $licenseStats | Export-Csv -Path $licenseStatsFile -NoTypeInformation -Encoding UTF8
                        $exportedFiles += $licenseStatsFile
                    }
                    
                    Write-LogMessage "✓ Summary reports created" "SUCCESS" "Green"
                }
            }
            
            # Final summary
            $summary = @"
 
📊 EXPORT SUMMARY
========================================
Users Processed: $($processedUsers.Count)
Files Created: $($exportedFiles.Count)
Export Location: $ExportPath
Include Disabled Users: $IncludeDisabledUsers
Include License Details: $IncludeLicenseDetails
Include Group Memberships: $IncludeGroupMemberships
Include Sign-in Activity: $IncludeSignInActivity
Completed: $(Get-Date)
========================================
"@

            
            Write-Host $summary -ForegroundColor Cyan
            Write-LogMessage $summary "INFO" "Cyan"
            
            # List exported files
            if ($exportedFiles.Count -gt 0) {
                Write-Host "📁 Exported Files:" -ForegroundColor Cyan
                foreach ($file in $exportedFiles) {
                    $fileName = Split-Path $file -Leaf
                    $fileSize = [math]::Round((Get-Item $file).Length / 1KB, 2)
                    Write-Host " • $fileName ($fileSize KB)" -ForegroundColor Green
                }
            }
            
            # Open results if requested
            if ($OpenResults -and $exportedFiles.Count -gt 0) {
                Write-LogMessage "Opening export folder and main file..." "INFO" "Yellow"
                
                try {
                    # Open the export folder
                    Start-Process explorer.exe -ArgumentList $ExportPath
                    
                    # Open the main export file
                    $mainFile = $exportedFiles | Where-Object { $_ -like "*ActiveUsers*" } | Select-Object -First 1
                    if ($mainFile) {
                        Start-Sleep -Seconds 2  # Wait for explorer to open
                        Start-Process $mainFile
                    }
                    
                    Write-LogMessage "✓ Opened export folder and main file" "SUCCESS" "Green"
                }
                catch {
                    Write-LogMessage "Failed to open results: $($_.Exception.Message)" "WARNING" "Yellow"
                }
            }
            
            Write-LogMessage "Azure AD active users export completed successfully" "INFO" "Green"
            
            # Return summary object
            return [PSCustomObject]@{
                UsersProcessed = $processedUsers.Count
                FilesCreated = $exportedFiles.Count
                ExportPath = $ExportPath
                ExportedFiles = $exportedFiles
                LogFile = $logFile
                IncludedDisabledUsers = $IncludeDisabledUsers
                IncludedLicenseDetails = $IncludeLicenseDetails
                IncludedGroupMemberships = $IncludeGroupMemberships
                IncludedSignInActivity = $IncludeSignInActivity
                CompletedAt = Get-Date
            }
            
        }
        catch {
            Write-LogMessage "Error during export finalization: $($_.Exception.Message)" "ERROR" "Red"
            throw
        }
        finally {
            # Disconnect from Microsoft Graph if we connected in this session
            try {
                # Note: We don't automatically disconnect to preserve the session for other operations
                # Users can manually disconnect with Disconnect-MgGraph if needed
                Write-LogMessage "Microsoft Graph session preserved for additional operations" "INFO" "Gray"
                Write-LogMessage "Use 'Disconnect-MgGraph' to disconnect when finished" "INFO" "Yellow"
            }
            catch {
                # Ignore disconnect errors
            }
        }
    }
}

# Export the function
Export-ModuleMember -Function Export-AzureADActiveUsers