Private/Show-AACPSRuleView.ps1
|
function Show-AACPSRuleView { <# .SYNOPSIS Renders Invoke-AACPSRule's results as a Spectre.Console view. .DESCRIPTION The account and scope, tiles (objects checked, rules, passed, failed, could not evaluate, pass rate), failures by Well-Architected pillar, then one table per pillar with each failing rule - most severe first, then most failures - its severity, name and title, how many resources failed, which (the first few) and why. ── Security · 14 rules failed on 32 resources ────────────────────── │ Severity │ Rule │ Failed │ Resources │ │ Critical │ Azure.Storage.MinTLS │ 3 │ stdev01, stlogs │ │ │ Use secure protocols for ... │ │ Min TLS is 1.0 │ Output goes straight to the Spectre console; wrap the call in Invoke-AACPagedOutput to page it. #> [CmdletBinding()] param( [Parameter(Mandatory)] [AllowEmptyCollection()] [object[]] $Result, [System.Collections.IDictionary] $Scope, [int] $Rules, [int] $Objects, [string[]] $Warning = @() ) $escape = { param($Text) [Spectre.Console.Markup]::Escape([string]$Text) } $glyph = Get-AACGlyph $pillars = [ordered]@{ 'Security' = 'indianred1' 'Reliability' = 'deepskyblue1' 'Cost Optimization' = 'green3' 'Operational Excellence' = 'mediumpurple2' 'Performance Efficiency' = 'gold1' 'Other' = 'grey70' } $severityStyle = @{ Critical = '[bold white on red3] CRITICAL [/]' Important = '[bold black on orange1] IMPORTANT [/]' Awareness = '[black on grey70] AWARENESS [/]' Error = '[bold white on red3] ERROR [/]' Warning = '[bold black on orange1] WARNING [/]' } # The facts line. $facts = [System.Collections.Generic.List[string]]::new() if ($script:AACSession) { $facts.Add("[white]$(& $escape $script:AACSession.Account)[/]") $facts.Add("tenant $(& $escape $script:AACSession.TenantId)") } if ($Scope) { foreach ($key in $Scope.Keys) { $facts.Add("$(& $escape $key): $(& $escape $Scope[$key])") } } Write-AACMarkup "[grey58]$($facts -join " $($glyph.Dot) ")[/]" [Spectre.Console.AnsiConsole]::WriteLine() $passed = @($Result | Where-Object Outcome -EQ 'Pass').Count $failed = @($Result | Where-Object Outcome -EQ 'Fail') $errors = @($Result | Where-Object Outcome -EQ 'Error') $decided = $passed + $failed.Count Show-AACTileRow -Tile @( @{ Value = '{0:N0}' -f $Objects; Caption = 'objects checked'; Color = 'deepskyblue1' } @{ Value = '{0:N0}' -f $Rules; Caption = 'rules'; Color = 'mediumpurple2' } @{ Value = '{0:N0}' -f $passed; Caption = 'passed'; Color = 'green3' } @{ Value = '{0:N0}' -f $failed.Count; Caption = 'failed'; Color = $(if ($failed.Count) { 'red3' } else { 'grey50' }) } @{ Value = '{0:N0}' -f $errors.Count; Caption = 'could not evaluate'; Color = $(if ($errors.Count) { 'orange1' } else { 'grey50' }) } @{ Value = $(if ($decided) { '{0:N1}%' -f (100 * $passed / $decided) } else { '-' }); Caption = 'pass rate'; Color = 'gold1' } ) [Spectre.Console.AnsiConsole]::WriteLine() foreach ($line in $Warning | Select-Object -First 5) { Write-AACMarkup "[orange1]![/] [grey58]$(& $escape $line)[/]" } if (@($Warning).Count -gt 5) { Write-AACMarkup "[grey58] ... and $(@($Warning).Count - 5) more settings that could not be read[/]" } $problems = @($failed + $errors) if ($problems.Count -eq 0) { Show-AACPanel -Content $(if ($Result.Count) { '[bold green3]Every rule passed.[/]' } else { '[bold]No rule applied[/] [grey58]to the resources in scope.[/]' }) -BorderColor $(if ($Result.Count) { 'green3' } else { 'grey50' }) -AllowMarkup return } # Failures by pillar. $bars = @(foreach ($pillar in $pillars.Keys) { $n = @($problems | Where-Object Pillar -EQ $pillar).Count if ($n) { @{ Label = $pillar; Value = $n; Color = $pillars[$pillar] } } }) Show-AACBarChart -Item $bars -Title 'Failed results by Well-Architected pillar' [Spectre.Console.AnsiConsole]::WriteLine() foreach ($pillar in $pillars.Keys) { $inPillar = @($problems | Where-Object Pillar -EQ $pillar) if ($inPillar.Count -eq 0) { continue } $color = $pillars[$pillar] # Most severe first, then the rules that fail most. $rank = @{ Critical = 0; Error = 0; Important = 1; Warning = 1; Awareness = 2 } $byRule = @($inPillar | Group-Object -Property RuleName | Sort-Object -Property @( @{ Expression = { $r = $rank[[string]$_.Group[0].Severity]; if ($null -eq $r) { 3 } else { $r } } } @{ Expression = 'Count'; Descending = $true } 'Name' )) $resourceCount = @($inPillar | ForEach-Object { if ($_.ResourceId) { $_.ResourceId.ToLowerInvariant() } else { $_.ResourceName } } | Select-Object -Unique).Count $table = [Spectre.Console.Table]::new() $table.Border = [Spectre.Console.TableBorder]::Rounded $table.BorderStyle = [Spectre.Console.Style]::Parse($color) $table.Expand = $true $table.Title = [Spectre.Console.TableTitle]::new("[bold $color]$($glyph.Bullet) $(& $escape $pillar)[/] [grey58]$($glyph.Dot)[/] $($byRule.Count) rule$(if ($byRule.Count -ne 1) { 's' }) failed on $resourceCount resource$(if ($resourceCount -ne 1) { 's' })") foreach ($header in @(@('Severity', 12, $true), @('Rule', 0, $false), @('Failed', 7, $true), @('Resources and reasons', 0, $false))) { $column = [Spectre.Console.TableColumn]::new("[grey62]$($header[0])[/]") if ($header[1]) { $column.Width = $header[1] } $column.NoWrap = $header[2] if ($header[0] -eq 'Failed') { $column.Alignment = [Spectre.Console.Justify]::Right } $table.AddColumn($column) | Out-Null } foreach ($group in $byRule) { $first = $group.Group[0] $severity = if ($severityStyle.ContainsKey([string]$first.Severity)) { $severityStyle[[string]$first.Severity] } else { "[grey70]$(& $escape $first.Severity)[/]" } $ruleText = "[bold]$(& $escape $first.RuleName)[/]" if ($first.Title) { $ruleText += "`n[grey58]$(& $escape $first.Title)[/]" } if ($first.Source -ne 'PSRule for Azure') { $ruleText += "`n[italic mediumpurple2]$(& $escape $first.Source)[/]" } $shown = @($group.Group | Select-Object -First 3) $lines = @(foreach ($item in $shown) { $mark = if ($item.Outcome -eq 'Error') { "[orange1]$($glyph.Bullet)[/] " } else { '' } "$mark[white]$(& $escape $item.ResourceName)[/] [grey50]$(& $escape $item.ResourceGroup)[/]" if ($item.Reason) { " [grey58]$(& $escape ($item.Reason.Substring(0, [Math]::Min(110, $item.Reason.Length))))[/]" } }) if ($group.Count -gt $shown.Count) { $lines += "[grey50]and $($group.Count - $shown.Count) more[/]" } $cells = @( [Spectre.Console.Markup]::new($severity) [Spectre.Console.Markup]::new($ruleText) [Spectre.Console.Markup]::new("[bold $color]$($group.Count)[/]") [Spectre.Console.Markup]::new(($lines -join "`n")) ) [Spectre.Console.TableExtensions]::AddRow($table, [Spectre.Console.Rendering.IRenderable[]]$cells) | Out-Null } [Spectre.Console.AnsiConsole]::Write($table) [Spectre.Console.AnsiConsole]::WriteLine() } } |