Scripts/Get-UsersInfo.ps1

function Get-Users {
<#
    .SYNOPSIS
    Retrieves the creation time and date of the last password change for all users.
    Script inspired by: https://github.com/tomwechsler/Microsoft_Graph/blob/main/Entra_ID/Create_time_last_password.ps1
 
    .DESCRIPTION
    Retrieves the creation time and date of the last password change for all users.
 
    .PARAMETER OutputDir
    OutputDir is the parameter specifying the output directory.
    Default: Output\Users
 
    .PARAMETER Encoding
    Encoding is the parameter specifying the encoding of the CSV output file.
    Default: UTF8
 
    .PARAMETER LogLevel
    Specifies the level of logging:
    None: No logging
    Minimal: Critical errors only
    Standard: Normal operational logging
    Debug: Verbose logging for debugging purposes
    Default: Standard
 
    .PARAMETER UserIds
    UserId is the parameter specifying a single user ID or UPN to filter the results.
    Default: All users will be included if not specified.
     
    .EXAMPLE
    Get-Users
    Retrieves the creation time and date of the last password change for all users.
 
    .EXAMPLE
    Get-Users -Encoding utf32
    Retrieves the creation time and date of the last password change for all users and exports the output to a CSV file with UTF-32 encoding.
         
    .EXAMPLE
    Get-Users -OutputDir C:\Windows\Temp
    Retrieves the creation time and date of the last password change for all users and saves the output to the C:\Windows\Temp folder.
#>

    [CmdletBinding()]
    param(
        [string]$OutputDir,
        [string]$Encoding = "UTF8",
        [string]$UserIds,
        [ValidateSet('None', 'Minimal', 'Standard', 'Debug')]
        [string]$LogLevel = 'Standard'
    )

    Init-Logging
    Init-OutputDir -Component "Users" -FilePostfix "Users" -CustomOutputDir $OutputDir

    $requiredScopes = @("User.Read.All")
    $graphAuth = Get-GraphAuthType -RequiredScopes $RequiredScopes
    Write-LogFile -Message "=== Starting Users Collection ===" -Color "Cyan" -Level Standard

    try {
        $selectobjects = "UserPrincipalName","DisplayName","Id","CompanyName","Department","JobTitle","City","Country","Identities","UserType","LastPasswordChangeDateTime","AccountEnabled","CreatedDateTime","CreationType","ExternalUserState","ExternalUserStateChangeDateTime","SignInActivity","OnPremisesSyncEnabled"
        $mgUsers = @()

        if ($UserIds) {
            Write-LogFile -Message "[INFO] Filtering results for user: $UserIds" -Level Standard
            
            try {
                $mgUsers = Get-Mguser -Filter "userPrincipalName eq '$UserIds'" -select $selectobjects
                                
                if (-not $mgUsers) {
                    Write-LogFile -Message "[WARNING] User not found: $UserIds" -Color "Yellow" -Level Standard
                    $mgUsers = @()
                }
            } catch {
                Write-LogFile -Message "[WARNING] Error retrieving user $UserIds`: $($_.Exception.Message)" -Color "Yellow" -Level Standard
                $mgUsers = @()
            }
        } else {
            $mgUsers = Get-MgUser -All -Select $selectobjects
            Write-LogFile -Message "[INFO] Found $($mgUsers.Count) users" -Level Standard
        }

        $formattedUsers = $mgUsers | ForEach-Object {
            [PSCustomObject]@{
                UserPrincipalName = $_.UserPrincipalName
                DisplayName = $_.DisplayName
                Id = $_.Id
                Department = $_.Department
                JobTitle = $_.JobTitle
                AccountEnabled = $_.AccountEnabled
                CreatedDateTime = $_.CreatedDateTime
                LastPasswordChangeDateTime = $_.LastPasswordChangeDateTime
                UserType = $_.UserType
                OnPremisesSyncEnabled = $_.OnPremisesSyncEnabled
                Mail = $_.Mail
                LastSignInDateTime = $_.SignInActivity.LastSignInDateTime
                LastNonInteractiveSignInDateTime = $_.SignInActivity.LastNonInteractiveSignInDateTime
                IdentityProvider = ($_.Identities | Where-Object { $_.SignInType -eq "federated" }).Issuer
                City = $_.City
                Country = $_.Country
                UsageLocation = $_.UsageLocation
            }
        }

        if ($isDebugEnabled) {
            Write-LogFile -Message "[DEBUG] User formatting completed:" -Level Debug
            Write-LogFile -Message "[DEBUG] Original users: $($mgUsers.Count)" -Level Debug
            Write-LogFile -Message "[DEBUG] Formatted users: $($formattedUsers.Count)" -Level Debug
            Write-LogFile -Message "[DEBUG] Starting user analysis by creation date..." -Level Debug
        }

        $now  = Get-Date
        $d7   = $now.AddDays(-7)
        $d30  = $now.AddDays(-30)
        $d90  = $now.AddDays(-90)
        $d180 = $now.AddDays(-180)
        $d360 = $now.AddDays(-360)

        $counts = @{ Week=0; Month=0; ThreeMonth=0; SixMonth=0; Year=0; Enabled=0; Disabled=0; OnPrem=0; Guest=0 }
        foreach ($u in $mgUsers) {
            if ($u.CreatedDateTime -gt $d7)   { $counts.Week++ }
            if ($u.CreatedDateTime -gt $d30)  { $counts.Month++ }
            if ($u.CreatedDateTime -gt $d90)  { $counts.ThreeMonth++ }
            if ($u.CreatedDateTime -gt $d180) { $counts.SixMonth++ }
            if ($u.CreatedDateTime -gt $d360) { $counts.Year++ }
            if ($u.AccountEnabled)            { $counts.Enabled++ }
            else                              { $counts.Disabled++ }
            if ($u.OnPremisesSyncEnabled)     { $counts.OnPrem++ }
            if ($u.UserType -eq "Guest")      { $counts.Guest++ }
        }

        $formattedUsers | Export-Csv -Path $script:outputFile -NoTypeInformation -Encoding $Encoding

        $summary = [ordered]@{
            "User Counts" = [ordered]@{
                "Total Users"            = $mgUsers.Count
                "Enabled Users"          = $counts.Enabled
                "Disabled Users"         = $counts.Disabled
                "Synced from On-Premises" = $counts.OnPrem
                "Guest Users"            = $counts.Guest
            }
            "Recent Account Creation" = [ordered]@{
                "Last 7 days"   = $counts.Week
                "Last 30 days"  = $counts.Month
                "Last 90 days"  = $counts.ThreeMonth
                "Last 6 months" = $counts.SixMonth
                "Last 1 year"   = $counts.Year
            }
        }

        Write-Summary -Summary $summary -Title "User Analysis Summary"
    }
    catch {
        Write-logFile -Message "[ERROR] An error occurred: $($_.Exception.Message)" -Color "Red" -Level Minimal
        if ($isDebugEnabled) {
            Write-LogFile -Message "[DEBUG] Error details:" -Level Debug
            Write-LogFile -Message "[DEBUG] Exception type: $($_.Exception.GetType().Name)" -Level Debug
            Write-LogFile -Message "[DEBUG] Error message: $($_.Exception.Message)" -Level Debug
            Write-LogFile -Message "[DEBUG] Stack trace: $($_.ScriptStackTrace)" -Level Debug
        }
        throw
    }   
}

