Public/Permissions/Invoke-365TuneRevokeAzure.ps1

function Invoke-365TuneRevokeAzure {
    <#
    .SYNOPSIS
        Revokes Reader permissions previously granted to the 365TUNE Enterprise App in Azure.

    .DESCRIPTION
        Removes Reader access from root scope "/" and AAD IAM scope
        "/providers/Microsoft.aadiam". Safe to re-run — exits cleanly
        if no assignments are found.

        Run from local PowerShell or Cloud Shell.
        Your account must have Global Administrator rights and
        "Access management for Azure resources" enabled in Entra ID > Properties.

    .EXAMPLE
        Invoke-365TuneRevokeAzure

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


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

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

    Write-Host "`n══════════════════════════════════════════════════════" -ForegroundColor Cyan
    Write-Host " 365TUNE — Revoke Azure Permissions" -ForegroundColor Cyan
    Write-Host "══════════════════════════════════════════════════════`n" -ForegroundColor Cyan

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

    # Step 2 — Authenticate
    Write-Host "`n[2/5] Authenticating..." -ForegroundColor Cyan

    $inCloudShell = ($env:ACC_CLOUD -eq "PROD") -or
                    ($env:POWERSHELL_DISTRIBUTION_CHANNEL -like "*CloudShell*") -or
                    ($env:AZUREPS_HOST_ENVIRONMENT -like "*cloud-shell*")

    if (-not $SkipAuth) {
        if ($inCloudShell) {
            Write-Host " Cloud Shell detected — using existing session." -ForegroundColor Gray
        } else {
            Disconnect-AzAccount -ErrorAction SilentlyContinue | Out-Null
            Connect-AzAccount -WarningAction SilentlyContinue | Out-Null
        }
    }
    $context = Get-AzContext
    if (-not $context) { throw "Not authenticated. Please try again." }
    Write-Host " Tenant : $($context.Tenant.Id)" -ForegroundColor Gray
    Write-Host " Account : $($context.Account.Id)" -ForegroundColor Gray
    Write-Host " ✅ Authenticated." -ForegroundColor Green

    # Step 3 — Fetch Service Principal via Graph API (avoids Az.MSGraph dependency issues)
    Write-Host "`n[3/5] Looking up 365TUNE Service Principal..." -ForegroundColor Cyan
    $graphTokenObj = Get-AzAccessToken -ResourceUrl "https://graph.microsoft.com"
    if ($graphTokenObj.Token -is [System.Security.SecureString]) {
        $graphToken = [System.Net.NetworkCredential]::new("", $graphTokenObj.Token).Password
    } else {
        $graphToken = $graphTokenObj.Token
    }
    $graphHeaders = @{ Authorization = "Bearer $graphToken"; "Content-Type" = "application/json" }

    function Find-365TuneSP ($name) {
        $encoded  = [Uri]::EscapeDataString("displayName eq '$name'")
        $response = Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/servicePrincipals?`$filter=$encoded" -Headers $graphHeaders -Method GET
        $response.value | Select-Object -First 1 | Select-Object id, appId, displayName
    }

    $sp = Find-365TuneSP $displayNameProd
    if (-not $sp) {
        Write-Host " '$displayNameProd' not found — trying Beta..." -ForegroundColor Yellow
        $sp = Find-365TuneSP $displayNameBeta
    }
    if (-not $sp) {
        throw "Service Principal not found. Tried '$displayNameProd' and '$displayNameBeta'. Ensure the 365TUNE app has been consented to in this tenant."
    }
    $displayName        = $sp.displayName
    $servicePrincipalId = $sp.id
    Write-Host " Object ID : $servicePrincipalId"

    # Step 4 — Pre-flight check
    Write-Host "`n[4/5] Checking for existing assignments..." -ForegroundColor Cyan
    $existing       = Invoke-AzRestMethod `
        -Path   "/providers/Microsoft.aadiam/providers/Microsoft.Authorization/roleAssignments?api-version=2022-04-01&`$filter=principalId eq '$servicePrincipalId'" `
        -Method GET
    $existingValues = ($existing.Content | ConvertFrom-Json).value

    if ($existingValues.Count -eq 0) {
        Write-Host "`n══════════════════════════════════════════════════════" -ForegroundColor Cyan
        Write-Host " No assignments found — nothing to revoke. ✅" -ForegroundColor Green
        Write-Host "══════════════════════════════════════════════════════`n" -ForegroundColor Cyan
        return
    }
    Write-Host " Found $($existingValues.Count) assignment(s) — proceeding with removal." -ForegroundColor Yellow

    # Step 5 — Elevate and remove
    Write-Host "`n[5/5] Elevating and removing Reader permissions..." -ForegroundColor Cyan
    Invoke-365TuneElevation

    try {
        Remove-AzRoleAssignment `
            -ObjectId           $servicePrincipalId `
            -Scope              "/" `
            -RoleDefinitionName "Reader" `
            -SkipClientSideScopeValidation `
            -ErrorAction Stop
        Write-Host " ✅ Reader removed from '/'" -ForegroundColor Green
    } catch {
        if ($_.Exception.Message -like "*does not exist*" -or
            $_.Exception.Message -like "*NotFound*"       -or
            $_.Exception.Message -like "*does not map*"   -or
            $_.Exception.Message -like "*Forbidden*") {
            Write-Warning " ⚠️ Reader at '/' not found — already removed"
        } else { throw }
    }

    $aadIamValues = $existingValues |
        Where-Object { $_.properties.scope -eq "/providers/Microsoft.aadiam" }

    if ($aadIamValues) {
        foreach ($assignment in $aadIamValues) {
            $del = Invoke-AzRestMethod `
                -Path   "/providers/Microsoft.aadiam/providers/Microsoft.Authorization/roleAssignments/$($assignment.name)?api-version=2022-04-01" `
                -Method DELETE
            if ($del.StatusCode -in @(200, 204)) {
                Write-Host " ✅ Reader removed from '/providers/Microsoft.aadiam'" -ForegroundColor Green
            } else {
                Write-Warning " ⚠️ aadiam removal returned status $($del.StatusCode)"
            }
        }
    } else {
        Write-Warning " ⚠️ Reader at '/providers/Microsoft.aadiam' not found — already removed"
    }

    $remaining       = Invoke-AzRestMethod `
        -Path   "/providers/Microsoft.aadiam/providers/Microsoft.Authorization/roleAssignments?api-version=2022-04-01&`$filter=principalId eq '$servicePrincipalId'" `
        -Method GET
    $remainingValues = ($remaining.Content | ConvertFrom-Json).value

    if ($remainingValues.Count -eq 0) {
        Write-Host " ✅ Verified — no assignments remain." -ForegroundColor Green
    } else {
        Write-Warning " ⚠️ $($remainingValues.Count) assignment(s) still remain — check Azure Portal."
    }

    Remove-365TuneElevation

    Write-Host "`n══════════════════════════════════════════════════════" -ForegroundColor Cyan
    Write-Host " 365TUNE Azure permissions revoked. ✅" -ForegroundColor Green
    Write-Host " Tenant : $($context.Tenant.Id)" -ForegroundColor Green
    Write-Host " Account : $($context.Account.Id)" -ForegroundColor Green
    Write-Host "══════════════════════════════════════════════════════`n" -ForegroundColor Cyan
}