Public/Entra/Applications/Get-MgApplicationSCIM.ps1

<#
.SYNOPSIS
    Retrieves all Entra ID applications configured for SCIM provisioning.
 
.DESCRIPTION
    This function returns a list of all Entra ID applications with SCIM provisioning enabled,
    along with their synchronization job details and settings.
 
.EXAMPLE
    $scimApps = Get-MgApplicationSCIM
 
    Retrieves all Entra ID applications with SCIM provisioning enabled.
 
.EXAMPLE
    Get-MgApplicationSCIM -ForceNewToken
 
    Forces the function to disconnect and reconnect to Microsoft Graph to obtain a new access token.
 
    .EXAMPLE
    Get-MgApplicationSCIM -Export
 
    Exports the SCIM configuration details to a CSV file.
 
    .EXAMPLE
    Get-MgApplicationSCIM -ObjectID "xxx-xxx-xxx"
 
    Retrieves the SCIM configuration for a specific application by its ObjectID.
 
    .NOTES
    .REQUIREMENTS
    This function requires the Microsoft.Graph.Applications and Microsoft.Graph.Authentication modules.
 
    .AUTHOR
    Bastien Perez
 
    .LIMITATIONS
    The groups assignments are not retrieved because based on https://main.iam.ad.ext.azure.com
 
    .CHANGELOG
    ## [1.4.0] - 2025-11-28
    ### Added
    - Add parameter `ExportToExcel` to export the report to an Excel file.
 
    ## [1.3.0] - 2025-10-23
    ### Added
    - Added error handling when adding member to job
    - Add `DisplayName` parameter
     
    ## [1.2.0] - 2025-xx-xx
    ### Added
    - Export functionality for synchronization job details
    - Support for additional synchronization job properties
 
    ## [1.1] - 2025-02-26
    ### Changed
    - Transform the script into a function
    - Replace `Write-Host` with `Write-Verbose`
 
    ## [1.0] - 2024-xx-xx
    ### Initial Release
#>


