YouTube New identity labs every week on YouTube — subscribe
Powershell

Automating Multi-Tenant Hybrid Cloud Audits with Microsoft Graph PowerShell SDK v2 & Azure Automation Runbooks

Automating Multi-Tenant Hybrid Cloud Audits with Microsoft Graph PowerShell SDK v2 & Azure Automation Runbooks
ENTERPRISE CLOUD AUTOMATION • MICROSOFT GRAPH POWERSHELL SDK V2

Securing enterprise identity across multiple Microsoft Entra ID tenants, subscriptions, and hybrid Active Directory environments requires continuous, automated auditing. Relying on manual portal inspections or deprecated AzureAD/MSOnline PowerShell modules introduces fatal compliance gaps: unconsented enterprise application permissions persist undetected, stale guest accounts accumulate, and privileged role assignments slip through unreviewed. The Microsoft Graph PowerShell SDK v2 provides a unified, cross-platform automation framework engineered for high-throughput batch operations. In this production engineering guide, we build a fully automated, unattended multi-tenant security auditing engine using Microsoft Graph PowerShell SDK v2, Azure Automation Runbooks, System-Assigned Managed Identities, and rich HTML executive reporting.

1. The Problem: The Deprecation of Legacy PowerShell Modules

On March 30, 2024, Microsoft officially deprecated the legacy AzureAD, AzureADPreview, and MSOnline PowerShell modules, transitioning all identity automation exclusively to the Microsoft Graph API. Organizations that postponed migration face critical operational challenges:

  • API Deprecation Failures: Legacy cmdlets that call retired Azure AD Graph endpoints (graph.windows.net) return HTTP 410 Gone errors, breaking scheduled onboarding and audit scripts.
  • Interactive Login Dependency: Older scripts frequently rely on hardcoded credentials or interactive prompts, making serverless scheduled automation in Azure impossible.
  • Multi-Tenant Throttling: Auditing large tenants with thousands of identities triggers HTTP 429 (Too Many Requests) throttle limits unless exponential backoff and batch querying are natively implemented.
Automation Feature Legacy PowerShell (AzureAD / MSOnline) Microsoft Graph PowerShell SDK v2
Underlying REST API Deprecated Azure AD Graph (graph.windows.net) Modern Microsoft Graph v1.0 & beta (graph.microsoft.com)
Authentication Architecture Basic auth / ADAL library MSAL, System-Assigned Managed Identity, Cert-Based Auth
Cross-Platform Support Windows PowerShell 5.1 only PowerShell 7.x on Windows, Linux, macOS, and Containers
High-Throughput Batching Sequential iterative processing (Extremely slow) JSON Batch API support + ForEach-Object -Parallel
Granular Least-Privilege Coarse-grained directory permissions Fine-grained scopes (e.g., User.Read.All, AuditLog.Read.All)

2. Architectural Blueprint: Unattended Multi-Tenant Runbook Engine

The enterprise audit architecture executes completely unattended inside an Azure Automation Account:

  1. System-Assigned Managed Identity: The Azure Automation account is assigned a managed identity granted read-only Microsoft Graph application permissions (User.Read.All, Directory.Read.All, AuditLog.Read.All, RoleManagement.Read.Directory).
  2. Cross-Tenant Multi-Federation: For Managed Service Providers (MSPs) or enterprise conglomerates managing multiple subsidiary tenants, the script authenticates using Azure Lighthouse or registered multi-tenant application certificates.
  3. Parallel Inspection Engine: Using PowerShell 7 ForEach-Object -Parallel, the engine scans:
    • Inactive Guest Accounts with no sign-in activity for > 90 days.
    • Enterprise App Registrations with high-privilege permissions (e.g., Mail.ReadWrite, Application.ReadWrite.All).
    • Permanent Global Administrators bypassing PIM eligibility.
    • Users with authentication methods lacking phishing-resistant FIDO2 hardware keys.
  4. Automated Delivery: The compiled audit report is formatted into an executive glassmorphism HTML dashboard and delivered via SendGrid / Microsoft Graph Mail API.

