Public/Export-GkTenantAssessment.ps1
|
function Export-GkTenantAssessment { <# .SYNOPSIS Run the PSGraphKit read suite and export a single self-contained HTML assessment (and optionally CSVs) — an engagement deliverable. .DESCRIPTION Runs a curated set of read-only reports and renders them into one self-contained HTML file (inline CSS, no external assets), suitable to hand to a customer. Each report is invoked with -AsReport so nested/array values are already flattened for tabular output. A report that fails (e.g. a missing scope or license) is skipped with a warning and noted in the document, so one gap does not abort the whole assessment. Uses only Microsoft.PowerShell.Utility (ConvertTo-Html / Export-Csv) — no reporting-module dependency. Read-only; it changes nothing in the tenant. Each underlying report validates its own scopes, so connect with a broad read set first (e.g. Connect-GkGraph -AllCommands). .PARAMETER Path Output HTML file path. Defaults to .\PSGraphKit-Assessment.html. .PARAMETER CsvFolder If set, also writes one CSV per section into this folder. .PARAMETER Include Section keys to include (default all). Keys: StaleUsers, Guests, Licenses, Roles, Mfa, Apps, Groups, ConditionalAccess, Devices, ServicePrincipals, AuthMethods, NamedLocations, CustomRoles, LicenseErrors. .PARAMETER PassThru Return the generated file object. .EXAMPLE Connect-GkGraph -AllCommands Export-GkTenantAssessment -Path .\contoso.html Full assessment to one HTML file. .EXAMPLE Export-GkTenantAssessment -Include StaleUsers, Guests, Licenses, Roles -CsvFolder .\out A focused assessment plus per-section CSVs. .EXAMPLE Export-GkTenantAssessment -Path .\a.html -PassThru | Invoke-Item Generate and open the report. .OUTPUTS System.IO.FileInfo (with -PassThru) #> [CmdletBinding()] [OutputType([System.IO.FileInfo])] param( [string] $Path = (Join-Path (Get-Location) 'PSGraphKit-Assessment.html'), [string] $CsvFolder, [string[]] $Include, [switch] $PassThru ) begin { $ctx = Test-GkConnection -FunctionName 'Export-GkTenantAssessment' $now = [datetime]::UtcNow Add-Type -AssemblyName System.Web -ErrorAction SilentlyContinue } process { $sections = [ordered]@{ StaleUsers = @{ Title = 'Stale Users'; Script = { Get-GkStaleUser -AsReport } } Guests = @{ Title = 'Guest Inventory'; Script = { Get-GkGuestInventory -SkipSponsor -AsReport } } Licenses = @{ Title = 'License Overview'; Script = { Get-GkLicenseOverview -AsReport } } Roles = @{ Title = 'Admin Role Assignments'; Script = { Get-GkAdminRoleAssignment -AsReport } } Mfa = @{ Title = 'MFA Registration'; Script = { Get-GkUserMfaStatus -AsReport } } Apps = @{ Title = 'App Registrations'; Script = { Get-GkAppRegistrationReport -AsReport } } Groups = @{ Title = 'Groups'; Script = { Get-GkGroupReport -SkipMemberCount -AsReport } } ConditionalAccess = @{ Title = 'Conditional Access Policies'; Script = { Get-GkCaPolicyReport -AsReport } } Devices = @{ Title = 'Devices'; Script = { Get-GkDeviceInventory -AsReport } } ServicePrincipals = @{ Title = 'Service Principals'; Script = { Get-GkServicePrincipalReport -AsReport } } AuthMethods = @{ Title = 'Authentication Methods'; Script = { Get-GkAuthMethodPolicy -AsReport } } NamedLocations = @{ Title = 'Named Locations'; Script = { Get-GkNamedLocation -AsReport } } CustomRoles = @{ Title = 'Custom Roles'; Script = { Get-GkCustomRole -AsReport } } LicenseErrors = @{ Title = 'License Assignment Errors'; Script = { Get-GkLicenseAssignmentError -AsReport } } } $keys = if ($Include) { $Include } else { @($sections.Keys) } if ($CsvFolder -and -not (Test-Path $CsvFolder)) { New-Item -ItemType Directory -Path $CsvFolder -Force | Out-Null } $fragments = [System.Collections.Generic.List[string]]::new() $tenant = if ($ctx) { $ctx.TenantId } else { 'unknown' } $account = if ($ctx) { $ctx.Account } else { 'unknown' } $fragments.Add("<h1>Microsoft Entra Tenant Assessment</h1>") $fragments.Add("<p class='meta'>Tenant $tenant · generated $($now.ToString('u')) by $account</p>") foreach ($key in $keys) { if (-not $sections.Contains($key)) { Write-Warning "Unknown section '$key' — skipped."; continue } $section = $sections[$key] Write-Verbose "Assessment section: $($section.Title)" try { $data = @(& $section.Script -WarningAction SilentlyContinue -ErrorAction Stop) if ($data.Count -eq 0) { $fragments.Add("<h2>$($section.Title)</h2><p class='empty'>No data.</p>") } else { $fragments.Add(($data | ConvertTo-Html -Fragment -PreContent "<h2>$($section.Title) <span class='count'>($($data.Count))</span></h2>" | Out-String)) if ($CsvFolder) { $data | Export-Csv -Path (Join-Path $CsvFolder "$key.csv") -NoTypeInformation -Encoding UTF8 } } } catch { Write-Warning "Section '$($section.Title)' failed and was skipped: $($_.Exception.Message)" $fragments.Add("<h2>$($section.Title)</h2><p class='error'>Unavailable: $([System.Web.HttpUtility]::HtmlEncode($_.Exception.Message))</p>") } } $head = @' <style> body { font-family: Segoe UI, Arial, sans-serif; margin: 2rem; color: #222; } h1 { border-bottom: 3px solid #0067b8; padding-bottom: .3rem; } h2 { color: #0067b8; margin-top: 2rem; border-bottom: 1px solid #ddd; padding-bottom: .2rem; } .meta { color: #666; } .count { color: #888; font-weight: normal; font-size: .8em; } .empty { color: #888; font-style: italic; } .error { color: #a80000; } table { border-collapse: collapse; width: 100%; margin: .5rem 0; font-size: .85rem; } th { background: #0067b8; color: #fff; text-align: left; padding: 6px 8px; } td { border: 1px solid #ddd; padding: 4px 8px; vertical-align: top; } tr:nth-child(even) td { background: #f6f8fa; } </style> <title>Entra Tenant Assessment</title> '@ ConvertTo-Html -Head $head -Body ($fragments -join "`n") | Out-File -FilePath $Path -Encoding utf8 Write-Verbose "Wrote assessment to $Path" if ($PassThru) { Get-Item -Path $Path } } } |