Scripts/Run-AzureAppRegAudit.ps1
|
<#
.SYNOPSIS Orchestration script for Azure App Registration audit. .DESCRIPTION End-to-end orchestration script that handles: 1. Pre-requisite module verification and installation 2. Authentication to Microsoft Graph and Azure 3. Output directory setup with timestamped folders 4. Calling Invoke-PSUAzureAppRegAudit with the chosen audit mode 5. Post-processing: HTML report generation, folder opening, console summary Three audit modes are available: - Quick : Skips per-app sign-in log queries (~30 min for 9,000 apps) - Full : Collects all 13 signals including sign-in logs (~3-4 hours) - DryRun : Processes only the first N apps for testing This script is NOT a module function. Run it directly from the Scripts/ folder. .PARAMETER AuditMode Select the audit mode. - Quick : SkipSignInLogs enabled (~30 min) - Full : All signals collected (~3-4 hrs) - DryRun : Process only -DryRunCount apps Default is Quick. .PARAMETER TenantId (Optional) The Entra ID tenant ID. Auto-detected from Graph context if omitted. .PARAMETER OutputRoot (Optional) Root folder for audit output. A timestamped subfolder is created. Default is C:\AuditOutput. .PARAMETER DryRunCount (Optional) Number of apps to process in DryRun mode. Default is 50. .PARAMETER SkipAzureRBAC (Optional) Skip Azure RBAC data collection even in Full mode. .PARAMETER ThrottleDelayMs (Optional) Base delay in milliseconds between per-app Graph calls. Default is 200. .PARAMETER SkipHtmlReport (Optional) Skip HTML report generation after audit. .PARAMETER NoOpenFolder (Optional) Do not auto-open the output folder after audit. .PARAMETER GraphScopes (Optional) Graph scopes to request during Connect-MgGraph. Default includes Application.Read.All, Directory.Read.All, AuditLog.Read.All, Policy.Read.All. .EXAMPLE .\Run-AzureAppRegAudit.ps1 Runs a Quick audit with default settings. .EXAMPLE .\Run-AzureAppRegAudit.ps1 -AuditMode Full -OutputRoot "D:\Audits" Runs a Full audit with output saved under D:\Audits. .EXAMPLE .\Run-AzureAppRegAudit.ps1 -AuditMode DryRun -DryRunCount 20 -SkipAzureRBAC Processes 20 apps without Azure RBAC, for testing. .EXAMPLE .\Run-AzureAppRegAudit.ps1 -AuditMode Quick -TenantId "abc-123" -SkipHtmlReport Quick audit for specific tenant, no HTML report. .NOTES Author: Lakshmanachari Panuganti Created: 7th March 2026 Last Modified: 7th March 2026 Version: 1.0 .LINK https://github.com/lakshmanachari-panuganti/OMG.PSUtilities/tree/main/OMG.PSUtilities.AzureCore #> #Requires -Version 7.2 [CmdletBinding(SupportsShouldProcess)] param ( [Parameter()] [ValidateSet("Quick", "Full", "DryRun")] [string]$AuditMode = "Quick", [Parameter()] [string]$TenantId, [Parameter()] [string]$OutputRoot = "C:\AuditOutput", [Parameter()] [int]$DryRunCount = 50, [Parameter()] [switch]$SkipAzureRBAC, [Parameter()] [int]$ThrottleDelayMs = 200, [Parameter()] [switch]$SkipHtmlReport, [Parameter()] [switch]$NoOpenFolder, [Parameter()] [string[]]$GraphScopes = @( "Application.Read.All", "Directory.Read.All", "AuditLog.Read.All", "Policy.Read.All" ) ) $ErrorActionPreference = "Stop" $scriptStart = Get-Date #region Banner $bannerChar = [char]0x2550 Write-Host "" Write-Host ([string]::new($bannerChar, 70)) -ForegroundColor Cyan Write-Host " Azure App Registration Audit - Orchestration Script" -ForegroundColor Cyan Write-Host " Mode: $AuditMode | Started: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -ForegroundColor Cyan Write-Host ([string]::new($bannerChar, 70)) -ForegroundColor Cyan Write-Host "" #endregion #region Step 1: Pre-requisite Module Check Write-Host "[1/6] Checking pre-requisite modules..." -ForegroundColor White $requiredModules = @( @{ Name = "Microsoft.Graph.Authentication"; MinVer = "2.0.0" }, @{ Name = "Microsoft.Graph.Applications"; MinVer = "2.0.0" }, @{ Name = "Microsoft.Graph.Reports"; MinVer = "2.0.0" }, @{ Name = "Microsoft.Graph.Identity.Governance"; MinVer = "2.0.0" }, @{ Name = "Microsoft.Graph.Identity.DirectoryManagement"; MinVer = "2.0.0" } ) if (-not $SkipAzureRBAC) { $requiredModules += @{ Name = "Az.Resources"; MinVer = "6.0.0" } $requiredModules += @{ Name = "Az.Accounts"; MinVer = "2.0.0" } } $missingModules = @() foreach ($mod in $requiredModules) { $installed = Get-Module -ListAvailable -Name $mod.Name | Sort-Object Version -Descending | Select-Object -First 1 if ($null -eq $installed) { $missingModules += $mod.Name Write-Host " [MISSING] $($mod.Name)" -ForegroundColor Red } elseif ($installed.Version -lt [version]$mod.MinVer) { $missingModules += "$($mod.Name) (need >= $($mod.MinVer), found $($installed.Version))" Write-Host " [OUTDATED] $($mod.Name) v$($installed.Version) (need >= $($mod.MinVer))" -ForegroundColor Yellow } else { Write-Host " [OK] $($mod.Name) v$($installed.Version)" -ForegroundColor Green } } if ($missingModules.Count -gt 0) { Write-Host "" Write-Host " Missing or outdated modules detected:" -ForegroundColor Yellow $missingModules | ForEach-Object { Write-Host " - $_" -ForegroundColor Yellow } Write-Host "" $installChoice = Read-Host " Install missing modules now? (Y/N)" if ($installChoice -eq "Y" -or $installChoice -eq "y") { foreach ($mod in $requiredModules) { $installed = Get-Module -ListAvailable -Name $mod.Name | Sort-Object Version -Descending | Select-Object -First 1 if ($null -eq $installed -or $installed.Version -lt [version]$mod.MinVer) { Write-Host " Installing $($mod.Name)..." -ForegroundColor Cyan Install-Module -Name $mod.Name -MinimumVersion $mod.MinVer -Scope CurrentUser -Force -AllowClobber } } Write-Host " Module installation complete." -ForegroundColor Green } else { throw "Cannot proceed without required modules. Install them and re-run." } } # Import the OMG.PSUtilities.AzureCore module $moduleRoot = Split-Path $PSScriptRoot -Parent $modulePsd1 = Join-Path $moduleRoot "OMG.PSUtilities.AzureCore.psd1" if (Test-Path $modulePsd1) { Import-Module $modulePsd1 -Force Write-Host " [OK] OMG.PSUtilities.AzureCore imported" -ForegroundColor Green } else { throw "Cannot find OMG.PSUtilities.AzureCore.psd1 at: $modulePsd1" } Write-Host "" #endregion #region Step 2: Authentication Write-Host "[2/6] Authenticating..." -ForegroundColor White # Graph authentication $mgContext = Get-MgContext -ErrorAction SilentlyContinue if ($null -eq $mgContext) { Write-Host " Connecting to Microsoft Graph..." -ForegroundColor Cyan if ($TenantId) { Connect-MgGraph -Scopes $GraphScopes -TenantId $TenantId -NoWelcome } else { Connect-MgGraph -Scopes $GraphScopes -NoWelcome } $mgContext = Get-MgContext } else { # Validate existing scopes $currentScopes = $mgContext.Scopes $missingScopes = $GraphScopes | Where-Object { $_ -notin $currentScopes } if ($missingScopes.Count -gt 0) { Write-Host " Existing Graph session missing scopes: $($missingScopes -join ', ')" -ForegroundColor Yellow Write-Host " Reconnecting with required scopes..." -ForegroundColor Cyan Disconnect-MgGraph -ErrorAction SilentlyContinue if ($TenantId) { Connect-MgGraph -Scopes $GraphScopes -TenantId $TenantId -NoWelcome } else { Connect-MgGraph -Scopes $GraphScopes -NoWelcome } $mgContext = Get-MgContext } else { Write-Host " Reusing existing Graph session" -ForegroundColor Green } } $effectiveTenantId = $TenantId ?? $mgContext.TenantId $authIdentity = $mgContext.Account ?? $mgContext.AppName ?? "Unknown" Write-Host " Graph : $authIdentity | Tenant: $effectiveTenantId" -ForegroundColor Green # Azure authentication (if needed) if (-not $SkipAzureRBAC) { $azContext = Get-AzContext -ErrorAction SilentlyContinue if ($null -eq $azContext) { Write-Host " Connecting to Azure..." -ForegroundColor Cyan if ($TenantId) { Connect-AzAccount -TenantId $TenantId } else { Connect-AzAccount } $azContext = Get-AzContext } else { Write-Host " Reusing existing Azure session" -ForegroundColor Green } Write-Host " Azure : $($azContext.Account.Id) | Subscription: $($azContext.Subscription.Name)" -ForegroundColor Green } else { Write-Host " Azure : SKIPPED (SkipAzureRBAC)" -ForegroundColor DarkGray } Write-Host "" #endregion #region Step 3: Output Directory Setup Write-Host "[3/6] Setting up output directories..." -ForegroundColor White $runTimestamp = Get-Date -Format "yyyyMMdd_HHmmss" $runFolder = Join-Path $OutputRoot "AppRegAudit_${effectiveTenantId}_${runTimestamp}" if (-not (Test-Path $runFolder)) { New-Item -Path $runFolder -ItemType Directory -Force | Out-Null } $logDir = Join-Path $runFolder "Logs" if (-not (Test-Path $logDir)) { New-Item -Path $logDir -ItemType Directory -Force | Out-Null } Write-Host " Output : $runFolder" -ForegroundColor Green Write-Host " Logs : $logDir" -ForegroundColor Green Write-Host "" #endregion #region Step 4: Mode Configuration Write-Host "[4/6] Configuring audit mode: $AuditMode..." -ForegroundColor White $auditParams = @{ TenantId = $effectiveTenantId OutputDirectory = $runFolder LogDirectory = $logDir ThrottleDelayMs = $ThrottleDelayMs VerboseMode = $true } switch ($AuditMode) { "Quick" { $auditParams["SkipSignInLogs"] = $true $estimatedTime = "~30 minutes" Write-Host " Skip Sign-in Logs : YES (fast mode)" -ForegroundColor Yellow } "Full" { $estimatedTime = "~3-4 hours" Write-Host " Skip Sign-in Logs : NO (full signal coverage)" -ForegroundColor Green } "DryRun" { $auditParams["SkipSignInLogs"] = $true $auditParams["DryRunLimit"] = $DryRunCount $estimatedTime = "~2-5 minutes" Write-Host " Dry Run Limit : $DryRunCount apps" -ForegroundColor Cyan Write-Host " Skip Sign-in Logs : YES" -ForegroundColor Yellow } } if ($SkipAzureRBAC) { $auditParams["SkipAzureRBAC"] = $true Write-Host " Skip Azure RBAC : YES" -ForegroundColor Yellow } Write-Host " Estimated Time : $estimatedTime" -ForegroundColor White Write-Host "" #endregion #region Step 5: Run Audit Write-Host "[5/6] Running Azure App Registration Audit ($AuditMode mode)..." -ForegroundColor White Write-Host " Started at: $(Get-Date -Format 'HH:mm:ss')" -ForegroundColor DarkGray Write-Host "" $auditResult = Invoke-PSUAzureAppRegAudit @auditParams $auditDuration = (Get-Date) - $scriptStart Write-Host "" Write-Host " Audit completed in $([int]$auditDuration.TotalMinutes) min $($auditDuration.Seconds) sec" -ForegroundColor Green Write-Host "" #endregion #region Step 6: Post-Processing Write-Host "[6/6] Post-processing..." -ForegroundColor White # Generate HTML report if (-not $SkipHtmlReport) { $htmlScript = Join-Path $PSScriptRoot "ConvertTo-AuditHtmlReport.ps1" if (Test-Path $htmlScript) { Write-Host " Generating HTML report..." -ForegroundColor Cyan try { & $htmlScript -InputDirectory $runFolder -TenantId $effectiveTenantId $htmlFile = Get-ChildItem -Path $runFolder -Filter "*.html" -File | Select-Object -First 1 if ($htmlFile) { Write-Host " HTML Report: $($htmlFile.FullName)" -ForegroundColor Green } } catch { Write-Host " HTML report generation failed: $($_.Exception.Message)" -ForegroundColor Yellow } } else { Write-Host " ConvertTo-AuditHtmlReport.ps1 not found, skipping HTML report" -ForegroundColor DarkGray } } # Open output folder if (-not $NoOpenFolder) { Write-Host " Opening output folder..." -ForegroundColor Cyan if ($IsWindows -or $PSVersionTable.PSVersion.Major -le 5) { Start-Process explorer.exe -ArgumentList $runFolder } elseif ($IsMacOS) { Start-Process "open" -ArgumentList $runFolder } elseif ($IsLinux) { Start-Process "xdg-open" -ArgumentList $runFolder } } # Final summary $totalDuration = (Get-Date) - $scriptStart $totalDurationStr = "$([int]$totalDuration.TotalMinutes) min $($totalDuration.Seconds) sec" Write-Host "" Write-Host ([string]::new([char]0x2550, 70)) -ForegroundColor Cyan Write-Host " ORCHESTRATION COMPLETE" -ForegroundColor Cyan Write-Host ([string]::new([char]0x2550, 70)) -ForegroundColor Cyan Write-Host " Mode : $AuditMode" Write-Host " Duration : $totalDurationStr" Write-Host " Tenant : $effectiveTenantId" Write-Host " Apps Processed : $(@($auditResult).Count)" Write-Host " Output Folder : $runFolder" # List generated files $outputFiles = Get-ChildItem -Path $runFolder -File -Recurse | Where-Object { $_.Extension -in @('.csv', '.html', '.log') } Write-Host "" Write-Host " Generated Files ($($outputFiles.Count)):" -ForegroundColor White foreach ($file in $outputFiles | Sort-Object Extension, Name) { $sizeKB = [math]::Round($file.Length / 1KB, 1) $relPath = $file.FullName.Replace($runFolder, "").TrimStart('\') Write-Host " $($file.Extension.PadRight(6)) ${sizeKB}KB $relPath" -ForegroundColor DarkGray } Write-Host "" Write-Host ([string]::new([char]0x2550, 70)) -ForegroundColor Cyan Write-Host "" #endregion |