src/collect/Get-ScoutOperationalCollectorEnrichment.ps1
|
#Requires -Version 7.0 Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' <# .SYNOPSIS Prefetch live operational data used by legacy inventory collectors. .DESCRIPTION Produces stable synthetic envelopes for collectors that currently make ARM or Az cmdlet calls inside a row loop. This is intentionally an isolated collect-phase capability: it does not yet alter raw-inventory integration, collector scripts, or definitions. A failed request is recorded on only its owning parent envelope. Other parents and datasets continue. Payloads are retained as returned (rather than prematurely shaping report fields), so the subsequent definition migration has one auditable source contract. .OUTPUTS Typed envelopes: AZSC/Operational/VirtualMachine, AZSC/Operational/VMOperationalData, AZSC/Operational/ArcServerOperationalData, AZSC/Operational/ARCServers, AZSC/Operational/StorageAccount, and AZSC/Management/SubscriptionEnrichment. #> function Get-ScoutOperationalCollectorEnrichment { [CmdletBinding()] [OutputType([PSCustomObject])] param( [Parameter(Mandatory)] [AllowEmptyCollection()] [object[]]$Resources, [Parameter()] [AllowEmptyCollection()] [object[]]$Subscriptions = @() ) function Get-ScoutValue { param([AllowNull()]$InputObject, [Parameter(Mandatory)][string[]]$Name) if ($null -eq $InputObject) { return $null } foreach ($Candidate in $Name) { $Property = $InputObject.PSObject.Properties[$Candidate] if ($null -ne $Property) { return $Property.Value } } return $null } function Invoke-ScoutOperationalArm { param( [Parameter(Mandatory)][string]$Dataset, [Parameter(Mandatory)][string]$ParentId, [Parameter(Mandatory)][string]$Path, [ValidateSet('GET', 'POST')][string]$Method = 'GET', [AllowNull()]$Payload ) try { $Arguments = @{ Path = $Path; Method = $Method; ErrorAction = 'Stop' } if ($null -ne $Payload) { $Arguments['Payload'] = $Payload } $Response = Invoke-AzRestMethod @Arguments if ($null -eq $Response) { throw 'ARM returned no response.' } $Status = Get-ScoutValue -InputObject $Response -Name @('StatusCode') if ($null -ne $Status -and ([int]$Status -lt 200 -or [int]$Status -ge 300)) { throw "ARM returned status $Status." } $Content = Get-ScoutValue -InputObject $Response -Name @('Content') if ($Content -is [string]) { if ([string]::IsNullOrWhiteSpace($Content)) { return $null } return ($Content | ConvertFrom-Json) } return $Content } catch { Write-Warning "Get-ScoutOperationalCollectorEnrichment: $Dataset failed for '$ParentId': $($_.Exception.Message)" return [PSCustomObject]@{ __AZSCError = $_.Exception.Message } } } function ConvertTo-ScoutOperationalEnvelope { param( [Parameter(Mandatory)][string]$Type, [Parameter(Mandatory)]$Parent, [Parameter(Mandatory)][hashtable]$Properties ) $Id = [string](Get-ScoutValue $Parent @('id', 'ID')) [PSCustomObject][ordered]@{ type = $Type id = $Id subscriptionId = Get-ScoutValue $Parent @('subscriptionId', 'SubscriptionId') RESOURCEGROUP = Get-ScoutValue $Parent @('resourceGroup', 'RESOURCEGROUP') name = Get-ScoutValue $Parent @('name', 'NAME') properties = [PSCustomObject]$Properties } } $VirtualMachines = @($Resources | Where-Object { (Get-ScoutValue $_ @('type', 'TYPE')) -ieq 'microsoft.compute/virtualmachines' }) foreach ($Vm in $VirtualMachines) { $Id = [string](Get-ScoutValue $Vm @('id', 'ID')) if ([string]::IsNullOrWhiteSpace($Id)) { continue } $Now = (Get-Date).ToUniversalTime().ToString('o') $Start = (Get-Date).AddDays(-7).ToUniversalTime().ToString('o') $BaseMetric = "$Id/providers/microsoft.insights/metrics?api-version=2019-07-01×pan=$Start/$Now&interval=P1D&aggregation=Average" # Keep the operational payloads at the collect boundary. The two Compute report # collectors consume these envelopes later; they must never issue a per-row ARM call. # The Cost Management body deliberately retains the legacy ResourceId filter -- an # unfiltered `{}` query is not equivalent and can report a subscription total. $SubscriptionId = [string](Get-ScoutValue $Vm @('subscriptionId')) $VmName = [string](Get-ScoutValue $Vm @('name', 'NAME')) $Eligibility = Invoke-ScoutOperationalArm -Dataset 'VirtualMachine.ReplicationEligibility' -ParentId $Id -Path "/subscriptions/$SubscriptionId/providers/Microsoft.RecoveryServices/replicationEligibilityResults/${VmName}?api-version=2022-10-01" $Vaults = Invoke-ScoutOperationalArm -Dataset 'VirtualMachine.ReplicationVaults' -ParentId $Id -Path "/subscriptions/$SubscriptionId/providers/Microsoft.RecoveryServices/vaults?api-version=2023-04-01" $ProtectedItems = @() if ($null -eq (Get-ScoutValue $Vaults @('__AZSCError'))) { foreach ($Vault in @(Get-ScoutValue $Vaults @('value'))) { $VaultId = [string](Get-ScoutValue $Vault @('id')) $VaultName = [string](Get-ScoutValue $Vault @('name')) $VaultSegments = @($VaultId -split '/') $VaultResourceGroup = if ($VaultSegments.Count -gt 4) { $VaultSegments[4] } else { $null } if ([string]::IsNullOrWhiteSpace($VaultResourceGroup) -or [string]::IsNullOrWhiteSpace($VaultName)) { continue } $ProtectedItems += Invoke-ScoutOperationalArm -Dataset 'VirtualMachine.ReplicationProtectedItems' -ParentId $Id -Path "/subscriptions/$SubscriptionId/resourceGroups/$VaultResourceGroup/providers/Microsoft.RecoveryServices/vaults/$VaultName/replicationProtectedItems?api-version=2022-10-01" } } $CostPayload = @{ type = 'Usage' timeframe = 'MonthToDate' dataset = @{ granularity = 'None' filter = @{ dimensions = @{ name = 'ResourceId'; operator = 'In'; values = @($Id) } } aggregation = @{ totalCost = @{ name = 'PreTaxCost'; function = 'Sum' } } } } | ConvertTo-Json -Depth 10 $Properties = @{ CpuMetrics = Invoke-ScoutOperationalArm -Dataset 'VirtualMachine.CpuMetrics' -ParentId $Id -Path "$BaseMetric&metricnames=Percentage+CPU" MemoryMetrics = Invoke-ScoutOperationalArm -Dataset 'VirtualMachine.MemoryMetrics' -ParentId $Id -Path "$BaseMetric&metricnames=Available+Memory+Bytes" ReplicationEligibility = $Eligibility ReplicationProtectedItems = $ProtectedItems EstimatedCost = Invoke-ScoutOperationalArm -Dataset 'VirtualMachine.EstimatedCost' -ParentId $Id -Method POST -Path "/subscriptions/$SubscriptionId/providers/Microsoft.CostManagement/query?api-version=2023-03-01" -Payload $CostPayload } ConvertTo-ScoutOperationalEnvelope -Type 'AZSC/Operational/VirtualMachine' -Parent $Vm -Properties $Properties } foreach ($Vm in $VirtualMachines) { $Id = [string](Get-ScoutValue $Vm @('id', 'ID')) if ([string]::IsNullOrWhiteSpace($Id)) { continue } $Patch = Invoke-ScoutOperationalArm -Dataset 'VMOperationalData.PatchAssessment' -ParentId $Id -Method POST -Path "$Id/assessPatches?api-version=2023-03-01" ConvertTo-ScoutOperationalEnvelope -Type 'AZSC/Operational/VMOperationalData' -Parent $Vm -Properties @{ PatchAssessment = $Patch } } $ArcMachines = @($Resources | Where-Object { (Get-ScoutValue $_ @('type', 'TYPE')) -ieq 'microsoft.hybridcompute/machines' }) foreach ($Arc in $ArcMachines) { $Id = [string](Get-ScoutValue $Arc @('id', 'ID')) if ([string]::IsNullOrWhiteSpace($Id)) { continue } $Patch = Invoke-ScoutOperationalArm -Dataset 'ArcServerOperationalData.PatchAssessment' -ParentId $Id -Method POST -Path "$Id/assessPatches?api-version=2023-06-20-preview" ConvertTo-ScoutOperationalEnvelope -Type 'AZSC/Operational/ArcServerOperationalData' -Parent $Arc -Properties @{ PatchAssessment = $Patch } $Now = (Get-Date).ToUniversalTime().ToString('o') $Start = (Get-Date).AddDays(-7).ToUniversalTime().ToString('o') $SubId = [string](Get-ScoutValue $Arc @('subscriptionId')) $Policy = Invoke-ScoutOperationalArm -Dataset 'ARCServers.PolicyCompliance' -ParentId $Id -Method POST -Path "/subscriptions/$SubId/providers/Microsoft.PolicyInsights/policyStates/latest/queryResults?api-version=2019-10-01&`$filter=resourceId eq '$Id'&`$top=100" -Payload '{}' $Cpu = Invoke-ScoutOperationalArm -Dataset 'ARCServers.CpuMetrics' -ParentId $Id -Path "$Id/providers/microsoft.insights/metrics?api-version=2019-07-01&metricnames=cpu_usage_active×pan=$Start/$Now&interval=P1D&aggregation=Average" $ArcCostPayload = @{ type = 'Usage'; timeframe = 'MonthToDate'; dataset = @{ granularity = 'None'; filter = @{ dimensions = @{ name = 'ResourceId'; operator = 'In'; values = @($Id) } }; aggregation = @{ totalCost = @{ name = 'PreTaxCost'; function = 'Sum' } } } } | ConvertTo-Json -Depth 10 $Cost = Invoke-ScoutOperationalArm -Dataset 'ARCServers.EstimatedCost' -ParentId $Id -Method POST -Path "/subscriptions/$SubId/providers/Microsoft.CostManagement/query?api-version=2023-03-01" -Payload $ArcCostPayload ConvertTo-ScoutOperationalEnvelope -Type 'AZSC/Operational/ARCServers' -Parent $Arc -Properties @{ PolicyCompliance = $Policy; CpuMetrics = $Cpu; EstimatedCost = $Cost } } $StorageAccounts = @($Resources | Where-Object { (Get-ScoutValue $_ @('type', 'TYPE')) -ieq 'microsoft.storage/storageaccounts' }) foreach ($Account in $StorageAccounts) { $Id = [string](Get-ScoutValue $Account @('id', 'ID')) try { $Blob = Get-AzStorageBlobServiceProperty -ResourceGroupName (Get-ScoutValue $Account @('resourceGroup', 'RESOURCEGROUP')) -Name (Get-ScoutValue $Account @('name', 'NAME')) -ErrorAction Stop } catch { Write-Warning "Get-ScoutOperationalCollectorEnrichment: StorageAccounts.BlobService failed for '$Id': $($_.Exception.Message)" $Blob = [PSCustomObject]@{ __AZSCError = $_.Exception.Message } } try { $File = Get-AzStorageFileServiceProperty -ResourceGroupName (Get-ScoutValue $Account @('resourceGroup', 'RESOURCEGROUP')) -Name (Get-ScoutValue $Account @('name', 'NAME')) -ErrorAction Stop } catch { Write-Warning "Get-ScoutOperationalCollectorEnrichment: StorageAccounts.FileService failed for '$Id': $($_.Exception.Message)" $File = [PSCustomObject]@{ __AZSCError = $_.Exception.Message } } ConvertTo-ScoutOperationalEnvelope -Type 'AZSC/Operational/StorageAccount' -Parent $Account -Properties @{ BlobService = $Blob; FileService = $File } } $ResourceCounts = @{} $ResourceGroupCounts = @{} foreach ($Resource in @($Resources)) { $SubId = [string](Get-ScoutValue $Resource @('subscriptionId')) if ([string]::IsNullOrWhiteSpace($SubId)) { continue } $ResourceCounts[$SubId] = 1 + [int]($ResourceCounts[$SubId]) if ((Get-ScoutValue $Resource @('type', 'TYPE')) -ieq 'microsoft.resources/subscriptions/resourcegroups') { $ResourceGroupCounts[$SubId] = 1 + [int]($ResourceGroupCounts[$SubId]) } } $ManagementGroups = @{} try { $GraphRows = @(Search-AzGraph -Query "resourcecontainers | where type == 'microsoft.resources/subscriptions' | extend mgChain = properties.managementGroupAncestorsChain | project subscriptionId, mgChain" -First 1000 -ErrorAction Stop) foreach ($GraphRow in $GraphRows) { $Chain = @(Get-ScoutValue $GraphRow @('mgChain')) [array]::Reverse($Chain) $ManagementGroups[[string](Get-ScoutValue $GraphRow @('subscriptionId'))] = if ($Chain.Count -gt 0) { ($Chain | ForEach-Object { Get-ScoutValue $_ @('displayName') }) -join ' / ' } else { 'Tenant Root' } } } catch { Write-Warning "Get-ScoutOperationalCollectorEnrichment: AllSubscriptions.ManagementGroupPath failed: $($_.Exception.Message)" } foreach ($Subscription in @($Subscriptions)) { $SubId = [string](Get-ScoutValue $Subscription @('id', 'Id')) if ([string]::IsNullOrWhiteSpace($SubId)) { continue } [PSCustomObject][ordered]@{ type = 'AZSC/Management/SubscriptionEnrichment'; id = $SubId; subscriptionId = $SubId name = Get-ScoutValue $Subscription @('name', 'Name') properties = [PSCustomObject]@{ ResourceCount = [int]$ResourceCounts[$SubId] ResourceGroupCount = [int]$ResourceGroupCounts[$SubId] ManagementGroupPath = if ($ManagementGroups.ContainsKey($SubId)) { $ManagementGroups[$SubId] } else { 'Unknown' } Subscription = $Subscription } } } } |