wara.psm1
<#
.SYNOPSIS Starts the WARA Collector process. .DESCRIPTION The Start-WARACollector function initiates the WARA Collector process, which collects and processes data based on the specified parameters. It supports multiple parameter sets, including Default, Specialized, and ConfigFileSet. .PARAMETER SAP Switch to enable SAP workload processing. .PARAMETER AVD Switch to enable AVD workload processing. .PARAMETER AVS Switch to enable AVS workload processing. .PARAMETER HPC Switch to enable HPC workload processing. .PARAMETER PassThru Switch to enable the PassThru parameter. PassThru returns the output object. .PARAMETER SubscriptionIds Array of subscription IDs to include in the process. Validated using Test-WAFSubscriptionId. .PARAMETER ResourceGroups Array of resource groups to include in the process. Validated using Test-WAFResourceGroupId. .PARAMETER TenantID The tenant ID to use for the process. This parameter is mandatory and validated using Test-WAFIsGuid. .PARAMETER Tags Array of tags to include in the process. Validated using Test-WAFTagPattern. .PARAMETER AzureEnvironment Specifies the Azure environment to use. Default is 'AzureCloud'. Valid values are 'AzureCloud', 'AzureUSGovernment', 'AzureGermanCloud', and 'AzureChinaCloud'. .PARAMETER ConfigFile Path to the configuration file. This parameter is mandatory for the ConfigFileSet parameter set and validated using Test-Path. .PARAMETER RecommendationDataUri URI for the recommendation data. Default is 'https://raw.githubusercontent.com/Azure/Azure-Proactive-Resiliency-Library-v2/refs/heads/main/tools/data/recommendations.json'. .PARAMETER RecommendationResourceTypesUri URI for the recommendation resource types. Default is 'https://raw.githubusercontent.com/Azure/Azure-Proactive-Resiliency-Library-v2/refs/heads/main/tools/WARAinScopeResTypes.csv'. .PARAMETER UseImplicitRunbookSelectors Switch to enable the use of implicit runbook selectors. .PARAMETER RunbookFile Path to the runbook file. Validated using Test-Path. .EXAMPLE Start-WARACollector -TenantID "00000000-0000-0000-0000-000000000000" -SubscriptionIds "/subscriptions/00000000-0000-0000-0000-000000000000" .EXAMPLE Start-WARACollector -ConfigFile "C:\path\to\config.txt" .EXAMPLE Start-WARACollector -TenantID "00000000-0000-0000-0000-000000000000" -SubscriptionIds "/subscriptions/00000000-0000-0000-0000-000000000000" -ResourceGroups "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/RG-001" -Tags "Env||Environment!~Dev||QA" -AVD -SAP -HPC .EXAMPLE Start-WARACollector -ConfigFile "C:\path\to\config.txt" -SAP -AVD .NOTES Author: Kyle Poineal Date: 12/11/2024 #> function Start-WARACollector { [CmdletBinding(DefaultParameterSetName = 'Default')] param ( [Parameter(ParameterSetName = 'Default')] [Parameter(ParameterSetName = 'Specialized')] [Parameter(ParameterSetName = 'ConfigFileSet')] [switch] $SAP, [Parameter(ParameterSetName = 'Default')] [Parameter(ParameterSetName = 'Specialized')] [Parameter(ParameterSetName = 'ConfigFileSet')] [switch] $AVD, [Parameter(ParameterSetName = 'Default')] [Parameter(ParameterSetName = 'Specialized')] [Parameter(ParameterSetName = 'ConfigFileSet')] [switch] $AVS, [Parameter(ParameterSetName = 'Default')] [Parameter(ParameterSetName = 'Specialized')] [Parameter(ParameterSetName = 'ConfigFileSet')] [switch] $HPC, [Parameter(ParameterSetName = 'Default')] [Parameter(ParameterSetName = 'Specialized')] [Parameter(ParameterSetName = 'ConfigFileSet')] [switch] $PassThru, [Parameter(ParameterSetName = 'Default')] [ValidateScript({ Test-WAFSubscriptionId $_ })] [string[]] $SubscriptionIds, [Parameter(ParameterSetName = 'Default')] [ValidateScript({ Test-WAFResourceGroupId $_ })] [string[]] $ResourceGroups, [Parameter(Mandatory = $true, ParameterSetName = 'Default')] [ValidateScript({ Test-WAFIsGuid $_ })] [GUID] $TenantID, [Parameter(ParameterSetName = 'Default')] [ValidateScript({ Test-WAFTagPattern $_ })] [string[]] $Tags, [Parameter(ParameterSetName = 'Default')] [ValidateSet('AzureCloud', 'AzureUSGovernment', 'AzureGermanCloud', 'AzureChinaCloud')] [string] $AzureEnvironment = 'AzureCloud', [Parameter(ParameterSetName = 'ConfigFileSet', Mandatory = $true)] [ValidateScript({ Test-Path $_ -PathType Leaf })] [string] $ConfigFile, [Parameter(ParameterSetName = 'Default')] [ValidatePattern('^https:\/\/.+$')] [string] $RecommendationDataUri = 'https://raw.githubusercontent.com/Azure/Azure-Proactive-Resiliency-Library-v2/refs/heads/main/tools/data/recommendations.json', [Parameter(ParameterSetName = 'Default')] [ValidatePattern('^https:\/\/.+$')] [string] $RecommendationResourceTypesUri = 'https://raw.githubusercontent.com/Azure/Azure-Proactive-Resiliency-Library-v2/refs/heads/main/tools/WARAinScopeResTypes.csv', # Runbook parameters... [Parameter(ParameterSetName = 'Default')] [switch] $UseImplicitRunbookSelectors, [Parameter(ParameterSetName = 'Default')] [ValidateScript({ Test-Path $_ -PathType Leaf })] [string] $RunbookFile ) Write-host "Checking Version.." -ForegroundColor Cyan $LocalVersion = $(Get-Module -Name $MyInvocation.MyCommand.ModuleName).Version $GalleryVersion = (Find-Module -Name $MyInvocation.MyCommand.ModuleName).Version if($LocalVersion -lt $GalleryVersion){ Write-Host "A newer version of the module is available. Please update the module to the latest version and re-run the command." -ForegroundColor Cyan - Write-host "You can update by running 'Update-Module -Name $($MyInvocation.MyCommand.ModuleName)'" -ForegroundColor Cyan Write-Host "Local Install Version: $LocalVersion" -ForegroundColor Yellow Write-Host "PowerShell Gallery Version: $GalleryVersion" -ForegroundColor Green throw 'Module is out of date.' } $stopWatch = [System.Diagnostics.Stopwatch]::StartNew() $scriptParams = foreach ($param in $PSBoundParameters.GetEnumerator()) { Write-Debug "Parameter: $($param.Key) Value: $($param.Value)" [PSCustomObject]@{ $param.key = $param.value } } Write-Debug 'Debugging mode is enabled' Write-Progress -Activity 'WARA Collector' -Status 'Starting WARA Collector' -PercentComplete 0 -Id 1 # Determine which parameter set is active switch ($PSCmdlet.ParameterSetName) { 'ConfigFileSet' { Write-Debug 'Using ConfigFileSet parameter set' Write-Debug "ConfigFile: $ConfigFile" Write-Debug 'Importing ConfigFile data' $ConfigData = Import-WAFConfigFileData -ConfigFile $ConfigFile Write-Debug 'Testing TenantId, SubscriptionIds, ResourceGroups, and Tags' $ConfigData.TenantId = ([guid][string]$ConfigData.TenantId).Guid $null = Test-WAFIsGuid -StringGuid $ConfigData.TenantId $null = if ($ConfigData.SubscriptionIds) { Test-WAFSubscriptionId -InputValue $ConfigData.SubscriptionIds } $null = if ($ConfigData.ResourceGroups) { Test-WAFResourceGroupId -InputValue $ConfigData.ResourceGroups } $null = if ($ConfigData.Tags) { Test-WAFTagPattern -InputValue $ConfigData.Tags } } 'Default' { Write-Debug 'Using Default parameter set' Write-Debug "Parameter set values: $($PSBoundParameters.Keys)" if ($PSBoundParameters.keys.contains('SubscriptionIds') -or $PSBoundParameters.keys.contains('ResourceGroups')) { Write-Debug 'We contain the parameters.' } else { Write-Debug 'We do not contain the parameters.' throw 'The parameter SubscriptionIds or ResourceGroups is required when using the Default parameter set.' } } } #Use Null Coalescing to set the values of parameters. Write-Progress -Activity 'WARA Collector' -Status 'Setting Scope' -PercentComplete 1 -Id 1 $Scope_TenantId = $ConfigData.TenantId ?? $TenantID ?? (throw 'Tenant ID is required.') $Scope_SubscriptionIds = $ConfigData.SubscriptionIds ?? $SubscriptionIds ?? @() $Scope_ResourceGroups = $ConfigData.ResourceGroups ?? $ResourceGroups ?? @() $Scope_Tags = $ConfigData.Tags ?? $Tags ?? @() $Scope_TenantId = ([guid][string]$Scope_TenantId).Guid Write-Progress -Activity 'WARA Collector' -Status 'Setting Scope' -PercentComplete 3 -Id 1 $Scope_SubscriptionIds = Repair-WAFSubscriptionId -SubscriptionIds $Scope_SubscriptionIds Write-Debug "Tenant ID: $Scope_TenantId" Write-Debug "Subscription IDs: $Scope_SubscriptionIds" Write-Debug "Resource Groups: $Scope_ResourceGroups" Write-Debug "Tags: $Scope_Tags" $SpecializedWorkloads = @() if ($SAP) { Write-Debug 'SAP switch is enabled' $SpecializedWorkloads += 'SAP' } if ($AVD) { Write-Debug 'AVD switch is enabled' $SpecializedWorkloads += 'AVD' } if ($AVS) { Write-Debug 'AVS switch is enabled' $SpecializedWorkloads += 'AVS' } if ($HPC) { Write-Debug 'HPC switch is enabled' $SpecializedWorkloads += 'HPC' } if ($SpecializedWorkloads) { Write-Debug "Specialized Workloads: $SpecializedWorkloads" } #Import Recommendation Object from APRL Write-Progress -Activity 'WARA Collector' -Status 'Importing APRL Recommendation Object from GitHub' -PercentComplete 5 -Id 1 Write-Debug 'Importing APRL Recommendation Object from GitHub' $RecommendationObject = Invoke-RestMethod $RecommendationDataUri Write-Debug "Count of APRL Recommendation Object: $($RecommendationObject.count)" #Create Recommendation Object HashTable for faster lookup Write-Debug 'Creating Recommendation Object HashTable for faster lookup' Write-Progress -Activity 'WARA Collector' -Status 'Creating Recommendation Object HashTable' -PercentComplete 8 -Id 1 $RecommendationObjectHash = @{} $RecommendationObject.ForEach({ $RecommendationObjectHash[$_.aprlGuid] = $_ }) Write-Debug "Count of Recommendation Object Hashtable: $($RecommendationObjectHash.count)" #Import WARA InScope Resource Types CSV from APRL Write-Debug 'Importing WARA InScope Resource Types CSV from GitHub' Write-Progress -Activity 'WARA Collector' -Status 'Importing WARA InScope Resource Types CSV' -PercentComplete 11 -Id 1 $RecommendationResourceTypes = Invoke-RestMethod $RecommendationResourceTypesUri | ConvertFrom-Csv | Where-Object { $_.WARAinScope -eq 'yes' } Write-Debug "Count of WARA InScope Resource Types: $($RecommendationResourceTypes.count)" #Add Specialized Workloads to WARA InScope Resource Types Write-Debug 'Adding Specialized Workloads to WARA InScope Resource Types' Write-Progress -Activity 'WARA Collector' -Status 'Adding Specialized Workloads to WARA InScope Resource Types' -PercentComplete 14 -Id 1 $RecommendationResourceTypes += $SpecializedWorkloads Write-Debug "Count of WARA InScope Resource Types with Specialized Workloads: $($RecommendationResourceTypes.count)" #Create TypesNotInAPRLOrAdvisor Object from WARA InScope Resource Types Write-Debug 'Creating TypesNotInAPRLOrAdvisor Object from WARA InScope Resource Types' Write-Progress -Activity 'WARA Collector' -Status 'Creating TypesNotInAPRLOrAdvisor Object' -PercentComplete 17 -Id 1 $TypesNotInAPRLOrAdvisor = ($RecommendationResourceTypes | Where-Object { $_.InAprlAndOrAdvisor -eq "No" }).ResourceType Write-Debug "Count of TypesNotInAPRLOrAdvisor: $($TypesNotInAPRLOrAdvisor.count)" #Connect to Azure Write-Debug 'Connecting to Azure if not connected.' Write-Progress -Activity 'WARA Collector' -Status 'Connecting to Azure' -PercentComplete 20 -Id 1 Connect-WAFAzure -TenantId $Scope_TenantId -AzureEnvironment $AzureEnvironment #Get Implicit Subscription Ids from Scope Write-Debug 'Getting Implicit Subscription Ids from Scope' Write-Progress -Activity 'WARA Collector' -Status 'Getting Implicit Subscription Ids' -PercentComplete 23 -Id 1 $Scope_ImplicitSubscriptionIds = Get-WAFImplicitSubscriptionId -SubscriptionFilters $Scope_SubscriptionIds -ResourceGroupFilters $Scope_ResourceGroups Write-Debug "Implicit Subscription Ids: $Scope_ImplicitSubscriptionIds" #Get all resources from the Implicit Subscription ID scope - We use this later to add type, location, subscriptionid, resourcegroup to the impactedResourceObj objects Write-Debug 'Getting all resources from the Implicit Subscription ID scope' Write-Progress -Activity 'WARA Collector' -Status 'Getting All Resources' -PercentComplete 26 -Id 1 $AllResources = Invoke-WAFQuery -SubscriptionIds $Scope_ImplicitSubscriptionIds.replace('/subscriptions/', '') Write-Debug "Count of Resources: $($AllResources.count)" #Create HashTable of all resources for faster lookup Write-Debug 'Creating HashTable of all resources for faster lookup' Write-Progress -Activity 'WARA Collector' -Status 'Creating All Resources HashTable' -PercentComplete 29 -Id 1 $AllResourcesHash = @{} $AllResources.ForEach({ $AllResourcesHash[$_.id] = $_ }) Write-Debug "All Resources Hash: $($AllResourcesHash.count)" #Filter all resources by subscription, resourcegroup, and resource scope Write-Debug 'Filtering all resources by subscription, resourcegroup, and resource scope' Write-Progress -Activity 'WARA Collector' -Status 'Filtering All Resources' -PercentComplete 32 -Id 1 $Scope_AllResources = Get-WAFFilteredResourceList -UnfilteredResources $AllResources -SubscriptionFilters $Scope_SubscriptionIds -ResourceGroupFilters $Scope_ResourceGroups Write-Debug "Count of filtered Resources: $($Scope_AllResources.count)" #Create Resource Inventory object Write-Debug 'Creating Resource Inventory object' Write-Progress -Activity 'WARA Collector' -Status 'Creating Resource Inventory' -PercentComplete 34 -Id 1 $ResourceInventory = $Scope_AllResources Write-Debug "Count of Resource Inventory: $($ResourceInventory.count)" #Filter all resources by InScope Resource Types - We do this because we need to be able to compare resource ids to generate the generic recommendations(Resource types that have no recommendations or are not in advisor but also need to be validated) Write-Debug 'Filtering all resources by WARA InScope Resource Types' Write-Progress -Activity 'WARA Collector' -Status 'Filtering All Resources by WARA InScope Resource Types' -PercentComplete 35 -Id 1 $Scope_AllResources = Get-WAFResourcesByList -ObjectList $Scope_AllResources -FilterList $RecommendationResourceTypes.ResourceType -KeyColumn 'type' Write-Debug "Count of filtered by type Resources: $($Scope_AllResources.count)" #Get all APRL recommendations from the Implicit Subscription ID scope Write-Debug 'Getting all APRL recommendations from the Implicit Subscription ID scope' Write-Progress -Activity 'WARA Collector' -Status 'Getting APRL Recommendations' -PercentComplete 38 -Id 1 $Recommendations = Invoke-WAFQueryLoop -SubscriptionIds $Scope_ImplicitSubscriptionIds.replace('/subscriptions/', '') -RecommendationObject $RecommendationObject -AddedTypes $SpecializedWorkloads -ProgressId 2 Write-Debug "Count of Recommendations: $($Recommendations.count)" #Filter resource recommendation objects by subscription, resourcegroup, and resource scope Write-Debug 'Filtering APRL recommendation objects by subscription, resourcegroup, and resource scope' Write-Progress -Activity 'WARA Collector' -Status 'Filtering APRL Recommendations' -PercentComplete 41 -Id 1 $Filter_Recommendations = Get-WAFFilteredResourceList -UnfilteredResources $Recommendations -SubscriptionFilters $Scope_SubscriptionIds -ResourceGroupFilters $Scope_ResourceGroups Write-Debug "Count of APRL recommendation objects: $($Filter_Recommendations.count)" #Create impactedResourceObj objects from the recommendations Write-Debug 'Creating impactedResourceObj objects from the recommendations' Write-Progress -Activity 'WARA Collector' -Status 'Creating Impacted Resource Objects' -PercentComplete 44 -Id 1 $impactedResourceObj = Build-ImpactedResourceObj -ImpactedResource $Filter_Recommendations -AllResources $AllResourcesHash -RecommendationObject $RecommendationObjectHash Write-Debug "Count of impactedResourceObj objects: $($impactedResourceObj.count)" #Create list of validationResourceIds from the impactedResourceObj objects Write-Debug 'Creating hashtable of validationResources from the impactedResourceObj objects' Write-Progress -Activity 'WARA Collector' -Status 'Creating Validation Resources' -PercentComplete 47 -Id 1 $validationResources = @{} foreach ($obj in $impactedResourceObj | Select-Object id, name, type, location, subscriptionid, resourcegroup, checkname, selector) { $key = "$($obj.id)" if (-not $validationResources.ContainsKey($key)) { $validationResources[$key] = $obj } } Write-Debug "Count of validationResourceIds: $($validationResources.count)" #Add In Scope resources to validationResources HashTable #By adding the $Scope_AllResources to the validationResources HashTable, we can ensure that we have all resources in the scope that need to be validated. #Adding the resources AFTER the first loop ensures that we do not add resources that are already in the impactedResourceObj objects. #This means we do not have to worry about overwriting the objects. Write-Debug 'Add In Scope resources to validationResources HashTable' Write-Progress -Activity 'WARA Collector' -Status 'Adding In Scope Resources to Validation Resources' -PercentComplete 50 -Id 1 foreach ($obj in $Scope_AllResources) { $key = "$($obj.id)" if (-not $validationResources.ContainsKey($key)) { $validationResources[$key] = $obj } } Write-Debug "Count of validationResourceIds: $($validationResources.count)" #Create validationResourceObj objects from the impactedResourceObj objects Write-Debug 'Creating validationResourceObj objects from the impactedResourceObj objects' Write-Progress -Activity 'WARA Collector' -Status 'Creating Validation Resource Objects' -PercentComplete 53 -Id 1 $validationResourceObj = Build-ValidationResourceObj -ValidationResources $validationResources -RecommendationObject $RecommendationObject -TypesNotInAPRLOrAdvisor $TypesNotInAPRLOrAdvisor Write-Debug "Count of validationResourceObj objects: $($validationResourceObj.count)" #Combine impactedResourceObj and validationResourceObj objects Write-Debug 'Combining impactedResourceObj and validationResourceObj objects' Write-Progress -Activity 'WARA Collector' -Status 'Combining Impacted and Validation Resource Objects' -PercentComplete 56 -Id 1 $impactedResourceObj += $validationResourceObj Write-Debug "Count of combined validationResourceObj impactedResourceObj objects: $($impactedResourceObj.count)" #Get Advisor Metadata to include recommendations that are not in Advisor under 'HighAvailability' Write-Debug 'Getting Advisor Metadata' Write-Progress -Activity 'WARA Collector' -Status 'Getting Advisor Metadata' -PercentComplete 59 -Id 1 $AdvisorMetadata = Get-WAFAdvisorMetadata Write-Debug "Count of Advisor Metadata: $($AdvisorMetadata.count)" #Get Other Recommendations Write-Debug 'Getting Other Recommendations' Write-Progress -Activity 'WARA Collector' -Status 'Getting Other Recommendations' -PercentComplete 62 -Id 1 $OtherRecommendations = Get-WARAOtherRecommendations -RecommendationObject $RecommendationObject -AdvisorMetadata $AdvisorMetadata Write-Debug "Count of Other Recommendations: $($OtherRecommendations.count)" #Get Advisor Recommendations Write-Debug 'Getting Advisor Recommendations' Write-Progress -Activity 'WARA Collector' -Status 'Getting Advisor Recommendations' -PercentComplete 65 -Id 1 $advisorResourceObj = Get-WAFAdvisorRecommendation -AdditionalRecommendationIds $OtherRecommendations -SubscriptionIds $Scope_ImplicitSubscriptionIds.replace('/subscriptions/', '') -HighAvailability Write-Debug "Count of Advisor Recommendations: $($advisorResourceObj.count)" #Prior to filtering, capture all "global" recommendations that are microsoft.subscriptions/subscriptions since these get filtered out. Write-Debug 'Capturing global recommendations that are microsoft.subscriptions/subscriptions' Write-Progress -Activity 'WARA Collector' -Status 'Capturing Global Recommendations' -PercentComplete 68 -Id 1 $globalRecommendations = $advisorResourceObj | Where-Object { $_.type -eq 'microsoft.subscriptions/subscriptions' } Write-Debug "Count of global recommendations: $($globalRecommendations.count)" #Filter Advisor Recommendations by subscription, resource group, and resource scope Write-Debug 'Filtering Advisor Recommendations by subscription, resource group, and resource scope' Write-Progress -Activity 'WARA Collector' -Status 'Filtering Advisor Recommendations' -PercentComplete 71 -Id 1 $advisorResourceObj = Get-WAFFilteredResourceList -UnfilteredResources $advisorResourceObj -SubscriptionFilters $Scope_SubscriptionIds -ResourceGroupFilters $Scope_ResourceGroups Write-Debug "Count of filtered Advisor Recommendations: $($advisorResourceObj.count)" #If we passed tags, filter impactedResourceObj and advisorResourceObj by tagged resource group and tagged resource scope if (![string]::IsNullOrEmpty($Scope_Tags)) { Write-Debug 'Starting Tag Filtering' Write-Progress -Activity 'WARA Collector' -Status 'Starting Tag Filtering' -PercentComplete 72 -Id 1 Write-Debug "Scope Tags: $Scope_Tags" #Get all tagged resource groups from the Implicit Subscription ID scope Write-Debug 'Getting all tagged resource groups from the Implicit Subscription ID scope' Write-Progress -Activity 'WARA Collector' -Status 'Getting Tagged Resource Groups' -PercentComplete 72 -Id 1 $Filter_TaggedResourceGroupIds = Get-WAFTaggedResourceGroup -TagArray $Scope_Tags -SubscriptionIds $Scope_ImplicitSubscriptionIds.replace('/subscriptions/', '') Write-Debug "Count of Tagged Resource Group Ids: $($Filter_TaggedResourceGroupIds.count)" #Get all tagged resources from the Implicit Subscription ID scope Write-Debug 'Getting all tagged resources from the Implicit Subscription ID scope' Write-Progress -Activity 'WARA Collector' -Status 'Getting Tagged Resources' -PercentComplete 73 -Id 1 $Filter_TaggedResourceIds = Get-WAFTaggedResource -TagArray $Scope_Tags -SubscriptionIds $Scope_ImplicitSubscriptionIds.replace('/subscriptions/', '') Write-Debug "Count of Tagged Resource Ids: $($Filter_TaggedResourceIds.count)" #Filter ResourceInventory objects by tagged resource group and resource scope Write-Debug 'Filtering ResourceInventory objects by tagged resource group and resource scope' Write-Progress -Activity 'WARA Collector' -Status 'Filtering Resource Inventory' -PercentComplete 73 -Id 1 $ResourceInventory = Get-WAFFilteredResourceList -UnfilteredResources $ResourceInventory -ResourceGroupFilters $Filter_TaggedResourceGroupIds -ResourceFilters $Filter_TaggedResourceIds Write-Debug "Count of tag filtered ResourceInventory objects: $($ResourceInventory.count)" #Filter impactedResourceObj objects by tagged resource group and resource scope Write-Debug 'Filtering impactedResourceObj objects by tagged resource group and resource scope' Write-Progress -Activity 'WARA Collector' -Status 'Filtering Impacted Resource Objects' -PercentComplete 73 -Id 1 $impactedResourceObj = Get-WAFFilteredResourceList -UnfilteredResources $impactedResourceObj -ResourceGroupFilters $Filter_TaggedResourceGroupIds -ResourceFilters $Filter_TaggedResourceIds Write-Debug "Count of tag filtered impactedResourceObj objects: $($impactedResourceObj.count)" #Filter Advisor Recommendations by tagged resource group and resource scope Write-Debug 'Filtering Advisor Recommendations by tagged resource group and resource scope' Write-Progress -Activity 'WARA Collector' -Status 'Filtering Advisor Recommendations' -PercentComplete 73 -Id 1 $advisorResourceObj = Get-WAFFilteredResourceList -UnfilteredResources $advisorResourceObj -ResourceGroupFilters $Filter_TaggedResourceGroupIds -ResourceFilters $Filter_TaggedResourceIds Write-Debug "Count of tag filtered Advisor Recommendations: $($advisorResourceObj.count)" } #Build Specialized Resource Object if Specialized Workloads are selected but not present in the impactedResourceObj. #Some of the specialized workloads have queries that run. If this is the case then we need to check if the impactedResourceObj contains these resource types and if not add them to the impactedResourceObj. if ($SpecializedWorkloads) { Write-Debug 'Building Specialized Resource Object' Write-Progress -Activity 'WARA Collector' -Status 'Building Specialized Resource Object' -PercentComplete 74 -Id 1 $specializedResourceObj = Build-SpecializedResourceObj -SpecializedResourceObj $SpecializedWorkloads -RecommendationObject $RecommendationObject Write-Debug "Count of Specialized Resource Object: $($specializedResourceObj.count)" Write-Debug 'Adding Specialized Resource Object to impactedResourceObj' $impactedResourceObj += $specializedResourceObj Write-Debug "Count of impactedResourceObj with Specialized Resource Object: $($impactedResourceObj.count)" } #Add global recommendations back to advisorResourceObj Write-Debug 'Adding global recommendations back to advisorResourceObj' Write-Progress -Activity 'WARA Collector' -Status 'Adding Global Recommendations' -PercentComplete 75 -Id 1 $advisorResourceObj += $globalRecommendations Write-Debug "Count of advisorResourceObj with global recommendations: $($advisorResourceObj.count)" #Build Resource Type Object Write-Debug 'Building Resource Type Object with impactedResourceObj and advisorResourceObj' Write-Progress -Activity 'WARA Collector' -Status 'Building Resource Type Object' -PercentComplete 78 -Id 1 $resourceTypeObj = Build-ResourceTypeObj -ResourceObj $ResourceInventory <# Adjusting this but keeping old code just in case. ($impactedResourceObj + $advisorResourceObj)#> -TypesNotInAPRLOrAdvisor $TypesNotInAPRLOrAdvisor Write-Debug "Count of Resource Type Object : $($resourceTypeObj.count)" #Get Azure Outages Write-Debug 'Getting Azure Outages' Write-Progress -Activity 'WARA Collector' -Status 'Getting Azure Outages' -PercentComplete 81 -Id 1 $outageResourceObj = Get-WAFOldOutage -SubscriptionIds $Scope_ImplicitSubscriptionIds.replace('/subscriptions/', '') #Get Azure Retirements Write-Debug 'Getting Azure Retirements' Write-Progress -Activity 'WARA Collector' -Status 'Getting Azure Retirements' -PercentComplete 84 -Id 1 $retirementResourceObj = Get-WAFResourceRetirement -SubscriptionIds $Scope_ImplicitSubscriptionIds.replace('/subscriptions/', '') #Get Azure Support Tickets Write-Debug 'Getting Azure Support Tickets' Write-Progress -Activity 'WARA Collector' -Status 'Getting Azure Support Tickets' -PercentComplete 87 -Id 1 $supportTicketObjects = Get-WAFSupportTicket -SubscriptionIds $Scope_ImplicitSubscriptionIds.replace('/subscriptions/', '') #Get Azure Service Health Write-Debug 'Getting Azure Service Health' Write-Progress -Activity 'WARA Collector' -Status 'Getting Azure Service Health' -PercentComplete 90 -Id 1 $serviceHealthObjects = Get-WAFServiceHealth -SubscriptionIds $Scope_ImplicitSubscriptionIds.replace('/subscriptions/', '') $stopWatch.Stop() Write-Debug "Elapsed Time: $($stopWatch.Elapsed.toString('hh\:mm\:ss'))" #Create Script Details Object Write-Debug 'Creating Script Details Object' $scriptDetails = [PSCustomObject]@{ Version = "2.1.19"#$(Get-Module -Name $MyInvocation.MyCommand.ModuleName).Version ElapsedTime = $stopWatch.Elapsed.toString('hh\:mm\:ss') SAP = [bool]$SAP AVD = [bool]$AVD AVS = [bool]$AVS HPC = [bool]$HPC TenantId = $Scope_TenantId SubscriptionIds = $Scope_SubscriptionIds ResourceGroups = $Scope_ResourceGroups ImplicitSubscriptionIds = $Scope_ImplicitSubscriptionIds Tags = $Scope_Tags AzureEnvironment = $AzureEnvironment RecommendationDataUri = $RecommendationDataUri RecommendationResourceTypesUri = $RecommendationResourceTypesUri UseImplicitRunbookSelectors = $UseImplicitRunbookSelectors RunbookFile = $RunbookFile ConfigFile = $ConfigFile ConfigData = $ConfigData RunTimeParameters = $scriptParams } #Create output JSON Write-Debug 'Creating output JSON' Write-Progress -Activity 'WARA Collector' -Status 'Creating Output JSON' -PercentComplete 93 -Id 1 $outputJson = [PSCustomObject]@{ scriptDetails = $scriptDetails impactedResources = $impactedResourceObj resourceType = $resourceTypeObj advisory = $advisorResourceObj outages = $outageResourceObj retirements = $retirementResourceObj supportTickets = $supportTicketObjects serviceHealth = $serviceHealthObjects resourceInventory = $ResourceInventory } Write-Debug 'Output JSON' Write-Progress -Activity 'WARA Collector' -Status 'Output JSON' -PercentComplete 100 -Id 1 -Completed #Output JSON to file $outputPath = ('.\WARA-File-' + (Get-Date -Format 'yyyy-MM-dd-HH-mm') + '.json') #Output JSON to file Write-Host "Output Path: $outputPath" -ForegroundColor Yellow if ($PassThru) { return $outputJson } $outputJson | ConvertTo-Json -Depth 15 | Out-file $outputPath } function Build-ImpactedResourceObj { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [PSCustomObject] $ImpactedResources, [Parameter(Mandatory = $true)] [Hashtable] $AllResources, [Parameter(Mandatory = $true)] [Hashtable] $RecommendationObject ) $impactedResourceObj = [impactedResourceFactory]::new($ImpactedResources, $AllResources, $RecommendationObject) $r = $impactedResourceObj.createImpactedResourceObjects() return ,$r } function Build-ValidationResourceObj { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [Hashtable] $ValidationResources, [Parameter(Mandatory = $true)] [PSObject] $RecommendationObject, [Parameter(Mandatory = $true)] [PSObject] $TypesNotInAPRLOrAdvisor ) $validatorObj = [validationResourceFactory]::new($RecommendationObject, $validationResources, $TypesNotInAPRLOrAdvisor) $r = $validatorObj.createValidationResourceObjects() return ,$r } function Build-ResourceTypeObj { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [PSObject] $ResourceObj, [Parameter(Mandatory = $true)] [PSObject] $TypesNotInAPRLOrAdvisor ) $return = [resourceTypeFactory]::new($ResourceObj, $TypesNotInAPRLOrAdvisor).createResourceTypeObjects() return ,$return } function Build-SpecializedResourceObj { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [PSObject] $SpecializedResourceObj, [Parameter(Mandatory = $true)] [PSObject] $RecommendationObject ) $return = [specializedResourceFactory]::new($SpecializedResourceObj, $RecommendationObject).createSpecializedResourceObjects() return ,$return } function Get-WARAOtherRecommendations { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [PSObject] $RecommendationObject, [Parameter(Mandatory = $true)] [PSObject] $AdvisorMetadata ) $metadata = $AdvisorMetadata.where({ $_.recommendationCategory -ne 'HighAvailability' }).id #Returns recommendations that are in APRL but not in Advisor under 'HighAvailability' $return = $RecommendationObject.recommendationTypeId | Where-Object { $_ -in $metadata } return ,$return } <# .CLASS impactedResourceObj .SYNOPSIS Represents a resource type object for APRL. .DESCRIPTION The `aprlResourceTypeObj` class encapsulates the details of a resource type in APRL, including the number of resources, availability in APRL/ADVISOR, assessment owner, status, and notes. .PROPERTY Resource Type The type of the resource. .PROPERTY Number Of Resources The number of resources of this type. .PROPERTY Available in APRL/ADVISOR? Indicates whether the resource type is available in APRL or ADVISOR. .PROPERTY Assessment Owner The owner of the assessment. .PROPERTY Status The status of the resource type. .PROPERTY Notes Additional notes about the resource type. .NOTES Author: Kyle Poineal Date: 2023-10-07 #> class aprlResourceTypeObj { [string] ${Resource Type} [int] ${Number Of Resources} [string] ${Available in APRL/ADVISOR?} [string] ${Assessment Owner} [string] $Status [string] $Notes } <# .CLASS validationResourceFactory .PROPERTY RecommendationObject The recommendation object. .PROPERTY validationResources The validation resources. .SYNOPSIS Factory class to create resource type objects. .DESCRIPTION The `resourceTypeFactory` class is responsible for creating instances of `aprlResourceTypeObj` based on impacted resources and types not in APRL or ADVISOR. .CONSTRUCTORS resourceTypeFactory([PSObject]$impactedResourceObj, [PSObject]$TypesNotInAPRLOrAdvisor) Initializes a new instance of the `resourceTypeFactory` class. .NOTES Author: Kyle Poineal Date: 2023-10-07 #> class resourceTypeFactory { [PSObject]$impactedResourceObj [PSObject]$TypesNotInAPRLOrAdvisor resourceTypeFactory([PSObject]$impactedResourceObj, [PSObject]$TypesNotInAPRLOrAdvisor) { $this.impactedResourceObj = $impactedResourceObj | Group-Object -Property type | Select-Object Name, Count $this.TypesNotInAPRLOrAdvisor = $TypesNotInAPRLOrAdvisor } <# .CLASS aprlResourceTypeObj .METHOD createResourceTypeObjects .SYNOPSIS Creates resource type objects. .DESCRIPTION The `createResourceTypeObjects` method creates and returns an array of `aprlResourceTypeObj` instances based on the impacted resources and types not in APRL or ADVISOR. .OUTPUTS System.Object[]. Returns an array of `aprlResourceTypeObj` instances. .EXAMPLE $factory = [resourceTypeFactory]::new($impactedResourceObj, $TypesNotInAPRLOrAdvisor) $resourceTypes = $factory.createResourceTypeObjects() .NOTES Author: Kyle Poineal Date: 2023-10-07 #> [object[]] createResourceTypeObjects() { $return = foreach ($type in $this.impactedResourceObj) { $r = [aprlResourceTypeObj]::new() $r.'Resource Type' = $type.Name $r.'Number Of Resources' = $type.Count $r.'Available in APRL/ADVISOR?' = $(($this.TypesNotInAPRLOrAdvisor -contains $type.Name) ? "No" : "Yes") $r.'Assessment Owner' = "" $r.Status = "" $r.notes = "" $r } return $return } } <# .CLASS aprlResourceObj .SYNOPSIS Represents an APRL resource object. .DESCRIPTION The `aprlResourceObj` class encapsulates the details of an APRL resource, including validation action, recommendation ID, name, ID, type, location, subscription ID, resource group, parameters, check name, and selector. .PROPERTY validationAction The validation action for the resource. .PROPERTY recommendationId The recommendation ID for the resource. .PROPERTY name The name of the resource. .PROPERTY id The ID of the resource. .PROPERTY type The type of the resource. .PROPERTY location The location of the resource. .PROPERTY subscriptionId The subscription ID of the resource. .PROPERTY resourceGroup The resource group of the resource. .PROPERTY param1 Additional parameter 1. .PROPERTY param2 Additional parameter 2. .PROPERTY param3 Additional parameter 3. .PROPERTY param4 Additional parameter 4. .PROPERTY param5 Additional parameter 5. .PROPERTY checkName The check name for the resource. .PROPERTY selector The selector for the resource. .NOTES Author: Kyle Poineal Date: 2023-10-07 #> class aprlResourceObj { [string] $validationAction [string] $recommendationId [string] $name [string] $id [string] $type [string] $location [string] $subscriptionId [string] $resourceGroup [string] $param1 [string] $param2 [string] $param3 [string] $param4 [string] $param5 [string] $checkName [string] $selector } <# .CLASS impactedResourceFactory .PROPERTY impactedResources The impacted resources. .PROPERTY allResources All resources. .PROPERTY RecommendationObject The recommendation object. .SYNOPSIS Factory class to create impacted resource objects. .DESCRIPTION The `impactedResourceFactory` class is responsible for creating instances of `aprlResourceObj` based on impacted resources, all resources, and recommendation objects. .CONSTRUCTORS impactedResourceFactory([PSObject]$impactedResources, [hashtable]$allResources, [hashtable]$RecommendationObject) Initializes a new instance of the `impactedResourceFactory` class. .METHODS [object[]] createImpactedResourceObjects() Creates and returns an array of `aprlResourceObj` instances. .NOTES Author: Kyle Poineal Date: 2023-10-07 #> class impactedResourceFactory { [PSObject] $impactedResources [hashtable] $allResources [hashtable] $RecommendationObject impactedResourceFactory([PSObject]$impactedResources, [hashtable]$allResources, [hashtable]$RecommendationObject) { $this.impactedResources = $impactedResources $this.allResources = $allResources $this.RecommendationObject = $RecommendationObject } <# .CLASS impactedResourceFactory .METHOD createImpactedResourceObjects .SYNOPSIS Creates impacted resource objects. .DESCRIPTION The `createImpactedResourceObjects` method creates and returns an array of `aprlResourceObj` instances based on the impacted resources, all resources, and recommendation objects. .OUTPUTS System.Object[]. Returns an array of `aprlResourceObj` instances. .EXAMPLE $factory = [impactedResourceFactory]::new($impactedResources, $allResources, $RecommendationObject) $impactedResources = $factory.createImpactedResourceObjects() .NOTES Author: Kyle Poineal Date: 2023-10-07 #> [object[]] createImpactedResourceObjects() { $return = foreach ($impactedResource in $this.impactedResources) { $r = [aprlResourceObj]::new() $r.validationAction = "APRL - Queries" $r.RecommendationId = $impactedResource.recommendationId $r.Name = $impactedResource.name $r.Id = $impactedResource.id $r.type = $this.RecommendationObject[$r.recommendationId].recommendationResourceType ?? $this.allResources[$r.id].type ?? "Unknown" $r.location = $this.allResources[$r.id].location ?? "Unknown" $r.subscriptionId = $this.allResources[$r.id].subscriptionId ?? $r.id.split("/")[2] ?? "Unknown" $r.resourceGroup = $this.allResources[$r.id].resourceGroup ?? $r.id.split("/")[4] ?? "Unknown" $r.Param1 = $impactedResource.param1 $r.Param2 = $impactedResource.param2 $r.Param3 = $impactedResource.param3 $r.Param4 = $impactedResource.param4 $r.Param5 = $impactedResource.param5 $r.checkName = $impactedResource.checkName $r.selector = $impactedResource.selector ?? "APRL" $r } return $return } } <# .CLASS specializedResourceFactory .SYNOPSIS Factory class to create validation resource objects. .DESCRIPTION The `validationResourceFactory` class is responsible for creating instances of `aprlResourceObj` for validation purposes based on recommendation objects, validation resources, and types not in APRL or ADVISOR. .CONSTRUCTORS validationResourceFactory([PSObject]$recommendationObject, [hashtable]$validationResources, [PSObject]$TypesNotInAPRLOrAdvisor) Initializes a new instance of the `validationResourceFactory` class. .METHODS [object[]] createValidationResourceObjects() Creates and returns an array of `aprlResourceObj` instances for validation purposes. static [string] getValidationAction($query) Determines the validation action based on the query. .PROPERTY recommendationObject The recommendation object. .PROPERTY validationResources The validation resources. .PROPERTY TypesNotInAPRLOrAdvisor Resource types that we want to create a recommendation for but do not have a recommendation for. .NOTES Author: Kyle Poineal Date: 2023-10-07 #> class validationResourceFactory { # This class is used to create validationResourceObj objects # Properties [PSObject] $recommendationObject # The recommendation object [hashtable] $validationResources # The validation resources [PSObject] $TypesNotInAPRLOrAdvisor # Resource types that we want to create a recommendation for but do not have a recommendation for. validationResourceFactory([PSObject]$recommendationObject, [hashtable]$validationResources, [PSObject]$TypesNotInAPRLOrAdvisor) { $this.recommendationObject = $recommendationObject $this.validationResources = $validationResources $this.TypesNotInAPRLOrAdvisor = $TypesNotInAPRLOrAdvisor } <# .CLASS validationResourceFactory .METHOD createValidationResourceObjects .SYNOPSIS Creates validation resource objects. .DESCRIPTION The `createValidationResourceObjects` method creates and returns an array of `aprlResourceObj` instances for validation purposes based on the recommendation objects, validation resources, and types not in APRL or ADVISOR. .OUTPUTS System.Object[]. Returns an array of `aprlResourceObj` instances for validation purposes. .EXAMPLE $factory = [validationResourceFactory]::new($recommendationObject, $validationResources, $TypesNotInAPRLOrAdvisor) $validationResources = $factory.createValidationResourceObjects() .NOTES Author: Kyle Poineal Date: 2023-10-07 #> [object[]] createValidationResourceObjects() { $return = @() $return = foreach ($v in $this.validationResources.GetEnumerator()) { $impactedResource = $v.value $recommendationByType = $this.recommendationObject.where({ $_.automationAvailable -eq $false -and $impactedResource.type -eq $_.recommendationResourceType -and $_.recommendationMetadataState -eq "Active" -and [string]::IsNullOrEmpty($_.recommendationTypeId) }) if ($null -ne $recommendationByType) { foreach ($rec in $recommendationByType) { $r = [aprlResourceObj]::new() $r.validationAction = [validationResourceFactory]::getValidationAction($rec.query) $r.recommendationId = $rec.aprlGuid $r.name = $impactedResource.name $r.id = $impactedResource.id $r.type = $impactedResource.type $r.location = $impactedResource.location $r.subscriptionId = $impactedResource.subscriptionId $r.resourceGroup = $impactedResource.resourceGroup $r.param1 = '' $r.param2 = '' $r.param3 = '' $r.param4 = '' $r.param5 = '' $r.checkName = '' $r.selector = $impactedResource.selector ?? "APRL" $r } } elseif ($impactedResource.type -in $this.TypesNotInAPRLOrAdvisor) { $r = [aprlResourceObj]::new() $r.validationAction = [validationResourceFactory]::getValidationAction("No Recommendations") $r.recommendationId = '' $r.name = $impactedResource.name $r.id = $impactedResource.id $r.type = $impactedResource.type $r.location = $impactedResource.location $r.subscriptionId = $impactedResource.subscriptionId $r.resourceGroup = $impactedResource.resourceGroup $r.param1 = '' $r.param2 = '' $r.param3 = '' $r.param4 = '' $r.param5 = '' $r.checkName = '' $r.selector = $impactedResource.selector ?? "APRL" $r } else { Write-Error "No recommendation found for $($impactedResource.type) with resource id $($impactedResource.id)" } } return $return } <# .CLASS validationResourceFactory .METHOD getValidationAction .SYNOPSIS Determines the validation action based on the query. .DESCRIPTION The `getValidationAction` method determines the validation action based on the provided query string. .PARAMETER query The query string to evaluate. .OUTPUTS System.String. Returns the validation action as a string. .EXAMPLE $action = [validationResourceFactory]::getValidationAction("No Recommendations") .NOTES Author: Kyle Poineal Date: 2023-10-07 #> static [string] getValidationAction($query) { $return = switch -wildcard ($query) { "*development*" { 'IMPORTANT - Query under development - Validate Resources manually' } "*cannot-be-validated-with-arg*" { 'IMPORTANT - Recommendation cannot be validated with ARGs - Validate Resources manually' } "*Azure Resource Graph*" { 'IMPORTANT - Query under development - Validate Resources manually'} "No Recommendations" { 'IMPORTANT - Resource Type is not available in either APRL or Advisor - Validate Resources manually if applicable, if not delete this line' } default { "IMPORTANT - Query does not exist - Validate Resources Manually" } } return $return } } <# .CLASS specializedResourceFactory .PROPERTY recommendationObject The recommendation object. .PROPERTY specializedResources The specialized resources. .SYNOPSIS Factory class to create specialized resource objects. .DESCRIPTION The `specializedResourceFactory` class is responsible for creating instances of `aprlResourceObj` for specialized resources based on recommendation objects. .CONSTRUCTORS specializedResourceFactory([PSObject]$specializedResources, [PSObject]$RecommendationObject) Initializes a new instance of the `specializedResourceFactory` class. .EXAMPLE $factory = [specializedResourceFactory]::new($specializedResources, $RecommendationObject) $specializedResources = $factory.createSpecializedResourceObjects() .NOTES Author: Kyle Poineal Date: 2023-10-07 #> class specializedResourceFactory { # This class is used to create specializedResourceObj objects # Properties [PSObject] $specializedResources # The specialized resources [PSObject] $RecommendationObject # The recommendation object specializedResourceFactory([PSObject]$specializedResources, [PSObject]$RecommendationObject) { $this.specializedResources = $specializedResources $this.RecommendationObject = $RecommendationObject } <# .CLASS specializedResourceFactory .METHOD createSpecializedResourceObjects .SYNOPSIS Creates specialized resource objects. .DESCRIPTION The `createSpecializedResourceObjects` method creates and returns an array of `aprlResourceObj` instances for specialized resources based on the recommendation objects. .OUTPUTS System.Object[]. Returns an array of `aprlResourceObj` instances for specialized resources. .EXAMPLE $factory = [specializedResourceFactory]::new($specializedResources, $RecommendationObject) $specializedResources = $factory.createSpecializedResourceObjects() .NOTES Author: Kyle Poineal Date: 2023-10-07 #> [object[]] createSpecializedResourceObjects() { $return = foreach ($s in $this.specializedResources) { $thisType = $this.RecommendationObject.where({ $s -in $_.tags -and $_.recommendationMetadataState -eq "Active" }) foreach ($type in $thisType) { $r = [aprlResourceObj]::new() $r.validationAction = [specializedResourceFactory]::getValidationAction($type.query) $r.recommendationId = $type.aprlGuid $r.name = '' $r.id = '' $r.type = $type.recommendationResourceType $r.location = '' $r.subscriptionId = '' $r.resourceGroup = '' $r.param1 = '' $r.param2 = '' $r.param3 = '' $r.param4 = '' $r.param5 = '' $r.checkName = '' $r.selector = "APRL" $r } } return $return } <# .CLASS specializedResourceFactory .METHOD getValidationAction .SYNOPSIS Determines the validation action based on the query. .DESCRIPTION The `getValidationAction` method determines the validation action based on the provided query string. .PARAMETER query The query string to evaluate. .OUTPUTS System.String. Returns the validation action as a string. .EXAMPLE $action = [specializedResourceFactory]::getValidationAction("No Recommendations") .NOTES Author: Kyle Poineal Date: 2023-10-07 #> static [string] getValidationAction($query) { $return = switch -wildcard ($query) { "*development*" { 'IMPORTANT - Query under development - Validate Resources manually' } "*cannot-be-validated-with-arg*" { 'IMPORTANT - Recommendation cannot be validated with ARGs - Validate Resources manually' } "*Azure Resource Graph*" { 'IMPORTANT - This resource has a query but the automation is not available - Validate Resources manually' } "No Recommendations" { 'IMPORTANT - Resource Type is not available in either APRL or Advisor - Validate Resources manually if applicable, if not delete this line' } default { "IMPORTANT - Query does not exist - Validate Resources Manually" } } return $return } } |