Grant-TDCErhvervSecurityInsightsPermissions.ps1


<#PSScriptInfo
 
.VERSION 1.1.0
 
.GUID dd8034ed-6988-4ae3-b0ce-47cca42c19a5
 
.AUTHOR TDC Erhverv
 
.COMPANYNAME
 
.COPYRIGHT
 
.TAGS TDC Security Insights Entra Microsoft365
 
.LICENSEURI
 
.PROJECTURI
 
.ICONURI
 
.EXTERNALMODULEDEPENDENCIES
 
.REQUIREDSCRIPTS
 
.EXTERNALSCRIPTDEPENDENCIES
 
.RELEASENOTES
 
 
.PRIVATEDATA
 
#>


<#
 
.DESCRIPTION
 Configures TDC Erhverv Security Insights permissions in your tenant.
 
#>
 


param(
    [Switch]$Force
)

function ConvertTo-SecureStringFromPlainText {
    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        [String]$PlainText
    )
    $SecureString = New-Object System.Security.SecureString
    $PlainText.ToCharArray() | ForEach-Object { $SecureString.AppendChar($_) }
    $SecureString.MakeReadOnly()
    return $SecureString
}
function Initialize-TDCErhvervSecurityInsightsMsalPackages {
    try {
        if ($PSVersionTable.PSEdition -eq 'Core') {
            $MicrosoftIdentityModelAbstractionsVersion = '8.8.0'
        } else {
            $MicrosoftIdentityModelAbstractionsVersion = '6.35.0'
        }
        $MicrosoftIdentityModelAbstractions = Get-Package -Name 'Microsoft.IdentityModel.Abstractions' -RequiredVersion $MicrosoftIdentityModelAbstractionsVersion -WarningAction SilentlyContinue -ErrorAction Stop
        $MicrosoftIdentityClient = Get-Package -Name 'Microsoft.Identity.Client' -RequiredVersion '4.70.2' -WarningAction SilentlyContinue -ErrorAction Stop
    } catch {
        Install-Package -Scope CurrentUser -Source 'https://www.nuget.org/api/v2' -Name 'Microsoft.IdentityModel.Abstractions' -RequiredVersion $MicrosoftIdentityModelAbstractionsVersion -Force -WarningAction SilentlyContinue -ErrorAction Stop | Out-Null
        Install-Package -Scope CurrentUser -Source 'https://www.nuget.org/api/v2' -Name 'Microsoft.Identity.Client' -RequiredVersion '4.70.2' -Force -WarningAction SilentlyContinue -ErrorAction Stop | Out-Null
        $MicrosoftIdentityModelAbstractions = Get-Package -Name 'Microsoft.IdentityModel.Abstractions' -RequiredVersion $MicrosoftIdentityModelAbstractionsVersion -WarningAction SilentlyContinue -ErrorAction Stop
        $MicrosoftIdentityClient = Get-Package -Name 'Microsoft.Identity.Client' -RequiredVersion '4.70.2' -WarningAction SilentlyContinue -ErrorAction Stop
    }
    $MicrosoftIdentityModelAbstractionsPath = $MicrosoftIdentityModelAbstractions.Source | Split-Path | Join-Path -ChildPath 'lib\net472\Microsoft.IdentityModel.Abstractions.dll'
    $MicrosoftIdentityClientPath = $MicrosoftIdentityClient.Source | Split-Path | Join-Path -ChildPath 'lib\net472\Microsoft.Identity.Client.dll'
    Add-Type -Path $MicrosoftIdentityModelAbstractionsPath -WarningAction SilentlyContinue -ErrorAction Stop
    Add-Type -Path $MicrosoftIdentityClientPath -WarningAction SilentlyContinue -ErrorAction Stop
}
function Initialize-TDCErhvervSecurityInsightsMsalApp {
    param (
        [String]$ClientId = '81e6c433-88f4-490b-9266-f266a1f89c55',
        [String]$RedirectUri = 'https://login.microsoftonline.com/common/oauth2/nativeclient'
    )
    Initialize-TDCErhvervSecurityInsightsMsalPackages
    $Script:ClientApplication = [Microsoft.Identity.Client.PublicClientApplicationBuilder]::Create($ClientId).WithRedirectUri($RedirectUri).Build()
}
function Get-TDCErhvervSecurityInsightsToken {
    param(
        [String]$Scope = 'https://graph.microsoft.com/.default',
        [Switch]$Force
    )
    try {
        if ($env:AZUREPS_HOST_ENVIRONMENT -like 'cloud-shell/*') {
            $TaskAuthenticationResult = Get-AzAccessToken -ResourceUrl $Scope -AsSecureString -WarningAction Ignore
            $AuthenticationResult = [PSCustomObject]@{
                AccessToken       = [System.Net.NetworkCredential]::new('', $TaskAuthenticationResult.Token).Password
                SecureAccessToken = $TaskAuthenticationResult.Token
                AccountId         = $TaskAuthenticationResult.UserId
            }
        } else {
            if (-not $Script:ClientApplication) {
                throw 'ClientApplication not initialized. Please call Initialize-TDCErhvervSecurityInsightsMsalApp first.'
            }
            [Microsoft.Identity.Client.IAccount]$Account = $Script:ClientApplication.GetAccountsAsync().GetAwaiter().GetResult() | Select-Object -First 1
            [String[]]$Scope = $Scope
            if ($Account -and -not $Force) {
                $AquireTokenParameters = $Script:ClientApplication.AcquireTokenSilent($Scope, $Account)
            } else {
                $AquireTokenParameters = $Script:ClientApplication.AcquireTokenInteractive($Scope).WithPrompt([Microsoft.Identity.Client.Prompt]::Consent)
            }
            $TokenSource = New-Object System.Threading.CancellationTokenSource
            $TaskAuthenticationResult = $AquireTokenParameters.ExecuteAsync($TokenSource.Token)
            try {
                if (!$Timeout) {
                    $Timeout = [System.TimeSpan]::Zero
                }
                $EndTime = [System.DateTime]::Now.Add($Timeout)
                while (!$TaskAuthenticationResult.IsCompleted) {
                    if ($Timeout -eq [System.TimeSpan]::Zero -or [System.DateTime]::Now -lt $EndTime) {
                        Start-Sleep -Seconds 1
                    } else {
                        $TokenSource.Cancel()
                        try { $TaskAuthenticationResult.Wait() }
                        catch { }
                        Write-Error -Exception (New-Object System.TimeoutException) -Category ([System.Management.Automation.ErrorCategory]::OperationTimeout) -CategoryActivity $MyInvocation.MyCommand -ErrorId 'GetMsalTokenFailureOperationTimeout' -TargetObject $Script:ClientApplication -ErrorAction Stop
                    }
                }
            } finally {
                if (!$TaskAuthenticationResult.IsCompleted) {
                    $TokenSource.Cancel()
                }
                $TokenSource.Dispose()
            }
            if ($TaskAuthenticationResult.IsFaulted) {
                Write-Error -Exception $TaskAuthenticationResult.Exception.InnerException -Category ([System.Management.Automation.ErrorCategory]::AuthenticationError) -CategoryActivity $MyInvocation.MyCommand -ErrorId 'GetMsalTokenFailureAuthenticationError' -TargetObject $Script:ClientApplication -ErrorAction Stop
            }
            if ($TaskAuthenticationResult.IsCanceled) {
                Write-Error -Exception (New-Object System.Threading.Tasks.TaskCanceledException $TaskAuthenticationResult) -Category ([System.Management.Automation.ErrorCategory]::OperationStopped) -CategoryActivity $MyInvocation.MyCommand -ErrorId 'GetMsalTokenFailureOperationStopped' -TargetObject $Script:ClientApplication -ErrorAction Stop
            } else {
                $AuthenticationResult = [PSCustomObject]@{
                    AccessToken       = $TaskAuthenticationResult.Result.AccessToken
                    SecureAccessToken = $TaskAuthenticationResult.Result.AccessToken | ConvertTo-SecureStringFromPlainText
                    AccountId         = $TaskAuthenticationResult.Result.Account.Username
                }
            }
        }
        return $AuthenticationResult
    } catch {
        throw
    }
}
function Grant-TDCErhvervSecurityInsightsPermissions {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [Switch]$Force = $false
    )
    try {
        try {
            if ((Get-Host -ErrorAction SilentlyContinue).UI.RawUI.BufferSize.Width -ge 114) {
                $motd = @'

 _____________________________________________________________________________________________________________________
| |
| _____________________________________ |
| /████████████████████████████████████/ ███████╗███████╗ ██████╗██╗ ██╗██████╗ ██╗████████╗██╗ ██╗ |
| /████████████████████████████████████/ ██╔════╝██╔════╝██╔════╝██║ ██║██╔══██╗██║╚══██╔══╝╚██╗ ██╔╝ |
| /████████████████████████████████████/ ███████╗█████╗ ██║ ██║ ██║██████╔╝██║ ██║ ╚████╔╝ |
| /████████████████████████████████████/ ╚════██║██╔══╝ ██║ ██║ ██║██╔══██╗██║ ██║ ╚██╔╝ |
| /████ ██ ████ ████████/ ███████║███████╗╚██████╗╚██████╔╝██║ ██║██║ ██║ ██║ |
| /████████ ███ ██ ██ ██████████/ ╚══════╝╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ |
| /█████████ ███ ███ █ ██████████/ |
| /██████████ ███ ██ ██ ████████/ ██╗███╗ ██╗███████╗██╗ ██████╗ ██╗ ██╗████████╗███████╗ |
| /███████████ ███ ████ ████/ ██║████╗ ██║██╔════╝██║██╔════╝ ██║ ██║╚══██╔══╝██╔════╝ |
| /████████████████████████████████████/ ██║██╔██╗ ██║███████╗██║██║ ███╗███████║ ██║ ███████╗ |
| /████████████████████████████████████/ ██║██║╚██╗██║╚════██║██║██║ ██║██╔══██║ ██║ ╚════██║ |
| /████████████████████████████████████/ ██║██║ ╚████║███████║██║╚██████╔╝██║ ██║ ██║ ███████║ |
| /████████████████████████████████████/ ╚═╝╚═╝ ╚═══╝╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ |
| |
|_____________________________________________________________________________________________________________________|


'@

                Write-Host -ForegroundColor White -BackgroundColor DarkBlue $motd
            }
            Write-Host -NoNewline 'Initializing: '
            Write-Progress -Id 0 -Activity 'Configuring TDC Erhverv Security Insights permissions' -Status 'Initializing' -PercentComplete ((1 / 9) * 100) -ErrorAction SilentlyContinue
            if ($env:AZUREPS_HOST_ENVIRONMENT -notlike 'cloud-shell/*') {
                if (-not (Get-Module -Name Microsoft.Graph.Authentication -ListAvailable)) {
                    Install-Module -Name 'Microsoft.Graph.Authentication' -Scope CurrentUser -AllowClobber -Force -WarningAction SilentlyContinue -ErrorAction Stop | Out-Null
                }
                Initialize-TDCErhvervSecurityInsightsMsalApp
            }
            Write-Host -ForegroundColor Black -BackgroundColor Green -Object ' OK '
        } catch {
            Write-Host -ForegroundColor White -BackgroundColor Red -Object ' Failed '
            throw
        }
        try {
            Write-Progress -Id 0 -Activity 'Configuring TDC Erhverv Security Insights permissions' -Status 'Authenticating' -PercentComplete ((2 / 9) * 100) -ErrorAction SilentlyContinue
            Write-Host -NoNewline 'Authenticating: '
            $Authentication = Get-TDCErhvervSecurityInsightsToken -Force:$Force -ErrorAction Stop
            Connect-MgGraph -AccessToken $Authentication.SecureAccessToken -NoWelcome | Out-Null
            Write-Host -ForegroundColor Black -BackgroundColor Green -Object ' OK '
        } catch {
            Write-Host -ForegroundColor White -BackgroundColor Red -Object ' Failed '
            throw
        }
        try {
            Write-Progress -Id 0 -Activity 'Configuring TDC Erhverv Security Insights permissions' -Status 'Preparing modules' -PercentComplete ((3 / 9) * 100) -ErrorAction SilentlyContinue
            Write-Host -NoNewline 'Preparing modules: '
            $EntraIDDirectoryRoles = (Invoke-MgGraphRequest -Method GET -Uri 'https://graph.microsoft.com/v1.0/directoryRoles' -OutputType PSObject -ErrorAction Stop).value
            $ServicePrincipals = (Invoke-MgGraphRequest -Method GET -Uri 'https://graph.microsoft.com/v1.0/servicePrincipals?$select=displayName,appId,id&filter=appId in ("1c47d605-cb57-4fe8-81f8-a878d718ada3", "a1a66b43-7a53-43a8-b986-41299f22becd", "59e16c46-95c2-442b-a45d-aa795c4892b7", "44c5021d-baf4-47b5-b35f-545b8ed1d464", "7d726de8-d48e-4304-9b21-f6eaf8d825e5", "b52dcb6f-c5df-436a-ae43-c039113ad7fa", "a6ff5070-d517-432b-8ed6-042c2a3560e1", "3286cce6-8b4a-4e8d-bd59-a02a9474e33d", "f0279a37-2a96-4d3d-8f19-d4eee0af7fc4", "ddfdf184-9b52-4d19-b5e6-79c8965cd0a5", "8e507df8-4bdc-44cb-bba4-b3d9812d8576", "80d592c4-5e3b-4469-8586-8d7a5d7fc3f6", "dcf00d66-0307-413a-8ff6-f6dede0d584f", "19641d75-3997-468f-84de-ddfa7e5e4e74")' -OutputType PSObject -ErrorAction Stop).value
            $TDCErhvervSecurityInsightsAnalyticsMSAzure = $ServicePrincipals | Where-Object { $_.AppId -in ('59e16c46-95c2-442b-a45d-aa795c4892b7', '44c5021d-baf4-47b5-b35f-545b8ed1d464') }
            $TDCErhvervSecurityInsightsAnalyticsMSExchangeOnline = $ServicePrincipals | Where-Object { $_.AppId -in ('7d726de8-d48e-4304-9b21-f6eaf8d825e5', 'b52dcb6f-c5df-436a-ae43-c039113ad7fa') }
            $TDCErhvervSecurityInsightsAnalyticsMSTeams = $ServicePrincipals | Where-Object { $_.AppId -in ('f0279a37-2a96-4d3d-8f19-d4eee0af7fc4', 'ddfdf184-9b52-4d19-b5e6-79c8965cd0a5') }
            $TDCErhvervSecurityInsightsAnalyticsMSPowerPlatform = $ServicePrincipals | Where-Object { $_.AppId -in ('8e507df8-4bdc-44cb-bba4-b3d9812d8576', '80d592c4-5e3b-4469-8586-8d7a5d7fc3f6') }
            $TDCErhvervSecurityInsightsAnalyticsMSPowerBI = $ServicePrincipals | Where-Object { $_.AppId -in ('dcf00d66-0307-413a-8ff6-f6dede0d584f', '19641d75-3997-468f-84de-ddfa7e5e4e74') }
            if ($env:AZUREPS_HOST_ENVIRONMENT -notlike 'cloud-shell/*') {
                [System.Collections.ArrayList]$ModulesToInstall = @()
                if ($TDCErhvervSecurityInsightsAnalyticsMSExchangeOnline -and -not (Get-Module -Name ExchangeOnlineManagement -ListAvailable)) {
                    $ModulesToInstall.Add('ExchangeOnlineManagement') | Out-Null
                }
                if ($TDCErhvervSecurityInsightsAnalyticsMSAzure -and -not (Get-Module -Name Az.Accounts -ListAvailable)) {
                    $ModulesToInstall.Add('Az.Accounts') | Out-Null
                }
                if ($TDCErhvervSecurityInsightsAnalyticsMSAzure -and -not (Get-Module -Name Az.Resources -ListAvailable)) {
                    $ModulesToInstall.Add('Az.Resources') | Out-Null
                }
                if ($TDCErhvervSecurityInsightsAnalyticsMSPowerPlatform -and -not (Get-Module -Name Microsoft.PowerApps.Administration.PowerShell -ListAvailable)) {
                    $ModulesToInstall.Add('Microsoft.PowerApps.Administration.PowerShell') | Out-Null
                }
                if ($ModulesToInstall.Count -gt 0) {
                    Install-Module -Name $ModulesToInstall -Scope CurrentUser -AllowClobber -Force -WarningAction SilentlyContinue -ErrorAction Stop | Out-Null
                }
            }
            Write-Host -ForegroundColor Black -BackgroundColor Green -Object ' OK '
        } catch {
            Write-Host -ForegroundColor White -BackgroundColor Red -Object ' Failed '
            throw
        }
        Write-Host -NoNewline 'Configuring module [MSTeams]: '
        if ($TDCErhvervSecurityInsightsAnalyticsMSTeams) {
            try {
                try {
                    Write-Progress -Id 0 -Activity 'Configuring TDC Erhverv Security Insights permissions' -Status 'Configuring additional permissions for Microsoft Teams' -PercentComplete ((4 / 9) * 100) -ErrorAction SilentlyContinue
                    $EntraIDDirectoryRoleTeamsReader = $EntraIDDirectoryRoles | Where-Object { $_.roleTemplateId -eq '1076ac91-f3d9-41a7-a339-dcdf5f480acc' }
                    if (-not $EntraIDDirectoryRoleTeamsReader) {
                        $EntraIDDirectoryRoleTeamsReader = Invoke-MgGraphRequest -Method POST -Uri 'https://graph.microsoft.com/v1.0/directoryRoles' -Body @{roleTemplateId = '1076ac91-f3d9-41a7-a339-dcdf5f480acc' } -OutputType PSObject -ErrorAction Stop
                        Write-Verbose -Message '[OK] Enabled Entra ID role: Teams Administrator'
                    }
                } catch {
                    Write-Verbose -Message "[Error] Failed to enable Entra ID role [Teams Administrator]: $($_.Exception.Message)"
                    throw
                }
                try {
                    $EntraIDDirectoryRoleTeamsReaderMembers = (Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/beta/directoryRoles/$($EntraIDDirectoryRoleTeamsReader.id)/members" -OutputType PSObject).value
                    foreach ($Module in $TDCErhvervSecurityInsightsAnalyticsMSTeams) {
                        if ($Module.id -notin $EntraIDDirectoryRoleTeamsReaderMembers.id) {
                            Invoke-MgGraphRequest -Method POST -Uri "https://graph.microsoft.com/v1.0/directoryRoles/$($EntraIDDirectoryRoleTeamsReader.id)/members/`$ref" -Body @{'@odata.id' = "https://graph.microsoft.com/v1.0/directoryObjects/$($Module.id)" } -OutputType PSObject -ErrorAction Stop | Out-Null
                            Write-Verbose -Message "[OK] Added Teams Administrator role to module: $($Module.id)"
                        }
                    }
                } catch {
                    Write-Verbose -Message "[Error] Failed to assign Entra ID role [Teams Administrator] to module id [$($Module.id)]: $($_.Exception.Message)"
                    throw
                }
                Write-Host -ForegroundColor Black -BackgroundColor Green -Object ' OK '
            } catch {
                Write-Host -ForegroundColor White -BackgroundColor Red -Object ' Failed '
            }
        } else {
            Write-Host -ForegroundColor Black -BackgroundColor Gray -Object ' Skipped '
        }
        Write-Host -NoNewline 'Configuring module [MSAzure]: '
        if ($TDCErhvervSecurityInsightsAnalyticsMSAzure) {
            try {
                try {
                    Write-Progress -Id 0 -Activity 'Configuring TDC Erhverv Security Insights permissions' -Status 'Configuring additional permissions for Azure' -PercentComplete ((5 / 9) * 100) -ErrorAction SilentlyContinue
                    Write-Verbose -Message 'Connecting to Azure'

                    Update-AzConfig -Scope Process -DefaultSubscriptionForLogin '' -LoginExperienceV2 Off -CheckForUpgrade $false -DisplayBreakingChangeWarning $false -DisplayRegionIdentified $false -DisplaySecretsWarning $false -DisplaySurveyMessage $false -EnableDataCollection $false -EnableErrorRecordsPersistence $false -EnableLoginByWam $false -WarningAction SilentlyContinue | Out-Null
                    $Authentication = Get-TDCErhvervSecurityInsightsToken -Scope 'https://management.azure.com/.default' -ErrorAction Stop
                    $AzAccountContext = Connect-AzAccount -AccessToken $Authentication.AccessToken -AccountId $Authentication.AccountId -WarningAction Ignore -InformationAction Ignore -ErrorAction Stop

                    $TenantId = (Get-AzContext).Tenant.Id
                    Write-Verbose -Message '[OK] Connected to Azure'
                } catch {
                    Write-Verbose -Message "[Error] Failed to connect to Azure: $($_.Exception.Message)"
                    throw
                }
                try {
                    # Use the bearer token directly for management group lookup — Connect-AzAccount
                    # -AccessToken creates a token-only session with no refresh token, so Az cmdlets
                    # that internally re-acquire tokens (e.g. Get-AzManagementGroup) will hit MFA/CA
                    # policies. Calling the REST API directly with our MSAL token avoids this entirely.
                    $AzureAuthHeader = @{ Authorization = "Bearer $($Authentication.AccessToken)" }
                    $ManagementGroups = try {
                        (Invoke-RestMethod -Method GET -Uri 'https://management.azure.com/providers/Microsoft.Management/managementGroups?api-version=2024-02-01-preview' -Headers $AzureAuthHeader -ErrorAction Stop).value
                    } catch { $null }
                    $TenantRootGroup = $ManagementGroups | Where-Object { $_.name -eq $TenantId }
                    if ($TenantRootGroup) {
                        [Array]$AssignableScopes = $TenantRootGroup.id
                    } else {
                        $Subscriptions = (Invoke-RestMethod -Method GET -Uri "https://management.azure.com/subscriptions?api-version=2025-04-01" -Headers $AzureAuthHeader -ErrorAction Stop).value
                        [Array]$AssignableScopes = $Subscriptions | Where-Object { $_.tenantId -eq $TenantId } | ForEach-Object { $_.id }
                    }
                    Write-Verbose -Message "[OK] Found $($AssignableScopes.Count) assignable scopes."
                } catch {
                    Write-Verbose -Message "[Error] Failed to get Azure assignable scopes: $($_.Exception.Message)"
                    throw
                }
                try {
                    # Use REST directly — Get/New-AzRoleAssignment also internally re-acquire tokens
                    # which fails under MFA/CA policies when using a token-only Az session.
                    $ReaderRoleDefinitionId = 'acdd72a7-3385-48ef-bd42-f606fba81ae7'
                    foreach ($Scope in $AssignableScopes) {
                        foreach ($Module in $TDCErhvervSecurityInsightsAnalyticsMSAzure) {
                            Write-Verbose -Message "Assigning Azure role [Reader] to scope: ${Scope}"
                            $ExistingAssignments = (Invoke-RestMethod -Method GET -Uri "https://management.azure.com${Scope}/providers/Microsoft.Authorization/roleAssignments?api-version=2022-04-01&`$filter=principalId eq '$($Module.id)'" -Headers $AzureAuthHeader -ErrorAction Stop).value
                            $ExistingAssignment = $ExistingAssignments | Where-Object { $_.properties.roleDefinitionId -like "*/$ReaderRoleDefinitionId" }
                            if (-not $ExistingAssignment) {
                                $RoleAssignmentId = [System.Guid]::NewGuid().ToString()
                                Invoke-RestMethod -Method PUT -Uri "https://management.azure.com${Scope}/providers/Microsoft.Authorization/roleAssignments/${RoleAssignmentId}?api-version=2022-04-01" -Headers $AzureAuthHeader -ContentType 'application/json' -Body (@{
                                        properties = @{
                                            roleDefinitionId = "${Scope}/providers/Microsoft.Authorization/roleDefinitions/${ReaderRoleDefinitionId}"
                                            principalId      = $Module.id
                                            principalType    = 'ServicePrincipal'
                                        }
                                    } | ConvertTo-Json) -ErrorAction Stop | Out-Null
                            }
                            Write-Verbose -Message "[OK] Assigned Azure role [Reader] to scope: ${Scope}"
                        }
                    }
                } catch {
                    throw "Unable to assign Azure role [Reader]: $($_.Exception.Message)"
                }
                Write-Host -ForegroundColor Black -BackgroundColor Green -Object ' OK '
            } catch {
                Write-Host -ForegroundColor White -BackgroundColor Red -Object " Failed: $($_.Exception.Message)"
            }
        } else {
            Write-Host -ForegroundColor Black -BackgroundColor Gray -Object ' Skipped '
        }
        Write-Host -NoNewline 'Configuring module [MSExchangeOnline]: '
        if ($TDCErhvervSecurityInsightsAnalyticsMSExchangeOnline) {
            try {
                try {
                    Write-Progress -Id 0 -Activity 'Configuring TDC Erhverv Security Insights permissions' -Status 'Configuring additional permissions for Microsoft Exchange Online' -PercentComplete ((6 / 9) * 100) -ErrorAction SilentlyContinue
                    Write-Verbose -Message 'Connecting to Exchange Online'
                    if ($env:AZUREPS_HOST_ENVIRONMENT -like 'cloud-shell/*') {
                        Connect-ExchangeOnline -ShowBanner:$false -ErrorAction Stop | Out-Null
                    } else {
                        $Authentication = Get-TDCErhvervSecurityInsightsToken -Scope 'https://outlook.office.com/.default' -ErrorAction Stop
                        Connect-ExchangeOnline -AccessToken $Authentication.AccessToken -UserPrincipalName $Authentication.AccountId -ShowBanner:$false -ErrorAction Stop | Out-Null 2>&1
                    }
                    Write-Verbose -Message '[OK] Connected to Exchange Online'
                } catch {
                    Write-Verbose -Message "[Error] Failed to connect to Exchange Online: $($_.Exception.Message)"
                    throw
                }
                try {
                    $EntraIDDirectoryRoleSecurityReader = $EntraIDDirectoryRoles | Where-Object { $_.roleTemplateId -eq '5d6b6bb7-de71-4623-b4af-96380a352509' }
                    if (-not $EntraIDDirectoryRoleSecurityReader) {
                        $EntraIDDirectoryRoleSecurityReader = Invoke-MgGraphRequest -Method POST -Uri 'https://graph.microsoft.com/v1.0/directoryRoles' -Body @{roleTemplateId = '5d6b6bb7-de71-4623-b4af-96380a352509' } -OutputType PSObject -ErrorAction Stop
                        Write-Verbose -Message '[OK] Enabled Entra ID role: Security Reader'
                    }
                } catch {
                    Write-Verbose -Message "[Error] Failed to enable Entra ID role [Security Reader]: $($_.Exception.Message)"
                    throw
                }
                try {
                    $EntraIDDirectoryRoleSecurityReaderMembers = (Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/beta/directoryRoles/$($EntraIDDirectoryRoleSecurityReader.id)/members" -OutputType PSObject).value
                    foreach ($Module in $TDCErhvervSecurityInsightsAnalyticsMSExchangeOnline) {
                        if ($Module.id -notin $EntraIDDirectoryRoleSecurityReaderMembers.id) {
                            Invoke-MgGraphRequest -Method POST -Uri "https://graph.microsoft.com/v1.0/directoryRoles/$($EntraIDDirectoryRoleSecurityReader.id)/members/`$ref" -Body @{'@odata.id' = "https://graph.microsoft.com/v1.0/directoryObjects/$($Module.id)" } -OutputType PSObject -ErrorAction Stop | Out-Null
                            Write-Verbose -Message "[OK] Assigned Security Reader role to: $($Module.id)"
                        }
                    }
                } catch {
                    Write-Verbose -Message "[Error] Failed to assign Entra ID role [Security Reader] to module id [$($Module.id)]: $($_.Exception.Message)"
                    throw
                }
                try {
                    foreach ($Module in $TDCErhvervSecurityInsightsAnalyticsMSExchangeOnline) {
                        if (-not (Get-ServicePrincipal -Identity $Module.id -ErrorAction SilentlyContinue)) {
                            New-ServicePrincipal -AppId $Module.appId -ObjectId $Module.id -DisplayName $Module.displayName -ErrorAction Stop | Out-Null
                            Write-Verbose -Message "[OK] Created Exchange Online Service Principal for module: $($Module.displayName)"
                        }
                    }
                } catch {
                    Write-Verbose -Message "[Error] Failed to create Exchange Online principal for module [$($Module.displayName)]: $($_.Exception.Message)"
                    throw
                }
                try {
                    foreach ($Module in $TDCErhvervSecurityInsightsAnalyticsMSExchangeOnline) {
                        if (-not (Get-ManagementRoleAssignment -Role 'View-Only Configuration' -RoleAssignee $Module.id -ErrorAction SilentlyContinue)) {
                            New-ManagementRoleAssignment -Name "View-Only Configuration-$($Module.appId)" -Role 'View-Only Configuration' -App $Module.displayName -ErrorAction Stop | Out-Null
                            Write-Verbose -Message "[OK] Assigned Exchange Online role [View-Only Configuration] to module: $($Module.displayName)"
                        }
                    }
                } catch {
                    Write-Verbose -Message "[Error] Failed to assign Exchange Online role [View-Only Configuration] to module [$($Module.displayName)]: $($_.Exception.Message)"
                    throw
                }
                try {
                    foreach ($Module in $TDCErhvervSecurityInsightsAnalyticsMSExchangeOnline) {
                        if (-not (Get-ManagementRoleAssignment -Role 'View-Only Recipients' -RoleAssignee $Module.id -ErrorAction SilentlyContinue)) {
                            New-ManagementRoleAssignment -Name "View-Only Recipients-$($Module.appId)" -Role 'View-Only Recipients' -App $Module.displayName -ErrorAction Stop | Out-Null
                            Write-Verbose -Message "[OK] Assigned Exchange Online role [View-Only Recipients] to module: $($Module.displayName)"
                        }
                    }
                } catch {
                    Write-Verbose -Message "[Error] Failed to assign Exchange Online role [View-Only Recipients] to module [$($Module.displayName)]: $($_.Exception.Message)"
                    throw
                }
                Write-Host -ForegroundColor Black -BackgroundColor Green -Object ' OK '
            } catch {
                Write-Host -ForegroundColor White -BackgroundColor Red -Object ' Failed '
            }
        } else {
            Write-Host -ForegroundColor Black -BackgroundColor Gray -Object ' Skipped '
        }
        Write-Host -NoNewline 'Configuring module [MSPowerPlatform]: '
        if ($TDCErhvervSecurityInsightsAnalyticsMSPowerPlatform) {
            try {
                try {
                    Write-Progress -Id 0 -Activity 'Configuring TDC Erhverv Security Insights permissions' -Status 'Configuring additional permissions for Microsoft Power Platform' -PercentComplete ((7 / 9) * 100) -ErrorAction SilentlyContinue
                    Write-Verbose 'Connecting to Power Platform'
                    $Authentication = Get-TDCErhvervSecurityInsightsToken -Scope 'https://management.azure.com/.default' -ErrorAction Stop
                    $Authorization = @{Authorization = "Bearer $($Authentication.AccessToken)" }
                    $PowerAppManagementApps = (Invoke-RestMethod -Method GET -Uri 'https://api.bap.microsoft.com/providers/Microsoft.BusinessAppPlatform/adminApplications?api-version=2020-06-01' -Headers $Authorization -ErrorAction Stop).value
                    Write-Verbose '[OK] Connected to Power Platform'
                } catch {
                    Write-Verbose -Message "[Error] Failed to connect to Power Platform: $($_.Exception.Message)"
                    throw
                }
                try {
                    foreach ($Module in $TDCErhvervSecurityInsightsAnalyticsMSPowerPlatform) {
                        if ($Module.appId -notin $PowerAppManagementApps.applicationId) {
                            Write-Verbose "Adding Power Platform management app: $($Module.appId)"
                            Invoke-RestMethod -Method PUT -Uri "https://api.bap.microsoft.com/providers/Microsoft.BusinessAppPlatform/adminApplications/$($Module.appId)?api-version=2020-06-01" -Headers $Authorization -ErrorAction Stop | Out-Null
                            Write-Verbose "[OK] Added Power Platform management app: $($Module.appId)"
                        }
                    }
                } catch {
                    Write-Verbose -Message "[Error] Failed to add Power Platform management app [$($Module.appId)]: $($_.Exception.Message)"
                    throw
                }
                Write-Host -ForegroundColor Black -BackgroundColor Green -Object ' OK '
            } catch {
                Write-Host -ForegroundColor White -BackgroundColor Red -Object ' Failed: A license is required (e.g. Power Automate Free). Please assign this to your user and try again. '
            }
        } else {
            Write-Host -ForegroundColor Black -BackgroundColor Gray -Object ' Skipped '
        }
        Write-Host -NoNewline 'Configuring module [MSPowerBI]: '
        if ($TDCErhvervSecurityInsightsAnalyticsMSPowerBI) {
            try {
                try {
                    Write-Progress -Id 0 -Activity 'Configuring TDC Erhverv Security Insights permissions' -Status 'Configuring additional permissions for Microsoft Power BI' -PercentComplete ((8 / 9) * 100) -ErrorAction SilentlyContinue
                    Write-Verbose -Message 'Connecting to Power BI'
                    $Authentication = Get-TDCErhvervSecurityInsightsToken -Scope 'https://analysis.windows.net/powerbi/api/.default' -ErrorAction Stop
                    $Authorization = @{Authorization = "Bearer $($Authentication.AccessToken)" }
                    Write-Verbose -Message '[OK] Connected to Power BI'
                } catch {
                    Write-Verbose -Message "[Error] Failed to connect to Power BI: $($_.Exception.Message)"
                    throw
                }
                try {
                    $EntraIDPowerBIGroup = (Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/groups?`$filter=displayName eq 'TDC Erhverv Security Insights Analytics - MS Power BI'" -OutputType PSObject -ErrorAction Stop).value
                    if ($EntraIDPowerBIGroup.Count -gt 1) {
                        throw 'There are more than one group with the name: TDC Erhverv Security Insights Analytics - MS Power BI'
                    } elseif ($EntraIDPowerBIGroup) {
                        $EntraIDPowerBIGroupMembers = (Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/beta/groups/$($EntraIDPowerBIGroup.id)/members" -OutputType PSObject -ErrorAction Stop).value
                        $EntraIDPowerBIGroupMemberCompare = Compare-Object -ReferenceObject $TDCErhvervSecurityInsightsAnalyticsMSPowerBI -DifferenceObject @($EntraIDPowerBIGroupMembers | Select-Object) -Property 'appId' -PassThru
                        if ($EntraIDPowerBIGroupMemberCompare | Where-Object { $_.SideIndicator -eq '=>' }) {
                            throw 'Unknown members in Entra ID Group: TDC Erhverv Security Insights Analytics - MS Power BI'
                        } else {
                            foreach ($Module in $EntraIDPowerBIGroupMemberCompare | Where-Object { $_.SideIndicator -eq '<=' }) {
                                Invoke-MgGraphRequest -Method POST -Uri "https://graph.microsoft.com/v1.0/groups/$($EntraIDPowerBIGroup.id)/members/`$ref" -Body @{'@odata.id' = "https://graph.microsoft.com/v1.0/directoryObjects/$($Module.id)" } -OutputType PSObject -ErrorAction Stop
                                Write-Verbose -Message "[OK] Added group member to [TDC Erhverv Security Insights Analytics - MS Power BI]: $($Module.id)"
                            }
                        }
                    } else {
                        $EntraIDPowerBIGroup = Invoke-MgGraphRequest -Method POST -Uri 'https://graph.microsoft.com/v1.0/groups' -Body @{'displayName' = 'TDC Erhverv Security Insights Analytics - MS Power BI'; description = 'This group is used for allowing TDC Erhverv Security Insights read-only access to Power BI Admin API.'; mailEnabled = $false; securityEnabled = $true; mailNickname = 'NotSet' } -OutputType PSObject -ErrorAction Stop
                        do {
                            $EntraIDPowerBIGroupAttempts++
                            if ($EntraIDPowerBIGroupAttempts -gt 10) {
                                throw "Waiting for Power BI Group creation exceeded attempts: ${EntraIDPowerBIGroupAttempts}"
                            }
                            Start-Sleep -Seconds 5
                            Write-Verbose -Message "Waiting for Power BI Group creation attempt: ${EntraIDPowerBIGroupAttempts}"
                            $EntraIDPowerBIGroup = (Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/groups?`$filter=displayName eq 'TDC Erhverv Security Insights Analytics - MS Power BI'" -OutputType PSObject -ErrorAction SilentlyContinue).value
                        } while (-not $EntraIDPowerBIGroup)
                        Write-Verbose -Message '[OK] Created Entra ID group: TDC Erhverv Security Insights Analytics - MS Power BI'
                        foreach ($Module in $TDCErhvervSecurityInsightsAnalyticsMSPowerBI) {
                            Invoke-MgGraphRequest -Method POST -Uri "https://graph.microsoft.com/v1.0/groups/$($EntraIDPowerBIGroup.id)/members/`$ref" -Body @{'@odata.id' = "https://graph.microsoft.com/v1.0/directoryObjects/$($Module.id)" } -OutputType PSObject -ErrorAction Stop | Out-Null
                            Write-Verbose -Message "[OK] Added group member: $($Module.id)"
                        }
                    }
                } catch {
                    Write-Verbose -Message "[Error] Failed to configure Entra ID group [TDC Erhverv Security Insights Analytics - MS Power BI]: $($_.Exception.Message)"
                    throw
                }
                try {
                    $PowerBIAdminAPISettings = Invoke-RestMethod -UseBasicParsing -Uri 'https://api.fabric.microsoft.com/v1/admin/tenantsettings' -Method 'GET' -Headers $Authorization -ContentType 'application/json;charset=UTF-8' -ErrorAction Stop
                    if ($EntraIDPowerBIGroup.id -notin $PowerBIAdminAPISettings.tenantSettings.Where({ $_.settingName -eq 'AllowServicePrincipalsUseReadAdminAPIs' }).enabledSecurityGroups.graphId) {
                        $PowerBIAdminAPIAllowedSecurityGroups = New-Object System.Collections.ArrayList($null)
                        if ($PowerBIAdminAPISettings.tenantSettings.Where({ $_.settingName -eq 'AllowServicePrincipalsUseReadAdminAPIs' }).enabledSecurityGroups.graphId) {
                            $PowerBIAdminAPIAllowedSecurityGroups.AddRange(@($PowerBIAdminAPISettings.tenantSettings.Where({ $_.settingName -eq 'AllowServicePrincipalsUseReadAdminAPIs' }).enabledSecurityGroups))
                        }
                        $PowerBIAdminAPIAllowedSecurityGroups.Add([PSCustomObject]@{graphId = $EntraIDPowerBIGroup.id; name = $EntraIDPowerBIGroup.DisplayName }) | Out-Null
                        Invoke-RestMethod -UseBasicParsing -Uri 'https://api.fabric.microsoft.com/v1/admin/tenantsettings/AllowServicePrincipalsUseReadAdminAPIs/update' -Method 'POST' -Headers $Authorization -ContentType 'application/json;charset=UTF-8' -Body "{`"enabled`": true,`"enabledSecurityGroups`": $(ConvertTo-Json -InputObject $PowerBIAdminAPIAllowedSecurityGroups -Compress)}" -ErrorAction Stop | Out-Null
                        Write-Verbose -Message '[OK] Added security group'
                    }
                } catch {
                    Write-Verbose -Message "[Error] Failed to add security group [TDC Erhverv Security Insights Analytics - MS Power BI] to [Service principals can access read-only admin APIs]: $($_.Exception.Message)"
                    throw
                }
                Write-Host -ForegroundColor Black -BackgroundColor Green -Object ' OK '
            } catch {
                Write-Host -ForegroundColor White -BackgroundColor Red -Object ' Failed: A license is required (e.g. Microsoft Fabric Free). Please assign this to your user and try again. '
            }
        } else {
            Write-Host -ForegroundColor Black -BackgroundColor Gray -Object ' Skipped '
        }
        if ($env:AZUREPS_HOST_ENVIRONMENT -notlike 'cloud-shell/*') {
            Write-Host -Object 'The enterprise registered application "TDC Erhverv Security Insights Configuration" should be removed from Entra ID.'
        }
    } catch {
        throw
    } finally {
        #Disconnect-MgGraph -WarningAction SilentlyContinue -ErrorAction SilentlyContinue | Out-Null
        #$AzAccountContext | Disconnect-AzAccount -WarningAction SilentlyContinue -ErrorAction SilentlyContinue | Out-Null
        #Disconnect-ExchangeOnline -Confirm:$false -WarningAction SilentlyContinue -ErrorAction SilentlyContinue | Out-Null
    }
}

Grant-TDCErhvervSecurityInsightsPermissions @PSBoundParameters