Private/Write-AACNsgPdf.ps1
|
function Write-AACNsgPdf { <# .SYNOPSIS Writes the network security group assessment (ConvertTo-AACNsgAssessment) as a landscape A4 PDF report. .DESCRIPTION 1. Summary: title, scope, tiles, and a table of the NSGs - applied to, rules, flow logs, diagnostics, findings by severity. 2. Findings, most severe first: severity (High red, Medium amber, Low blue, Info grey), check, NSG, rule, what was found, what to do. 3. One page per NSG: its metadata, associations and telemetry, then its inbound and outbound rules in evaluation order - Allow green, Deny red, default rules grey, risky rules flagged. -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 ) $stats = $Assessment.Stats $groups = @($Assessment.Groups) $pdf = New-AACPdfDocument -Title $Title -Subject "$($stats.Groups) network security groups" -Landscape $section = $pdf.Section $colors = $pdf.Colors $pt = $pdf.Pt $right = [MigraDoc.DocumentObjectModel.ParagraphAlignment]::Right $blue = [MigraDoc.DocumentObjectModel.Color]::Parse('#0369A1') $severityColor = @{ High = $pdf.Tone.Bad.Solid; Medium = $pdf.Tone.Warn.Solid; Low = $blue; Info = $colors.Muted } $small = { param($Paragraph) $Paragraph.Format.Font.Size = 7.5; $Paragraph } $colored = { param($Cell, [string] $Text, $Color, [switch] $Bold) $p = $Cell.AddParagraph($Text) if ($Color) { $p.Format.Font.Color = $Color } if ($Bold) { $p.Format.Font.Name = 'Segoe UI Semibold' } $p } $findingCounts = { param($Cell, $Item) $p = $Cell.AddParagraph() $p.Format.Alignment = $right foreach ($level in 'High', 'Medium', 'Low', 'Info') { $n = [int]$Item.$level if (-not $n) { continue } $t = $p.AddFormattedText("$($level.Substring(0, 1))$n ") $t.Color = $severityColor[$level] $t.Bold = $true } } # --- 1. Summary --------------------------------------------------------------------------------- & $pdf.AddTitle "Network security group 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] } } $facts['How rules are read'] = 'Per direction, custom rules by priority (lowest number first), then the default rules; the first rule that matches decides. Traffic to a NIC passes its subnet''s NSG and its own NSG: both must allow it.' $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 = $stats.Groups; Label = 'NSGs' } @{ Value = $stats.Rules; Label = 'custom rules' } @{ Value = $stats.High; Label = 'high findings'; Color = $(if ($stats.High) { $pdf.Tone.Bad.Solid }) } @{ Value = $stats.Medium; Label = 'medium findings'; Color = $(if ($stats.Medium) { $pdf.Tone.Warn.Solid }) } @{ Value = $stats.Unassociated; Label = 'unassociated'; Color = $(if ($stats.Unassociated) { $pdf.Tone.Warn.Solid }) } @{ Value = $stats.WithoutFlowLogs; Label = 'without flow logs'; Color = $(if ($stats.WithoutFlowLogs) { $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(('{0:N0}' -f $tileData[$i].Value)) $value.Format.Alignment = [MigraDoc.DocumentObjectModel.ParagraphAlignment]::Center $value.Format.Font.Size = 18 $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 } $section.AddParagraph('Network security groups', 'Heading2') | Out-Null $table = & $pdf.NewTable @(5.0, 7.4, 1.4, 1.4, 4.2, 2.4, 4.3) & $pdf.AddHeaderRow $table @('NSG', 'Applied to', 'In', 'Out', 'Flow logs', 'Diagnostics', 'Findings') @(2, 3, 6) foreach ($nsg in $groups) { $row = & $pdf.AddBodyRow $table $name = $row.Cells[0].AddParagraph($nsg.Name) $name.Format.Font.Name = 'Segoe UI Semibold' & $small ($row.Cells[0].AddParagraph("$($nsg.ResourceGroup) · $($nsg.SubscriptionName)")) | ForEach-Object { $_.Format.Font.Color = $colors.Muted } if ($nsg.Associated) { & $small ($row.Cells[1].AddParagraph(($nsg.AppliedTo -replace '; ', "`n"))) | Out-Null } else { & $colored $row.Cells[1] 'nothing' $pdf.Tone.Warn.Solid | Out-Null } & $small ($row.Cells[2].AddParagraph("$($nsg.InboundRules)")) | ForEach-Object { $_.Format.Alignment = $right } & $small ($row.Cells[3].AddParagraph("$($nsg.OutboundRules)")) | ForEach-Object { $_.Format.Alignment = $right } & $colored $row.Cells[4] $nsg.FlowLogs $(if ($nsg.FlowLogs -like 'Enabled*') { $pdf.Tone.Good.Solid } elseif ($nsg.Associated) { $pdf.Tone.Warn.Solid } else { $colors.Muted }) | Out-Null if ($nsg.FlowLogRetention) { & $small ($row.Cells[4].AddParagraph("$($nsg.FlowLogRetention)$(if ($nsg.TrafficAnalytics) { ' · Traffic Analytics' })")) | ForEach-Object { $_.Format.Font.Color = $colors.Muted } } & $colored $row.Cells[5] $nsg.Diagnostics $(if ($nsg.Diagnostics -eq 'Enabled') { $pdf.Tone.Good.Solid } elseif ($nsg.Diagnostics -eq 'Disabled') { $pdf.Tone.Warn.Solid } else { $colors.Muted }) | Out-Null & $findingCounts $row.Cells[6] $nsg } # --- 2. Findings -------------------------------------------------------------------------------------- $findings = @($Assessment.Findings | Sort-Object -Property @{ Expression = { @{ High = 0; Medium = 1; Low = 2; Info = 3 }[$_.Severity] } }, Nsg, Check) if ($findings.Count) { $section.AddPageBreak() $section.AddParagraph('Findings', 'Heading2') | Out-Null $table = & $pdf.NewTable @(1.8, 3.4, 3.4, 3.4, 7.4, 6.7) & $pdf.AddHeaderRow $table @('Severity', 'Check', 'NSG', 'Rule', 'What was found', 'What to do') foreach ($finding in $findings) { $row = & $pdf.AddBodyRow $table & $colored $row.Cells[0] $finding.Severity $severityColor[$finding.Severity] -Bold | Out-Null $row.Cells[1].AddParagraph($finding.Check) | Out-Null $row.Cells[2].AddParagraph($finding.Nsg) | Out-Null & $small ($row.Cells[3].AddParagraph($finding.Rule)) | Out-Null & $small ($row.Cells[4].AddParagraph($finding.Detail)) | Out-Null & $small ($row.Cells[5].AddParagraph($finding.Recommendation)) | ForEach-Object { $_.Format.Font.Color = $colors.Muted } } } # --- 3. Each NSG ---------------------------------------------------------------------------------------- foreach ($nsg in $groups) { $section.AddPageBreak() $section.AddParagraph($nsg.Name, 'Heading1') | Out-Null & $small ($section.AddParagraph($nsg.Id)) | ForEach-Object { $_.Format.Font.Color = $colors.Muted } $meta = & $pdf.NewTable @(4.0, 8.9, 4.0, 9.2) $pairs = @( , @('Subscription', $nsg.SubscriptionName, 'Resource group', $nsg.ResourceGroup) , @('Location', $nsg.Location, 'Tags', $nsg.Tags) , @('Subnets', $((@($nsg.Subnets | ForEach-Object { "$($_.VirtualNetwork)/$($_.Name) ($($_.Prefix))" })) -join "`n"), 'Network interfaces', $((@($nsg.NetworkInterfaces | ForEach-Object { "$($_.Name)$(if ($_.VirtualMachine) { " · $($_.VirtualMachine)" })$(if ($_.PrivateIp) { " · $($_.PrivateIp)" })" })) -join "`n")) , @('Flow logs', "$($nsg.FlowLogs)$(foreach ($log in $nsg.FlowLogDetail) { "`n$($log.Name): $($log.Kind), $(if ($log.Enabled) { 'enabled' } else { 'disabled' })$(if ($log.Storage) { ", storage $($log.Storage)" }), $(if ($log.RetentionEnabled -and $log.RetentionDays) { "$($log.RetentionDays) days" } else { 'no retention limit' })$(if ($log.Analytics) { ', Traffic Analytics' })" })", 'Diagnostics', "$($nsg.Diagnostics)$(if ($nsg.LogDestinations) { "`n$($nsg.LogDestinations)" })$(foreach ($setting in $nsg.DiagnosticSettings) { "`n$($setting.Name): $($setting.Categories)" })") ) foreach ($pair in $pairs) { $row = & $pdf.AddBodyRow $meta $row.Cells[0].AddParagraph($pair[0]).Format.Font.Color = $colors.Muted & $small ($row.Cells[1].AddParagraph($(if ($pair[1]) { $pair[1] } else { '-' }))) | Out-Null $row.Cells[2].AddParagraph($pair[2]).Format.Font.Color = $colors.Muted & $small ($row.Cells[3].AddParagraph($(if ($pair[3]) { $pair[3] } else { '-' }))) | Out-Null } foreach ($direction in 'Inbound', 'Outbound') { $section.AddParagraph("$direction rules", 'Heading3') | Out-Null $table = & $pdf.NewTable @(1.5, 4.8, 1.6, 1.6, 4.6, 2.4, 4.6, 4.9) & $pdf.AddHeaderRow $table @('Priority', 'Name', 'Access', 'Protocol', 'Source', 'Ports', 'Destination', 'Finding') @(0) foreach ($rule in @($nsg.Rules | Where-Object Direction -EQ $direction)) { $row = & $pdf.AddBodyRow $table $muted = $rule.IsDefault $priority = & $small ($row.Cells[0].AddParagraph("$($rule.Priority)")) $priority.Format.Alignment = $right $ruleName = & $small ($row.Cells[1].AddParagraph($rule.Name)) & $colored $row.Cells[2] $rule.Access $(if ($rule.Access -eq 'Allow') { $pdf.Tone.Good.Solid } else { $pdf.Tone.Bad.Solid }) -Bold | Out-Null $cells = @( (& $small ($row.Cells[3].AddParagraph($rule.Protocol))) (& $small ($row.Cells[4].AddParagraph($rule.Source))) (& $small ($row.Cells[5].AddParagraph($rule.DestinationPorts))) (& $small ($row.Cells[6].AddParagraph($rule.Destination))) ) if ($muted) { foreach ($p in @($priority, $ruleName) + $cells) { $p.Format.Font.Color = $colors.Muted } } if ($rule.Risk) { $finding = & $small ($row.Cells[7].AddParagraph("$($rule.Risk): $($rule.Finding)")) $finding.Format.Font.Color = $severityColor[$rule.Risk] } } } } Save-AACPdfDocument -Pdf $pdf -Path $Path } |