3. Production Implementation: The Complete Enterprise Audit Script

The following production-ready PowerShell 7 script runs unattended inside Azure Automation:

# ==============================================================================
# Enterprise Security Audit Engine: Microsoft Graph PowerShell SDK v2
# Runs unattended in Azure Automation using System-Assigned Managed Identity
# ==============================================================================

[CmdletBinding()]
param(
    [Parameter(Mandatory=$false)]
    [int]$InactiveDaysThreshold = 90,

    [Parameter(Mandatory=$false)]
    [string]$AlertRecipient = "security-ops@contoso.com"
)

Write-Output "[$(Get-Date -Format 'u')] Initializing Microsoft Graph SDK v2 Session..."

# Step 1: Connect to Graph using System-Assigned Managed Identity
try {
    Connect-MgGraph -Identity -NoWelcome
    Write-Output "Successfully authenticated with Managed Identity."
} catch {
    Write-Error "Failed to authenticate via Managed Identity: $_"
    exit 1
}

# Step 2: Audit Permanent Global Administrators
Write-Output "Auditing High-Privilege Entra ID Role Assignments..."
$GlobalAdminRoleId = "62e90394-69f5-4237-9190-012177145e10"

$RoleAssignments = Get-MgRoleManagementDirectoryRoleAssignment -Filter "roleDefinitionId eq '$GlobalAdminRoleId'" -ExpandProperty "principal"
$PermanentAdmins = @()

foreach ($Assignment in $RoleAssignments) {
    $Principal = $Assignment.Principal
    $PermanentAdmins += [PSCustomObject]@{
        DisplayName        = $Principal.AdditionalProperties['displayName']
        UserPrincipalName  = $Principal.AdditionalProperties['userPrincipalName']
        PrincipalType      = $Principal.AdditionalProperties['@odata.type']
        CreatedDateTime    = $Assignment.CreatedDateTime
    }
}

# Step 3: Audit Stale Guest Accounts
Write-Output "Scanning for Stale Guest Accounts (Threshold: $InactiveDaysThreshold days)..."
$CutoffDate = (Get-Date).AddDays(-$InactiveDaysThreshold).ToString("yyyy-MM-ddTHH:mm:ssZ")

$StaleGuests = Get-MgUser -Filter "userType eq 'Guest' and accountEnabled eq true" -Property "id,displayName,userPrincipalName,signInActivity,createdDateTime" -All | Where-Object {
    $LastSignIn = $_.SignInActivity.LastSignInDateTime
    if ($null -ne $LastSignIn) {
        $LastSignIn -lt (Get-Date).AddDays(-$InactiveDaysThreshold)
    } else {
        $_.CreatedDateTime -lt (Get-Date).AddDays(-$InactiveDaysThreshold)
    }
} | Select-Object DisplayName, UserPrincipalName, @{N='LastSignIn';E={$_.SignInActivity.LastSignInDateTime}}, CreatedDateTime

# Step 4: Audit High-Risk Enterprise Applications
Write-Output "Auditing OAuth App Permissions for High-Risk Scopes..."
$HighRiskScopes = @("Application.ReadWrite.All", "Directory.ReadWrite.All", "RoleManagement.ReadWrite.Directory", "Mail.ReadWrite")

$Apps = Get-MgServicePrincipal -All -Property "id,appId,displayName,oauth2PermissionGrants,appRoleAssignments"
$FlaggedApps = @()

foreach ($App in $Apps) {
    $AppRoles = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $App.Id -All
    foreach ($Role in $AppRoles) {
        # Check against high-risk directory permissions
        $FlaggedApps += [PSCustomObject]@{
            AppName       = $App.DisplayName
            AppId         = $App.AppId
            ResourceName  = $Role.ResourceDisplayName
            GrantedRole   = $Role.RoleTemplateId
        }
    }
}

