Private/ConvertTo-AACDiagnosticBundle.ps1
|
function ConvertTo-AACDiagnosticBundle { <# .SYNOPSIS Organizes what Export-AACDiagnosticBundle read into one set of sections per resource - configuration, health, activity, diagnostic settings, locks, recommendations, compliance, alerts, changes and role assignments - with a summary row each. .DESCRIPTION No Azure calls, no files. -ResourceId is the resources asked for; -Read is Invoke-AACGraphBatch's result for Get-AACDiagnosticBundleQuery (every row with a 'target'); -Arm maps a resource ID (lower case) to @{ Activity; Diagnostics; Locks; History } - Invoke-AACArmParallel's answers (Items, Error) for its Activity Log, diagnostic settings, locks and Resource Health history. Returns @{ Resources (@{ Id; Name; Folder; Sections (ordered: file name -> data); Summary (AAC.DiagnosticBundleResource) }); Warnings }. Nothing is redacted here: the writer does it. #> [CmdletBinding()] [OutputType([hashtable])] param( [Parameter(Mandatory)] [string[]] $ResourceId, [Parameter(Mandatory)] [hashtable] $Read, [hashtable] $Arm = @{}, [hashtable] $SubscriptionName = @{} ) $value = { param($Object, [string] $Key) if ($Object -is [System.Collections.IDictionary] -and $Object.Contains($Key)) { $Object[$Key] } } $text = { param($Object, [string] $Key) $v = & $value $Object $Key; if ($null -eq $v) { '' } else { [string]$v } } $byTarget = @{} foreach ($name in @($Read.Rows.Keys)) { $byTarget[$name] = @{} foreach ($row in @($Read.Rows[$name] | Where-Object { $_ })) { $t = & $text $row 'target' if (-not $byTarget[$name].Contains($t)) { $byTarget[$name][$t] = [System.Collections.Generic.List[object]]::new() } $byTarget[$name][$t].Add($row) } } $rowsFor = { param([string] $Name, [string] $Id) if ($byTarget.Contains($Name) -and $byTarget[$Name].Contains($Id)) { @($byTarget[$Name][$Id]) } else { @() } } $warnings = [System.Collections.Generic.List[string]]::new() foreach ($key in @($Read.Errors.Keys | Sort-Object)) { $warnings.Add("Resource Graph: the $key couldn't be read: $($Read.Errors[$key] -replace '\s+', ' ')") } $answer = { param([string] $Id, [string] $Part, [string] $Label) $a = if ($Arm.Contains($Id)) { $Arm[$Id][$Part] } else { $null } if ($null -eq $a) { return @{ Read = $false; Items = @() } } if ($a.Error) { $warnings.Add("$(($Id -split '/')[-1]): the $Label couldn't be read: $($a.Error -replace '\s+', ' ')"); return @{ Read = $false; Items = @() } } @{ Read = $true; Items = @($a.Items | Where-Object { $_ }) } } $folders = @{} $resources = @(foreach ($raw in @($ResourceId | ForEach-Object { $_.TrimEnd('/').ToLowerInvariant() } | Select-Object -Unique)) { $config = @(& $rowsFor 'config' $raw) | Select-Object -First 1 $name = if ($config) { & $text $config 'name' } else { ($raw -split '/')[-1] } $folder = ($name -replace '[^\w.\-]', '_') if ($folders.Contains($folder.ToLowerInvariant())) { $folder = "$folder-$($folders.Count + 1)" } $folders[$folder.ToLowerInvariant()] = $true $activity = & $answer $raw 'Activity' 'Activity Log' $diagnostics = & $answer $raw 'Diagnostics' 'diagnostic settings' $locks = & $answer $raw 'Locks' 'locks' $history = & $answer $raw 'History' 'Resource Health history' $health = @(& $rowsFor 'health' $raw) | Select-Object -First 1 $failedOps = @($activity.Items | Where-Object { [string](& $value (& $value $_ 'status') 'value') -eq 'Failed' }) $sections = [ordered]@{ 'config.json' = $config 'health.json' = [ordered]@{ current = $health; history = $history.Items; events = @(& $rowsFor 'healthEvents' $raw) } 'activity-log.json' = $activity.Items 'diagnostic-settings.json' = $diagnostics.Items 'locks.json' = $locks.Items 'advisor.json' = @(& $rowsFor 'advisor' $raw) 'defender.json' = @(& $rowsFor 'defender' $raw) 'policy.json' = @(& $rowsFor 'policy' $raw) 'alerts.json' = @(& $rowsFor 'alerts' $raw) 'changes.json' = @(& $rowsFor 'changes' $raw) 'role-assignments.json' = @(& $rowsFor 'roleAssignments' $raw) } if (-not $config) { $warnings.Add("$name wasn't found in Resource Graph (deleted, a typo, or no access): its configuration is missing.") } $subscription = if ($raw -match '^/subscriptions/([^/]+)') { $Matches[1] } else { '' } $issues = @( if (-not $config) { 'not found' } if ($health -and (& $text $health 'state') -in 'Unavailable', 'Degraded') { "health $((& $text $health 'state').ToLowerInvariant())" } if ($failedOps.Count) { "$($failedOps.Count) failed operation(s)" } if (@(& $rowsFor 'alerts' $raw).Count) { "$(@(& $rowsFor 'alerts' $raw).Count) alert(s)" } if ($diagnostics.Read -and -not $diagnostics.Items.Count) { 'no diagnostic settings' } ) $summary = [pscustomobject][ordered]@{ Status = $(if (-not $config -or ($health -and (& $text $health 'state') -eq 'Unavailable')) { 'Failed' } elseif ($issues.Count) { 'Warning' } else { 'Success' }) Resource = $name ResourceType = $(if ($config) { & $text $config 'type' } else { '' }) ResourceGroup = $(if ($config) { & $text $config 'resourceGroup' } elseif ($raw -match '/resourcegroups/([^/]+)') { $Matches[1] } else { '' }) Subscription = $(if ($SubscriptionName.Contains($subscription)) { $SubscriptionName[$subscription] } else { $subscription }) Health = $(if ($health) { & $text $health 'state' } else { '' }) ActivityEvents = $activity.Items.Count FailedOperations = $failedOps.Count Alerts = @(& $rowsFor 'alerts' $raw).Count Advisor = @(& $rowsFor 'advisor' $raw).Count Defender = @(& $rowsFor 'defender' $raw).Count PolicyNonCompliant = @(& $rowsFor 'policy' $raw).Count Changes = @(& $rowsFor 'changes' $raw).Count Locks = $locks.Items.Count DiagnosticSettings = $(if ($diagnostics.Read) { $diagnostics.Items.Count } else { $null }) Issues = ($issues -join ', ') Folder = "resources/$folder" ResourceId = $raw } $summary.PSObject.TypeNames.Insert(0, 'AAC.DiagnosticBundleResource') @{ Id = $raw; Name = $name; Folder = $folder; Sections = $sections; Summary = $summary } }) @{ Resources = $resources; Warnings = @($warnings) } } |