Public/Configuration/New-EntraOpsWorkloadIdentity.ps1

<#
.SYNOPSIS
    Create required App Registrations to execute EntraOps in Automation runbook or DevOps pipelines.

.DESCRIPTION
    Create required Workload Identity (new or use existing Service Principal) to execute EntraOps in Automation runbook or DevOps pipelines.

.PARAMETER AppDisplayName
    Display name of the App Registration which will be created.

.PARAMETER ExistingSpObjectId
    ObjectId of the existing Service Principal which should be used. If not provided, a new App Registration will be created. Can be combined with CreateFederatedCredential to complete or repair federation on the existing application.

.PARAMETER ConfigFile
    Location of the config file which will be used to get required parameters. Default is ./EntraOpsConfig.json.

.PARAMETER CreateFederatedCredential
    Switch to create a federated credential for the App Registration.

.PARAMETER GitHubOrg
    GitHub organization or username name where the EntraOps repository is hosted which will be used for creating the federated credential.

.PARAMETER GitHubRepo
    GitHub repository name of EntraOps which will be used for creating the federated credential.

.PARAMETER FederatedEntityType
    Type of the entity (e.g., branch or environment) which will be used for creating the federated credential.
    By default, the value is "Branch".

.PARAMETER FederatedEntityName
    Name of the entity (e.g., branch name "main" or environment name "prod") which will be used for creating the federated credential.
    By default, the value is "main".

.EXAMPLE
    Create App Registration based on the configuration in the config file (default location: ./EntraOpsConfig.json).
    If the Ingestion to Log Analytics is defined in Config file, the required permissions will be added to the Resource Group of the Data Collection Rule.
    Same for defined option to ingest to Sentinel WatchLists, the related permissions will be added to the Resource Group of the Sentinel workspace.
    New-EntraOpsWorkloadIdentity -AppDisplayName "EntraOps Reporting"

.EXAMPLE
    Create App Registration based on the configuration in the config file (default location: ./EntraOpsConfig.json) and create a federated credential for GitHub repository branch defined in parameter.
    New-EntraOpsWorkloadIdentity -AppDisplayName "EntraOps Reporting" -CreateFederatedCredential -GitHubOrg "Cloud-Architekt" -GitHubRepo "EntraOps-TenantName" -FederatedEntityType "Branch" -FederatedEntityName "main"
 #>


