Public/Invoke-AzPolicyWatch.ps1
|
function Invoke-AzPolicyWatch { <# .SYNOPSIS Run a complete Azure Policy compliance analysis with a single command. .DESCRIPTION This function orchestrates the entire AzurePolicyWatch workflow: 1. Retrieve policy baselines (ALZ and/or MCSB) 2. Scan Azure policy assignments (Management Group or Subscription) 3. Compare assignments against baseline 4. Export reports (CSV and optionally HTML) This is a convenience wrapper around the 4 core functions: - Get-AzPolicyBaseline - Get-AzPolicyAssignmentScan - Compare-AzPolicyCompliance - Export-PolicyReport .PARAMETER ManagementGroupId Management Group ID to scan for policy assignments. Mutually exclusive with -SubscriptionId. .PARAMETER SubscriptionId Subscription ID to scan for policy assignments. Mutually exclusive with -ManagementGroupId. .PARAMETER OutputFolder Destination folder for generated reports. Default: C:\Temp\Policy-Compare .PARAMETER CsvPath Path to local AzAdvertizer CSV file (alternative to -CsvUrl). .PARAMETER ExcludeBaselineTypes Exclude specific baseline types from comparison. Valid values: "ALZ", "MCSB" Example: @("ALZ") to compare only against MCSB Example: @("MCSB") to compare only against ALZ Example: @("ALZ", "MCSB") to compare only custom policies .PARAMETER MatchByNameOnly Match policies by normalized name only (ignore IDs). Useful when baseline uses built-in IDs but assignments use custom definitions. Default: $true .PARAMETER HtmlReport Generate HTML report in addition to CSV files. .PARAMETER ProjectName Project name for HTML report header. Default: AzurePolicyWatch .PARAMETER ProjectVersion Version string for HTML report. Default: 1.0.0 .PARAMETER Quiet Suppress progress output (only show errors and final summary). .PARAMETER ExcludeExtraPolicies Exclude extra policies (not in baseline) from the comparison report. .EXAMPLE # Scan Management Group with HTML report Invoke-AzPolicyWatch -ManagementGroupId "MyRootMG" -HtmlReport .EXAMPLE # Scan specific subscription (ALZ only, no MCSB) Invoke-AzPolicyWatch -SubscriptionId "12345678-abcd-1234-abcd-123456789012" ` -IncludeMcsb:"]false .EXAMPLE # Custom output and filtered initiatives Invoke-AzPolicyWatch -ManagementGroupId "MyMG" ` -OutputFolder "C:\Reports\PolicyAudit" ` -SelectedAlzInitiatives @("alzroot", "alz-Identity") ` -HtmlReport .EXAMPLE # Quiet mode with custom CSV Invoke-AzPolicyWatch -SubscriptionId "xxx" ` -CsvPath "C:\Data\policies.csv" ` -Quiet .NOTES Requires: - PowerShell 7.0 - Az.Accounts module (Connect-AzAccount) - Az.Resources module Before running: 1. Connect-AzAccount 2. Set-AzContext (if multiple subscriptions) .LINK https://github.com/technicalandcloud/AzurePolicyWatch #> [CmdletBinding(DefaultParameterSetName = 'ManagementGroup')] param( [Parameter(ParameterSetName = 'ManagementGroup', Mandatory = $true)] [string]$ManagementGroupId, [Parameter(ParameterSetName = 'Subscription', Mandatory = $true)] [string]$SubscriptionId, [Parameter()] [ValidateSet("ALZ", "MCSB")] [string[]]$ExcludeBaselineTypes = @(), [string]$OutputFolder = "C:\Temp\Policy-Compare", [string]$CsvPath = "", [switch]$HtmlReport, [string]$ProjectName = "AzurePolicyWatch", [string]$ProjectVersion = "1.0.1", [switch]$Quiet, [Parameter()] [switch]$ExcludeExtraPolicies ) $IncludeAlz = $true $IncludeMcsb = $true $CsvUrl = "" $CacheMaxAgeHours = 24 $SelectedAlzInitiatives = @() $ExcludeAssignments = @("Microsoft cloud security benchmark") $MatchByNameOnly = $true function Format-CenteredLine { <# .SYNOPSIS Centers text within a specified width for display in a banner. #> param( [string]$Text, [int]$Width = 60 ) $padding = $Width - $Text.Length $leftPad = [math]::Floor($padding / 2) $rightPad = [math]::Ceiling($padding / 2) return (" " * $leftPad) + $Text + (" " * $rightPad) } try { $context = Get-AzContext if (-not $context) { throw "No Azure context found. Please run Connect-AzAccount first." } if (-not $Quiet) { Write-Host "✅ Azure context: $($context.Account.Id)" -ForegroundColor Green } } catch { Write-Error $_.Exception.Message Write-Host "" Write-Host "❌ Azure authentication required" -ForegroundColor Red Write-Host " Please run: Connect-AzAccount" -ForegroundColor Yellow return } $reportsFolder = Join-Path $OutputFolder "reports" $baselineFolder = Join-Path $OutputFolder "baseline" $initiativesFolder = Join-Path $OutputFolder "initiatives" @($reportsFolder, $baselineFolder, $initiativesFolder) | ForEach-Object { if (-not (Test-Path $_)) { New-Item -Path $_ -ItemType Directory -Force -ErrorAction SilentlyContinue | Out-Null } } if (-not $Quiet) { Write-Host "" Write-Host "╔════════════════════════════════════════════════════════════╗" -ForegroundColor Cyan $titleLine = Format-CenteredLine "🚀 $ProjectName v$ProjectVersion" Write-Host "║$titleLine║" -ForegroundColor Cyan $subtitleLine = Format-CenteredLine "Azure Policy Compliance Analysis" Write-Host "║$subtitleLine║" -ForegroundColor Cyan Write-Host "╚════════════════════════════════════════════════════════════╝" -ForegroundColor Cyan Write-Host "" $scopeDescription = if ($PSCmdlet.ParameterSetName -eq 'ManagementGroup') { "Management Group: $ManagementGroupId" } else { "Subscription: $SubscriptionId" } Write-Host "📍 Scope: $scopeDescription" -ForegroundColor White Write-Host "📁 Output: $OutputFolder" -ForegroundColor White Write-Host "" } if (-not $Quiet) { Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor DarkGray Write-Host "📊 Step 1/4: Retrieving policy baseline..." -ForegroundColor Cyan } try { $baselineParams = @{ IncludeAlz = $IncludeAlz IncludeMcsb = $IncludeMcsb CacheMaxAgeHours = $CacheMaxAgeHours } if ($CsvPath) { $baselineParams.CsvPath = $CsvPath } elseif ($CsvUrl) { $baselineParams.CsvUrl = $CsvUrl } if ($SelectedAlzInitiatives.Count -gt 0) { $baselineParams.SelectedAlzInitiatives = $SelectedAlzInitiatives } $baseline = Get-AzPolicyBaseline @baselineParams if ($ExcludeBaselineTypes.Count -gt 0) { Write-Verbose "Filtering baseline to exclude: $($ExcludeBaselineTypes -join ', ')" $originalCount = $baseline.Policies.Count $filteredPolicies = $baseline.Policies | Where-Object { $policy = $_ $shouldKeep = $true foreach ($excludeType in $ExcludeBaselineTypes) { if ($policy.BaselineSources -like "*$excludeType*") { $shouldKeep = $false break } } $shouldKeep } $baseline.Policies = $filteredPolicies $baseline.Index = New-BaselineIndex -Baseline $filteredPolicies $removedCount = $originalCount - $filteredPolicies.Count if ($removedCount -gt 0 -and -not $Quiet) { Write-Host " ℹ️ Filtered baseline: $originalCount → $($filteredPolicies.Count) policies (excluded $($ExcludeBaselineTypes -join ', '))" -ForegroundColor Cyan } Write-Verbose " Removed $removedCount baseline policies from comparison" } if (-not $Quiet) { $alzCount = ($baseline.Policies | Where-Object { $_.BaselineSources -like '*ALZ*' }).Count $mcsbCount = ($baseline.Policies | Where-Object { $_.BaselineSources -like '*MCSB*' }).Count Write-Host " ✅ Baseline loaded: $($baseline.Policies.Count) policies" -ForegroundColor Green if ($IncludeAlz -and $alzCount -gt 0) { Write-Host " ALZ: $alzCount policies" -ForegroundColor White } if ($IncludeMcsb -and $mcsbCount -gt 0) { Write-Host " MCSB: $mcsbCount policies" -ForegroundColor White } } } catch { Write-Error "Failed to load baseline: $($_.Exception.Message)" return } if (-not $Quiet) { Write-Host "" Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor DarkGray Write-Host "🔍 Step 2/4: Scanning policy assignments..." -ForegroundColor Cyan } try { $scanParams = @{ ExcludeAssignments = $ExcludeAssignments } if ($PSCmdlet.ParameterSetName -eq 'ManagementGroup') { $scanParams.ManagementGroupId = $ManagementGroupId } else { $scanParams.SubscriptionId = $SubscriptionId } $assignments = Get-AzPolicyAssignmentScan @scanParams if (-not $Quiet) { $totalPolicies = ($assignments | ForEach-Object { $_.ExpandedPolicies.Count } | Measure-Object -Sum).Sum Write-Host " ✅ Scan complete: $($assignments.Count) assignments ($totalPolicies expanded policies)" -ForegroundColor Green } } catch { Write-Error "Failed to scan assignments: $($_.Exception.Message)" return } if (-not $Quiet) { Write-Host "" Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor DarkGray Write-Host "⚖️ Step 3/4: Comparing against baseline..." -ForegroundColor Cyan } try { $comparisonParams = @{ Baseline = $baseline Assignments = $assignments MatchByNameOnly = $MatchByNameOnly InitiativesFolder = $initiativesFolder ExcludeExtraPolicies = $ExcludeExtraPolicies.IsPresent } $comparison = Compare-AzPolicyCompliance @comparisonParams if (-not $Quiet) { $matchedCount = if ($comparison.Details.InCommon) { $comparison.Details.InCommon.Count } else { 0 } $missingCount = if ($comparison.Details.Missing) { $comparison.Details.Missing.Count } else { 0 } $extraCount = if ($ExcludeExtraPolicies) { 0 } elseif ($comparison.Details.Extra) { $comparison.Details.Extra.Count } else { 0 } $mismatchCount = if ($comparison.Details.VersionMismatches) { $comparison.Details.VersionMismatches.Count } else { 0 } Write-Host " ✅ Analysis complete:" -ForegroundColor Green Write-Host " Matched: $matchedCount" -ForegroundColor White Write-Host " Missing: $missingCount" -ForegroundColor White if ($ExcludeExtraPolicies) { Write-Host " Extra: (filtered out)" -ForegroundColor DarkGray } else { Write-Host " Extra: $extraCount" -ForegroundColor White } Write-Host " Version Mismatch: $mismatchCount" -ForegroundColor White } } catch { Write-Error "Failed to compare policies: $($_.Exception.Message)" return } if (-not $Quiet) { Write-Host "" Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor DarkGray Write-Host "📄 Step 4/4: Generating reports..." -ForegroundColor Cyan } try { $exportFormat = if ($HtmlReport) { 'All' } else { 'CSV' } $scopeDescription = if ($PSCmdlet.ParameterSetName -eq 'ManagementGroup') { "Management Group: $ManagementGroupId" } else { "Subscription: $SubscriptionId" } $exportParams = @{ ComparisonResult = $comparison OutputFolder = $OutputFolder Baseline = $baseline Assignments = $assignments Format = $exportFormat MatchByNameOnly = $MatchByNameOnly HtmlOptions = @{ ProjectName = $ProjectName ProjectVersion = $ProjectVersion Scope = $scopeDescription ExcludeExtraPolicies = $ExcludeExtraPolicies.IsPresent } } Write-Verbose "Calling Export-PolicyReport..." $generatedFiles = @(Export-PolicyReport @exportParams) if (-not $Quiet) { Write-Host "" Write-Host " ✅ Reports generated successfully" -ForegroundColor Green Write-Host "" if ($generatedFiles -and $generatedFiles.Count -gt 0) { Write-Host " 📁 Generated files:" -ForegroundColor Cyan Write-Host "" foreach ($file in $generatedFiles) { if ($file -and (Test-Path $file)) { $fileInfo = Get-Item $file $fileName = $fileInfo.Name $fileSizeKB = [math]::Round($fileInfo.Length / 1KB, 1) # Icon based on file type $icon = switch -Wildcard ($fileName) { "*.html" { "🌐" } "*Baseline*" { "📊" } "*Summary*" { "📋" } "*Details*" { "📝" } "*Hierarchical*" { "🏗️" } default { "📄" } } Write-Host " $icon $fileName " -NoNewline -ForegroundColor White Write-Host "($fileSizeKB KB)" -ForegroundColor Gray } } } Write-Host "" Write-Host " 📂 Location: $OutputFolder" -ForegroundColor Cyan Write-Host "" Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor DarkGray Write-Host "✨ Analysis completed successfully!" -ForegroundColor Green Write-Host "" } } catch { Write-Error "Failed to export reports: $($_.Exception.Message)" return } } |