Private/Write-AACPolicyStatePdf.ps1
|
function Write-AACPolicyStatePdf { <# .SYNOPSIS Writes Get-AACPolicyState's result as a landscape A4 PDF. .DESCRIPTION 1. Summary: title, scope, tiles, notices. 2. Compliance by subscription and by resource group. 3. Assignments: scope, enforcement, compliance. 4. Non-compliant resources by policy - each policy, then its resources - up to -MaxRows; the CSV and HTML report have every state. -Path must be a full path; see Save-AACPdfDocument. #> [CmdletBinding()] [OutputType([System.IO.FileInfo])] param( [Parameter(Mandatory)] [hashtable] $Compliance, [Parameter(Mandatory)] [string] $Path, [Parameter(Mandatory)] [string] $Title, [System.Collections.IDictionary] $Detail, [int] $MaxRows = 5000 ) $stats = $Compliance.Stats $pdf = New-AACPdfDocument -Title $Title -Subject "Azure Policy: $($stats.States) states" -Landscape $section = $pdf.Section $colors = $pdf.Colors $pt = $pdf.Pt $right = [MigraDoc.DocumentObjectModel.ParagraphAlignment]::Right $rateColor = { param($Rate) if ($null -eq $Rate) { $colors.Muted } elseif ($Rate -ge 90) { $pdf.Tone.Good.Solid } elseif ($Rate -ge 70) { $pdf.Tone.Warn.Solid } else { $pdf.Tone.Bad.Solid } } $number = { param($Cell, $Value, $Color) $p = $Cell.AddParagraph(('{0:N0}' -f [double]$Value)); $p.Format.Alignment = $right; if ([double]$Value -eq 0) { $p.Format.Font.Color = $colors.Muted } elseif ($Color) { $p.Format.Font.Color = $Color; $p.Format.Font.Name = 'Segoe UI Semibold' } } $rate = { param($Cell, $Value) $p = $Cell.AddParagraph($(if ($null -ne $Value) { "$Value%" } else { '-' })); $p.Format.Alignment = $right; $p.Format.Font.Name = 'Segoe UI Semibold'; $p.Format.Font.Color = & $rateColor $Value } # --- 1. Summary ----------------------------------------------------------------------------- & $pdf.AddTitle "Azure Policy compliance · generated $($pdf.Generated.ToString('dddd d MMMM yyyy, HH:mm'))" $facts = [ordered]@{} if ($script:AACSession) { $facts['Azure account'] = [string]$script:AACSession.Account; $facts['Tenant'] = [string]$script:AACSession.TenantId } if ($Detail) { foreach ($key in $Detail.Keys) { $facts[[string]$key] = [string]$Detail[$key] } } $factTable = & $pdf.NewTable @(4.0, ($pdf.PageWidth - 4.0)) foreach ($key in $facts.Keys) { $row = & $pdf.AddBodyRow $factTable $row.Cells[0].AddParagraph($key).Format.Font.Color = $colors.Muted $row.Cells[1].AddParagraph($facts[$key]) | Out-Null } $section.AddParagraph().Format.SpaceAfter = & $pt 6 $tileData = @( @{ Value = $(if ($null -ne $stats.ComplianceRate) { "$($stats.ComplianceRate)%" } else { '-' }); Label = ('resource compliance ({0:N0} of {1:N0})' -f $stats.CompliantCounted, $stats.Resources); Color = (& $rateColor $stats.ComplianceRate) } @{ Value = '{0:N0}' -f $stats.NonCompliant; Label = 'non-compliant resources'; Color = $(if ($stats.NonCompliant) { $pdf.Tone.Bad.Solid }) } @{ Value = '{0:N0}' -f $stats.Compliant; Label = 'compliant resources' } @{ Value = '{0:N0}' -f $stats.Exempt; Label = 'exempt' } @{ Value = '{0:N0}' -f $stats.Assignments; Label = 'assignments' } @{ Value = '{0:N0} / {1:N0}' -f $stats.NonCompliantInitiatives, $stats.Initiatives; Label = 'non-compliant initiatives' } @{ Value = '{0:N0} / {1:N0}' -f $stats.NonCompliantPolicies, $stats.Policies; Label = 'non-compliant policies' } ) $tiles = & $pdf.NewTable @(1..$tileData.Count | ForEach-Object { $pdf.PageWidth / $tileData.Count }) $tiles.TopPadding = & $pt 8 $tiles.BottomPadding = & $pt 8 $tileRow = $tiles.AddRow() for ($i = 0; $i -lt $tileData.Count; $i++) { $cell = $tileRow.Cells[$i] $cell.Shading.Color = $colors.Panel $cell.Borders.Left.Width = $(if ($i -gt 0) { 2 } else { 0 }) $cell.Borders.Left.Color = $colors.White $value = $cell.AddParagraph([string]$tileData[$i].Value) if ($tileData[$i].Contains('Color') -and $tileData[$i].Color) { $value.Format.Font.Color = $tileData[$i].Color } $value.Format.Alignment = [MigraDoc.DocumentObjectModel.ParagraphAlignment]::Center $value.Format.Font.Size = 18 $value.Format.Font.Name = 'Segoe UI Semibold' $caption = $cell.AddParagraph($tileData[$i].Label) $caption.Format.Alignment = [MigraDoc.DocumentObjectModel.ParagraphAlignment]::Center $caption.Format.Font.Size = 8 $caption.Format.Font.Color = $colors.Muted } foreach ($text in @(@($Compliance.Notice | Where-Object { $_ }) + 'Compliance: (compliant + exempt + unknown + protected resources) / every resource evaluated, as the Azure portal counts it.')) { $p = $section.AddParagraph($text); $p.Format.Font.Size = 8; $p.Format.Font.Color = $colors.Muted; $p.Format.SpaceBefore = & $pt 4 } # --- 2. By scope ------------------------------------------------------------------------------ foreach ($level in 'Subscription', 'ResourceGroup') { $rows = @($Compliance.Scopes | Where-Object Level -EQ $level) if (-not $rows.Count) { continue } $section.AddParagraph($(if ($level -eq 'Subscription') { 'Compliance by subscription' } else { 'Compliance by resource group' }), 'Heading2') | Out-Null $table = & $pdf.NewTable @(8.0, 6.0, 2.8, 2.8, 2.4, 2.0, 2.4) & $pdf.AddHeaderRow $table @($(if ($level -eq 'Subscription') { 'Subscription' } else { 'Resource group' }), $(if ($level -eq 'Subscription') { '' } else { 'Subscription' }), 'Compliance', 'Non-compliant', 'Compliant', 'Exempt', 'Resources') @(2, 3, 4, 5, 6) foreach ($item in $rows) { $row = & $pdf.AddBodyRow $table $row.Cells[0].AddParagraph($item.Name).Format.Font.Name = 'Segoe UI Semibold' if ($level -eq 'ResourceGroup') { $row.Cells[1].AddParagraph($item.SubscriptionName) | Out-Null } & $rate $row.Cells[2] $item.ComplianceRate & $number $row.Cells[3] $item.NonCompliant $pdf.Tone.Bad.Solid & $number $row.Cells[4] $item.Compliant & $number $row.Cells[5] $item.Exempt & $number $row.Cells[6] $item.Resources } } # --- 3. Assignments ------------------------------------------------------------------------------ if ($Compliance.Assignments.Count) { $section.AddPageBreak() $section.AddParagraph('Assignments', 'Heading2') | Out-Null $table = & $pdf.NewTable @(8.0, 7.4, 2.6, 2.6, 2.6, 2.4) & $pdf.AddHeaderRow $table @('Assignment', 'Assigned at', 'Compliance', 'Non-compliant', 'Compliant', 'Policies failing') @(2, 3, 4, 5) foreach ($item in $Compliance.Assignments) { $row = & $pdf.AddBodyRow $table $row.Cells[0].AddParagraph($item.Assignment).Format.Font.Name = 'Segoe UI Semibold' if ($item.Enforcement -eq 'DoNotEnforce') { $n = $row.Cells[0].AddParagraph('not enforced'); $n.Format.Font.Size = 7; $n.Format.Font.Color = $colors.Amber } $row.Cells[1].AddParagraph($item.Scope).Format.Font.Color = $colors.Muted & $rate $row.Cells[2] $item.ComplianceRate & $number $row.Cells[3] $item.NonCompliant $pdf.Tone.Bad.Solid & $number $row.Cells[4] $item.Compliant & $number $row.Cells[5] $item.NonCompliantPolicies $pdf.Tone.Warn.Solid } } # --- 4. Non-compliant resources by policy --------------------------------------------------------------- $bad = @($Compliance.States | Where-Object ComplianceState -EQ 'NonCompliant' | Group-Object -Property Policy | Sort-Object -Property @{ Expression = 'Count'; Descending = $true }, Name) if ($bad.Count) { $section.AddPageBreak() $section.AddParagraph('Non-compliant resources by policy', 'Heading2') | Out-Null $shown = 0 foreach ($policy in $bad) { if ($shown -ge $MaxRows) { break } $first = $policy.Group[0] $section.AddParagraph("$($policy.Name) ($($policy.Count))", 'Heading3') | Out-Null $context = @($(if ($first.PolicySet) { "Initiative: $($first.PolicySet)" }), "Assignment: $($first.Assignment)", $(if ($first.Effect) { "Effect: $($first.Effect)" })) | Where-Object { $_ } $p = $section.AddParagraph($context -join ' · '); $p.Format.Font.Size = 8; $p.Format.Font.Color = $colors.Muted; $p.Format.SpaceAfter = & $pt 3 $table = & $pdf.NewTable @(7.0, 6.0, 5.0, 5.2, 2.8) & $pdf.AddHeaderRow $table @('Resource', 'Type', 'Resource group', 'Subscription', 'Evaluated') foreach ($item in @($policy.Group | Select-Object -First ($MaxRows - $shown))) { $row = & $pdf.AddBodyRow $table $row.Cells[0].AddParagraph($item.Resource) | Out-Null $row.Cells[1].AddParagraph($item.ResourceType).Format.Font.Size = 7 $row.Cells[2].AddParagraph($item.ResourceGroup) | Out-Null $row.Cells[3].AddParagraph($item.SubscriptionName) | Out-Null $row.Cells[4].AddParagraph($(if ($item.EvaluatedAt) { $item.EvaluatedAt.ToLocalTime().ToString('d MMM yyyy') } else { '' })).Format.Font.Color = $colors.Muted $shown++ } } if ($shown -lt @($Compliance.States | Where-Object ComplianceState -EQ 'NonCompliant').Count) { $p = $section.AddParagraph("The first $shown non-compliant states are listed; the CSV and HTML report have them all."); $p.Format.Font.Color = $colors.Muted } } Save-AACPdfDocument -Pdf $pdf -Path $Path } |