function New-EntraOpsWorkloadIdentity {

    [CmdletBinding()]
    param (
        [parameter(Mandatory = $True)]
        [string]$AppDisplayName,

        [Parameter(Mandatory = $False)]
        [string]$ExistingSpObjectId,

        [Parameter(Mandatory = $False)]
        [string]$ConfigFile = "$EntraOpsBasefolder/EntraOpsConfig.json",

        [Parameter(Mandatory = $False)]
        [switch]$CreateFederatedCredential,

        [Parameter(Mandatory = $False)]
        [string]$GitHubOrg,

        [Parameter(Mandatory = $False)]
        [string]$GitHubRepo,

        [Parameter(Mandatory = $False)]
        [ValidateSet("Branch", "Environment")]
        [string]$FederatedEntityType = "Branch",

        [Parameter(Mandatory = $False)]
        [string]$FederatedEntityName = "main",

        # Assign Reader at the ARM tenant root scope ("/") in addition to the root management group.
        # Opt-in because a "/"-scoped assignment is only creatable after elevateAccess, is not shown in the
        # portal RBAC blades, and is therefore routinely missed in access reviews and offboarding. It is only
        # needed to enumerate role assignments made directly at "/"; the root management group assignment
        # already covers every management group, subscription and resource below it.
        [Parameter(Mandatory = $False)]
        [switch]$GrantArmRootScopeReader
    )

    $ErrorActionPreference = "Stop"
    $ProvisioningFailures = [System.Collections.Generic.List[string]]::new()

    function Add-EntraOpsProvisioningFailure {
        param ([Parameter(Mandatory = $true)][string]$Message)

        $ProvisioningFailures.Add($Message)
        Write-Warning $Message
    }

    # Load configuration file
    $Config = Get-Content -Path $ConfigFile | ConvertFrom-Json
    if ($CreateFederatedCredential) {
        $GitHubOrg = $GitHubOrg.Trim()
        $GitHubRepo = $GitHubRepo.Trim()
        $FederatedEntityName = $FederatedEntityName.Trim()
        if ([string]::IsNullOrWhiteSpace($GitHubOrg) -or [string]::IsNullOrWhiteSpace($GitHubRepo) -or [string]::IsNullOrWhiteSpace($FederatedEntityName)) {
            throw "GitHubOrg, GitHubRepo, and FederatedEntityName must contain non-whitespace values when CreateFederatedCredential is specified."
        }
    }

    #region Import module and check connection to Graph and Azure Resource Manager API
    # Check if required Graph module is available
    Install-EntraOpsRequiredModule -ModuleName Microsoft.Graph.Applications

    # Connect to Graph
    Write-Host "Connect to Microsoft Graph..."
    $GraphScopes = @(
        "AdministrativeUnit.ReadWrite.All",
        "Application.ReadWrite.All",
        "AppRoleAssignment.ReadWrite.All",
        "RoleManagement.ReadWrite.Directory"
    )
    Connect-MgGraph -Scopes $GraphScopes -TenantId $Config.TenantId

    Write-Host "Connect to Azure..."
    $AzContext = Get-AzContext
    if ($AzContext.Tenant.Id -ne $Config.TenantId) {
        Connect-AzAccount -Tenant $Config.TenantId
    }
    #endregion

    #region Create or update existing App Registration
    if ($ExistingSpObjectId) {
        Write-Verbose "Get details of existing Service Principal with ObjectId $ExistingSpObjectId..."
        try {
            $SpObject = Get-MgServicePrincipal -ServicePrincipalId $ExistingSpObjectId
            if ($CreateFederatedCredential) {
                $MatchingApplications = @(Get-MgApplication -Filter "appId eq '$($SpObject.AppId)'")
                if ($MatchingApplications.Count -ne 1) {
                    throw "Expected exactly one application for service principal '$ExistingSpObjectId' with appId '$($SpObject.AppId)', but found $($MatchingApplications.Count)."
                }
                $AppObject = $MatchingApplications[0]
            }
        } catch {
            Write-Error "Failed to get Service Principal with ObjectId $ExistingSpObjectId. Error: $_"
        }
    } else {
        # Create App Registration
        Write-Output "Create App Registration $AppDisplayName..."
        try {
            $AppObject = New-MgApplication -DisplayName $AppDisplayName -SignInAudience AzureADMyOrg
        } catch {
            Write-Error "Failed to create $AppDisplayName. Error: $_"
        }

        # Short delay before contiune and wait sync
        Write-Verbose "Wait 3 seconds before create Service Principal from App Registration..."
        Start-Sleep 3

        # Create Service Principal
        Write-Verbose "Create Service Principal from $AppDisplayName $($AppObject.Id)..."
        try {
            $SpObject = New-MgServicePrincipal -DisplayName $AppDisplayName -AppId $AppObject.AppId
        } catch {
            Write-Error "Failed to create Service Principal for $AppDisplayName. Error: $_"
        }
        #endregion
    }

    #region Add required Microsoft Graph API Permissions for Pull (Read) Operations

    # Get Graph API App Roles to map required App Role Names to App Role IDs
    Write-Verbose "Get Microsoft Graph API App Roles..."
    $MsGraph = Get-MgServicePrincipal -Filter "AppId eq '00000003-0000-0000-c000-000000000000'"

    try {
        $ExistingGraphAppRoleAssignments = [System.Collections.Generic.List[object]]::new()
        foreach ($Assignment in @(Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $SpObject.Id -All)) {
            $ExistingGraphAppRoleAssignments.Add($Assignment)
        }
    } catch {
        throw "Failed to read existing Microsoft Graph application permission assignments for $AppDisplayName. No permissions were changed. Error: $($_.Exception.Message)"
    }

    function Add-EntraOpsGraphApplicationPermissions {
        param (
            [Parameter(Mandatory = $true)][string[]]$PermissionNames,
            [Parameter(Mandatory = $true)][string]$Purpose
        )

        foreach ($PermissionName in $PermissionNames) {
            $MatchingRoles = @($MsGraph.AppRoles | Where-Object { $_.Value -eq $PermissionName })
            if ($MatchingRoles.Count -ne 1) {
                Add-EntraOpsProvisioningFailure -Message "Could not resolve the Microsoft Graph application permission '$PermissionName' for $Purpose."
                continue
            }

            $GraphApiPermission = $MatchingRoles[0]
            $ExistingAssignment = $ExistingGraphAppRoleAssignments | Where-Object {
                $_.ResourceId -eq $MsGraph.Id -and $_.AppRoleId -eq $GraphApiPermission.Id
            } | Select-Object -First 1
            if ($ExistingAssignment) {
                Write-Host "- Microsoft Graph API Permission $PermissionName is already assigned"
                continue
            }

            Write-Host "- Adding $($GraphApiPermission.Origin) API Permission $PermissionName"
            try {
                $NewAssignment = New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $SpObject.Id -PrincipalId $SpObject.Id -ResourceId $MsGraph.Id -AppRoleId $GraphApiPermission.Id
                $ExistingGraphAppRoleAssignments.Add($NewAssignment)
            } catch {
                Add-EntraOpsProvisioningFailure -Message "Failed to add Microsoft Graph API Permission '$PermissionName' to $AppDisplayName for $Purpose. Error: $($_.Exception.Message)"
            }
        }
    }

    # Graph API permissions for Pull operations
    $PullPermissionsToAdd = @(
        "AdministrativeUnit.Read.All",
        "Application.Read.All",
        "CustomSecAttributeAssignment.Read.All",
        "DeviceManagementConfiguration.Read.All",
        "DeviceManagementManagedDevices.Read.All",
        "DeviceManagementRBAC.Read.All",
        "DeviceManagementServiceConfig.Read.All",
        "Directory.Read.All",
        "DirectoryRecommendations.Read.All",
        "EntitlementManagement.Read.All",
        "Group.Read.All",
        "RemoteTenantGroups.Read.All",
        "PrivilegedAccess.Read.AzureADGroup",
        "PrivilegedEligibilitySchedule.Read.AzureADGroup",
        "Policy.Read.All",
        "RoleManagement.Read.All",  
        "TenantGovernance-Relationship.Read.All",  
        "ThreatHunting.Read.All",
        "User.Read.All",
        "Zone.Read.All"
    )

    Write-Output "Adding Pull permissions..."
    Add-EntraOpsGraphApplicationPermissions -PermissionNames $PullPermissionsToAdd -Purpose "Pull operations"
    #endregion

    #region Add required Microsoft Graph API Permissions for Push (Change) Operations

    # Graph API permissions for Push operations
    if ($Config.AutomatedAdministrativeUnitManagement.ApplyAdministrativeUnitAssignments -eq $true -or $Config.AutomatedRmauAssignmentsForUnprotectedObjects.ApplyRmauAssignmentsForUnprotectedObjects -eq $true) {
        $PushPermissionsToAdd = @(
            "AdministrativeUnit.ReadWrite.All"
        )
    
        Write-Output "Adding Push permissions..."
        Add-EntraOpsGraphApplicationPermissions -PermissionNames $PushPermissionsToAdd -Purpose "Push operations"
    } else {
        Write-Output "Skipping Push permissions... (ApplyAdministrativeUnitAssignments and/or ApplyRmauAssignmentsForUnprotectedObjects is set to false)"
    }

    # Graph API permission for updating Entitlement Management catalogs to privileged catalogs
    # Caution: Elevated permission, only assigned if ApplyPrivilegedElmCatalogProtection is enabled in the config file
    # Required permission reference: https://learn.microsoft.com/en-us/graph/api/entitlementmanagement-update?view=graph-rest-beta
    if ($Config.AutomatedElmCatalogProtection.ApplyPrivilegedElmCatalogProtection -eq $true) {
        $ElmPushPermissionsToAdd = @(
            "EntitlementManagement.ReadWrite.All"
        )

        Write-Output "Adding Push permissions for privileged ELM catalog protection..."
        Write-Warning "EntitlementManagement.ReadWrite.All is an elevated permission and should be used with caution."
        Add-EntraOpsGraphApplicationPermissions -PermissionNames $ElmPushPermissionsToAdd -Purpose "privileged ELM catalog protection"
    } else {
        Write-Output "Skipping Push permissions for privileged ELM catalog protection... (ApplyPrivilegedElmCatalogProtection is set to false)"
    }

    #endregion

    #region Add required Microsoft Graph API permission for Tenant Governance Snapshot (UTCM)
    # ConfigurationMonitoring.ReadWrite.All is Microsoft's documented least-privileged permission for
    # the createSnapshot API (required for both delegated and application auth - there is no
    # Read.All-only option for creating a snapshot job, only for reading/listing existing ones).
    # Reference: https://learn.microsoft.com/en-us/graph/api/configurationbaseline-createsnapshot
    if ($Config.TenantGovernanceSnapshot.EnableTenantGovernanceSnapshot -eq $true) {
        $TenantGovernancePermissionsToAdd = @(
            "ConfigurationMonitoring.ReadWrite.All"
        )

        Write-Output "Adding Tenant Governance Snapshot permissions..."
        Add-EntraOpsGraphApplicationPermissions -PermissionNames $TenantGovernancePermissionsToAdd -Purpose "Tenant Governance snapshots"
    } else {
        Write-Output "Skipping Tenant Governance Snapshot permissions... (EnableTenantGovernanceSnapshot is set to false)"
    }
    #endregion

    #region Configure Microsoft Tenant Configuration Management (UTCM) service principal permissions
    # The Tenant Governance Snapshot feature relies on the first-party "Microsoft Tenant Configuration
    # Management" service principal to read the configured Microsoft Entra resources on EntraOps'
    # behalf. Delegate the creation and least-privileged permission assignment to the dedicated
    # configuration cmdlet so it can also be run standalone (e.g. after extending
    # TenantGovernanceSnapshot.ResourcesToInclude with new resource types) without re-running the
    # whole workload identity setup.
    # Reference: https://learn.microsoft.com/en-us/graph/utcm-entra-resources
    if ($Config.TenantGovernanceSnapshot.EnableTenantGovernanceSnapshot -eq $true) {
        try {
            Register-EntraOpsTenantGovernanceServicePrincipal -ResourcesToInclude $Config.TenantGovernanceSnapshot.ResourcesToInclude -TenantId $Config.TenantId | Out-Null
        } catch {
            Add-EntraOpsProvisioningFailure -Message "Failed to configure the Microsoft Tenant Configuration Management service principal. Error: $($_.Exception.Message)"
        }
    } else {
        Write-Output "Skipping Microsoft Tenant Configuration Management (UTCM) service principal setup... (EnableTenantGovernanceSnapshot is set to false)"
    }
    #endregion

    #region Add required Microsoft Entra ID (scoped) directory role for managing Conditional Access Target Groups
    if ($Config.AutomatedConditionalAccessTargetGroups.ApplyConditionalAccessTargetGroups -eq $true) {
        $AdminUnitName = $Config.AutomatedConditionalAccessTargetGroups.AdminUnitName
        $AdminUnits = @(Invoke-EntraOpsMsGraphQuery -Method "GET" -Uri "/beta/administrativeUnits?`$filter=DisplayName eq '$(ConvertTo-EntraOpsODataStringLiteral -Value $AdminUnitName)'" -OutputType PSObject -DisableCache)
        # -AllowNotFound: a missing AU is the expected first-run state - the create branch below handles it.
        $AdminUnitId = (Select-EntraOpsUniqueGraphObject -InputObject $AdminUnits -ObjectDescription "administrative unit '$AdminUnitName'" -AllowNotFound).id
        #region Create Administrative Unit if it does not exist
        if (-not $AdminUnitId) {
            Write-Host "Creating Administrative Unit $($AdminUnitName)"

            $AuParams = @{
                DisplayName                  = $AdminUnitName
                Description                  = "This administrative unit contains groups for Conditional Access Targeting"
                isMemberManagementRestricted = $true
            }

            $Body = $AuParams | ConvertTo-Json -Depth 10
            try {
                $NewAdminUnitId = (Invoke-MgGraphRequest -Method "POST" -Body $Body -Uri "https://graph.microsoft.com/beta/administrativeUnits").id
            } catch {
                Add-EntraOpsProvisioningFailure -Message "Cannot create required Administrative Unit '$AdminUnitName'. Error: $($_.Exception.Message)"
            }

            # Check if AU has been created successfully, wait for delay and retry if not available yet
            Try {
                Do { Start-Sleep -Seconds 1 }
                Until ($AdminUnitId = (Invoke-EntraOpsMsGraphQuery -Method "GET" -Uri "/beta/administrativeUnits/$($NewAdminUnitId)" -DisableCache).Id)
                Write-Host "$($AdminUnitName) - $($AdminUnitId) has been created successfully" -f Green
            } Catch {
                Add-EntraOpsProvisioningFailure -Message "Required Administrative Unit '$AdminUnitName' is not available after creation. Error: $($_.Exception.Message)"
            }
        } else {
            Write-Host "Administrative Unit $($AdminUnitName) - $($AdminUnitId) already exists"
        }

        # Add scoped permissions as Group Administrator to the Administrative Unit
        $ScopedGroupAdminRoleParams = @{
            '@odata.type'    = "#microsoft.graph.unifiedRoleAssignment"
            principalId      = $($SpObject.Id)
            roleDefinitionId = "fdd7a751-b60b-444a-984c-02652fe8fa1c" # Group Administrator
            directoryScopeId = "/administrativeUnits/$($AdminUnitId)"
        }

        try {
            $ExistingScopedRoleAssignments = @(Invoke-EntraOpsMsGraphQuery -Method "GET" -Uri "/beta/roleManagement/directory/roleAssignments?`$filter=principalId eq '$($SpObject.Id)'" -OutputType PSObject -DisableCache)
            $ExistingScopedGroupAdminRole = $ExistingScopedRoleAssignments | Where-Object {
                $_.roleDefinitionId -eq $ScopedGroupAdminRoleParams.roleDefinitionId -and
                $_.directoryScopeId -eq $ScopedGroupAdminRoleParams.directoryScopeId
            } | Select-Object -First 1

            if ($ExistingScopedGroupAdminRole) {
                Write-Host "The scoped Group Administrator role on Administrative Unit $AdminUnitName is already assigned to $($SpObject.Id)." -f Green
            } else {
                $Body = $ScopedGroupAdminRoleParams | ConvertTo-Json -Depth 10
                $DirectoryRoleAssignmentId = (Invoke-MgGraphRequest -Method "POST" -Body $Body -Uri "https://graph.microsoft.com/beta/roleManagement/directory/roleAssignments").id
                Write-Host "Assigned permissions to Administrative Unit $($AdminUnitName) for $($SpObject.Id) - $($DirectoryRoleAssignmentId)" -f Green
            }
        } catch {
            Add-EntraOpsProvisioningFailure -Message "Cannot verify or assign the required scoped Group Administrator role on Administrative Unit '$AdminUnitName' to '$($SpObject.Id)'. Error: $($_.Exception.Message)"
        }

    } else {
        Write-Output "Skipping permissions to manage Conditional Access Groups... (ApplyConditionalAccessTargetGroups is set to false)"
    }    
    #endregion

    #region Add required role assignments in Azure RBAC
    function Add-EntraOpsAzureRoleAssignment {
        param (
            [Parameter(Mandatory = $true)][string]$RoleDefinitionName,
            [Parameter(Mandatory = $true)][string]$Scope,
            [Parameter(Mandatory = $true)][string]$ObjectId,
            [string]$ApplicationId,
            [Parameter(Mandatory = $true)][string]$Purpose
        )

        try {
            $ExistingAssignments = @(Get-AzRoleAssignment -ObjectId $ObjectId -RoleDefinitionName $RoleDefinitionName -Scope $Scope -ErrorAction Stop)
        } catch {
            Add-EntraOpsProvisioningFailure -Message "Failed to check the existing '$RoleDefinitionName' Azure role assignment at '$Scope' for $Purpose. Error: $($_.Exception.Message)"
            return
        }

        if ($ExistingAssignments.Count -gt 0) {
            Write-Output "The '$RoleDefinitionName' role at '$Scope' is already assigned for $Purpose."
            return
        }

        try {
            if ([string]::IsNullOrWhiteSpace($ApplicationId)) {
                New-AzRoleAssignment -ObjectId $ObjectId -RoleDefinitionName $RoleDefinitionName -Scope $Scope | Out-Null
            } else {
                New-AzRoleAssignment -ApplicationId $ApplicationId -RoleDefinitionName $RoleDefinitionName -Scope $Scope | Out-Null
            }
        } catch {
            Add-EntraOpsProvisioningFailure -Message "Failed to assign the '$RoleDefinitionName' Azure role at '$Scope' for $Purpose. Error: $($_.Exception.Message)"
        }
    }

    # Logic to add required role assignment in Azure RBAC
    function Add-AzureRolePermissions ($RoleDefinitionName, $ResourceGroupName, $SubscriptionId) {
        if (!$RoleDefinitionName -or !$ResourceGroupName -or !$SubscriptionId) {
            Add-EntraOpsProvisioningFailure -Message "Resource group name, subscription ID, and role definition name must be configured before assigning Azure permissions for Push operations."
        } else {
            try {
                Set-AzContext -SubscriptionId $SubscriptionId | Out-Null
                $ResourceGroup = Get-AzResourceGroup -Name $ResourceGroupName
            } catch {
                Add-EntraOpsProvisioningFailure -Message "Failed to resolve Azure resource group '$ResourceGroupName' in subscription '$SubscriptionId' for the '$RoleDefinitionName' assignment. Error: $($_.Exception.Message)"
                return
            }

            Add-EntraOpsAzureRoleAssignment -RoleDefinitionName $RoleDefinitionName -Scope $ResourceGroup.ResourceId -ObjectId $SpObject.Id -Purpose "resource group '$ResourceGroupName'"
        }
    }
    Start-Sleep 5 # Wait for adding permission to new created Service Principal

    if ($Config.LogAnalytics.IngestToLogAnalytics -eq $true) {
        try {
            Write-Output "Adding permissions to Resource Group of Data Collection Rule on $($Config.LogAnalytics.DataCollectionResourceGroupName)..."
            Add-AzureRolePermissions -RoleDefinitionName "Monitoring Metrics Publisher" -ResourceGroupName $Config.LogAnalytics.DataCollectionResourceGroupName -SubscriptionId $Config.LogAnalytics.DataCollectionRuleSubscriptionId
            Add-AzureRolePermissions -RoleDefinitionName "Reader" -ResourceGroupName $Config.LogAnalytics.DataCollectionResourceGroupName -SubscriptionId $Config.LogAnalytics.DataCollectionRuleSubscriptionId    
        } catch {
            Add-EntraOpsProvisioningFailure -Message "Failed to assign required roles on Log Analytics resource group '$($Config.LogAnalytics.DataCollectionResourceGroupName)'. Error: $($_.Exception.Message)"
        }
    }

    if ($Config.SentinelWatchLists.IngestToWatchLists -eq $true) {
        try {
            Write-Output "Adding permissions to Resource Group of Sentinel Workspace on $($Config.SentinelWatchLists.SentinelResourceGroupName)..."
            Add-AzureRolePermissions -RoleDefinitionName "Microsoft Sentinel Contributor" -ResourceGroupName $Config.SentinelWatchLists.SentinelResourceGroupName -SubscriptionId $Config.SentinelWatchLists.SentinelSubscriptionId    
        } catch {
            Add-EntraOpsProvisioningFailure -Message "Failed to assign required roles on Microsoft Sentinel resource group '$($Config.SentinelWatchLists.SentinelResourceGroupName)'. Error: $($_.Exception.Message)"
        }
    }

    # Azure collection and Azure Resource Graph-backed features require Reader on the tenant root
    # management group so all management groups, subscriptions and resources below it are visible.
    if (($Config.RbacSystems -contains "Azure") `
            -or ($Config.AutomatedControlPlaneScopeUpdate.ApplyAutomatedControlPlaneScopeUpdate -eq $true -and $Config.AutomatedControlPlaneScopeUpdate.PrivilegedObjectClassificationSource -contains "PrivilegedRolesFromAzGraph") `
            -or ($Config.SentinelWatchLists.WatchListTemplates -contains "HighValueAssets") -or ($Config.SentinelWatchLists.WatchListWorkloadIdentity -contains "WorkloadIdentityAttackPaths") -or ($Config.SentinelWatchLists.WatchListWorkloadIdentity -contains "ManagedIdentityAssignedResourceId")) {
        Write-Output "Adding permissions as Reader on Tenant Root Group for analyzing RBAC and/or Managed Identity resources..."
        Add-EntraOpsAzureRoleAssignment -RoleDefinitionName "Reader" -Scope "/providers/Microsoft.Management/managementGroups/$($Config.TenantId)" -ObjectId $SpObject.Id -Purpose "Azure collection and Resource Graph analysis"
    }

    # Add required permissions as Reader on the ARM tenant root scope ("/") when Azure is included as
    # RBAC system, to allow scanning of Azure RBAC role assignments (incl. elevateAccess-scoped
    # assignments) across the entire tenant hierarchy.
    if ($Config.RbacSystems -contains "Azure") {
        if ($GrantArmRootScopeReader) {
            Write-Warning "Assigning Reader at the ARM tenant root scope (/). This assignment is only visible via 'az role assignment list --scope /' and is NOT shown in the portal RBAC blades - include it in access reviews and offboarding, and remove it with 'az role assignment delete --scope /' when decommissioning EntraOps."
            Write-Output "Adding permissions as Reader on ARM tenant root scope (/) for scanning Azure RBAC role assignments..."
            $WorkloadIdentityAppId = if ($AppObject) { $AppObject.AppId } else { $SpObject.AppId }
            Add-EntraOpsAzureRoleAssignment -RoleDefinitionName "Reader" -Scope "/" -ObjectId $SpObject.Id -ApplicationId $WorkloadIdentityAppId -Purpose "ARM tenant-root role-assignment discovery"
        } else {
            Write-Output "Skipping Reader assignment on ARM tenant root scope (/). Azure RBAC assignments made directly at '/' (after elevateAccess) will not be visible to EntraOps; everything under the root management group is still covered. Re-run with -GrantArmRootScopeReader to enable tenant-root visibility."
        }
    }
    #endregion

    #region Add ClientId to environment file
    Write-Output "Write $AppDisplayName AppId to environment file $($ConfigFile)..."
    $Config.ClientId = if ($AppObject) { $AppObject.AppId } else { $SpObject.AppId }
    $Config | ConvertTo-Json -Depth 10 | Set-Content -Path $ConfigFile
    #endregion

    #region Add Federated Credential to Application object
    if ($Config.AuthenticationType -eq "FederatedCredentials" -and $CreateFederatedCredential) {
        if ($Config.DevOpsPlatform -eq "GitHub") {
            Write-Output "Add Federated Credential to $AppDisplayName..."

            switch ($FederatedEntityType) {
                Branch {
                    $Entity = "ref:refs/heads/$($FederatedEntityName)"
                }
                Environment {
                    $Entity = "environment:$($FederatedEntityName)"
                }
            }

            $FederatedCredentialParam = @{
                name      = "$($GitHubRepo)-$($FederatedEntityType)-$($FederatedEntityName)"
                issuer    = "https://token.actions.githubusercontent.com"
                subject   = "repo:$($GitHubOrg)/$($GitHubRepo):$($Entity)"
                audiences = @(
                    "api://AzureADTokenExchange"
                )
            }

            $ExistingFederatedCredentials = @()
            $CanCreateFederatedCredential = $true
            try {
                $ExistingFederatedCredentials = @(Get-MgApplicationFederatedIdentityCredential -ApplicationId $AppObject.Id -All)
            } catch {
                $CanCreateFederatedCredential = $false
                Add-EntraOpsProvisioningFailure -Message "Failed to read existing federated credentials for $AppDisplayName. Error: $($_.Exception.Message)"
            }

            if ($CanCreateFederatedCredential) {
                $ExistingFederatedCredential = $ExistingFederatedCredentials | Where-Object { $_.Name -eq $FederatedCredentialParam.name } | Select-Object -First 1
                if ($ExistingFederatedCredential) {
                    $AudienceDifference = @(Compare-Object -ReferenceObject @($FederatedCredentialParam.audiences) -DifferenceObject @($ExistingFederatedCredential.Audiences))
                    if ($ExistingFederatedCredential.Issuer -ceq $FederatedCredentialParam.issuer -and
                        $ExistingFederatedCredential.Subject -ceq $FederatedCredentialParam.subject -and
                        $AudienceDifference.Count -eq 0) {
                        Write-Output "Federated Credential '$($FederatedCredentialParam.name)' is already configured."
                    } else {
                        Add-EntraOpsProvisioningFailure -Message "Federated Credential '$($FederatedCredentialParam.name)' already exists but its issuer, subject, or audience does not match the requested GitHub identity. Remove or correct the existing credential before rerunning setup."
                    }
                } else {
                    try {
                        New-MgApplicationFederatedIdentityCredential -ApplicationId $AppObject.Id -BodyParameter $FederatedCredentialParam | Out-Null
                    } catch {
                        Add-EntraOpsProvisioningFailure -Message "Failed to add Federated Credential '$($FederatedCredentialParam.name)' to $AppDisplayName. Error: $($_.Exception.Message)"
                    }
                }
            }
        } else {
            Write-Warning "Automation configuration of federated credential for DevOps Platform $($Config.DevOpsPlatform) is not implemented yet."
        }
    } else {
        Write-Verbose "Skipping Federated Credential configuration... (AuthenticationType is not Federated)"
    }
    #endregion

    if ($ProvisioningFailures.Count -gt 0) {
        $FailureSummary = ($ProvisioningFailures | ForEach-Object { "- $_" }) -join [Environment]::NewLine
        throw "Workload identity provisioning did not complete successfully for service principal '$($SpObject.Id)'. Correct the following issue(s), then rerun the command with -ExistingSpObjectId '$($SpObject.Id)':$([Environment]::NewLine)$FailureSummary"
    }
}

