src/ingest/Import-AdvisorScores.ps1
|
#Requires -Version 7.0 Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' <# .SYNOPSIS Pull Azure Advisor recommendations per enabled subscription into collect.advisor. .DESCRIPTION Two sources, in preference order. When the caller hands over rows the inventory pass already collected (`-FromInventory`), they are shaped into the contract below and no Azure call is made. This is the collect-once path: inventory reads Advisor from the `advisorresources` Resource Graph table on every run, so in a combined run the per-subscription `Get-AzAdvisorRecommendation` sweep re-fetched data already sitting in memory, through a slower API (AB#6777, audit section 10 defect 4). Otherwise it falls back to the per-subscription cmdlet, which is still the only source for an assessment-only run. .PARAMETER Collect The collect object to attach `advisor` to. .PARAMETER FromInventory `advisorresources` rows from an inventory pass -- `$ExtractionData.Advisories`. Rows carry the full ARM `properties` bag; this function projects the same seven fields the cmdlet path produces, because the rule files query them by name (`$.advisor[?(@.Category == 'Cost')]`) and a shape difference between the two paths would make findings depend on how the run was started. .NOTES Read-only. Tracks ADO Story AB#5040; collect-once rework AB#6777 (Story AB#6773). #> function Import-AdvisorScores { param( $Collect, [object[]] $FromInventory ) # ---- collect-once path ---- if ($null -ne $FromInventory -and @($FromInventory).Count -gt 0) { $shaped = @( foreach ($row in @($FromInventory)) { if ($null -eq $row) { continue } # Every read below is guarded. These are raw Resource Graph rows: `properties` # is whatever ARM indexed, `shortDescription` is absent on some recommendation # types, and a chained dot into a missing key throws under StrictMode rather # than returning $null. $props = if ($row.PSObject.Properties['properties']) { $row.properties } else { $null } if ($null -eq $props) { continue } $short = if ($props.PSObject.Properties['shortDescription']) { $props.shortDescription } else { $null } [pscustomobject]@{ Category = if ($props.PSObject.Properties['category']) { [string] $props.category } else { $null } Impact = if ($props.PSObject.Properties['impact']) { [string] $props.impact } else { $null } ImpactedField = if ($props.PSObject.Properties['impactedField']) { [string] $props.impactedField } else { $null } ImpactedValue = if ($props.PSObject.Properties['impactedValue']) { [string] $props.impactedValue } else { $null } Subscription = if ($row.PSObject.Properties['subscriptionId']) { [string] $row.subscriptionId } else { $null } ShortDescriptionProblem = if ($short -and $short.PSObject.Properties['problem']) { [string] $short.problem } else { $null } ShortDescriptionSolution = if ($short -and $short.PSObject.Properties['solution']) { [string] $short.solution } else { $null } } } ) Write-Verbose "Import-AdvisorScores: shaped $($shaped.Count) advisor rows from the inventory pass -- no Azure call made (AB#6777)." $Collect | Add-Member -NotePropertyName advisor -NotePropertyValue $shaped -Force return $Collect } # ---- fallback: per-subscription cmdlet sweep ---- $context = Get-AzContext -ErrorAction SilentlyContinue $tenantId = if ($context -and $context.Tenant -and $context.Tenant.Id) { $context.Tenant.Id } else { $null } # AB#6777. The loop below calls Set-AzContext per subscription and used to leave the caller # on whichever one happened to be last -- so anything running after an assessment in the # same session silently inherited a different subscription. try/finally, and the restore is # written inline rather than through a helper because this file is dot-sourced standalone. $originalSubscriptionId = $null if ($context -and $context.PSObject.Properties.Name -contains 'Subscription' -and $context.Subscription) { if ($context.Subscription.PSObject.Properties.Name -contains 'Id') { $originalSubscriptionId = $context.Subscription.Id } } try { $subs = (Get-AzSubscription -TenantId $tenantId | Where-Object State -eq 'Enabled') $recs = foreach ($s in $subs) { $switchParams = @{ Subscription = $s.Id } $subscriptionTenantId = if ($s.PSObject.Properties.Name -contains 'TenantId') { $s.TenantId } else { $tenantId } if ($subscriptionTenantId) { $switchParams['Tenant'] = $subscriptionTenantId } Set-AzContext @switchParams | Out-Null Get-AzAdvisorRecommendation | Select-Object Category, Impact, ImpactedField, ImpactedValue, @{ n = 'Subscription'; e = { $s.Name } }, ShortDescriptionProblem, ShortDescriptionSolution } $Collect | Add-Member -NotePropertyName advisor -NotePropertyValue (@($recs)) -Force } finally { if ($originalSubscriptionId) { $restoreParams = @{ Subscription = $originalSubscriptionId; ErrorAction = 'SilentlyContinue' } if ($tenantId) { $restoreParams['Tenant'] = $tenantId } Set-AzContext @restoreParams | Out-Null } } return $Collect } |