modules/normalizers/Normalize-AksRightsizing.ps1
|
#Requires -Version 7.4 [CmdletBinding()] param () . "$PSScriptRoot\..\shared\Schema.ps1" . "$PSScriptRoot\..\shared\Canonicalize.ps1" function ConvertTo-AksRightsizingSeverity { param([string]$RawSeverity) switch -Regex (($RawSeverity ?? '').ToLowerInvariant()) { '^critical$' { 'Critical' } '^high$' { 'High' } '^medium$' { 'Medium' } '^low$' { 'Low' } '^info' { 'Info' } default { 'Info' } } } function Convert-ToStringArray { param ([object]$Value) if ($null -eq $Value) { return @() } $items = [System.Collections.Generic.List[string]]::new() if ($Value -is [string]) { if (-not [string]::IsNullOrWhiteSpace($Value)) { $items.Add($Value.Trim()) | Out-Null } } else { foreach ($item in @($Value)) { if ($null -eq $item) { continue } $text = [string]$item if (-not [string]::IsNullOrWhiteSpace($text)) { $items.Add($text.Trim()) | Out-Null } } } return @($items) } function Convert-ToHashtableArray { param ([object]$Value) $items = [System.Collections.Generic.List[hashtable]]::new() foreach ($entry in @($Value)) { if ($null -eq $entry) { continue } if ($entry -is [System.Collections.IDictionary]) { $map = @{} foreach ($key in $entry.Keys) { $map[[string]$key] = $entry[$key] } $items.Add($map) | Out-Null continue } if ($entry.PSObject -and $entry.PSObject.Properties.Count -gt 0) { $map = @{} foreach ($prop in @($entry.PSObject.Properties)) { $map[$prop.Name] = $prop.Value } $items.Add($map) | Out-Null } } return @($items) } function Resolve-AksRightsizingPillar { param([string]$FindingCategory) switch -Regex (($FindingCategory ?? '').ToLowerInvariant()) { 'overprovisioned|idle' { return 'Cost Optimization' } 'underprovisioned|oomkilled|missinghpa' { return 'Performance Efficiency' } default { return 'Performance Efficiency' } } } function Get-AksSignalPercent { param( [string]$FindingCategory, [double]$ObservedPercent ) if (($FindingCategory ?? '') -match '(?i)overprovisioned|idle') { return [math]::Round([math]::Max(0, (100.0 - $ObservedPercent)), 2) } return [math]::Round([math]::Max(0, $ObservedPercent), 2) } function Resolve-AksRightsizingImpact { param( [string]$FindingCategory, [double]$ObservedPercent ) $category = ($FindingCategory ?? '').ToLowerInvariant() $signal = Get-AksSignalPercent -FindingCategory $FindingCategory -ObservedPercent $ObservedPercent if ($category -match 'overprovisioned|idle') { if ($signal -ge 80) { return 'High' } if ($signal -ge 50) { return 'Medium' } return 'Low' } if ($signal -ge 90) { return 'High' } if ($signal -ge 50) { return 'Medium' } return 'Low' } function Resolve-AksRightsizingEffort { param([string]$FindingCategory) switch -Regex (($FindingCategory ?? '').ToLowerInvariant()) { 'missinghpa' { return 'Medium' } 'oomkilled' { return 'High' } default { return 'Low' } } } function Resolve-AksBaselineTags { param( [string]$FindingCategory, [string]$MetricType ) $tags = [System.Collections.Generic.List[string]]::new() $category = ($FindingCategory ?? '').Trim() $metric = ($MetricType ?? '').Trim() switch -Regex ($category.ToLowerInvariant()) { 'overprovisionedcpu|underprovisionedcpu' { $tags.Add('AKS-RightSizing-CPU') | Out-Null } 'overprovisionedmemory|underprovisionedmemory' { $tags.Add('AKS-RightSizing-Memory') | Out-Null } 'missinghpa' { $tags.Add('AKS-MissingHPA') | Out-Null } 'oomkilled' { $tags.Add('AKS-OOMKilled') | Out-Null } } if (-not [string]::IsNullOrWhiteSpace($category)) { $tags.Add("AKS-$category") | Out-Null } if (-not [string]::IsNullOrWhiteSpace($metric)) { $tags.Add("AKS-RightSizing-$($metric.ToUpperInvariant())") | Out-Null } return @($tags | Select-Object -Unique) } function New-AksWorkloadEntityId { param( [string]$ClusterResourceId, [string]$Namespace, [string]$WorkloadName ) $clusterId = ($ClusterResourceId ?? '').Trim().ToLowerInvariant() if ([string]::IsNullOrWhiteSpace($clusterId)) { return '' } $ns = if ([string]::IsNullOrWhiteSpace($Namespace)) { '_cluster' } else { $Namespace.Trim().ToLowerInvariant() } $workload = if ([string]::IsNullOrWhiteSpace($WorkloadName)) { '_cluster' } else { $WorkloadName.Trim().ToLowerInvariant() } return "$clusterId/namespaces/$ns/workloads/$workload" } function Normalize-AksRightsizing { [CmdletBinding()] param ( [Parameter(Mandatory)] [PSCustomObject] $ToolResult ) if ($ToolResult.Status -notin @('Success', 'PartialSuccess') -or -not $ToolResult.Findings) { return @() } $runId = [guid]::NewGuid().ToString() $normalized = [System.Collections.Generic.List[PSCustomObject]]::new() foreach ($f in $ToolResult.Findings) { $rawId = if ($f.PSObject.Properties['ResourceId'] -and $f.ResourceId) { [string]$f.ResourceId } else { '' } if ([string]::IsNullOrWhiteSpace($rawId)) { continue } $namespace = if ($f.PSObject.Properties['Namespace']) { [string]$f.Namespace } else { '' } $workloadName = if ($f.PSObject.Properties['WorkloadName']) { [string]$f.WorkloadName } else { '' } $findingCategory = if ($f.PSObject.Properties['FindingCategory']) { [string]$f.FindingCategory } else { '' } $metricType = if ($f.PSObject.Properties['MetricType']) { [string]$f.MetricType } else { '' } $observedPercent = if ($f.PSObject.Properties['ObservedPercent']) { [double]$f.ObservedPercent } else { 0.0 } $entityRawId = New-AksWorkloadEntityId -ClusterResourceId $rawId -Namespace $namespace -WorkloadName $workloadName if ([string]::IsNullOrWhiteSpace($entityRawId)) { $entityRawId = $rawId } $canonicalId = $entityRawId.ToLowerInvariant() try { $canonicalId = (ConvertTo-CanonicalEntityId -RawId $entityRawId -EntityType 'AzureResource').CanonicalId } catch { $canonicalId = $entityRawId.ToLowerInvariant() } $subId = '' $rg = '' if ($rawId -match '/subscriptions/([^/]+)') { $subId = $Matches[1] } if ($rawId -match '/resourceGroups/([^/]+)') { $rg = $Matches[1] } $severity = ConvertTo-AksRightsizingSeverity -RawSeverity ([string]$f.Severity) $findingId = if ($f.PSObject.Properties['Id'] -and $f.Id) { [string]$f.Id } else { [guid]::NewGuid().ToString() } $title = if ($f.PSObject.Properties['Title'] -and $f.Title) { [string]$f.Title } else { 'AKS rightsizing signal' } $detail = if ($f.PSObject.Properties['Detail'] -and $f.Detail) { [string]$f.Detail } else { '' } $remediation = if ($f.PSObject.Properties['Remediation'] -and $f.Remediation) { [string]$f.Remediation } else { '' } $category = if ($f.PSObject.Properties['Category'] -and $f.Category) { [string]$f.Category } else { 'Performance' } $learnMoreUrl = if ($f.PSObject.Properties['LearnMoreUrl']) { [string]$f.LearnMoreUrl } else { '' } $source = if ($f.PSObject.Properties['Source'] -and $f.Source) { [string]$f.Source } else { 'aks-rightsizing' } $compliant = $false if ($f.PSObject.Properties['Compliant']) { $compliant = [bool]$f.Compliant } $pillar = if ($f.PSObject.Properties['Pillar'] -and $f.Pillar) { [string]$f.Pillar } else { Resolve-AksRightsizingPillar -FindingCategory $findingCategory } $impact = if ($f.PSObject.Properties['Impact'] -and $f.Impact) { [string]$f.Impact } else { Resolve-AksRightsizingImpact -FindingCategory $findingCategory -ObservedPercent $observedPercent } $effort = if ($f.PSObject.Properties['Effort'] -and $f.Effort) { [string]$f.Effort } else { Resolve-AksRightsizingEffort -FindingCategory $findingCategory } $deepLinkUrl = if ($f.PSObject.Properties['DeepLinkUrl'] -and $f.DeepLinkUrl) { [string]$f.DeepLinkUrl } else { $learnMoreUrl } $remediationSnippets = if ($f.PSObject.Properties['RemediationSnippets']) { @(Convert-ToHashtableArray $f.RemediationSnippets) } else { @() } $evidenceUris = if ($f.PSObject.Properties['EvidenceUris']) { @(Convert-ToStringArray $f.EvidenceUris) } else { @() } $baselineTags = if ($f.PSObject.Properties['BaselineTags']) { @(Convert-ToStringArray $f.BaselineTags) } else { @(Resolve-AksBaselineTags -FindingCategory $findingCategory -MetricType $metricType) } $scoreDelta = if ($f.PSObject.Properties['ScoreDelta'] -and $null -ne $f.ScoreDelta) { [double]$f.ScoreDelta } else { [double](Get-AksSignalPercent -FindingCategory $findingCategory -ObservedPercent $observedPercent) } $entityRefs = if ($f.PSObject.Properties['EntityRefs']) { @(Convert-ToStringArray $f.EntityRefs) } else { @($rawId, "namespace:$namespace", "workload:$workloadName") } $toolVersion = if ($f.PSObject.Properties['ToolVersion'] -and $f.ToolVersion) { [string]$f.ToolVersion } elseif ($ToolResult.PSObject.Properties['ToolVersion']) { [string]$ToolResult.ToolVersion } else { '' } $row = New-FindingRow -Id $findingId ` -Source $source -EntityId $canonicalId -EntityType 'AzureResource' ` -Title $title -Compliant $compliant -ProvenanceRunId $runId ` -Platform 'Azure' -Category $category -Severity $severity ` -Detail $detail -Remediation $remediation ` -LearnMoreUrl $learnMoreUrl -ResourceId $rawId ` -SubscriptionId $subId -ResourceGroup $rg ` -Pillar $pillar -Impact $impact -Effort $effort ` -DeepLinkUrl $deepLinkUrl -RemediationSnippets $remediationSnippets ` -EvidenceUris $evidenceUris -BaselineTags $baselineTags ` -ScoreDelta $scoreDelta -EntityRefs $entityRefs -ToolVersion $toolVersion if ($null -eq $row) { continue } foreach ($extra in @( 'FindingCategory', 'ClusterName', 'ClusterResourceGroup', 'Namespace', 'WorkloadName', 'ContainerName', 'MetricType', 'ObservedPercent', 'RecommendedMillicores', 'RecommendedMemoryMiB' )) { if ($f.PSObject.Properties[$extra] -and $null -ne $f.$extra -and [string]$f.$extra -ne '') { $row | Add-Member -NotePropertyName $extra -NotePropertyValue $f.$extra -Force } } $normalized.Add($row) } return @($normalized) } |