# SIG # Begin signature block
# MIIniAYJKoZIhvcNAQcCoIIneTCCJ3UCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBbSIXbpSjrM6B8
# jPiF5Hi0QYMS1w5QYc8LAQM+lkBilqCCIDwwggXJMIIEsaADAgECAhAbtY8lKt8j
# AEkoya49fu0nMA0GCSqGSIb3DQEBDAUAMH4xCzAJBgNVBAYTAlBMMSIwIAYDVQQK
# ExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2Vy
# dGlmaWNhdGlvbiBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBUcnVzdGVkIE5l
# dHdvcmsgQ0EwHhcNMjEwNTMxMDY0MzA2WhcNMjkwOTE3MDY0MzA2WjCBgDELMAkG
# A1UEBhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMuQS4xJzAl
# BgNVBAsTHkNlcnR1bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIGA1UEAxMb
# Q2VydHVtIFRydXN0ZWQgTmV0d29yayBDQSAyMIICIjANBgkqhkiG9w0BAQEFAAOC
# Ag8AMIICCgKCAgEAvfl4+ObVgAxknYYblmRnPyI6HnUBfe/7XGeMycxca6mR5rlC
# 5SBLm9qbe7mZXdmbgEvXhEArJ9PoujC7Pgkap0mV7ytAJMKXx6fumyXvqAoAl4Va
# qp3cKcniNQfrcE1K1sGzVrihQTib0fsxf4/gX+GxPw+OFklg1waNGPmqJhCrKtPQ
# 0WeNG0a+RzDVLnLRxWPa52N5RH5LYySJhi40PylMUosqp8DikSiJucBb+R3Z5yet
# /5oCl8HGUJKbAiy9qbk0WQq/hEr/3/6zn+vZnuCYI+yma3cWKtvMrTscpIfcRnNe
# GWJoRVfkkIJCu0LW8GHgwaM9ZqNd9BjuiMmNF0UpmTJ1AjHuKSbIawLmtWJFfzcV
# WiNoidQ+3k4nsPBADLxNF8tNorMe0AZa3faTz1d1mfX6hhpneLO/lv403L3nUlbl
# s+V1e9dBkQXcXWnjlQ1DufyDljmVe2yAWk8TcsbXfSl6RLpSpCrVQUYJIP4ioLZb
# MI28iQzV13D4h1L92u+sUS4Hs07+0AnacO+Y+lbmbdu1V0vc5SwlFcieLnhO+Nqc
# noYsylfzGuXIkosagpZ6w7xQEmnYDlpGizrrJvojybawgb5CAKT41v4wLsfSRvbl
# jnX98sy50IdbzAYQYLuDNbdeZ95H7JlI8aShFf6tjGKOOVVPORa5sWOd/7cCAwEA
# AaOCAT4wggE6MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFLahVDkCw6A/joq8
# +tT4HKbROg79MB8GA1UdIwQYMBaAFAh2zcsH/yT2xc3tu5C84oQ3RnX3MA4GA1Ud
# DwEB/wQEAwIBBjAvBgNVHR8EKDAmMCSgIqAghh5odHRwOi8vY3JsLmNlcnR1bS5w
# bC9jdG5jYS5jcmwwawYIKwYBBQUHAQEEXzBdMCgGCCsGAQUFBzABhhxodHRwOi8v
# c3ViY2Eub2NzcC1jZXJ0dW0uY29tMDEGCCsGAQUFBzAChiVodHRwOi8vcmVwb3Np
# dG9yeS5jZXJ0dW0ucGwvY3RuY2EuY2VyMDkGA1UdIAQyMDAwLgYEVR0gADAmMCQG
# CCsGAQUFBwIBFhhodHRwOi8vd3d3LmNlcnR1bS5wbC9DUFMwDQYJKoZIhvcNAQEM
# BQADggEBAFHCoVgWIhCL/IYx1MIy01z4S6Ivaj5N+KsIHu3V6PrnCA3st8YeDrJ1
# BXqxC/rXdGoABh+kzqrya33YEcARCNQOTWHFOqj6seHjmOriY/1B9ZN9DbxdkjuR
# mmW60F9MvkyNaAMQFtXx0ASKhTP5N+dbLiZpQjy6zbzUeulNndrnQ/tjUoCFBMQl
# lVXwfqefAcVbKPjgzoZwpic7Ofs4LphTZSJ1Ldf23SIikZbr3WjtP6MZl9M7JYjs
# NhI9qX7OAo0FmpKnJ25FspxihjcNpDOO16hO0EoXQ0zF8ads0h5YbBRRfopUofbv
# n3l6XYGaFpAP4bvxSgD5+d2+7arszgowggZrMIIEU6ADAgECAhBX5gfP8LabvnxC
# XoItEBa/MA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNVBAYTAlBMMSEwHwYDVQQKExhB
# c3NlY28gRGF0YSBTeXN0ZW1zIFMuQS4xJDAiBgNVBAMTG0NlcnR1bSBDb2RlIFNp
# Z25pbmcgMjAyMSBDQTAeFw0yNjEwMDYwODI4MjNaFw0yNzEwMDYwODI4MjJaMIGQ
# MQswCQYDVQQGEwJERTEdMBsGA1UECAwUUmhpbmVsYW5kLVBhbGF0aW5hdGUxEjAQ
# BgNVBAcMCUxhaG5zdGVpbjEeMBwGA1UECgwVT3BlbiBTb3VyY2UgRGV2ZWxvcGVy
# MS4wLAYDVQQDDCVPcGVuIFNvdXJjZSBEZXZlbG9wZXIgVGhvbWFzIE5hdW5oZWlt
# MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAphp2ljSmVsRB6gvkEq7A
# DUMENpKZiDFoAfF1ktD1nCwaVyI+bJ0ROjHwAn4iJgS+vlBDPOUIw2GLHkiqUhOg
# HSwzg6gj45GNmt6zxsKP0zPlhMB/fb0zmz7/F7e3yfKCXLfhbHuk0Es9gJ+8Wmet
# 9StVwwSE/K0MpW06GKA9nPWyE4N/ADwigDIj9rsTKJR4cve9w8G1b3zohpUc/qMr
# onab+HVw5r9b3sOBnnaXC6zvrX4fovRz1ztwfWjRjILLoFG6mW/TuK5F2ICD3Rhl
# 9R2rJWWWhzTDMsQlzvX+/SaM74q3OFpIVNdhvtmWU9pKUBRAyYMijcDqKEFGSzrc
# 8JFmGMpP5TuroWF+N21d3px2eARvW1mV0DCfFAH+Og20SH/gVW6/ltQQEkH/z/Zt
# 49ARyJrgESLOQtlMCIYSoKISdkk4ml4VUS1E/PcbzXEYj3a7m6DOxcd3WuY03UT9
# cnk3p0LMN6LeLYAZgVY+HRloZlH+kW2SkOHNuWgtXvZhAgMBAAGjggF4MIIBdDAM
# BgNVHRMBAf8EAjAAMD0GA1UdHwQ2MDQwMqAwoC6GLGh0dHA6Ly9jY3NjYTIwMjEu
# Y3JsLmNlcnR1bS5wbC9jY3NjYTIwMjEuY3JsMHMGCCsGAQUFBwEBBGcwZTAsBggr
# BgEFBQcwAYYgaHR0cDovL2Njc2NhMjAyMS5vY3NwLWNlcnR1bS5jb20wNQYIKwYB
# BQUHMAKGKWh0dHA6Ly9yZXBvc2l0b3J5LmNlcnR1bS5wbC9jY3NjYTIwMjEuY2Vy
# MB8GA1UdIwQYMBaAFN10XUwA23ufoHTKsW73PMAywHDNMB0GA1UdDgQWBBTBvGyH
# MXj1dMbVnxuof9oP4PY7cDBLBgNVHSAERDBCMAgGBmeBDAEEATA2BgsqhGgBhvZ3
# AgUBBDAnMCUGCCsGAQUFBwIBFhlodHRwczovL3d3dy5jZXJ0dW0ucGwvQ1BTMBMG
# A1UdJQQMMAoGCCsGAQUFBwMDMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQsF
# AAOCAgEAkC165c7/r6y3VeEEpNXas9bQs6ZpKPdJffHkf9xoPi4kWvAIWGc4ocG1
# DJLb18RXiDYShDmT5sb/6I08FeipKi2DNJkuOq6yOTJ3S7+hNFIZCyd6i7WKO8ea
# P5I+FAbyJP4+5coZoMg45bUa9JoocaJCm+5n7WiCfN7/EQpClW5Trm4+ZMbp7EMN
# F1uLJgGCMnN0SsoUarp7PHlqupMxMC4Uhhe+lAP2ytcSIU81L97/qjrADbLWqICk
# 9il5iGM+aZndguq/cmfH9rJj4GRZHWnG6WwnzYD9+KgYlRI6em+/9dq+IRiXe2Bs
# thUuIaFW7U2gWw8tBTEYm1UBLHdJx+L1gjJCLs5KT/wy5wU32wZxNDqPUQejUC1x
# GvU3cKqmLgRq+yNf3gFVfzqIoggyM3U9wQvnk7nBtCkpjXS2hdx4yxzcmrlWsDHr
# z6+6/jeyQySzbYUlFmKnc4n9Mw2soHOajEua5poFARVRS5KgUBuiiQ21YFNYzWBo
# qGuECaaAI/C5rGxapMJ3dAOBnHw6NLqBet+ZXBJEccX5y89MLlOQu+jDmJP8HieR
# qDLlwfhVZnw4RY8Rus43NZBwhNlVuOUXqocHKmJhKNlekda3qsJj93eQBQOxKJcc
# w5O08q9jukpH2+yzFAj+7ll33DuNUgWptogc4Awtbx3glzcEvJQwggaCMIIEaqAD
# AgECAhAo8HfBHDa9/l90MkdwJy4DMA0GCSqGSIb3DQEBDAUAMFYxCzAJBgNVBAYT
# AlBMMSEwHwYDVQQKExhBc3NlY28gRGF0YSBTeXN0ZW1zIFMuQS4xJDAiBgNVBAMT
# G0NlcnR1bSBUaW1lc3RhbXBpbmcgMjAyMSBDQTAeFw0yNjAzMTEwNzM0NTRaFw0z
# NjAyMjcwNzM0NTRaMFAxCzAJBgNVBAYTAlBMMSEwHwYDVQQKDBhBc3NlY28gRGF0
# YSBTeXN0ZW1zIFMuQS4xHjAcBgNVBAMMFUNlcnR1bSBUaW1lc3RhbXAgMjAyNjCC
# AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALi9zfBw+xI12hi2F9uNQBNO
# STkTIi13fuh3DiWybI5392bBfJw6tO7zDs7UqP/rejVf4xFTU3XNOPsuOFkP5SPF
# snWwn3VJL+NhNhsuujvYsQtaQnvNo1wk46reTgws1L8OFEdKaxgyqT4zOQx81mLw
# NZSqXNU9Rrb9oAJWJF4Mydz+C1ebu0D0+vgM3tqAQ8TU203fqgKCPqIdHloaFqH9
# eyR+8tnpmy2hnnN59ZOtmsuHeRwf5iTOhlhf0d/qqquLOSIGPgGVL1Hj5XBUox2O
# E7tQRr+l/xxNbL6OuwBO4Aa2EbuxYg37cpXv5i2/lTL4NASB/duf9W4fjA3Ro+GP
# +bC54n2OYiM3Cq8z92ejx8F5HWO2e4/75eC95B9tMw/eq7109GDyW2+2ut5Bn0o8
# MXJTERkyystWbqcnUkomA97qfiDCwvpWuOMdfTOksXe9S+xDxOJaCbiyTivgPulx
# cT+tfQTRXBr5UwtPvc+INnafOEilzVHA11tv+pHcylryxBc08cUihf/ttqkZS0bm
# x3VxQay8uohaTp0//ECA9vYGW51SKdJMJhXvKyIdBLy8WYNchTt8yRITv9Adxg9J
# qGujh+TnP1bW3aQjFtlEl7SzgNeI8OVA/CJAWWUuRT3Au9ToOpHy/flrrknhN4he
# Ew7/6ILMv4HSsj0YZ4otAgMBAAGjggFQMIIBTDB1BggrBgEFBQcBAQRpMGcwOwYI
# KwYBBQUHMAKGL2h0dHA6Ly9zdWJjYS5yZXBvc2l0b3J5LmNlcnR1bS5wbC9jdHNj
# YTIwMjEuY2VyMCgGCCsGAQUFBzABhhxodHRwOi8vc3ViY2Eub2NzcC1jZXJ0dW0u
# Y29tMB8GA1UdIwQYMBaAFL5UAi+/QGxzQ86sCSVOnkNEGu7gMAwGA1UdEwEB/wQC
# MAAwOQYDVR0fBDIwMDAuoCygKoYoaHR0cDovL3N1YmNhLmNybC5jZXJ0dW0ucGwv
# Y3RzY2EyMDIxLmNybDAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAOBgNVHQ8BAf8E
# BAMCB4AwIgYDVR0gBBswGTAIBgZngQwBBAIwDQYLKoRoAYb2dwIFAQswHQYDVR0O
# BBYEFCM5aiirmhKnpKfyH/lFmr+N/oIDMA0GCSqGSIb3DQEBDAUAA4ICAQBm/pOb
# rAveTqYFPCSthxNVwBHTW4TfuBQY2KGCdMlH6ALjVPYf2XARNGMTXScR0JQ2/c2L
# xLpoHHhRwxFf3d933DhFS165rYj6iPSQKm41rc9AMpTCjMdmai1aK9o870pOzTzL
# RhWFVIxm+5Qf+t4V5z14tPoUsAWdfRKD1Zzvzfe5AHlE9rxOZcGROiULxs3Edf7Z
# J8RZI0jzD9juj2oEX2XS7BVKuNCK1W0h1mjapv028yqo8UtZh5/1Ib48Yyp88BDY
# Shh5PSGUUuS5tYXVfcduIPBe3PdNTYRYdT5VNxkwLgjMzIcr/6SH2fejFJDjnCxj
# vXLuarErJg/IdI+hXdGIBDVOGv1zrkF4ktoQvFZIp5DIAYh0GwyUhc87qqn5UaYT
# j2+iyiBDSNh7aGCrkuAnqg7Qh7frhX+QhMGeJ9SpLpnG76qN85hLyjvYR6gRYiLT
# j8G4fxjqV400/buPw0rBo9yJZyq/8cGdCOLPOYf9qpefTcrZKeO9pg+UJHx8UABb
# aTqizRxm8sUSEycccCCFhIit3AI/3wGuSOADeiA676NcQ7+dsP04YvzvdN7pTZNy
# g672WpBDJdg0eItHl4yomvNeIOWh1qs4QqOW6TI8Ocsi4uSDTLorxLUDnBTwKQzz
# pxd1YUctEgRh32R0J9FkFkeEruJU94RljFr1uDCCBrkwggShoAMCAQICEQCZo4AK
# JlU7ZavcboSms+o5MA0GCSqGSIb3DQEBDAUAMIGAMQswCQYDVQQGEwJQTDEiMCAG
# A1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVt
# IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3Rl
# ZCBOZXR3b3JrIENBIDIwHhcNMjEwNTE5MDUzMjE4WhcNMzYwNTE4MDUzMjE4WjBW
# MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEu
# MSQwIgYDVQQDExtDZXJ0dW0gQ29kZSBTaWduaW5nIDIwMjEgQ0EwggIiMA0GCSqG
# SIb3DQEBAQUAA4ICDwAwggIKAoICAQCdI88EMCM7wUYs5zNzPmNdenW6vlxNur3r
# Lfi+5OZ+U3iZIB+AspO+CC/bj+taJUbMbFP1gQBJUzDUCPx7BNLgid1TyztVLn52
# NKgxxu8gpyTr6EjWyGzKU/gnIu+bHAse1LCitX3CaOE13rbuHbtrxF2tPU8f253Q
# gX6eO8yTbGps1Mg+yda3DcTsOYOhSYNCJiL+5wnjZ9weoGRtvFgMHtJg6i671OPX
# IciiHO4Lwo2p9xh/tnj+JmCQEn5QU0NxzrOiRna4kjFaA9ZcwSaG7WAxeC/xoZSx
# F1oK1UPZtKVt+yrsGKqWONoK6f5EmBOAVEK2y4ATDSkb34UD7JA32f+Rm0wsr5aj
# zftDhA5mBipVZDjHpwzv8bTKzCDUSUuUmPo1govD0RwFcTtMXcfJtm1i+P2UNXad
# PyYVKRxKQATHN3imsfBiNRdN5kiVVeqP55piqgxOkyt+HkwIA4gbmSc3hD8ke66t
# 9MjlcNg73rZZlrLHsAIV/nJ0mmgSjBI/TthoGJDydekOQ2tQD2Dup/+sKQptalDl
# ui59SerVSJg8gAeV7N/ia4mrGoiez+SqV3olVfxyLFt3o/OQOnBmjhKUANoKLYlK
# mUpKEFI0PfoT8Q1W/y6s9LTI6ekbi0igEbFUIBE8KDUGfIwnisEkBw5KcBZ3XwnH
# mfznwlKo8QIDAQABo4IBVTCCAVEwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU
# 3XRdTADbe5+gdMqxbvc8wDLAcM0wHwYDVR0jBBgwFoAUtqFUOQLDoD+Oirz61Pgc
# ptE6Dv0wDgYDVR0PAQH/BAQDAgEGMBMGA1UdJQQMMAoGCCsGAQUFBwMDMDAGA1Ud
# HwQpMCcwJaAjoCGGH2h0dHA6Ly9jcmwuY2VydHVtLnBsL2N0bmNhMi5jcmwwbAYI
# KwYBBQUHAQEEYDBeMCgGCCsGAQUFBzABhhxodHRwOi8vc3ViY2Eub2NzcC1jZXJ0
# dW0uY29tMDIGCCsGAQUFBzAChiZodHRwOi8vcmVwb3NpdG9yeS5jZXJ0dW0ucGwv
# Y3RuY2EyLmNlcjA5BgNVHSAEMjAwMC4GBFUdIAAwJjAkBggrBgEFBQcCARYYaHR0
# cDovL3d3dy5jZXJ0dW0ucGwvQ1BTMA0GCSqGSIb3DQEBDAUAA4ICAQB1iFgP5Y9Q
# KJpTnxDsQ/z0O23JmoZifZdEOEmQvo/79PQg9nLF/GJe6ZiUBEyDBHMtFRK0mXj3
# Qv3gL0sYXe+PPMfwmreJHvgFGWQ7XwnfMh2YIpBrkvJnjwh8gIlNlUl4KENTK5DL
# qsYPEtRQCw7R6p4s2EtWyDDr/M58iY2UBEqfUU/ujR9NuPyKk0bEcEi62JGxauFY
# zZ/yld13fHaZskIoq2XazjaD0pQkcQiIueL0HKiohS6XgZuUtCKA7S6CHttZEsOb
# QJ1j2s0urIDdqF7xaXFVaTHKtAuMfwi0jXtF3JJphrJfc+FFILgCbX/uYBPBlbBI
# P4Ht4xxk2GmfzMn7oxPITpigQFJFWuzTMUUgdRHTxaTSKRJ/6Uh7ki/pFjf9sUAS
# WgxT69QF9Ki4JF5nBIujxZ2sOU9e1HSCJwOfK07t5nnzbs1LbHuAIGJsRJiQ6HX/
# DW1XFOlXY1rc9HufFhWU+7Uk+hFkJsfzqBz3pRO+5aI6u5abI4Qws4YaeJH7H7M8
# X/YNoaArZbV4Ql+jarKsE0+8XvC4DJB+IVcvC9Ydqahi09mjQse4fxfef0L7E3hh
# o2O3bLDM6v60rIRUCi2fJT2/IRU5ohgyTch4GuYWefSBsp5NPJh4QRTP9DC3gc5Q
# EKtbrTY0Ka87Web7/zScvLmvQBm8JDFpDjCCBrkwggShoAMCAQICEQDn/2nHOzXO
# S5Em2HR8aKWHMA0GCSqGSIb3DQEBDAUAMIGAMQswCQYDVQQGEwJQTDEiMCAGA1UE
# ChMZVW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENl
# cnRpZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3RlZCBO
# ZXR3b3JrIENBIDIwHhcNMjEwNTE5MDUzMjA3WhcNMzYwNTE4MDUzMjA3WjBWMQsw
# CQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMSQw
# IgYDVQQDExtDZXJ0dW0gVGltZXN0YW1waW5nIDIwMjEgQ0EwggIiMA0GCSqGSIb3
# DQEBAQUAA4ICDwAwggIKAoICAQDpEh8ENe25XXrFppVBvoplf0530W0lddNmjtv4
# YSh/f7eDQKFaIqc7tHj7ox+u8vIsJZlroakUeMS3i3T8aJRC+eQs4FF0GqvkM6+W
# ZO8kmzZfxmZaBYmMLs8FktgFYCzywmXeQ1fEExflee2OpbHVk665eXRHjH7MYZIz
# Nnjl2m8Hy8ulB9mR8wL/W0v0pjKNT6G0sfrx1kk+3OGosFUb7yWNnVkWKU4qSxLv
# 16kJ6oVJ4BSbZ4xMak6JLeB8szrK9vwGDpvGDnKCUMYL3NuviwH1x4gZG0JAXU3x
# 2pOAz91JWKJSAmRy/l0s0l5bEYKolg+DMqVhlOANd8Yh5mkQWaMEvBRE/kAGzIqg
# WhwzN2OsKIVtO8mf5sPWSrvyplSABAYa13rMYnzwfg08nljZHghquCJYCa/xHK9a
# cev9UD7Y+usr15d7mrszzxhF1JOr1Mpup2chNSBlyOObhlSO16rwrffVrg/SzaKf
# SndS5swRhr8bnDqNJY9TNyEYvBYpgF95K7p0g4LguR4A++Z1nFIHWVY5v0fNVZmg
# zxD9uVo/gta3onGOQj3JCxgYx0KrCXu4yc9QiVwTFLWbNdHFSjBCt5/8Q9pLuRhV
# ocdCunhcHudMS1CGQ/Rn0+7P+fzMgWdRKfEOh/hjLrnQ8BdJiYrZNxvIOhM2aa3z
# EDHNwwIDAQABo4IBVTCCAVEwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUvlQC
# L79AbHNDzqwJJU6eQ0Qa7uAwHwYDVR0jBBgwFoAUtqFUOQLDoD+Oirz61PgcptE6
# Dv0wDgYDVR0PAQH/BAQDAgEGMBMGA1UdJQQMMAoGCCsGAQUFBwMIMDAGA1UdHwQp
# MCcwJaAjoCGGH2h0dHA6Ly9jcmwuY2VydHVtLnBsL2N0bmNhMi5jcmwwbAYIKwYB
# BQUHAQEEYDBeMCgGCCsGAQUFBzABhhxodHRwOi8vc3ViY2Eub2NzcC1jZXJ0dW0u
# Y29tMDIGCCsGAQUFBzAChiZodHRwOi8vcmVwb3NpdG9yeS5jZXJ0dW0ucGwvY3Ru
# Y2EyLmNlcjA5BgNVHSAEMjAwMC4GBFUdIAAwJjAkBggrBgEFBQcCARYYaHR0cDov
# L3d3dy5jZXJ0dW0ucGwvQ1BTMA0GCSqGSIb3DQEBDAUAA4ICAQC4k1l3yUwV/ZQH
# CKCneqAs8EGTnwEUJLdDpokN/dMhKjK0rR5qX8nIIHzxpQR3TAw2IRw1Uxsr2Pli
# G3bCFqSdQTUbfaTq6V3vBzEebDru9QFjqlKnxCF2h1jhLNFFplbPJiW+JSnJTh1f
# KEqEdKdxgl9rVTvlxfEJ7exOn25MGbd/wGPwuSmMxRJVO0wnqgS7kmoJjNF9zqee
# hFSDDP8ZVkWg4EZ2tIS0M3uZmByRr+1Lkwjjt8AtW83mVnZTyTsOb+FNfwJY7DS4
# FmWhkRbgcHRetreoTirPOr/ozyDKhT8MTSTf6Lttg6s6T/u08mDWw6HK04ZRDfQ9
# sb77QV8mKgO44WGP31vXnVKoWVJpFBjPvjL8/Zck/5wXX2iqjOaLStFOR/IQki+E
# hn4zlcgVm22ZVCBPF+l8nAwUUShCtKuSU7GmZLKCmmxQMkSiWILTm8EtVD6AxnJh
# oq8EnhjEEyUoflkeRF2WhFiVQOmWTwZRr44IxWGkNJC6tTorW5rl2Zl+2e9JLPYf
# 3pStAPMDoPKIjVXd6NW2+fZrNUBeDo2eOa5Fn7Brs/HLQff5Xgris5MeUbdVgDrF
# 8uxO6cLPvZPo63j62SsNg55pTWk9fUIF9iPoRbb4QurjoY/woI1RAOKtYtTic6aA
# Jq3u83RIPpGXBSJKwx4KJAOZnCDCtTGCBqIwggaeAgEBMGowVjELMAkGA1UEBhMC
# UEwxITAfBgNVBAoTGEFzc2VjbyBEYXRhIFN5c3RlbXMgUy5BLjEkMCIGA1UEAxMb
# Q2VydHVtIENvZGUgU2lnbmluZyAyMDIxIENBAhBX5gfP8LabvnxCXoItEBa/MA0G
# CWCGSAFlAwQCAQUAoIGEMBgGCisGAQQBgjcCAQwxCjAIoAKAAKECgAAwGQYJKoZI
# hvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcC
# ARUwLwYJKoZIhvcNAQkEMSIEILSu/NMBpMkxIS9F4Jc1O9sSxozTZhoQWceYPYt+
# yjsBMA0GCSqGSIb3DQEBAQUABIIBgJv4TKGcpeaFbBSdh38LNlkISTAk0qF892c7
# AXQB1sEUVtOV0gtbUEkaA/T4czUhpF0JXS//ut7XaQNZBulSwrcEh/rRq1F168KP
# DsUyE2P1kUBZy//vOb/qLCUC3CaKQ4pXqUABMR9Hr7q2Lf8pguI7nhLx5hKkpqEF
# YgswfntREhPn4eKcLCNDKxHFx7B9hN4FE8ZfQLjQeU6JHED3j10scejr6ZheBOUq
# JEPjc0o1AQVGgMDIBAXPD9znAjtV48jFx++ecxQva520DuUTUXDHDEjlFXmez5xa
# 6bEfxiQZ28olM69RPiS4yhyBgBlQdW4ciHJkv2kra0RIyjWD0ThTzQu19xpkKljR
# iZvA18adDbf/+swhgmQplHWBO5MqXiZdMyfBQPSwtjGxGW1XIKwc23hmHDRmt0wU
# j77F6SV++R1o5kFg3JchU/dScA8wsoWwOfPzjQF48ua6IsEMZ2Ix/XA4+SIsnoY0
# EbvSYSzdQo+izSmOmHrzxHYXEya5sqGCBAIwggP+BgkqhkiG9w0BCQYxggPvMIID
# 6wIBATBqMFYxCzAJBgNVBAYTAlBMMSEwHwYDVQQKExhBc3NlY28gRGF0YSBTeXN0
# ZW1zIFMuQS4xJDAiBgNVBAMTG0NlcnR1bSBUaW1lc3RhbXBpbmcgMjAyMSBDQQIQ
# KPB3wRw2vf5fdDJHcCcuAzANBglghkgBZQMEAgIFAKCCAVYwGgYJKoZIhvcNAQkD
# MQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNjEwMTMwOTQwMzNaMDcG
# CyqGSIb3DQEJEAIvMSgwJjAkMCIEIIW+kOEK0kONfMkotq9IsJqyCBd87PiwEmxY
# 05EFJcQ8MD8GCSqGSIb3DQEJBDEyBDD9YBE90tEZ+oiSI3m6v0y/eEUHlWO/rDZg
# uoncrSvhmCLHbNXXnGFQMgsBrPop/YowgZ8GCyqGSIb3DQEJEAIMMYGPMIGMMIGJ
# MIGGBBRXFGhBDKha80JO+RZKUTYQ9NONmDBuMFqkWDBWMQswCQYDVQQGEwJQTDEh
# MB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMSQwIgYDVQQDExtDZXJ0
# dW0gVGltZXN0YW1waW5nIDIwMjEgQ0ECECjwd8EcNr3+X3QyR3AnLgMwDQYJKoZI
# hvcNAQEBBQAEggIAd19dBlW8Gk49LthaqUTF84K2ahYaKkY3DeTPNxqRZDiCV+GJ
# I/f3rGgfbmuLgjHhNNaGdEbpFaTjLBFSbyqTMswZUNai/GrXSVGf4KUcJfvhmo8l
# P5dv1pH9c3G/3ta4DSMYerHmutO82VGyLrSN+enU0GbJCl44WmG68K57Auv4CvvA
# XIFlFIQfSPrve4ZUx4heXofv1oUNI2qnA+VcTQ9pcAbJsxrPKk67JQFsy7RopMJD
# mrBVoNjE3snMWEvCbyiDtK8ZtZThdFdTh2MVV+V+exfXvNZyXIH9FkXrYNDCh8ha
# zzIhQwWhdtzi7JM6D97Uym/IMHfQ66PfLPbL+vLTfd0TfkxMqAGqoORnUyuJdQ01
# +ZVtDcjfIvnLymyXxKab5jfD1nxQaR4ipmUmP2Va8m2FSCd0cGqYJAxGKaDToizk
# jtYFGNjnfGpVxRKNaiEUGaRcH5egBzXgRf3T1qrO/bl8tdBVoK9+dkn77m/ugWha
# 738/62l2MvyCUzeD4Y0MJXwYphApibnPFIoq1GKgZjAn1OVZUoDgvkGL05OoWoEP
# QekIkwRlRejHEtpGd5KgRu9vXmWBfjCaimkY12ZyoQw3b8Kk1cCbI29ymqfrxpGO
# EEwGBHmsSW0reF6HvVn8rQN+iSPGDNVTSpUz6+w5BoaJjGgb2a7dhWedcRQ=
# SIG # End signature block