Private/Write-AACPolicyAssessmentPdf.ps1
|
function Write-AACPolicyAssessmentPdf { <# .SYNOPSIS Writes Invoke-AACPolicyAssessment's report as a landscape A4 PDF. .DESCRIPTION 1. Summary: the scope, tiles, compliance by subscription, management group and category. 2. Findings, most severe first. 3. Assignments, and compliance by policy (the least compliant). 4. Exemptions. 5. Initiatives and definitions. -Path must be a full path; see Save-AACPdfDocument. #> [CmdletBinding()] [OutputType([System.IO.FileInfo])] param( [Parameter(Mandatory)] [hashtable] $Assessment, [Parameter(Mandatory)] [string] $Path, [Parameter(Mandatory)] [string] $Title, [System.Collections.IDictionary] $Detail, [ValidateRange(10, 5000)] [int] $RowLimit = 300 ) $stats = $Assessment.Stats $pdf = New-AACPdfDocument -Title $Title -Subject "$($stats.Assignments) Azure Policy assignments" -Landscape $section = $pdf.Section $colors = $pdf.Colors $pt = $pdf.Pt $right = [MigraDoc.DocumentObjectModel.ParagraphAlignment]::Right $tone = @{ High = $pdf.Tone.Bad.Solid; Medium = $pdf.Tone.Warn.Solid; Poor = $pdf.Tone.Bad.Solid; Warning = $pdf.Tone.Warn.Solid; Good = $pdf.Tone.Good.Solid; Expired = $pdf.Tone.Bad.Solid; Expiring = $pdf.Tone.Warn.Solid; DoNotEnforce = $pdf.Tone.Warn.Solid } $table = { # Rows of objects, the properties to show (label = property), and the share of the width each gets. param([object[]] $Rows, [System.Collections.Specialized.OrderedDictionary] $Columns, [double[]] $Share) $labels = @($Columns.Keys) $total = ($Share | Measure-Object -Sum).Sum $t = & $pdf.NewTable @($Share | ForEach-Object { $pdf.PageWidth * $_ / $total }) $numeric = @(for ($i = 0; $i -lt $labels.Count; $i++) { $sample = @($Rows | ForEach-Object { $_.($Columns[$labels[$i]]) } | Where-Object { $null -ne $_ -and "$_" -ne '' } | Select-Object -First 1); if ($sample.Count -and $sample[0] -is [ValueType] -and $sample[0] -isnot [bool]) { $i } }) & $pdf.AddHeaderRow $t $labels $numeric foreach ($item in @($Rows) | Select-Object -First $RowLimit) { $row = & $pdf.AddBodyRow $t for ($i = 0; $i -lt $labels.Count; $i++) { $value = $item.($Columns[$labels[$i]]) $text = if ($null -eq $value) { '' } elseif ($value -is [double]) { '{0:N1}' -f $value } else { [string]$value } if ($text.Length -gt 220) { $text = $text.Substring(0, 217) + '...' } $p = $row.Cells[$i].AddParagraph($text) $p.Format.Font.Size = 7 if ($i -in $numeric) { $p.Format.Alignment = $right } if ($tone.Contains($text)) { $p.Format.Font.Color = $tone[$text]; $p.Format.Font.Bold = $true } } } if (@($Rows).Count -gt $RowLimit) { $more = $section.AddParagraph("The first $RowLimit of $(@($Rows).Count) rows - the HTML report and CSV files have them all."); $more.Format.Font.Size = 7; $more.Format.Font.Color = $colors.Muted } } $ordered = { param([string[]] $Pairs) $o = [ordered]@{}; for ($i = 0; $i -lt $Pairs.Count; $i += 2) { $o[$Pairs[$i]] = $Pairs[$i + 1] }; $o } $heading = { param([string] $Text, [string] $Style = 'Heading2') $section.AddParagraph($Text, $Style) | Out-Null } # --- 1. Summary ------------------------------------------------------------------------------------------ & $pdf.AddTitle "Azure Policy assessment · 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] } } foreach ($line in @($Assessment['Notices'])) { $facts['Note'] = $(if ($facts.Contains('Note')) { "$($facts['Note']) $line" } else { $line }) } $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.CompliancePercent) { "$($stats.CompliancePercent)%" } else { '-' }); Label = 'resources compliant'; Color = $tone[[string]$stats.Rating] } @{ Value = '{0:N0}' -f $stats.Assignments; Label = 'assignments' } @{ Value = '{0:N0}' -f $stats.NonCompliant; Label = 'non-compliant resources'; Color = $(if ($stats.NonCompliant) { $pdf.Tone.Warn.Solid }) } @{ Value = '{0:N0}' -f $stats.NotEnforced; Label = 'not enforced'; Color = $(if ($stats.NotEnforced) { $pdf.Tone.Warn.Solid }) } @{ Value = '{0:N0}' -f $stats.ExpiringExemptions; Label = 'exemptions expired or expiring'; Color = $(if ($stats.ExpiringExemptions) { $pdf.Tone.Warn.Solid }) } @{ Value = '{0:N0}' -f $stats.High; Label = 'high findings'; Color = $(if ($stats.High) { $pdf.Tone.Bad.Solid }) } @{ Value = '{0:N0}' -f $stats.Medium; Label = 'medium findings'; Color = $(if ($stats.Medium) { $pdf.Tone.Warn.Solid }) } ) $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); $value.Format.Alignment = [MigraDoc.DocumentObjectModel.ParagraphAlignment]::Center; $value.Format.Font.Size = 16; $value.Format.Font.Name = 'Segoe UI Semibold' if ($tileData[$i].Contains('Color') -and $tileData[$i].Color) { $value.Format.Font.Color = $tileData[$i].Color } $caption = $cell.AddParagraph($tileData[$i].Label); $caption.Format.Alignment = [MigraDoc.DocumentObjectModel.ParagraphAlignment]::Center; $caption.Format.Font.Size = 8; $caption.Format.Font.Color = $colors.Muted } $states = 'Compliance %', 'CompliancePercent', 'Rating', 'Rating', 'Non-compliant', 'NonCompliant', 'Compliant', 'Compliant', 'Exempt', 'Exempt' if (@($Assessment.Subscriptions).Count) { & $heading 'Compliance by subscription'; & $table $Assessment.Subscriptions (& $ordered (@('Subscription', 'Subscription', 'Management groups', 'ManagementGroups') + $states + @('Assignments', 'Assignments', 'Exemptions', 'Exemptions'))) @(3, 4, 1.2, 1.2, 1.2, 1.2, 1, 1.2, 1.2) } if (@($Assessment.ManagementGroups).Count) { & $heading 'Compliance by management group'; & $table $Assessment.ManagementGroups (& $ordered (@('Management group', 'ManagementGroup', 'Parent', 'Parent') + $states + @('Assigned here', 'Assignments', 'Subscriptions', 'Subscriptions'))) @(3, 3, 1.2, 1.2, 1.2, 1.2, 1, 1.2, 1.2) } if (@($Assessment.Categories).Count) { & $heading 'Compliance by category'; & $table $Assessment.Categories (& $ordered (@('Category', 'Category', 'Policies', 'Policies', 'Assignments', 'Assignments') + $states)) @(3, 1, 1, 1.2, 1.2, 1.2, 1.2, 1) } # --- 2. Findings --------------------------------------------------------------------------------------------- if (@($Assessment.Findings).Count) { $section.AddPageBreak() & $heading 'Findings' 'Heading1' & $table $Assessment.Findings (& $ordered 'Severity', 'Severity', 'Area', 'Area', 'Finding', 'Finding', 'Item', 'Item', 'Scope', 'Scope', 'What was found', 'Detail', 'What to do', 'Recommendation') @(1, 1.2, 2.2, 2.4, 1.8, 4, 3.4) } # --- 3. Assignments and policies ------------------------------------------------------------------------------------ if (@($Assessment.Assignments).Count) { $section.AddPageBreak() & $heading 'Assignments' 'Heading1' & $table $Assessment.Assignments (& $ordered 'Assignment', 'Assignment', 'Scope', 'Scope', 'Assigns', 'Definition', 'Kind', 'Kind', 'Enforcement', 'Enforcement', 'Compliance %', 'CompliancePercent', 'Rating', 'Rating', 'Non-compliant', 'NonCompliant', 'Exemptions', 'Exemptions', 'Identity', 'Identity') @(3, 2.4, 3.4, 1.1, 1.3, 1.1, 1, 1.1, 1, 1.3) $worst = @($Assessment.Policies | Where-Object NonCompliant -GT 0) if ($worst.Count) { & $heading 'Policies with non-compliant resources'; & $table $worst (& $ordered 'Policy', 'Policy', 'Assignment', 'Assignment', 'Effect', 'Effect', 'Category', 'Category', 'Compliance %', 'CompliancePercent', 'Non-compliant', 'NonCompliant', 'Resources', 'Resources') @(4.4, 2.6, 1.1, 1.6, 1.1, 1.1, 1) } } # --- 4. Exemptions ------------------------------------------------------------------------------------------------- if (@($Assessment.Exemptions).Count) { $section.AddPageBreak() & $heading 'Exemptions' 'Heading1' & $table $Assessment.Exemptions (& $ordered 'Exemption', 'Exemption', 'Status', 'Status', 'Expires', 'ExpiresOn', 'Days left', 'DaysLeft', 'Category', 'Category', 'Assignment', 'Assignment', 'Scope', 'Scope', 'Policies', 'Policies') @(3, 1, 1.1, 0.8, 1, 2.6, 2.6, 2) } # --- 5. Definitions ------------------------------------------------------------------------------------------------ if (@($Assessment.Initiatives).Count -or @($Assessment.Definitions).Count) { $section.AddPageBreak() & $heading 'Initiatives and definitions' 'Heading1' if (@($Assessment.Initiatives).Count) { & $heading 'Initiatives'; & $table $Assessment.Initiatives (& $ordered 'Initiative', 'Initiative', 'Type', 'PolicyType', 'Category', 'Category', 'Version', 'Version', 'Policies', 'Policies', 'Assigned', 'Assigned', 'Deprecated', 'Deprecated', 'Defined at', 'DefinedAt') @(4.4, 1, 1.6, 1, 0.9, 0.9, 1, 2.4) } if (@($Assessment.Definitions).Count) { & $heading 'Policy definitions'; & $table $Assessment.Definitions (& $ordered 'Definition', 'Definition', 'Type', 'PolicyType', 'Category', 'Category', 'Effect', 'Effect', 'Roles needed', 'RolesNeeded', 'Initiatives', 'Initiatives', 'Assigned', 'Assigned', 'Deprecated', 'Deprecated') @(4.4, 1, 1.6, 1.4, 2, 0.9, 0.9, 1) } } Save-AACPdfDocument -Pdf $pdf -Path $Path } |