Function Get-AdminUsers {
<#
    .SYNOPSIS
    Retrieves all Administrator directory roles.
 
    .DESCRIPTION
    Retrieves Administrator directory roles, including the identification of users associated with each specific role.
 
    .PARAMETER OutputDir
    OutputDir is the parameter specifying the output directory.
    Default: Output\Admins
 
    .PARAMETER Encoding
    Encoding is the parameter specifying the encoding of the CSV output file.
    Default: UTF8
 
    .PARAMETER LogLevel
    Specifies the level of logging:
    None: No logging
    Minimal: Critical errors only
    Standard: Normal operational logging
    Debug: Verbose logging for debugging purposes
    Default: Standard
     
    .EXAMPLE
    Get-AdminUsers
    Retrieves Administrator directory roles, including the identification of users associated with each specific role.
     
    .EXAMPLE
    Get-AdminUsers -Encoding utf32
    Retrieves Administrator directory roles, including the identification of users associated with each specific role and exports the output to a CSV file with UTF-32 encoding.
         
    .EXAMPLE
    Get-AdminUsers -OutputDir C:\Windows\Temp
    Retrieves Administrator directory roles, including the identification of users associated with each specific role and saves the output to the C:\Windows\Temp folder.
#>
    

    [CmdletBinding()]
    param(
        [string]$OutputDir,
        [string]$Encoding = "UTF8",
        [ValidateSet('None', 'Minimal', 'Standard', 'Debug')]
        [string]$LogLevel = 'Standard'
    )

    Init-Logging
    Init-OutputDir -Component "Admins" -FilePostfix "AdminUsers" -CustomOutputDir $OutputDir

    Write-LogFile -Message "=== Starting Admin Users Collection ===" -Color "Cyan" -Level Standard

    $requiredScopes = @("User.Read.All", "Directory.Read.All")
    $graphAuth = Get-GraphAuthType -RequiredScopes $RequiredScopes

    if ($isDebugEnabled) {
        Write-LogFile -Message "[DEBUG] Graph authentication details:" -Level Debug
        Write-LogFile -Message "[DEBUG] Required scopes: $($requiredScopes -join ', ')" -Level Debug
        Write-LogFile -Message "[DEBUG] Authentication type: $($graphAuth.AuthType)" -Level Debug
        Write-LogFile -Message "[DEBUG] Current scopes: $($graphAuth.Scopes -join ', ')" -Level Debug
    }

    Write-LogFile -Message "[INFO] Analyzing administrator roles..." -Level Standard
    $rolesWithUsers = [System.Collections.Generic.List[object]]::new()
    $rolesWithoutUsers = [System.Collections.Generic.List[object]]::new()
    $exportedFiles = [System.Collections.Generic.List[object]]::new()
    $totalAdminCount = 0
    $inactiveAdminCount = 0
    $inactiveThreshold = (Get-Date).AddDays(-30)
    $inactiveAdmins = [System.Collections.Generic.List[object]]::new()

    try {
        if ($isDebugEnabled) {
            Write-LogFile -Message "[DEBUG] Retrieving all directory roles..." -Level Debug
            $performance = Measure-Command { $getRoles = Get-MgDirectoryRole -all }
            Write-LogFile -Message "[DEBUG] Directory roles retrieval took $([math]::round($performance.TotalSeconds, 2)) seconds" -Level Debug
        } else {
            $getRoles = Get-MgDirectoryRole -all
        }
        
        foreach ($role in $getRoles) {
            $roleId = $role.Id
            $roleName = $role.DisplayName
        
            if ($roleName -like "*Admin*") {
                $areThereUsers = Get-MgDirectoryRoleMember -DirectoryRoleId $roleId

                if ($null -eq $areThereUsers) {
                    $rolesWithoutUsers.Add($roleName)
                    continue
                }

                $results = New-Object System.Collections.Generic.List[PSObject]
                
                # 1. Verzamel en filter ID's
                $validUserIds = [System.Collections.Generic.List[string]]::new()
                foreach ($member in $areThereUsers) {
                    if (-not [string]::IsNullOrWhiteSpace($member.Id) -and $member.Id -ne ".") {
                        $cleanId = $member.Id -replace '[^a-zA-Z0-9\-]', ''
                        if ($cleanId) { $validUserIds.Add($cleanId) }
                    }
                }
                
                if ($validUserIds.Count -eq 0) { continue }

                # 2. Bulk OData Filtering (15 gebruikers per keer)
                for ($i = 0; $i -lt $validUserIds.Count; $i += 15) {
                    
                    $chunkSize = [math]::Min(15, $validUserIds.Count - $i)
                    $currentChunk = $validUserIds.GetRange($i, $chunkSize)
                    
                    $filterValues = $currentChunk | ForEach-Object { "'$_'" }
                    $filterString = "id in ($($filterValues -join ','))"

                    try {
                        $getUsers = Get-MgUser -Filter $filterString -Property "UserPrincipalName","DisplayName","Id","Department","JobTitle","AccountEnabled","CreatedDateTime","SignInActivity" -ErrorAction Stop

                        [array]$retrievedUsers = $getUsers

                        foreach ($u in $retrievedUsers) {
                            $userName = $u.UserPrincipalName

                            $userObject = [PSCustomObject]@{
                                UserName = $userName
                                UserId = $u.Id
                                Role = $roleName
                                DisplayName = $u.DisplayName
                                Department = $u.Department
                                JobTitle = $u.JobTitle
                                AccountEnabled = $u.AccountEnabled
                                CreatedDateTime = $u.CreatedDateTime
                                LastInteractiveSignIn = $u.SignInActivity.LastSignInDateTime
                                LastNonInteractiveSignIn = $u.SignInActivity.LastNonInteractiveSignInDateTime
                            }

                            if ($u.SignInActivity.LastSignInDateTime) {
                                $lastSignInDate = [datetime]$u.SignInActivity.LastSignInDateTime
                                $daysSinceSignIn = (New-TimeSpan -Start $lastSignInDate -End (Get-Date)).Days
                                $userObject | Add-Member -MemberType NoteProperty -Name "DaysSinceLastSignIn" -Value $daysSinceSignIn
                                
                                if ($lastSignInDate -lt $inactiveThreshold) {
                                    $inactiveAdminCount++
                                    $inactiveAdmins.Add("$($u.DisplayName) ($userName) - $daysSinceSignIn days")
                                }
                            } else {
                                $userObject | Add-Member -MemberType NoteProperty -Name "DaysSinceLastSignIn" -Value "No sign-in data"
                                $inactiveAdminCount++
                                $inactiveAdmins.Add("$($u.DisplayName) ($userName) - No sign-in data")                 
                            }
                            
                            $results.Add($userObject)
                        }
                    } catch {
                        $errMsg = $_.Exception.Message
                        Write-LogFile -Message "[WARNING] Bulk fetch failed for role $roleName`: $errMsg" -Color "Yellow" -Level Standard
                    }
                }

                # Export per rol
                if ($results.Count -gt 0) {
                    $totalAdminCount += $results.Count
                    $rolesWithUsers.Add("$roleName ($($results.Count) users)")
                    
                    $date = [datetime]::Now.ToString('yyyyMMdd')
                    $safeRoleName = $roleName -replace '[^\w\-_\.]', '_'
                    $rolePath = Split-Path $script:outputFile -Parent
                    $roleFilePath = Join-Path $rolePath "$date-$safeRoleName.csv"

                    $results.ToArray() | Export-Csv -Path $roleFilePath -NoTypeInformation -Encoding $Encoding
                    $exportedFiles.Add($roleFilePath)
                }
                else {
                    $rolesWithoutUsers.Add($roleName)
                }
            }
        }

        # Create merged file
        $outputDirPath = Split-Path $script:outputFile -Parent
        $outputDirMerged = Join-Path $outputDirPath "Merged"
        if (!(Test-Path $outputDirMerged)) {
            New-Item -ItemType Directory -Force -Path $outputDirMerged > $null
        }

        $date = [datetime]::Now.ToString('yyyyMMdd')
        $mergedFile = Join-Path $outputDirMerged "$date-All-Administrators.csv"

        $adminFiles = Get-ChildItem $outputDirPath -Filter "*Admin*.csv" -ErrorAction SilentlyContinue
        if ($adminFiles.Count -gt 0) {
            $adminFiles | ForEach-Object { Import-Csv $_.FullName } | Export-Csv $mergedFile -NoTypeInformation -Encoding $Encoding
        }

        $summary = [ordered]@{
            "Role Summary" = [ordered]@{
                "Total admin roles" = ($rolesWithUsers.Count + $rolesWithoutUsers.Count)
                "Roles with users" = $rolesWithUsers.Count
                "Empty roles" = $rolesWithoutUsers.Count
                "Total administrators" = $totalAdminCount
                "Inactive administrators (30+ days)" = $inactiveAdminCount
            }
        }

        Write-LogFile -Message "`nRoles with users:" -Color "Green" -Level Standard
        foreach ($role in $rolesWithUsers) { Write-LogFile -Message " + $role" -Level Standard }

        Write-LogFile -Message "`nEmpty roles:" -Color "Yellow" -Level Standard
        foreach ($role in $rolesWithoutUsers) { Write-LogFile -Message " - $role" -Level Standard }

        if ($inactiveAdmins.Count -gt 0) {
            Write-LogFile -Message "`nInactive administrators (30+ days):" -Color "Yellow" -Level Standard
            foreach ($admin in $inactiveAdmins) { Write-LogFile -Message " ! $admin" -Level Standard }
        }

        Write-Summary -Summary $summary -Title "Admin Users Summary"
    }
    catch {
        Write-logFile -Message "[ERROR] An error occurred: $($_.Exception.Message)" -Color "Red" -Level Minimal
        throw
    }
}