AzPolicyCompare.psm1
function Test-RequiredModules { Write-Host "Checking required modules..." -ForegroundColor Cyan $requiredModules = @("Az.Resources") foreach ($module in $requiredModules) { $moduleInfo = Get-Module -Name $module -ListAvailable if ($moduleInfo) { Write-Host "✓ $module module found" -ForegroundColor Green } else { Write-Error "$module module is not installed. Install with: Install-Module $module" exit 1 } } } function Connect-ToAzure { Write-Host "Connecting to Azure..." -ForegroundColor Cyan try { $context = Get-AzContext if (-not $context) { Write-Host "No Azure context found. Please sign in..." -ForegroundColor Yellow Connect-AzAccount $context = Get-AzContext } Write-Host "✓ Connected to Azure as: $($context.Account.Id)" -ForegroundColor Green Write-Host "✓ Current subscription: $($context.Subscription.Name)" -ForegroundColor Green } catch { Write-Error "Failed to connect to Azure: $($_.Exception.Message)" exit 1 } } function Get-AllInitiatives { Write-Host "Retrieving all policy initiatives..." -ForegroundColor Cyan try { $initiatives = Get-AzPolicySetDefinition Write-Host "Found $($initiatives.Count) policy initiatives" -ForegroundColor Green $sortedInitiatives = $initiatives | Sort-Object { if ($_.Properties.DisplayName) { $_.Properties.DisplayName } elseif ($_.DisplayName) { $_.DisplayName } else { $_.Name } } $numberedInitiatives = @() for ($i = 0; $i -lt $sortedInitiatives.Count; $i++) { $initiative = $sortedInitiatives[$i] $displayName = if ($initiative.Properties.DisplayName) { $initiative.Properties.DisplayName } elseif ($initiative.DisplayName) { $initiative.DisplayName } else { "Unknown Initiative" } $policyType = if ($initiative.Properties.PolicyType) { $initiative.Properties.PolicyType } elseif ($initiative.PolicyType) { $initiative.PolicyType } else { "Unknown" } $policyCount = if ($initiative.Properties.PolicyDefinitions) { $initiative.Properties.PolicyDefinitions.Count } elseif ($initiative.PolicyDefinitions) { $initiative.PolicyDefinitions.Count } elseif ($initiative.PolicyDefinition) { $initiative.PolicyDefinition.Count } else { 0 } $numberedInitiatives += [PSCustomObject]@{ Number = $i + 1 Name = $displayName Type = if ($policyType -eq "BuiltIn") { "Built-in" } else { "Custom" } Policies = $policyCount OriginalName = if ($policyType -eq "BuiltIn") { $initiative.Name } else { $initiative.Id } } } return $numberedInitiatives } catch { Write-Error "Failed to retrieve initiatives: $($_.Exception.Message)" exit 1 } } function Select-Initiative { param([array]$Initiatives, [string]$Purpose) Write-Host "`nAvailable Policy Initiatives:" -ForegroundColor Cyan Write-Host "=============================" -ForegroundColor Cyan $Initiatives | Format-Table -AutoSize | Out-Host Write-Host "`n" do { $selection = Read-Host "$Purpose Enter the number (1-$($Initiatives.Count))" if ([string]::IsNullOrWhiteSpace($selection)) { Write-Host "Error: Please enter a valid number. Empty values are not accepted." -ForegroundColor Red continue } try { $selectedNumber = [int]$selection if ($selectedNumber -ge 1 -and $selectedNumber -le $Initiatives.Count) { $selectedInitiative = $Initiatives[$selectedNumber - 1] Write-Host "`nYou selected: $($selectedInitiative.Name)" -ForegroundColor Green return $selectedInitiative } else { Write-Host "Error: Please enter a number between 1 and $($Initiatives.Count)" -ForegroundColor Red } } catch { Write-Host "Error: Please enter a valid number." -ForegroundColor Red } } while ($true) } function Get-InitiativePolicies { param([string]$InitiativeId, [string]$InitiativeName) try { Write-Progress -Activity "Policy Analysis" -Status "Retrieving policies from $InitiativeName" -PercentComplete 0 $initiative = $null if ($InitiativeId.StartsWith("/")) { $initiative = Get-AzPolicySetDefinition -Id $InitiativeId -ErrorAction SilentlyContinue } if (-not $initiative) { $initiative = Get-AzPolicySetDefinition -Name $InitiativeId -ErrorAction SilentlyContinue } if (-not $initiative) { $allInitiatives = Get-AzPolicySetDefinition $initiative = $allInitiatives | Where-Object { $_.Name -eq $InitiativeId -or $_.Id -eq $InitiativeId } } if (-not $initiative) { Write-Error "Could not find initiative with ID: $InitiativeId" return @() } $policies = @() $policyDefs = if ($initiative.Properties.PolicyDefinitions) { $initiative.Properties.PolicyDefinitions } elseif ($initiative.PolicyDefinitions) { $initiative.PolicyDefinitions } elseif ($initiative.PolicyDefinition) { $initiative.PolicyDefinition } else { @() } $totalPolicies = $policyDefs.Count $currentPolicy = 0 foreach ($policyRef in $policyDefs) { $currentPolicy++ $percentComplete = ($currentPolicy / $totalPolicies) * 100 Write-Progress -Activity "Policy Analysis" -Status "Processing policy $currentPolicy of $totalPolicies" -PercentComplete $percentComplete $policyDef = Get-AzPolicyDefinition -Id $policyRef.PolicyDefinitionId -ErrorAction SilentlyContinue if ($policyDef) { $policyName = if ($policyDef.Properties.DisplayName) { $policyDef.Properties.DisplayName } elseif ($policyDef.DisplayName) { $policyDef.DisplayName } else { "Unknown Policy" } $policyCategory = if ($policyDef.Properties.Metadata.category) { $policyDef.Properties.Metadata.category } elseif ($policyDef.Metadata.category) { $policyDef.Metadata.category } else { "Unknown" } $policies += [PSCustomObject]@{ PolicyId = $policyRef.PolicyDefinitionId PolicyName = $policyName Category = $policyCategory } } } Write-Progress -Activity "Policy Analysis" -Completed return $policies } catch { Write-Error "Failed to get policies for initiative $InitiativeId : $($_.Exception.Message)" return @() } } function Compare-Initiatives { param([object]$SourceInitiative, [object]$CompareInitiative) Write-Host "`nAnalyzing policy overlap..." -ForegroundColor Cyan $sourcePolicies = Get-InitiativePolicies -InitiativeId $SourceInitiative.OriginalName -InitiativeName $SourceInitiative.Name $comparePolicies = Get-InitiativePolicies -InitiativeId $CompareInitiative.OriginalName -InitiativeName $CompareInitiative.Name Write-Progress -Activity "Policy Comparison" -Status "Finding overlaps and differences" -PercentComplete 50 $overlap = @() $missingInCompare = @() $missingInSource = @() foreach ($sourcePolicy in $sourcePolicies) { $foundInCompare = $comparePolicies | Where-Object { $_.PolicyId -eq $sourcePolicy.PolicyId } if ($foundInCompare) { $overlap += $sourcePolicy } else { $missingInCompare += $sourcePolicy } } foreach ($comparePolicy in $comparePolicies) { $foundInSource = $sourcePolicies | Where-Object { $_.PolicyId -eq $comparePolicy.PolicyId } if (-not $foundInSource) { $missingInSource += $comparePolicy } } Write-Progress -Activity "Policy Comparison" -Completed return [PSCustomObject]@{ OverlapCount = $overlap.Count OverlapPolicies = $overlap MissingInCompareCount = $missingInCompare.Count MissingInComparePolicies = $missingInCompare MissingInSourceCount = $missingInSource.Count MissingInSourcePolicies = $missingInSource SourceTotal = $sourcePolicies.Count CompareTotal = $comparePolicies.Count } } function Export-ToHtml { param([object]$SourceInitiative, [object]$CompareInitiative, [object]$Comparison, [string]$FilePath) $html = @" <!DOCTYPE html> <html> <head> <title>Azure Policy Initiative Comparison</title> <style> body { font-family: Arial, sans-serif; margin: 20px; } .header { background-color: #0078d4; color: white; padding: 20px; border-radius: 5px; } .section { margin: 20px 0; padding: 15px; border: 1px solid #ddd; border-radius: 5px; } .overlap { background-color: #d4edda; } .missing { background-color: #f8d7da; } .extra { background-color: #e2e3f1; } .policy-list { margin: 10px 0; } .policy-item { margin: 5px 0; padding: 5px; background-color: #f8f9fa; border-left: 3px solid #007bff; } table { width: 100%; border-collapse: collapse; margin: 10px 0; } th, td { border: 1px solid #ddd; padding: 8px; text-align: left; } th { background-color: #f2f2f2; } </style> </head> <body> <div class="header"> <h1>Azure Policy Initiative Comparison Report</h1> <p>Generated on: $(Get-Date -Format "yyyy-MM-dd HH:mm:ss")</p> </div> <div class="section"> <h2>Initiative Details</h2> <table> <tr><th>Aspect</th><th>Source Initiative</th><th>Compare Initiative</th></tr> <tr><td>Name</td><td>$($SourceInitiative.Name)</td><td>$($CompareInitiative.Name)</td></tr> <tr><td>Type</td><td>$($SourceInitiative.Type)</td><td>$($CompareInitiative.Type)</td></tr> <tr><td>Total Policies</td><td>$($Comparison.SourceTotal)</td><td>$($Comparison.CompareTotal)</td></tr> </table> </div> <div class="section overlap"> <h2>Policy Overlap ($($Comparison.OverlapCount) policies)</h2> <p>These policies exist in both initiatives:</p> <div class="policy-list"> "@ foreach ($policy in $Comparison.OverlapPolicies) { $html += "<div class='policy-item'>$($policy.PolicyName)</div>" } $html += @" </div> </div> <div class="section missing"> <h2>Policies Missing in Compare Initiative ($($Comparison.MissingInCompareCount) policies)</h2> <p>These policies from the source initiative are not found in the compare initiative:</p> <div class="policy-list"> "@ foreach ($policy in $Comparison.MissingInComparePolicies) { $html += "<div class='policy-item'>$($policy.PolicyName)</div>" } $html += @" </div> </div> <div class="section extra"> <h2>Extra Policies in Compare Initiative ($($Comparison.MissingInSourceCount) policies)</h2> <p>These policies are in the compare initiative but not in the source initiative:</p> <div class="policy-list"> "@ foreach ($policy in $Comparison.MissingInSourcePolicies) { $html += "<div class='policy-item'>$($policy.PolicyName)</div>" } $html += @" </div> </div> </body> </html> "@ $html | Out-File -FilePath $FilePath -Encoding UTF8 Write-Host "HTML report exported to: $FilePath" -ForegroundColor Green } function Start-AzPolicyInitiativeComparison { <# .SYNOPSIS Azure Policy Initiative Comparison Tool v1.0 .DESCRIPTION Compares two Azure Policy Initiatives and identifies overlapping policies, missing policies, and provides detailed analysis for compliance mapping. .PARAMETER OutputHtml Optional path to export the comparison results as HTML .EXAMPLE Start-AzPolicyInitiativeComparison Interactive mode - prompts for initiative selection .EXAMPLE Start-AzPolicyInitiativeComparison -OutputHtml "comparison-report.html" Interactive mode with HTML export #> [CmdletBinding()] param( [Parameter(Mandatory = $false)] [string]$OutputHtml ) try { Write-Host "Azure Policy Initiative Comparison Tool v1.0" -ForegroundColor Cyan Write-Host "=============================================" -ForegroundColor Cyan Test-RequiredModules Connect-ToAzure $initiatives = Get-AllInitiatives Write-Host "`nSTEP 1: Select the SOURCE initiative" -ForegroundColor Yellow $sourceInitiative = Select-Initiative -Initiatives $initiatives -Purpose "Which initiative do you want to use as SOURCE?" Write-Host "`nSTEP 2: Select the initiative to COMPARE with" -ForegroundColor Yellow $compareInitiative = Select-Initiative -Initiatives $initiatives -Purpose "Which initiative do you want to COMPARE with the source?" $comparison = Compare-Initiatives -SourceInitiative $sourceInitiative -CompareInitiative $compareInitiative Write-Host "`nCOMPARISON SUMMARY" -ForegroundColor Green Write-Host "==================" -ForegroundColor Green if ($sourceInitiative.Name -eq $compareInitiative.Name) { Write-Host "`nWarning: You selected the same initiative for both source and comparison." -ForegroundColor Yellow } Write-Host "`n1. SOURCE Initiative:" -ForegroundColor Cyan Write-Host " Name: $($sourceInitiative.Name)" -ForegroundColor White Write-Host " Type: $($sourceInitiative.Type)" -ForegroundColor White Write-Host " Number of policies: $($comparison.SourceTotal)" -ForegroundColor White Write-Host "`n2. COMPARE Initiative:" -ForegroundColor Cyan Write-Host " Name: $($compareInitiative.Name)" -ForegroundColor White Write-Host " Type: $($compareInitiative.Type)" -ForegroundColor White Write-Host " Number of policies: $($comparison.CompareTotal)" -ForegroundColor White Write-Host "`n3. POLICY ANALYSIS:" -ForegroundColor Yellow Write-Host " Overlapping policies: $($comparison.OverlapCount)" -ForegroundColor Green Write-Host " Policies missing in '$($compareInitiative.Name)': $($comparison.MissingInCompareCount)" -ForegroundColor Red Write-Host " Extra policies in '$($compareInitiative.Name)': $($comparison.MissingInSourceCount)" -ForegroundColor Magenta if ($comparison.MissingInCompareCount -gt 0) { Write-Host "`n Policies from SOURCE missing in COMPARE initiative:" -ForegroundColor Red foreach ($missingPolicy in $comparison.MissingInComparePolicies) { Write-Host " - $($missingPolicy.PolicyName)" -ForegroundColor White } } if ($comparison.MissingInSourceCount -gt 0) { Write-Host "`n Extra policies in COMPARE initiative (not in SOURCE):" -ForegroundColor Magenta foreach ($extraPolicy in $comparison.MissingInSourcePolicies) { Write-Host " + $($extraPolicy.PolicyName)" -ForegroundColor White } } if ($OutputHtml) { Export-ToHtml -SourceInitiative $sourceInitiative -CompareInitiative $compareInitiative -Comparison $comparison -FilePath $OutputHtml } Write-Host "`nScript completed successfully!" -ForegroundColor Green } catch { Write-Error "Script execution failed: $($_.Exception.Message)" exit 1 } } Export-ModuleMember -Function Start-AzPolicyInitiativeComparison |