Public/Export-NSPFortiGateVpnReport.ps1
|
function Export-NSPFortiGateVpnReport { <# .SYNOPSIS Writes a printable HTML document and an Excel workbook of a VPN tunnel's access chain. .DESCRIPTION One sheet per topic; in the HTML each starts on a new printed page (landscape) so the printout can be separated and cross-referenced, and in the workbook each is its own tab (header row frozen and filterable) after an About tab: Cover tunnel settings, sources, checks worth a look, contents 1 firewall policies using the tunnel 2 user groups, the VSA each expects, and the NPS policy that sends it 3 service groups expanded to protocol/port 4 address groups expanded to addresses 5 certificate peer groups, the subject each peer matches, and its template 6 NPS network policies: conditions, VSAs returned, where each lands 7 AD group membership tree 8 certificate templates: enrollment groups and TameMyCerts OU stamp 9 effective access by AD group, including users an earlier NPS policy shadows Sheets 1-5 need only the FortiGate captures. Sheets 6 and 9 need -NpsConfig, sheets 7 and 8 need -AdInventory; without them those sheets print as headed placeholders. The Checks list on the cover collects mismatches found along the way (VSA case differences, VSAs no FortiGate group matches, shadowed users, peers no template stamps). .PARAMETER Path FortiGate capture files, parsed together (policies, groups, services, addresses, RADIUS, peers, IPsec phase1/phase2), or a full configuration backup. .PARAMETER Tunnel Phase1 interface name. Only policies with it as source or destination interface are documented. Omit to document every policy loaded. .PARAMETER OutputPath The .html file to write. .PARAMETER ExcelPath The .xlsx file to write. Defaults to OutputPath with an .xlsx extension. .PARAMETER NoExcel Write the HTML only. .PARAMETER NpsConfig A copy of the NPS server's ias.xml (C:\Windows\System32\ias\ias.xml) or a 'netsh nps export'. Only policies and profiles are read; RADIUS client entries (shared secrets) are not. .PARAMETER AdInventory The ADInventory_*.json written by AD-Manager menu 6. .PARAMETER DeviceName Defaults to the hostname in the capture's CLI prompt. .EXAMPLE Export-NSPFortiGateVpnReport -Path .\captures\*.txt -Tunnel 'Dialup-IKEv2' -OutputPath .\Dialup-IKEv2.html .EXAMPLE Export-NSPFortiGateVpnReport -Path .\captures\*.txt -Tunnel 'Dialup-IKEv2' -NpsConfig .\ias.xml -AdInventory .\ADInventory_example.json -OutputPath .\Dialup-IKEv2.html #> [CmdletBinding()] [OutputType([System.IO.FileInfo])] param( [Parameter(Mandatory, Position = 0)][ValidateNotNullOrEmpty()][string[]]$Path, [string]$Tunnel, [Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$OutputPath, [string]$ExcelPath, [switch]$NoExcel, [string]$NpsConfig, [string]$AdInventory, [string]$DeviceName, [string]$Title ) $report = Get-NSPFortiGateVpnReportData -Path $Path -Tunnel $Tunnel -NpsConfig $NpsConfig -AdInventory $AdInventory -DeviceName $DeviceName -Title $Title $encode = { param($Value) [System.Net.WebUtility]::HtmlEncode([string]$Value) } $generated = Get-Date -Format 'yyyy-MM-dd HH:mm' $css = @' @page { size: letter landscape; margin: 0.45in; } * { box-sizing: border-box; } body { font-family: "Segoe UI", Calibri, Arial, sans-serif; font-size: 10pt; color: #1a1a1a; margin: 0 auto; max-width: 1700px; padding: 16px; background: #fff; } h1 { font-size: 20pt; margin: 0 0 4px; } h2 { font-size: 14pt; margin: 0 0 6px; } .sheet { break-before: page; page-break-before: always; } .sheet-head { display: flex; justify-content: space-between; border-bottom: 2px solid #1a1a1a; padding-bottom: 4px; margin-bottom: 10px; font-size: 9pt; color: #444; } .sheet-head strong { color: #1a1a1a; } p { margin: 0 0 8px; } ul { margin: 0 0 10px; padding-left: 20px; } table { width: 100%; border-collapse: collapse; font-size: 9pt; } thead { display: table-header-group; } tr { break-inside: avoid; page-break-inside: avoid; } th { background: #e8e8e8; text-align: left; } th, td { border: 1px solid #9a9a9a; padding: 3px 6px; vertical-align: top; overflow-wrap: break-word; } td.group { font-weight: 600; background: #f6f6f6; } th.nw, td.nw { white-space: nowrap; overflow-wrap: normal; } tr.disabled td { color: #777; font-style: italic; } tr.disabled td.mixed { color: inherit; font-style: normal; } .empty, .todo { border: 1px dashed #9a9a9a; padding: 18px; color: #555; } .todo { min-height: 4in; } dl { display: grid; grid-template-columns: max-content 1fr; gap: 3px 16px; margin: 12px 0; } dt { font-weight: 600; } dd { margin: 0; } .checks li { margin: 3px 0; } .contents li { margin: 2px 0; } .meta { color: #555; font-size: 9pt; } @media screen { .sheet { margin-top: 36px; border-top: 1px dashed #bbb; padding-top: 16px; } } @media print { body { max-width: none; padding: 0; } } '@ $html = New-Object System.Text.StringBuilder [void]$html.Append("<!DOCTYPE html>`n<html lang=""en""><head><meta charset=""utf-8""><title>$(& $encode $report.Title)</title><style>$css</style></head><body>") [void]$html.Append("<section><h1>$(& $encode $report.Title)</h1><p class=""meta"">Generated $generated.</p>") if ($report.Facts.Count) { [void]$html.Append('<dl>') foreach ($key in $report.Facts.Keys) { if ($report.Facts[$key]) { [void]$html.Append("<dt>$(& $encode $key)</dt><dd>$(& $encode $report.Facts[$key])</dd>") } } [void]$html.Append('</dl>') } [void]$html.Append("<p><strong>$($report.PolicyCount)</strong> firewall policies reference this tunnel.</p>") if ($report.Checks.Count) { [void]$html.Append('<h2>Checks</h2><ul class="checks">') foreach ($check in $report.Checks) { [void]$html.Append("<li>$(& $encode $check)</li>") } [void]$html.Append('</ul>') } [void]$html.Append('<h2>Contents</h2><ol class="contents">') foreach ($sheet in $report.Sheets) { [void]$html.Append("<li>$(& $encode $sheet.Title)</li>") } [void]$html.Append('</ol><p class="meta">Sources:<br>' + (($report.Sources | ForEach-Object { & $encode $_ }) -join '<br>') + '</p></section>') for ($i = 0; $i -lt $report.Sheets.Count; $i++) { $sheet = $report.Sheets[$i] [void]$html.Append("<section class=""sheet""><div class=""sheet-head""><span><strong>Sheet $($i + 1) of $($report.Sheets.Count): $(& $encode $sheet.Title)</strong></span><span>$(& $encode "$($report.DeviceName) / $($report.TunnelName)")</span></div>") [void]$html.Append("<h2>$($i + 1). $(& $encode $sheet.Title)</h2><p>$(& $encode $sheet.Intro)</p>") if (@($sheet.Notes).Count) { [void]$html.Append('<ul>') foreach ($note in $sheet.Notes) { [void]$html.Append("<li>$(& $encode $note)</li>") } [void]$html.Append('</ul>') } if ($sheet.Placeholder) { [void]$html.Append("<div class=""todo"">$(& $encode $sheet.Placeholder)</div>") } else { $empty = if ($sheet.Empty) { $sheet.Empty } else { 'None referenced.' } $span = if ($sheet.SpanColumns) { $sheet.SpanColumns } else { @() } $nowrap = if ($sheet.NoWrapColumns) { $sheet.NoWrapColumns } else { @() } $widths = if ($sheet.ColumnWidth) { $sheet.ColumnWidth } else { @{} } [void]$html.Append((Format-NSPFortiGateHtmlTable -Columns $sheet.Columns -Rows @($sheet.Rows) -SpanColumns $span -NoWrapColumns $nowrap -ColumnWidth $widths -Empty $empty)) } [void]$html.Append('</section>') } [void]$html.Append('</body></html>') $OutputPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($OutputPath) $directory = Split-Path -Parent $OutputPath if ($directory -and -not (Test-Path -LiteralPath $directory)) { New-Item -ItemType Directory -Path $directory -Force | Out-Null } [IO.File]::WriteAllText($OutputPath, $html.ToString(), (New-Object System.Text.UTF8Encoding $true)) Get-Item -LiteralPath $OutputPath if (-not $NoExcel) { if (-not $ExcelPath) { $ExcelPath = [IO.Path]::ChangeExtension($OutputPath, '.xlsx') } # About tab: what the HTML cover and sheet introductions say, as rows. $about = New-Object System.Collections.Generic.List[hashtable] $about.Add(@{ Item = 'Title'; Detail = $report.Title }) $about.Add(@{ Item = 'Generated'; Detail = $generated }) foreach ($key in $report.Facts.Keys) { if ($report.Facts[$key]) { $about.Add(@{ Item = $key; Detail = $report.Facts[$key] }) } } foreach ($source in $report.Sources) { $about.Add(@{ Item = 'Source'; Detail = $source }) } foreach ($check in $report.Checks) { $about.Add(@{ Item = 'Check'; Detail = $check }) } for ($i = 0; $i -lt $report.Sheets.Count; $i++) { $sheet = $report.Sheets[$i] $about.Add(@{ Item = "Sheet $($i + 1): $($sheet.Title)"; Detail = (@($sheet.Intro) + @($sheet.Notes | ForEach-Object { "- $_" }) + @($sheet.Placeholder | Where-Object { $_ })) -join "`n" }) } $workbook = @(@{ Name = 'About'; Columns = @('Item', 'Detail'); Rows = $about.ToArray() }) for ($i = 0; $i -lt $report.Sheets.Count; $i++) { $sheet = $report.Sheets[$i] $workbook += @{ Name = "$($i + 1) $($sheet.Title)"; Columns = $sheet.Columns; Rows = @($sheet.Rows) } } Write-NSPFortiGateXlsx -Path $ExcelPath -Sheet $workbook } } |