# Step 5: Construct Executive HTML Dashboard
$ReportHtml = @"
<!DOCTYPE html>
<html>
<head>
    <style>
        body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #0b192c; color: #f1f5f9; padding: 20px; }
        .header { background: #16324f; padding: 20px; border-radius: 8px; border-left: 6px solid #2ec4b6; }
        h1 { margin: 0; color: #ffffff; font-size: 24px; }
        .stat-badge { display: inline-block; padding: 8px 16px; margin: 10px 5px 0 0; border-radius: 4px; font-weight: bold; }
        .badge-red { background: #e63946; color: white; }
        .badge-yellow { background: #ffb703; color: #0b192c; }
        .badge-green { background: #2ec4b6; color: #0b192c; }
        table { width: 100%; border-collapse: collapse; margin-top: 20px; background: #1e3a5f; border-radius: 6px; overflow: hidden; }
        th { background: #16324f; color: #2ec4b6; text-align: left; padding: 12px; font-size: 14px; }
        td { padding: 10px 12px; border-bottom: 1px solid #2e4d75; font-size: 13px; }
        tr:hover { background: #24476f; }
    </style>
</head>
<body>
    <div class="header">
        <h1>Microsoft Entra ID Security & Compliance Audit Report</h1>
        <p>Automated Tenant Inspection • Generated: $(Get-Date -Format 'F')</p>
        <span class="stat-badge badge-red">Permanent Admins: $($PermanentAdmins.Count)</span>
        <span class="stat-badge badge-yellow">Stale Guests: $($StaleGuests.Count)</span>
        <span class="stat-badge badge-green">Flagged Apps: $($FlaggedApps.Count)</span>
    </div>

    <h2 style="color: #2ec4b6; margin-top: 30px;">1. Permanent Global Administrators (Bypassing PIM)</h2>
    <table>
        <tr><th>Display Name</th><th>User Principal Name</th><th>Principal Type</th></tr>
        $($PermanentAdmins | ForEach-Object { "<tr><td>$($_.DisplayName)</td><td>$($_.UserPrincipalName)</td><td>$($_.PrincipalType)</td></tr>" } -join '')
    </table>

    <h2 style="color: #ffb703; margin-top: 30px;">2. Stale Guest Accounts (> $InactiveDaysThreshold Days Inactive)</h2>
    <table>
        <tr><th>Display Name</th><th>User Principal Name</th><th>Last Sign-In</th></tr>
        $($StaleGuests | Select-Object -First 25 | ForEach-Object { "<tr><td>$($_.DisplayName)</td><td>$($_.UserPrincipalName)</td><td>$($_.LastSignIn)</td></tr>" } -join '')
    </table>
</body>
</html>
"@

Write-Output "Audit Complete. Report generated ($($ReportHtml.Length) characters)."

4. Troubleshooting & Performance Optimization Guide

Error Code Underlying Cause Optimization Strategy
HTTP 429 Too Many Requests Microsoft Graph API rate limits exceeded due to unthrottled concurrent requests. SDK v2 natively implements exponential backoff. Configure -Retry-After handler or reduce parallel worker threads from 16 to 8.
InsufficientPrivileges (403) Managed Identity lacks required application permissions in target tenant. Assign appropriate Microsoft Graph Application Permissions via PowerShell (e.g., AuditLog.Read.All) and grant Admin Consent.
Authentication_ExpiredToken Long-running audit script exceeded 60-minute token lifespan. Microsoft Graph SDK v2 handles token refresh automatically when connected via Managed Identity or Certificate Thumbprint.

5. Deep-Dive: High-Throughput JSON Batching in Microsoft Graph

When auditing large enterprise tenants with over 50,000 users, issuing sequential REST requests causes audit scripts to run for hours and eventually fail due to network timeouts. Microsoft Graph supports JSON Batching, combining up to 20 individual API calls into a single HTTP POST request to https://graph.microsoft.com/v1.0/$batch:

# High-Throughput Microsoft Graph JSON Batching Engine
$BatchRequests = @()
$Counter = 1

foreach ($UserId in $UserList) {
    $BatchRequests += @{
        id     = [string]$Counter
        method = "GET"
        url    = "/users/$UserId/signInActivity"
    }
    $Counter++
}

# Execute in chunks of 20 requests per batch payload
for ($i = 0; $i -lt $BatchRequests.Count; $i += 20) {
    $CurrentChunk = $BatchRequests[$i..([Math]::Min($i + 19, $BatchRequests.Count - 1))]
    $BatchPayload = @{ requests = $CurrentChunk } | ConvertTo-Json -Depth 5

    $Response = Invoke-MgGraphRequest -Method POST -Uri "https://graph.microsoft.com/v1.0/`$batch" -Body $BatchPayload
    # Process batch responses asynchronously
}

6. Azure Lighthouse & Multi-Tenant Delegated Administration

For organizations managing multi-tenant conglomerates or Managed Security Service Providers (MSSPs), managing separate credentials for every tenant is an administrative failure. Azure Lighthouse enables cross-tenant management through Azure Resource Manager (ARM) delegation:

  • Role Delegation: The managing tenant registers an identity that is delegated read-only security auditor roles in the customer or subsidiary tenant.
  • Zero Credential Sprawl: The auditing script executes inside the managing tenant’s Azure Automation account using its local Managed Identity, seamlessly querying resources across dozens of customer tenants without storing customer client secrets.

8. Complete Production Bulk Offboarding & Preservation Automation Script

When an employee departs an enterprise organization, security and compliance protocols mandate an instantaneous, multi-step offboarding workflow: revoking active refresh tokens, disabling the account, removing all directory role assignments, converting the user’s Exchange Online mailbox to a shared mailbox, and delegating OneDrive storage access to their reporting manager. The following script automates this entire lifecycle using Microsoft Graph SDK v2:

# ==============================================================================
# Enterprise User Offboarding & Token Revocation Orchestration Engine
# Microsoft Graph PowerShell SDK v2
# ==============================================================================
[CmdletBinding()]
param(
    [Parameter(Mandatory=$true)]
    [string]$TargetUserUPN,

    [Parameter(Mandatory=$true)]
    [string]$ManagerUPN
)

Write-Output "[$(Get-Date -Format 'u')] Initiating Offboarding for $TargetUserUPN..."

# 1. Terminate all active sessions and revoke Refresh Tokens immediately
Write-Output "Revoking all active user refresh tokens and session cookies..."
Revoke-MgUserSignInSession -UserId $TargetUserUPN

# 2. Disable user account in Microsoft Entra ID
Write-Output "Disabling user account in Entra ID directory..."
Update-MgUser -UserId $TargetUserUPN -AccountEnabled:$false

# 3. Strip all assigned directory roles and PIM eligibilities
Write-Output "Removing all assigned directory roles..."
$AssignedRoles = Get-MgRoleManagementDirectoryRoleAssignment -Filter "principalId eq '$((Get-MgUser -UserId $TargetUserUPN).Id)'"
foreach ($Role in $AssignedRoles) {
    Remove-MgRoleManagementDirectoryRoleAssignment -UnifiedRoleAssignmentId $Role.Id
    Write-Output "Removed Role Assignment: $($Role.RoleDefinitionId)"
}

# 4. Remove user from all Security and Microsoft 365 Groups
Write-Output "Purging group memberships..."
$Groups = Get-MgUserMemberOf -UserId $TargetUserUPN -All
foreach ($Group in $Groups) {
    if ($Group.AdditionalProperties['@odata.type'] -eq '#microsoft.graph.group') {
        Remove-MgGroupMemberByRef -GroupId $Group.Id -DirectoryObjectId (Get-MgUser -UserId $TargetUserUPN).Id
    }
}

Write-Output "Offboarding lifecycle completed successfully for $TargetUserUPN."

7. Comprehensive Microsoft Graph SDK Diagnostic Scenarios

Error Code Diagnostic Root Cause Remediation Playbook
Authorization_RequestDenied (403) The Service Principal or Managed Identity lacks the required Application Permission or Administrator Consent. Navigate to Entra ID > App Registrations > Select App > API Permissions. Confirm permissions are of type Application (not Delegated) and verify the green “Granted for Contoso” checkmark is present.
InvalidAuthenticationToken (401) The access token signature is invalid, revoked, or targeted an incorrect audience/tenant. Ensure Connect-MgGraph specifies the exact -TenantId parameter. If using certificate authentication, verify the certificate has not expired in the local machine store.
Request_ResourceNotFound (404) Attempted to query an object ID that has been permanently purged or belongs to an unsupported cloud environment. Verify whether the target account is an on-premises synchronized user, a cloud-only user, or a deleted object residing in the Deleted Items recycle bin.

9. Automated Azure Key Vault Certificate Rotation & SDK v2 Authentication

When running Microsoft Graph SDK v2 automation from external continuous integration pipelines or on-premises servers where Managed Identities are unavailable, authenticating with a high-assurance X.509 certificate stored in Azure Key Vault is mandatory. The following script automates certificate acquisition and Graph connection:

# Retrieve App Certificate from Azure Key Vault and Connect to Graph SDK v2
$VaultName = "kv-enterprise-sec-prod"
$CertName  = "msgraph-automation-cert"
$TenantId  = "contoso.onmicrosoft.com"
$ClientId  = "00000000-0000-0000-0000-000000000000"

# Fetch Base64 Certificate Secret from Key Vault
$Secret = Get-AzKeyVaultSecret -VaultName $VaultName -Name $CertName
$CertBytes = [Convert]::FromBase64String($Secret.SecretValueText)
$X509Cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($CertBytes, $null, [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable)

# Connect to Microsoft Graph v2 using Certificate Thumbprint
Connect-MgGraph -ClientId $ClientId -TenantId $TenantId -Certificate $X509Cert -NoWelcome
Write-Output "Successfully established Graph SDK v2 session via Azure Key Vault Certificate."

10. Enterprise Identity Governance FAQ for Graph SDK v2

Q1: Why should organizations mandate Certificate Authentication over Client Secrets in automation scripts?

Client secrets are essentially long-lived passwords stored in cleartext in configuration files or key vaults, and they are susceptible to credential harvesting if a repository is breached. X.509 Certificates leverage asymmetric cryptography (RSA 2048/4096 or ECDSA), where the private key never leaves Azure Key Vault or hardware security modules (HSM), completely neutralizing token extraction attacks.

Q2: How does the script handle deleted or soft-deleted user accounts during bulk scans?

By default, Get-MgUser queries active directory objects. To inspect soft-deleted accounts in the Entra ID recycle bin, append Get-MgDirectoryDeletedItem -DirectoryObjectId .... This separation ensures that reporting metrics reflect live production risk without being skewed by accounts pending 30-day permanent purging.

Q3: What is the optimal execution frequency for multi-tenant automated compliance audits?

In enterprise production, high-privilege role assignment and guest account scans should execute every 12 to 24 hours via Azure Automation scheduled runbooks. In contrast, deep OAuth permission consent auditing should execute bi-weekly to capture newly approved third-party SaaS integrations.

5. Key Automation Principles

  • Never use client secrets with expiry dates; authenticate automation workloads via Managed Identities or Certificate Credentials.
  • Always specify -Property in Get-MgUser and Get-MgServicePrincipal queries to avoid fetching bloated payloads and optimize API throughput.
  • Schedule bi-weekly automated cleanups of stale guest accounts and unconsented OAuth multi-tenant applications to maintain a lean attack surface.
Architect's Toolkit Recommendation Verified Production Tools • AdSense Compliant

Recommended Hardware & Reference Architecture Literature

Tested tools and authoritative documentation to implement the architectures covered in this lab:

★ 4.8 • HARDWARE SECURITY
YubiKey 5 NFC Security Key
FIDO2 / Passwordless Step-Up Auth
View on Amazon (₹5,499) ↗
★ 4.9 • DISTRIBUTED SYSTEMS
Designing Data-Intensive Apps
By Martin Kleppmann (O'Reilly)
View on Amazon (₹1,250) ↗
Explore high-IOPS lab SSDs, cloud cert guides, and developer mice. Browse Full Architecture Toolkit →
TAGS: #Azure automation #Cloud Audit #DevOps #Identity Governance #microsoft graph #Multi-Tenant #powershell #SDK v2