function Get-MgApplicationSCIM {
    [CmdletBinding(DefaultParameterSetName = 'All')]
    param (
        [Parameter(Mandatory = $false, ParameterSetName = 'ByObjectId')]
        [string]$ObjectID,
        [Parameter(Mandatory = $false, ParameterSetName = 'ByDisplayName')]
        [string]$DisplayName,
        [Parameter(Mandatory = $false)]
        [switch]$ForceNewToken,
        [Parameter(Mandatory = $false)]
        [switch]$ExportToExcel
    )

    [System.Collections.Generic.List[PSCustomObject]]$synchronizationJobsArray = @()
    [System.Collections.Generic.List[PSCustomObject]]$synchronizationJobsDetailsArray = @()

    if ($ForceNewToken.IsPresent) {
        if (Get-MgContext) {
            $null = Disconnect-MgGraph
        }

        $scopes = @(
            'Directory.Read.All'
        )

        Connect-MgGraph -Scopes $scopes -NoWelcome
    }
    # Determine how to search for the Service Principal(s): by ObjectID (GUID), by DisplayName, or all
    if ($ObjectID) {
        # If ObjectID looks like a GUID, use it directly
        $servicePrincipals = Get-MgServicePrincipal -ServicePrincipalId $ObjectID
    }
    elseif ($DisplayName) {
        # Use OData filter to search by DisplayName. Escape single quotes by doubling them.
        $escaped = $DisplayName -replace "'", "''"
        $filter = "DisplayName eq '$escaped'"
        Write-Verbose "Filtering service principals with: $filter"
        $servicePrincipals = Get-MgServicePrincipal -Filter $filter -All -Property DisplayName, Id
    }
    else {
        # Default: get all service principals (existing behavior)
        $servicePrincipals = Get-MgServicePrincipal -All -Property DisplayName, Id
    }

    Write-Host "$($servicePrincipals.Count) service principals found"

    $i = 0
    foreach ($servicePrincipal in $servicePrincipals) {
        $i++
        Write-Host "($i/$($servicePrincipals.Count)) - $($servicePrincipal.DisplayName): check for synchronization jobs " -ForegroundColor Cyan -NoNewline
        # Service principal is the ObjectID in Microsoft Entra ID
        $job = Get-MgServicePrincipalSynchronizationJob -ServicePrincipalId $servicePrincipal.Id -All
        
        if ($job) {
            Write-Host "$($servicePrincipal.DisplayName) - Synchronization job found" -ForegroundColor Green
            
            # We need to keep $servicePrincipal.Id in a new property ServicePrincipalID

            $job | Add-Member -MemberType NoteProperty -Name ServicePrincipalId -Value $servicePrincipal.Id
            $job | Add-Member -MemberType NoteProperty -Name DisplayName -Value $servicePrincipal.DisplayName
        
            $provisioningSettings = Invoke-GraphRequest -Uri "https://graph.microsoft.com/v1.0/servicePrincipals/$($job.ServicePrincipalId)/synchronization/secrets" -Method Get -OutputType PSObject

            $job | Add-Member -MemberType NoteProperty -Name ProvisioningBaseAddress -Value $($provisioningSettings.Value | Where-Object { $_.key -eq 'BaseAddress' }).Value
            $job | Add-Member -MemberType NoteProperty -Name ProvisioningSyncAll -Value $($provisioningSettings.Value | Where-Object { $_.key -eq 'SyncAll' }).Value
            $SyncNotificationSettings = ($provisioningSettings.Value | Where-Object { $_.key -eq 'SyncNotificationSettings' }).Value | ConvertFrom-Json
            $job | Add-Member -MemberType NoteProperty -Name ProvisioningNotificationEnabled -Value $SyncNotificationSettings.Enabled
            $job | Add-Member -MemberType NoteProperty -Name ProvisioningNotificationRecipientAddress -Value $SyncNotificationSettings.Recipients
            $job | Add-Member -MemberType NoteProperty -Name ProvisioningNotificationDeleteThresholdEnabled -Value $SyncNotificationSettings.DeleteThresholdEnabled
            $job | Add-Member -MemberType NoteProperty -Name ProvisioningNotificationDeleteThresholdValue -Value $SyncNotificationSettings.DeleteThresholdValue
            $job | Add-Member -MemberType NoteProperty -Name ProvisioningNotificationHumanResourcesLookaheadQueryEnabled -Value $SyncNotificationSettings.HumanResourcesLookaheadQueryEnabled    

            # status Value
            $job | Add-Member -MemberType NoteProperty -Name StatusCode -Value $job.Status.Code
            $job | Add-Member -MemberType NoteProperty -Name CountSuccessiveCompleteFailures -Value $job.Status.CountSuccessiveCompleteFailures
            $job | Add-Member -MemberType NoteProperty -Name EscrowsPruned -Value $job.Status.EscrowsPruned
            $job | Add-Member -MemberType NoteProperty -Name SteadyStateFirstAchievedTime -Value $job.Status.SteadyStateFirstAchievedTime
            $job | Add-Member -MemberType NoteProperty -Name SteadyStateLastAchievedTime -Value $job.Status.SteadyStateLastAchievedTime
            $job | Add-Member -MemberType NoteProperty -Name TroubleshootingUrl -Value $job.Status.TroubleshootingUrl

            # synchronizationJobSettings Value
            foreach ($property in $job.SynchronizationJobSettings) {
                $job | Add-Member -MemberType NoteProperty -Name $property.Name -Value $property.Value
            }

            # we exclude the Status and SynchronizationJobSettings properties because we already got its values
            $job = $job | Select-Object -ExcludeProperty Status, SynchronizationJobSettings
            $synchronizationJobsArray.Add($job)
        }
        else {
            Write-Host "$($servicePrincipal.DisplayName) - No synchronization job found" -ForegroundColor Yellow
        }
    }

    $j = 0
    foreach ($job in $synchronizationJobsArray) {
        $j++

        # Get information about

        Write-Host "Get synchronization settings $($job.DisplayName) ($j/$($synchronizationJobsArray.Count))"

        $jobSchema = Get-MgServicePrincipalSynchronizationJobSchema -ServicePrincipalId $job.ServicePrincipalId -SynchronizationJobId $job.Id

        $job | Add-Member -MemberType NoteProperty -Name Scheduling -Value $job.Schedule.Interval
        $job | Add-Member -MemberType NoteProperty -Name SchedulingState -Value $job.Schedule.State
        $job | Add-Member -MemberType NoteProperty -Name LastSuccessfulExecutionDate -Value $job.Status.LastSuccessfulExecution.TimeEnded
        $job | Add-Member -MemberType NoteProperty -Name LastSuccessfulExecutionState -Value $job.Status.LastSuccessfulExecution.State
        $job | Add-Member -MemberType NoteProperty -Name LastSuccessfulExecutionWithExportsDate -Value $job.Status.LastSuccessfulExecutionWithExports.TimeEnded
        $job | Add-Member -MemberType NoteProperty -Name LastSuccessfulExecutionWithExportsState -Value $job.Status.LastSuccessfulExecutionWithExports.State

        if ($job.Status.Quarantine.CurrentBegan) {
            $job | Add-Member -MemberType NoteProperty -Name Quarantined -Value $true
        }
        else {
            $job | Add-Member -MemberType NoteProperty -Name Quarantined -Value $false
        }

        foreach ($type in $job.Status.SynchronizedEntryCountByType) {
            # it's not an hashtable but an array
            $key = $type.Key
            $count = $type.Value

            $job | Add-Member -MemberType NoteProperty -Name "SynchronizedEntryCountByType_$key" -Value $count
        }

        [System.Collections.Generic.List[PSCustomObject]]$attributesArray = @()
    
        foreach ($objectMapping in $jobSchema.SynchronizationRules.ObjectMappings) {

            foreach ($mapping in $objectMapping) {
                $res = $null

                $targetObjectType = $mapping.TargetObjectName

                foreach ($attribute in $mapping.AttributeMappings) {
                
                    $object = [PSCustomObject][ordered]@{
                        Type                    = $targetObjectType
                        DefaultValue            = $attribute.DefaultValue
                        ExportMissingReferences = $attribute.ExportMissingReferences
                        FlowBehavior            = $attribute.FlowBehavior
                        FlowType                = $attribute.FlowType
                        MatchingPriority        = $attribute.MatchingPriority
                        SourceExpression        = $attribute.Source.Expression
                        SourceAttributeName     = $attribute.Source.AttributeName
                        TargetAttributeName     = $attribute.TargetAttributeName
                    }

                    $attributesArray.Add($object)

                    # add delimiter if not the first attribute
                    if ($null -ne $res) {
                        $res = "$res # "
                    }

                    if ([string]::IsNullOrWhitespace($object.SourceAttributeName)) {
                        $res = "$res$($object.SourceExpression) --> $($object.TargetAttributeName)"
                    }
                    else {
                        $res = "$res$($object.SourceAttributeName) --> $($object.TargetAttributeName)"
                    }
                }
                try {
                    $job | Add-Member -MemberType NoteProperty -Name "Attributes-$($object.Type)" -Value $res
                }
                catch {
                    Write-Host "Error adding member to job: $($_.Exception.Message)" -ForegroundColor Red
                }           
            }
        }
    
        # exclude properties because they are not needed
        $job = $job | Select-Object * -ExcludeProperty Schedule, Schema

        $synchronizationJobsDetailsArray.Add($job)
    }

    if ($ExportToExcel.IsPresent) {
        $now = Get-Date -Format 'yyyy-MM-dd_HHmmss'
        $excelFilePath = "$($env:userprofile)\$now-MgApplicationSCIM-SynchronizationJobsInfo.xlsx"
        Write-Host -ForegroundColor Cyan "Exporting SCIM synchronization jobs to Excel file: $excelFilePath"
        $synchronizationJobsDetailsArray | Export-Excel -Path $excelFilePath -AutoSize -AutoFilter -WorksheetName 'Entra-ApplicationSCIM'
        Write-Host -ForegroundColor Green "Export completed successfully!"
    }
    else {
        return $synchronizationJobsDetailsArray
    }
}