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 |