Public/Permissions/Invoke-365TuneTestAzure.ps1

function Invoke-365TuneTestAzure {
    <#
    .SYNOPSIS
        Tests whether Azure Reader permissions are correctly assigned to the 365TUNE Enterprise App.

    .DESCRIPTION
        Checks for Reader role assignments at root scope "/" and AAD IAM scope
        "/providers/Microsoft.aadiam" for the 365TUNE Service Principal.
        Read-only - makes no changes.

    .EXAMPLE
        Invoke-365TuneTestAzure

    .NOTES
        Author : Metawise Consulting LLC
        Module : 365TUNE
        Version : 2.2.7
    #>


    [CmdletBinding()]
    param(
        [switch]$SkipAuth
    )

    $displayNameProd = "365TUNE - Security and Compliance"
    $displayNameBeta = "365TUNE - Security and Compliance - Beta"

    Write-Host "`n======================================================" -ForegroundColor Cyan
    Write-Host " 365TUNE - Test Azure Permissions" -ForegroundColor Cyan
    Write-Host "======================================================`n" -ForegroundColor Cyan

    # Step 1 - Modules
    Write-Host "[1/4] Checking required modules..." -ForegroundColor Cyan
    foreach ($module in @("Az.Accounts", "Az.Resources")) {
        if (-not (Get-Module -ListAvailable -Name $module)) {
            Install-Module -Name $module -Force -Scope CurrentUser
        }
    }
    Import-Module Az.Accounts, Az.Resources
    Write-Host " [OK] Modules ready." -ForegroundColor Green

    # Step 2 - Authenticate
    Write-Host "`n[2/4] Authenticating..." -ForegroundColor Cyan
    if (-not $SkipAuth) {
        $inCloudShell = ($env:ACC_CLOUD -eq "PROD") -or
                        ($env:POWERSHELL_DISTRIBUTION_CHANNEL -like "*CloudShell*") -or
                        ($env:AZUREPS_HOST_ENVIRONMENT -like "*cloud-shell*")
        if ($inCloudShell) {
            Connect-AzAccount -Identity -WarningAction SilentlyContinue | Out-Null
        } else {
            Disconnect-AzAccount -ErrorAction SilentlyContinue | Out-Null
            Connect-AzAccount -WarningAction SilentlyContinue | Out-Null
        }
    }
    $context = Get-AzContext
    if (-not $context) { throw "Not authenticated." }
    Write-Host " Tenant : $($context.Tenant.Id)" -ForegroundColor Gray
    Write-Host " Account : $($context.Account.Id)" -ForegroundColor Gray
    Write-Host " [OK] Authenticated." -ForegroundColor Green

    # Step 3 - Resolve SP
    Write-Host "`n[3/4] Looking up 365TUNE Service Principal..." -ForegroundColor Cyan
    $graphToken    = Get-AzAccessToken -ResourceUrl "https://graph.microsoft.com" -ErrorAction Stop
    $graphTokenStr = if ($graphToken.Token -is [System.Security.SecureString]) { [System.Net.NetworkCredential]::new("", $graphToken.Token).Password } else { $graphToken.Token }
    $graphHeaders  = @{ Authorization = "Bearer $graphTokenStr"; "Content-Type" = "application/json" }

    $spFilter   = [Uri]::EscapeDataString("displayName eq '$displayNameProd'")
    $spResponse = Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/servicePrincipals?`$filter=$spFilter" -Headers $graphHeaders -Method GET
    $sp         = $spResponse.value | Select-Object -First 1
    if (-not $sp) {
        Write-Host " '$displayNameProd' not found - trying Beta..." -ForegroundColor Yellow
        $spFilter   = [Uri]::EscapeDataString("displayName eq '$displayNameBeta'")
        $spResponse = Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/servicePrincipals?`$filter=$spFilter" -Headers $graphHeaders -Method GET
        $sp         = $spResponse.value | Select-Object -First 1
    }
    if (-not $sp) { throw "Service Principal not found. Ensure the 365TUNE app has been consented to in this tenant." }
    $servicePrincipalId = $sp.id
    Write-Host " Display Name : $($sp.displayName)"
    Write-Host " Object ID : $servicePrincipalId"
    Write-Host " [OK] Service Principal found." -ForegroundColor Green

    # Step 4 - Elevate, check, then de-elevate (elevation required to see aadiam assignments)
    Write-Host "`n[4/4] Checking Azure role assignments..." -ForegroundColor Cyan
    Write-Host " Elevating temporarily to read all scopes..." -ForegroundColor Gray
    Invoke-365TuneElevation

    # Reconnect for fresh token with elevation baked in
    $currentTenantId = $context.Tenant.Id
    Disconnect-AzAccount -ErrorAction SilentlyContinue | Out-Null
    $inCloudShell = ($env:ACC_CLOUD -eq "PROD") -or
                    ($env:POWERSHELL_DISTRIBUTION_CHANNEL -like "*CloudShell*") -or
                    ($env:AZUREPS_HOST_ENVIRONMENT -like "*cloud-shell*")
    if ($inCloudShell) {
        Connect-AzAccount -Identity -WarningAction SilentlyContinue | Out-Null
    } else {
        Connect-AzAccount -TenantId $currentTenantId -WarningAction SilentlyContinue | Out-Null
    }

    $rootAssignment = Get-AzRoleAssignment `
        -ObjectId           $servicePrincipalId `
        -RoleDefinitionName "Reader" `
        -Scope              "/" `
        -SkipClientSideScopeValidation `
        -ErrorAction SilentlyContinue |
        Where-Object { $_.Scope -eq "/" }

    $aadIamAssignment = Get-AzRoleAssignment `
        -ObjectId           $servicePrincipalId `
        -RoleDefinitionName "Reader" `
        -Scope              "/providers/Microsoft.aadiam" `
        -SkipClientSideScopeValidation `
        -ErrorAction SilentlyContinue |
        Where-Object { $_.Scope -eq "/providers/Microsoft.aadiam" }

    Remove-365TuneElevation

    Write-Host ""
    if ($rootAssignment) {
        Write-Host " [OK] Reader at / ASSIGNED" -ForegroundColor Green
    } else {
        Write-Host " [FAIL] Reader at / MISSING" -ForegroundColor Red
    }

    if ($aadIamAssignment) {
        Write-Host " [OK] Reader at /providers/Microsoft.aadiam ASSIGNED" -ForegroundColor Green
    } else {
        Write-Host " [FAIL] Reader at /providers/Microsoft.aadiam MISSING" -ForegroundColor Red
    }

    $allOk = $rootAssignment -and $aadIamAssignment

    Write-Host ""
    Write-Host "======================================================" -ForegroundColor Cyan
    if ($allOk) {
        Write-Host " Azure permissions OK. [OK]" -ForegroundColor Green
    } else {
        Write-Host " Azure permissions INCOMPLETE. Run Invoke-365TuneConnectAzure." -ForegroundColor Red
    }
    Write-Host "======================================================`n" -ForegroundColor Cyan

    return $allOk
}